Skip to content

Commit 4fb6117

Browse files
committed
spec: support datasource and service lifecycles
1 parent b28b8d7 commit 4fb6117

File tree

4 files changed

+310
-8
lines changed

4 files changed

+310
-8
lines changed

lib/src/domain/models/leaf.dart

Lines changed: 9 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -14,14 +14,17 @@ abstract class Leaf<T> extends Model {
1414
/// This can be used to show a loading indicator or a placeholder while
1515
/// a guard is being checked.
1616
///
17-
/// Note: It is unsafe to perform navigation actions or to use
18-
/// any module-dependent resources in this method, as the module
19-
/// may not have been fully initialized yet when this method is called.
17+
/// **This method runs before required modules are guaranteed active.**
18+
///
19+
/// You must not:
20+
/// - trigger navigation
21+
/// - resolve or access module-scoped dependencies (Repo/Service/Datasource)
22+
/// - rely on lifecycle-managed resources being initialized or activated
23+
///
24+
/// Keep this method side-effect free and limited to static/synchronous
25+
/// placeholder rendering from data already available in [ctx].
2026
T preview(RouteContext ctx);
2127

2228
/// Builds the final presentation of [T] once the route has been validated.
2329
FutureOr<T> content(RouteContext ctx);
2430
}
25-
26-
// this is an extension of Route and not a view.
27-
// ignore: views_must_extend_view, views_must_have_view_suffix

lib/src/module.dart

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -193,9 +193,9 @@ typedef Resolver = T Function<T extends Object>();
193193
/// configuration ([Config]) as well as setting up core services like telemetry and analytics.
194194
abstract class RootModule<RouteType, Config extends Object>
195195
extends Module<RouteType, Config> {
196-
197196
/// Creates a new [RootModule] with the given [cfg].
198197
RootModule(this.cfg);
198+
199199
/// The configuration to use throughout the application.
200200
final Config cfg;
201201

lib/src/utils/lifecycle_mixin.dart

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ abstract mixin class LifecycleMixin implements Disposable {
2020
/// Called when the the object is instantiated in the constructor.
2121
///
2222
/// Any initial setup or resource allocation should be handled here.
23-
@MustCallInConstructor(exempt: [Module, Repo])
23+
@MustCallInConstructor(exempt: [Module, Injectable])
2424
FutureOr<void> initialize();
2525

2626
/// Called when the object is being activated (e.g. after object is created
Lines changed: 299 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,299 @@
1+
# Module-Managed Injectable Lifecycle Spec
2+
3+
Status: Proposed
4+
Owner: grumpy runtime
5+
Target: first-class module lifecycle orchestration for lifecycle-capable Services and Datasources
6+
7+
## 1. Problem Statement
8+
9+
`Module` currently orchestrates lifecycle only for `Repo` instances.
10+
`Service` and `Datasource` registrations are DI-only and are not lifecycle-managed by modules.
11+
12+
Consequences:
13+
14+
- lifecycle-capable service implementations can initialize themselves ad hoc (constructor-triggered `initialize()`), producing inconsistent behavior
15+
- warm module reactivation semantics (`activate` / `deactivate`) are unavailable or fragmented for injectables
16+
- dependency-change propagation (`dependenciesChanged`) is not consistently delivered to lifecycle-capable injectables
17+
- module lifecycle guarantees are asymmetric across runtime building blocks
18+
19+
## 2. Goals
20+
21+
1. Make module lifecycle orchestration consistent across:
22+
- `Repo`
23+
- `Service` implementations that mix in `LifecycleMixin`
24+
- `Datasource` implementations that mix in `LifecycleMixin`
25+
2. Keep API surface simple with no explicit opt-in flag on `Injectable`.
26+
3. Preserve backward compatibility for non-lifecycle services/datasources.
27+
4. Preserve warm lifecycle behavior:
28+
- initialize once per mounted singleton instance
29+
- activate/deactivate on module active-state transitions
30+
5. Keep DI usage and module binding ergonomics unchanged for existing users.
31+
32+
## 3. Non-Goals
33+
34+
1. Lifecycle orchestration for factory-scoped lifecycle injectables.
35+
2. Enforcing lifecycle support on all services/datasources.
36+
3. Replacing module registry behavior or route-driven activation model.
37+
4. Reworking `Repo` lifecycle design.
38+
39+
## 4. Decision Summary
40+
41+
Adopt **automatic lifecycle management by runtime type detection**:
42+
43+
- if a bound injectable instance `is LifecycleMixin`, module lifecycle manages it
44+
- if not, it remains plain DI-only
45+
46+
No new explicit property like `managedByModuleLifecycle` is introduced.
47+
48+
Safety rule:
49+
50+
- lifecycle-capable injectables must be singleton-registered (`singelton == true`)
51+
- attempting to bind a lifecycle-capable injectable as factory throws `StateError` during module initialization
52+
53+
Rationale:
54+
55+
- avoids API clutter and user error from manual flags
56+
- aligns behavior with existing runtime capabilities already present in concrete services
57+
- preserves simple default for non-lifecycle injectables
58+
59+
## 5. Scope of Change
60+
61+
Primary file:
62+
63+
- `lib/src/module.dart`
64+
65+
Secondary behavior/migration files:
66+
67+
- `lib/src/infra/services/routing_kit_routing_service.dart`
68+
- `lib/src/infra/services/canonical_module_registry_service.dart`
69+
- tests in `test/module_test.dart`
70+
- docs in `README.md` (module lifecycle section)
71+
72+
No required changes to:
73+
74+
- `lib/src/domain/domain.dart` injectable contract
75+
76+
## 6. Lifecycle Model
77+
78+
### 6.1 Definitions
79+
80+
Lifecycle-capable injectable:
81+
82+
- any `Service` or `Datasource` instance that mixes in `LifecycleMixin`
83+
84+
Module-managed injectable:
85+
86+
- lifecycle-capable injectable bound as singleton and discovered by module binder
87+
88+
### 6.1.1 Readiness Invariant (Critical)
89+
90+
Factory-pattern DI access remains synchronous (`Service()` / `Datasource()` style),
91+
but readiness is asynchronous and owned by module activation.
92+
93+
Invariant:
94+
95+
- concrete implementations that are lifecycle-managed must only be accessed
96+
after their owning module has completed `activate()`
97+
98+
Implications:
99+
100+
- routing/content paths must continue to await module activation before invoking
101+
code that may resolve lifecycle-managed injectables
102+
- preview paths may run before activation only if they do not access
103+
lifecycle-managed injectables
104+
- constructor-triggered `initialize()` is not an alternative readiness path
105+
106+
### 6.2 Lifecycle Phases
107+
108+
For each module-managed injectable instance:
109+
110+
1. `initialize()` is called exactly once (first module activation after creation)
111+
2. `activate()` is called each time containing module activates
112+
3. `deactivate()` is called each time containing module deactivates
113+
4. `dependenciesChanged()` is called when containing module receives dependency-change signal
114+
5. `free()` is invoked by DI scope disposal (existing behavior)
115+
116+
### 6.3 Ordering
117+
118+
Activation order inside module:
119+
120+
1. imported modules activate first (existing behavior)
121+
2. module-managed injectables activate next
122+
3. repos activate last (existing behavior remains)
123+
124+
Deactivation order inside module (reverse of acquisition semantics):
125+
126+
1. repos deactivate first
127+
2. module-managed injectables deactivate next, in reverse activation order
128+
3. imported modules deactivate last (existing behavior)
129+
130+
Dependencies changed order:
131+
132+
1. active module-managed injectables
133+
2. active repos
134+
3. imported modules may receive independently via their own invocations
135+
136+
## 7. Binding and Validation Rules
137+
138+
### 7.1 Detection Rule
139+
140+
During `bindServices` / `bindDatasources` registration path:
141+
142+
- create probe instance via builder (existing path already does this)
143+
- if probe `is LifecycleMixin`, mark type as lifecycle-managed
144+
145+
### 7.2 Scope Rule
146+
147+
If probe is lifecycle-capable and `probe.singelton == false`:
148+
149+
- throw `StateError` with actionable message:
150+
- includes injectable runtime type
151+
- states lifecycle-capable injectables must be singleton
152+
- suggests setting `singelton => true` or removing `LifecycleMixin`
153+
154+
### 7.3 Registration Rule
155+
156+
Existing DI registration style remains:
157+
158+
- singleton injectables use `registerLazySingleton`
159+
- factory injectables use `registerFactory`
160+
161+
Additional module bookkeeping is attached only for lifecycle-capable singleton injectables.
162+
163+
## 8. Module Runtime Bookkeeping
164+
165+
Module adds internal tracking, analogous to repo tracking:
166+
167+
- resolver list for lifecycle-managed injectables (service+datasource)
168+
- active instance list
169+
- active instance set for dedupe
170+
- initialized instance set
171+
172+
Behavior:
173+
174+
- on first resolve of an instance, call `initialize()` once and record initialized
175+
- when module activates, call `activate()` for each resolved managed instance
176+
- when module deactivates, call `deactivate()` reverse order and clear active list/set
177+
- initialized set remains so warm reactivation does not reinitialize
178+
179+
## 9. Migration Requirements for Existing Infra Services
180+
181+
### 9.1 Constructor-Driven Initialization
182+
183+
Lifecycle-capable services currently calling `initialize()` in constructor must stop doing so.
184+
185+
Affected examples:
186+
187+
- `lib/src/infra/services/routing_kit_routing_service.dart`
188+
- `lib/src/infra/services/canonical_module_registry_service.dart`
189+
190+
Required change:
191+
192+
- remove constructor-time `initialize()` invocation
193+
- rely on module lifecycle orchestration exclusively
194+
195+
Reason:
196+
197+
- avoids double initialization and ordering races once module begins lifecycle management
198+
199+
### 9.2 Singleton Confirmation
200+
201+
Lifecycle-capable services must remain singleton (`singelton => true`) either by default or explicit override.
202+
203+
## 10. Error Handling and Edge Cases
204+
205+
1. Duplicate resolution:
206+
- dedupe by object identity in active set
207+
2. Async lifecycle failures:
208+
- activation/deactivation should fail fast and bubble errors to caller
209+
3. Re-entrant activate/deactivate:
210+
- honor existing module `_isActive` guard behavior
211+
4. Import interactions:
212+
- imported modules manage their own injectables independently
213+
5. Disposal:
214+
- lifecycle-managed injectables still disposed by DI scope through existing disposable hooks
215+
216+
## 11. Testing Plan
217+
218+
### 11.1 New/Updated Unit Tests (`test/module_test.dart`)
219+
220+
1. lifecycle singleton service is initialized once and activated/deactivated per module cycle
221+
2. lifecycle singleton datasource is initialized once and activated/deactivated per module cycle
222+
3. lifecycle `dependenciesChanged` is propagated to active managed injectables
223+
4. non-lifecycle service/datasource remain unaffected (no lifecycle calls)
224+
5. lifecycle factory service binding throws `StateError`
225+
6. lifecycle factory datasource binding throws `StateError`
226+
7. managed injectables are deactivated before module free and disposed by scope teardown
227+
8. warm reactivation does not call initialize twice
228+
9. lifecycle-managed injectable resolved after `module.activate()` is ready
229+
(initialization+activation completed)
230+
10. accessing lifecycle-managed injectable before activation fails with clear error
231+
(or test-only readiness guard assertion), preventing silent race conditions
232+
11. routing integration: final content callback runs only after required modules
233+
are activated; service access from content path succeeds deterministically
234+
12. routing integration: preview callback executes before activation and must not
235+
depend on lifecycle-managed injectables
236+
237+
### 11.2 Regression Coverage
238+
239+
Ensure existing tests still pass for:
240+
241+
- repo activation/deactivation warm behavior
242+
- import module mount/dispose behavior
243+
- singleton/factory identity behavior for non-lifecycle injectables
244+
- module registry serialized activation (`sync`) remains the single readiness gate
245+
246+
## 12. Documentation Updates
247+
248+
Update `README.md`:
249+
250+
- clarify that services/datasources may mix in `LifecycleMixin`
251+
- specify automatic module management rule (`is LifecycleMixin`)
252+
- specify singleton requirement for lifecycle-capable injectables
253+
- note that constructor-triggered `initialize()` should not be used for module-managed injectables
254+
255+
## 13. Rollout Plan
256+
257+
1. Implement module lifecycle bookkeeping and validation.
258+
2. Remove constructor-time `initialize()` from lifecycle-capable infra services.
259+
3. Add/adjust tests.
260+
4. Update README and changelog.
261+
5. Run full test suite.
262+
263+
## 14. Compatibility and Risk Assessment
264+
265+
Compatibility:
266+
267+
- non-lifecycle injectables: fully backward compatible
268+
- lifecycle singleton injectables: behavior becomes deterministic and module-driven
269+
- lifecycle factory injectables: now hard-fail at bind time (intentional safety break)
270+
271+
Primary risks:
272+
273+
1. Existing projects with lifecycle factory injectables will fail at initialization.
274+
2. Implementations relying on constructor-side initialize side effects may observe ordering changes.
275+
276+
Mitigations:
277+
278+
- explicit error messages with migration guidance
279+
- release note callout and examples
280+
281+
## 15. Open Questions
282+
283+
1. Should `dependenciesChanged()` be invoked only when module is active, or always?
284+
- Proposed: active only (consistent with active runtime graph)
285+
2. Should managed injectable activation occur before or after repos?
286+
- Proposed: before repos, so repos can rely on active service dependencies
287+
3. Should lifecycle-managed injectable sets be exposed for diagnostics?
288+
- Proposed: no public API in v1; keep internal until observability need is demonstrated
289+
290+
## 16. Acceptance Criteria
291+
292+
1. Module lifecycle automatically manages lifecycle-capable singleton services/datasources.
293+
2. No explicit `Injectable` flag is required for lifecycle management.
294+
3. Lifecycle-capable factory bindings fail fast with clear errors.
295+
4. Infra lifecycle services do not self-initialize in constructors.
296+
5. All lifecycle/module tests pass with new coverage.
297+
6. README reflects finalized behavior.
298+
7. The readiness invariant is enforced: lifecycle-managed concrete implementations
299+
are not considered safe for access before owning module activation completes.

0 commit comments

Comments
 (0)