@playfast/reform 1.0.1 → 1.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (99) hide show
  1. package/README.md +103 -56
  2. package/package.json +17 -17
  3. package/src/boundary/boundary.test.ts +63 -49
  4. package/src/boundary/boundary.ts +144 -113
  5. package/src/calc/asyncCalc.invalidate.test.ts +518 -32
  6. package/src/calc/asyncCalc.test.ts +207 -213
  7. package/src/calc/asyncCalc.ts +131 -154
  8. package/src/calc/asyncData.ts +0 -37
  9. package/src/calc/calc.test.ts +7 -33
  10. package/src/calc/calc.ts +44 -58
  11. package/src/calc/calcFamily.test.ts +80 -34
  12. package/src/calc/calcFamily.ts +54 -65
  13. package/src/calc/compose.test.ts +1 -12
  14. package/src/calc/compose.ts +1 -36
  15. package/src/calc/queryState.ts +0 -23
  16. package/src/channel/channel.ts +126 -111
  17. package/src/compose/composition.test.ts +19 -0
  18. package/src/compose/composition.ts +351 -127
  19. package/src/compose/host.ts +0 -6
  20. package/src/compose/props.ts +13 -12
  21. package/src/compose/provide.ts +189 -62
  22. package/src/compose/slot.ts +158 -49
  23. package/src/compose/structure.test.ts +6 -9
  24. package/src/compose/structure.ts +108 -79
  25. package/src/compose/ui.test.ts +19 -7
  26. package/src/compose/ui.ts +155 -156
  27. package/src/compose/ui.typecheck.ts +40 -18
  28. package/src/definition/definition.ts +22 -41
  29. package/src/event/event.fromSource.test.ts +172 -0
  30. package/src/event/event.test.ts +10 -3
  31. package/src/event/event.ts +102 -27
  32. package/src/event/eventGroup.ts +0 -1
  33. package/src/feature/feature.mount.test.ts +122 -43
  34. package/src/feature/feature.test.ts +46 -21
  35. package/src/feature/feature.ts +1347 -256
  36. package/src/feature/feature.typecheck.ts +273 -68
  37. package/src/graph/closure.ts +403 -0
  38. package/src/index.ts +68 -126
  39. package/src/internal/bucketCache.ts +39 -0
  40. package/src/internal/capture.ts +9 -24
  41. package/src/internal/env.ts +7 -0
  42. package/src/internal/errors.test.ts +0 -6
  43. package/src/internal/errors.ts +37 -60
  44. package/src/internal/inspect.test.ts +0 -6
  45. package/src/internal/inspect.ts +0 -12
  46. package/src/internal/queryDriver.ts +105 -201
  47. package/src/internal/queryDriverStore.ts +156 -0
  48. package/src/internal/queryDriverTypes.ts +59 -0
  49. package/src/internal/queryEvents.ts +0 -12
  50. package/src/internal/queryStore.ts +0 -14
  51. package/src/internal/reuse.test.ts +10 -14
  52. package/src/internal/reuse.ts +5 -30
  53. package/src/internal/scheduler.ts +4 -44
  54. package/src/internal/seeds.ts +0 -14
  55. package/src/internal/sources.ts +26 -49
  56. package/src/internal/stateRegistry.ts +0 -14
  57. package/src/internal/store.test.ts +1 -3
  58. package/src/internal/store.ts +0 -37
  59. package/src/internal/track.ts +3 -20
  60. package/src/internal/variance.ts +5 -0
  61. package/src/internal.ts +222 -0
  62. package/src/namespace/namespace.test.ts +46 -0
  63. package/src/namespace/namespace.ts +106 -0
  64. package/src/procedure/procedure.ts +40 -35
  65. package/src/reducer/reducer.ts +78 -52
  66. package/src/remote/remoteState.test.ts +590 -397
  67. package/src/remote/remoteState.ts +425 -347
  68. package/src/remote/remoteState.typecheck.ts +47 -56
  69. package/src/runtime/appRuntime.activation.test.ts +100 -0
  70. package/src/runtime/appRuntime.test.ts +249 -0
  71. package/src/runtime/appRuntime.ts +415 -97
  72. package/src/runtime/bus.ts +2 -15
  73. package/src/runtime/eventBudget.test.ts +152 -0
  74. package/src/runtime/eventBudget.ts +120 -0
  75. package/src/runtime/hardening.test.ts +73 -13
  76. package/src/runtime/instrumentation.test.ts +55 -52
  77. package/src/runtime/instrumentation.ts +6 -60
  78. package/src/runtime/loop.test.ts +36 -17
  79. package/src/runtime/loop.ts +87 -88
  80. package/src/runtime/queries.ts +0 -17
  81. package/src/scene/featureScene.test.ts +61 -0
  82. package/src/scene/scene.ts +247 -104
  83. package/src/scene/seedScene.test.ts +42 -79
  84. package/src/state/state.nominal.typecheck.ts +68 -0
  85. package/src/state/state.test.ts +17 -0
  86. package/src/state/state.ts +80 -46
  87. package/src/state/stateFamily.test.ts +16 -11
  88. package/src/state/stateFamily.ts +55 -65
  89. package/src/state/stateGroup.ts +53 -64
  90. package/src/state/token.ts +40 -23
  91. package/src/synced/syncedStore.ts +46 -58
  92. package/src/testkit/flight.testkit.ts +75 -0
  93. package/src/ui/node.ts +0 -6
  94. package/src/ui/trigger.ts +0 -5
  95. package/src/wire/tree.test.ts +8 -7
  96. package/src/wire/tree.ts +0 -39
  97. package/src/wire/triggers.test.ts +6 -6
  98. package/src/wire/triggers.ts +14 -32
  99. package/src/internal/ctx.ts +0 -16
@@ -1,94 +1,227 @@
1
- import { type Context, Effect, Layer, Option, type Scope } from 'effect'
2
- import { type Engine } from '../runtime/loop'
3
- import { type StoresOf } from '../internal/sources'
4
- import { FeatureLoadFailed } from '../internal/errors'
5
- import { type Manifest, definitionClass } from '../definition/definition'
6
- import { type CompositionClass } from '../compose/composition'
1
+ import { Context, Effect, Layer, Option, Schema, type Scope } from 'effect'
2
+ import {
3
+ type AnyComposition,
4
+ CompositionIdTypeId,
5
+ type CompositionClass,
6
+ type HasExactCompositionId,
7
+ type CompositionPropsReceiver,
8
+ } from '../compose/composition'
7
9
  import { type UiContract } from '../compose/ui'
8
- import { publish, type Tagged } from '../runtime/bus'
9
-
10
- // A `Feature` is the unit of lazy code-split, ownership, reflection, and proof.
11
- // It splits on the seam Reform already has: definitions (manifests, schemas, the
12
- // composition class, placeholders) stay eager and reflectable; the heavy `.live`
13
- // layer loads on demand. The whole-app *shape* stays inspectable with no chunk
14
- // loaded. Everything here is additive — `provide(slot, Composition)` and eager
15
- // scenes are unchanged. See `LAZY.md` for the design rationale and variance proof.
16
-
17
- /**
18
- * The always-on engine services a feature builds against: the shared event `Bus`
19
- * plus the reducer/channel/procedure registries. Derived from the `Engine`
20
- * layer's output (`runtime/loop.ts`) so it never drifts. A feature layer may name
21
- * these only as OPEN requirements (its `RIn`) and must never *provide* them — that
22
- * is what keeps the engine single and shared across every lazily-mounted feature.
23
- */
24
- export type EngineServices = Layer.Layer.Success<typeof Engine>
25
-
26
- /**
27
- * A shared-state carrier a feature may declare in `requires`: a `State`,
28
- * `StateFamily`, `Calc`, or `AsyncCalc` (each carrying a `store` tag), or a
29
- * `StateGroup` (carrying `members`, each with its own `store`). Per the design,
30
- * a feature depends on shared state/calc only — never on another feature.
31
- */
10
+ import { type Manifest, definitionClass } from '../definition/definition'
11
+ import {
12
+ type AnyFeatureGraphSummary,
13
+ type FeatureGraphSummary,
14
+ FeatureGraphSummaryTypeId,
15
+ FeatureGraphValidTypeId,
16
+ FeatureChildSummaryTypeId,
17
+ FeatureModuleGraphValidTypeId,
18
+ type FeatureSummaryCompositionIdentity,
19
+ type FeatureSummaryCompositionProps,
20
+ type FeatureSummaryContract,
21
+ type FeatureSummaryInput,
22
+ type FeatureSummaryName,
23
+ type FeatureSummaryOpenServices,
24
+ type FeatureSummaryStates,
25
+ type FeatureSummarySupportOpenServices,
26
+ type FeatureSummaryUsesInput,
27
+ type ModuleOpenServices,
28
+ type OpenServicesFromFeatureSlotWitnesses,
29
+ SlotBindingSummaryTypeId,
30
+ type ValidateFeatureGraphLayer,
31
+ } from '../graph/closure'
32
+ import { FeatureLoadFailed } from '../internal/errors'
33
+ import { Bus, publish, type Tagged } from '../runtime/bus'
34
+ import { type EngineServices } from '../runtime/loop'
35
+
36
+ export type { EngineServices } from '../runtime/loop'
37
+ export { FeatureGraphValidTypeId }
38
+
39
+ const FeatureInputBase: Context.TagClass<FeatureInput, 'reform/FeatureInput', unknown> =
40
+ Context.Tag('reform/FeatureInput')<FeatureInput, unknown>()
41
+ export class FeatureInput extends FeatureInputBase {}
42
+
43
+ // Context.Tag is invariant, so erased requirements use its structural identity carrier.
44
+ interface FeatureServiceTag<out Identifier = unknown, out Service = unknown> {
45
+ readonly [Context.TagTypeId]: object
46
+ readonly Identifier: Identifier
47
+ readonly Service: Service
48
+ }
49
+
50
+ interface StoredFeatureRequirement<out Identifier = unknown, out Service = unknown> {
51
+ readonly store: FeatureServiceTag<Identifier, Service>
52
+ }
53
+
32
54
  export type RequireCarrier =
33
- | { readonly store: Context.Tag<any, any> }
34
- | { readonly members: ReadonlyArray<{ readonly store: Context.Tag<any, any> }> }
35
-
36
- /**
37
- * The Context services a `requires` tuple makes available to a feature layer: a
38
- * `StateGroup` contributes its members' stores (the shared `StoresOf`); every
39
- * other carrier contributes its own `store` service. This is the upper bound a
40
- * feature's open `RIn` is checked against. Because `Layer`'s `RIn` is covariant
41
- * (`Layer.d.ts` — `out RIn`), a layer that reads anything outside
42
- * `EngineServices | ProvidedBy<requires>` is simply not assignable, so the check
43
- * is ordinary assignability — no `as`, no `infer` gymnastics in user code.
44
- */
45
- export type ProvidedBy<Requires extends ReadonlyArray<RequireCarrier>> = {
46
- [I in keyof Requires]: Requires[I] extends {
47
- readonly members: infer M extends ReadonlyArray<unknown>
55
+ | StoredFeatureRequirement
56
+ | {
57
+ readonly members: ReadonlyArray<StoredFeatureRequirement>
58
+ }
59
+
60
+ export type FeatureRequirement = RequireCarrier | FeatureServiceTag
61
+
62
+ type TagRequirement<Tag> =
63
+ Tag extends FeatureServiceTag<infer Identifier, infer _Service> ? Identifier : never
64
+
65
+ type StoredRequirementIdentifier<Carrier> = Carrier extends {
66
+ readonly store: infer Tag
67
+ }
68
+ ? TagRequirement<Tag>
69
+ : never
70
+
71
+ type RequirementIdentifier<Requirement> = Requirement extends FeatureServiceTag
72
+ ? TagRequirement<Requirement>
73
+ : Requirement extends {
74
+ readonly members: infer Members extends ReadonlyArray<StoredFeatureRequirement>
75
+ }
76
+ ? StoredRequirementIdentifier<Members[number]>
77
+ : StoredRequirementIdentifier<Requirement>
78
+
79
+ export type ProvidedBy<Requires extends ReadonlyArray<FeatureRequirement>> = RequirementIdentifier<
80
+ Requires[number]
81
+ >
82
+
83
+ type DefaultFeatureOpenServices<ROut, DirectRequires extends ReadonlyArray<FeatureRequirement>> =
84
+ | ProvidedBy<DirectRequires>
85
+ | OpenServicesFromFeatureSlotWitnesses<ROut>
86
+
87
+ type CompositionNames<ROut> = ROut extends {
88
+ readonly [CompositionIdTypeId]: {
89
+ readonly name: infer Name extends string
90
+ }
91
+ }
92
+ ? Name
93
+ : never
94
+
95
+ type CompositionIdentities<ROut> = ROut extends {
96
+ readonly [CompositionIdTypeId]: {
97
+ readonly identity: infer Identity
48
98
  }
49
- ? StoresOf<M>
50
- : Requires[I] extends { readonly store: Context.Tag<infer Svc, any> }
51
- ? Svc
52
- : never
53
- }[number]
54
-
55
- /**
56
- * A loaded feature's two artifacts. `requires` is the runtime-enumerable list the
57
- * editor reads (types are erased, so the carriers are kept as values); `layer` is
58
- * the heavy `.live` half. The bound on `layer`'s `RIn` —
59
- * `EngineServices | ProvidedBy<Requires>` — makes the runtime list provably equal
60
- * the layer's real requirement set: list too little and the layer no longer type-
61
- * checks here. Built by `featureModule`, which is the one sanctioned way to publish
62
- * a feature's lazy half.
63
- */
64
- export interface FeatureModule<ROut, Requires extends ReadonlyArray<RequireCarrier>> {
65
- readonly requires: Requires
66
- readonly layer: Layer.Layer<ROut, never, EngineServices | ProvidedBy<Requires>>
67
- }
68
-
69
- /**
70
- * Publish a feature's lazy half: pair its `requires` (a runtime tuple of shared
71
- * carriers) with its `layer`. The covariant `RIn` bound does the checking — a
72
- * layer that reaches for any service outside `EngineServices | ProvidedBy<requires>`
73
- * is not assignable, so it fails to compile right here, next to the layer, with no
74
- * cast. Listing an unused carrier is harmless; omitting a needed one is a type error.
75
- *
76
- * The bound also forbids a feature from *providing* `EngineServices` (they may only
77
- * be open `RIn`), structurally enforcing the liveness rule: a feature can never
78
- * bundle its own engine it always shares the root's bus and registries.
79
- */
80
- export const featureModule = <const Requires extends ReadonlyArray<RequireCarrier>, ROut>(
81
- requires: Requires,
82
- layer: Layer.Layer<ROut, never, EngineServices | ProvidedBy<Requires>>,
83
- ): FeatureModule<ROut, Requires> => ({ requires, layer })
84
-
85
- /**
86
- * Wrap a dynamic `import()` of a feature's `.live` module as an `Effect` — Reform
87
- * never exposes a Promise/`await` on the feature surface. The unavoidable thenable
88
- * is sealed here; a rejected import (missing chunk, network failure, module-eval
89
- * throw) surfaces as a typed `FeatureLoadFailed` the host hands to
90
- * `placeholder.failed`. The module's default export is its `FeatureModule`.
91
- */
99
+ }
100
+ ? Identity
101
+ : never
102
+
103
+ type RootCompositionName<ROut> = [CompositionNames<ROut>] extends [never]
104
+ ? string
105
+ : CompositionNames<ROut>
106
+
107
+ type RootCompositionIdentity<ROut> = [CompositionIdentities<ROut>] extends [never]
108
+ ? unknown
109
+ : CompositionIdentities<ROut>
110
+
111
+ // Feature layers may require the shared engine but must never provide it.
112
+ export type FeatureModuleInput<OpenServices, UsesInput extends boolean> =
113
+ | EngineServices
114
+ | OpenServices
115
+ | (UsesInput extends true ? FeatureInput : never)
116
+
117
+ export interface FeatureModule<
118
+ ROut,
119
+ DirectRequires extends ReadonlyArray<FeatureRequirement>,
120
+ OpenServices = DefaultFeatureOpenServices<ROut, DirectRequires>,
121
+ UsesInput extends boolean = false,
122
+ Input = never,
123
+ > extends AnyLoadedModule {
124
+ readonly [FeatureModuleGraphValidTypeId]: FeatureModuleGraphValidTypeId
125
+ readonly requires: DirectRequires
126
+ readonly usesInput: UsesInput
127
+ readonly layer: Layer.Layer<ROut, never, FeatureModuleInput<OpenServices, UsesInput>>
128
+ readonly inputSchema: UsesInput extends true ? Schema.Schema.AnyNoContext : undefined
129
+ readonly Input: (input: Input) => void
130
+ }
131
+
132
+ export type LoadedModuleCapture<Result> = <
133
+ ROut,
134
+ DirectRequires extends ReadonlyArray<FeatureRequirement>,
135
+ OpenServices,
136
+ UsesInput extends boolean,
137
+ Input,
138
+ >(
139
+ module: LoadedModule<ROut, DirectRequires, OpenServices, UsesInput, Input>,
140
+ ) => Result
141
+
142
+ export interface AnyLoadedModule {
143
+ readonly capture: <Result>(visit: LoadedModuleCapture<Result>) => Result
144
+ }
145
+
146
+ type FeatureLayerOutput<L extends Layer.Layer.Any> = Layer.Layer.Success<L>
147
+
148
+ // Reject erased contexts before graph validation can mistake them for permissive inputs.
149
+ type ExactFeatureLayer<L extends Layer.Layer.Any, UsesInput extends boolean> = Layer.Layer<
150
+ FeatureLayerOutput<L>,
151
+ never,
152
+ FeatureModuleInput<ModuleOpenServices<L, UsesInput>, UsesInput>
153
+ > &
154
+ (unknown extends Layer.Layer.Context<L>
155
+ ? {
156
+ readonly featureLayerContextMustBeExact: 'A feature layer context cannot be erased'
157
+ }
158
+ : object)
159
+
160
+ export const featureModule = <
161
+ const DirectRequires extends ReadonlyArray<FeatureRequirement>,
162
+ L extends Layer.Layer.Any,
163
+ >(
164
+ requires: DirectRequires,
165
+ layer: L & ExactFeatureLayer<L, false> & ValidateFeatureGraphLayer<L, DirectRequires, false>,
166
+ ): FeatureModule<
167
+ FeatureLayerOutput<L>,
168
+ DirectRequires,
169
+ ModuleOpenServices<L, false>,
170
+ false,
171
+ never
172
+ > => {
173
+ const module: FeatureModule<
174
+ FeatureLayerOutput<L>,
175
+ DirectRequires,
176
+ ModuleOpenServices<L, false>,
177
+ false,
178
+ never
179
+ > = {
180
+ [FeatureModuleGraphValidTypeId]: FeatureModuleGraphValidTypeId,
181
+ requires,
182
+ usesInput: false,
183
+ layer,
184
+ inputSchema: undefined,
185
+ Input: () => undefined,
186
+ capture: <Result>(visit: LoadedModuleCapture<Result>): Result => visit(module),
187
+ }
188
+ return module
189
+ }
190
+
191
+ export const featureModuleFromInput = <
192
+ const InputSchema extends Schema.Schema.AnyNoContext,
193
+ const DirectRequires extends ReadonlyArray<FeatureRequirement>,
194
+ L extends Layer.Layer.Any,
195
+ >(
196
+ input: InputSchema,
197
+ requires: DirectRequires,
198
+ layer: L & ExactFeatureLayer<L, true> & ValidateFeatureGraphLayer<L, DirectRequires, true>,
199
+ ): FeatureModule<
200
+ FeatureLayerOutput<L>,
201
+ DirectRequires,
202
+ ModuleOpenServices<L, true>,
203
+ true,
204
+ Schema.Schema.Type<InputSchema>
205
+ > => {
206
+ const module: FeatureModule<
207
+ FeatureLayerOutput<L>,
208
+ DirectRequires,
209
+ ModuleOpenServices<L, true>,
210
+ true,
211
+ Schema.Schema.Type<InputSchema>
212
+ > = {
213
+ [FeatureModuleGraphValidTypeId]: FeatureModuleGraphValidTypeId,
214
+ requires,
215
+ usesInput: true,
216
+ layer,
217
+ inputSchema: input,
218
+ Input: () => undefined,
219
+ capture: <Result>(visit: LoadedModuleCapture<Result>): Result => visit(module),
220
+ }
221
+ return module
222
+ }
223
+
224
+ // Seal dynamic imports as typed Effects so chunk failures become FeatureLoadFailed.
92
225
  export const lazyImport = <M>(
93
226
  thunk: () => Promise<{ readonly default: M }>,
94
227
  ): Effect.Effect<M, FeatureLoadFailed> =>
@@ -97,18 +230,11 @@ export const lazyImport = <M>(
97
230
  catch: (cause) => new FeatureLoadFailed({ cause }),
98
231
  }).pipe(Effect.map((module) => module.default))
99
232
 
100
- /** The props the host injects into a lazy feature's `failed` placeholder. */
101
233
  export interface FailedProps {
102
234
  readonly error: FeatureLoadFailed
103
235
  readonly retry: () => void
104
236
  }
105
237
 
106
- /**
107
- * The reflectable shape of a feature: its strategy, the composition it mounts,
108
- * and (when lazy) its placeholder compositions — all as manifests, so the editor
109
- * route-map renders the whole feature graph with no chunk loaded. `boot` is
110
- * runtime data and lives on the `FeatureBinding`, not here.
111
- */
112
238
  export interface FeatureManifest extends Manifest {
113
239
  readonly kind: 'Feature'
114
240
  readonly name: string
@@ -120,201 +246,1166 @@ export interface FeatureManifest extends Manifest {
120
246
  }>
121
247
  }
122
248
 
123
- /**
124
- * A feature's loaded artifacts with type params erased, for the renderer-neutral,
125
- * strategy-uniform host. The erasure is cast-free *into*: because `Layer`'s `ROut`
126
- * is contravariant and `RIn` covariant, every `Layer<ROut, never, R>` is assignable
127
- * to `Layer<never, never, unknown>`, so any `FeatureModule<…>` is a `LoadedModule`.
128
- * The host re-attaches the concrete root context when it builds `layer` (one
129
- * documented boundary cast there, mirroring `buildRuntime`).
130
- */
131
- export interface LoadedModule {
132
- readonly requires: ReadonlyArray<RequireCarrier>
133
- readonly layer: Layer.Layer<never, never, unknown>
134
- }
249
+ export interface LoadedModule<
250
+ ROut,
251
+ DirectRequires extends ReadonlyArray<FeatureRequirement>,
252
+ OpenServices = DefaultFeatureOpenServices<ROut, DirectRequires>,
253
+ UsesInput extends boolean = false,
254
+ Input = never,
255
+ > extends FeatureModule<ROut, DirectRequires, OpenServices, UsesInput, Input> {}
135
256
 
136
- /** Nominal brand identifying the value a `provide(slot, Feature)` puts under a slot tag. */
137
257
  export const FeatureBindingTypeId: unique symbol = Symbol.for('reform/FeatureBinding')
138
258
  export type FeatureBindingTypeId = typeof FeatureBindingTypeId
259
+ export type FeatureBindingType = FeatureBindingTypeId
260
+
261
+ const FeatureTokenIdentityTypeId: unique symbol = Symbol('reform/FeatureTokenIdentity')
262
+ const FeatureNativeTypeId: unique symbol = Symbol('reform/FeatureNative')
139
263
 
140
- /**
141
- * What `provide(slot, Feature)` binds under the slot tag — everything the host
142
- * needs to mount the feature, with types erased (the host is renderer-neutral and
143
- * strategy-uniform). For `default` the host renders `composition` directly; for
144
- * `lazy` it drives `load` through an `AsyncData` lifecycle, painting `placeholder`
145
- * around it. The `composition`/`placeholder` here are the same `CompositionClass`
146
- * values a normal slot fill carries, so the host's existing `Compose` path renders
147
- * each.
148
- */
149
- export interface FeatureBinding {
264
+ export interface FeatureIdentityToken<out FeatureIdentity> {
265
+ readonly identity: symbol
266
+ readonly [FeatureTokenIdentityTypeId]: readonly [] | readonly [FeatureIdentity]
267
+ }
268
+
269
+ export interface FeatureToken<
270
+ out FeatureIdentity,
271
+ out NativeTree extends object,
272
+ > extends FeatureIdentityToken<FeatureIdentity> {
273
+ readonly [FeatureNativeTypeId]: NativeTree
274
+ }
275
+
276
+ export interface FeatureTreeCarrier {
277
+ readonly token: FeatureToken<unknown, object>
278
+ }
279
+
280
+ type Native<
281
+ Feature extends FeatureTreeCarrier,
282
+ Token extends Feature['token'] = Feature['token'],
283
+ > = Token[typeof FeatureNativeTypeId]
284
+
285
+ export const tree = <
286
+ Feature extends FeatureTreeCarrier,
287
+ Token extends Feature['token'] = Feature['token'],
288
+ >(
289
+ token: Token,
290
+ ): Native<Feature, Token> => token[FeatureNativeTypeId]
291
+
292
+ export interface ProvideableFeature<
293
+ out FeatureIdentity,
294
+ out Summary extends AnyFeatureGraphSummary,
295
+ > {
296
+ new (): {}
297
+ readonly manifest: FeatureManifest
298
+ readonly token: FeatureIdentityToken<FeatureIdentity>
299
+ readonly summary: Summary
300
+ }
301
+
302
+ export interface FeatureDefinition<
303
+ out FeatureIdentity,
304
+ out Summary extends AnyFeatureGraphSummary,
305
+ out NativeTree extends object,
306
+ > extends ProvideableFeature<FeatureIdentity, Summary> {
307
+ readonly token: FeatureToken<FeatureIdentity, NativeTree>
308
+ }
309
+
310
+ export type FeatureOpenServices<Feature extends { readonly summary: AnyFeatureGraphSummary }> =
311
+ FeatureSummaryOpenServices<Feature['summary']>
312
+
313
+ export type FeatureSupportOpenServices<
314
+ Feature extends { readonly summary: AnyFeatureGraphSummary },
315
+ > = FeatureSummarySupportOpenServices<Feature['summary']>
316
+
317
+ export interface AnyFeatureBindingExternalApi {
150
318
  readonly [FeatureBindingTypeId]: FeatureBindingTypeId
319
+ readonly [FeatureGraphValidTypeId]: FeatureGraphValidTypeId
320
+ readonly capture: <Result>(visit: FeatureBindingCapture<Result>) => Result
321
+ }
322
+
323
+ export type AnyFeatureBinding = AnyFeatureBindingExternalApi
324
+
325
+ export interface FeatureBinding<
326
+ Summary extends AnyFeatureGraphSummary,
327
+ ROut,
328
+ DirectRequires extends ReadonlyArray<FeatureRequirement>,
329
+ > extends AnyFeatureBindingExternalApi {
151
330
  readonly manifest: FeatureManifest
152
- readonly composition: CompositionClass<unknown>
331
+ readonly identity: symbol
332
+ readonly summary: Summary
333
+ readonly composition: CompositionClass<
334
+ FeatureSummaryCompositionProps<Summary>,
335
+ FeatureSummaryContract<Summary>,
336
+ FeatureSummaryStates<Summary>,
337
+ FeatureSummaryName<Summary>,
338
+ FeatureSummaryCompositionIdentity<Summary>
339
+ >
153
340
  readonly strategy: 'lazy' | 'default'
154
- readonly load: Effect.Effect<LoadedModule, FeatureLoadFailed>
155
- // oxlint-disable-next-line reform-rules/no-optional-fields -- read at the react host seam via `binding.placeholder === undefined`; an Option here would force a cross-package edit (react host) owned by another agent
156
- readonly placeholder?: {
157
- readonly loading: CompositionClass<unknown>
158
- readonly failed: CompositionClass<unknown>
159
- }
341
+ readonly load: Effect.Effect<
342
+ LoadedModule<
343
+ ROut,
344
+ DirectRequires,
345
+ FeatureSummaryOpenServices<Summary>,
346
+ FeatureSummaryUsesInput<Summary>,
347
+ FeatureSummaryInput<Summary>
348
+ >,
349
+ FeatureLoadFailed
350
+ >
351
+ readonly placeholder: Option.Option<{
352
+ readonly loading: AnyComposition
353
+ readonly failed: AnyComposition
354
+ }>
355
+ readonly parseInput: (input: unknown) => Option.Option<unknown>
160
356
  readonly boot: ReadonlyArray<Tagged>
161
357
  }
162
358
 
163
- /** Whether a slot's bound child is a lazy/eager feature (vs a plain composition). */
164
- export const isFeatureBinding = (candidate: unknown): candidate is FeatureBinding =>
165
- typeof candidate === 'object' && candidate !== null && FeatureBindingTypeId in candidate
359
+ export type CapturedFeatureBinding<
360
+ Summary extends AnyFeatureGraphSummary,
361
+ ROut,
362
+ DirectRequires extends ReadonlyArray<FeatureRequirement>,
363
+ > = FeatureBinding<Summary, ROut, DirectRequires>
364
+
365
+ export type FeatureBindingCapture<Result> = <
366
+ Summary extends AnyFeatureGraphSummary,
367
+ ROut,
368
+ DirectRequires extends ReadonlyArray<FeatureRequirement>,
369
+ >(
370
+ binding: FeatureBinding<Summary, ROut, DirectRequires>,
371
+ ) => Result
372
+
373
+ export const isFeatureBinding = (candidate: unknown): candidate is AnyFeatureBinding =>
374
+ typeof candidate === 'object' &&
375
+ candidate !== null &&
376
+ FeatureBindingTypeId in candidate &&
377
+ FeatureGraphValidTypeId in candidate
378
+
379
+ export interface FeatureNativeTree<
380
+ Summary extends AnyFeatureGraphSummary,
381
+ ROut,
382
+ DirectRequires extends ReadonlyArray<FeatureRequirement>,
383
+ SupportOut,
384
+ SupportDirectRequires extends ReadonlyArray<FeatureRequirement>,
385
+ Composition extends CompositionClass<
386
+ FeatureSummaryCompositionProps<Summary>,
387
+ FeatureSummaryContract<Summary>,
388
+ FeatureSummaryStates<Summary>,
389
+ FeatureSummaryName<Summary>,
390
+ FeatureSummaryCompositionIdentity<Summary>
391
+ > = CompositionClass<
392
+ FeatureSummaryCompositionProps<Summary>,
393
+ FeatureSummaryContract<Summary>,
394
+ FeatureSummaryStates<Summary>,
395
+ FeatureSummaryName<Summary>,
396
+ FeatureSummaryCompositionIdentity<Summary>
397
+ >,
398
+ > {
399
+ readonly identity: symbol
400
+ readonly summary: Summary
401
+ readonly composition: Composition
402
+ readonly load: Effect.Effect<
403
+ FeatureModule<
404
+ ROut,
405
+ DirectRequires,
406
+ FeatureSummaryOpenServices<Summary>,
407
+ FeatureSummaryUsesInput<Summary>,
408
+ FeatureSummaryInput<Summary>
409
+ >,
410
+ FeatureLoadFailed
411
+ >
412
+ readonly binding: CapturedFeatureBinding<Summary, ROut, DirectRequires>
413
+ readonly eagerModule:
414
+ | FeatureModule<ROut, DirectRequires, FeatureSummaryOpenServices<Summary>, false, never>
415
+ | undefined
416
+ readonly supportModule:
417
+ | FeatureModule<
418
+ SupportOut,
419
+ SupportDirectRequires,
420
+ FeatureSummarySupportOpenServices<Summary>,
421
+ false,
422
+ never
423
+ >
424
+ | undefined
425
+ }
426
+
427
+ export interface EagerFeatureNativeTree<
428
+ Summary extends AnyFeatureGraphSummary,
429
+ ROut,
430
+ DirectRequires extends ReadonlyArray<FeatureRequirement>,
431
+ Composition extends CompositionClass<
432
+ FeatureSummaryCompositionProps<Summary>,
433
+ FeatureSummaryContract<Summary>,
434
+ FeatureSummaryStates<Summary>,
435
+ FeatureSummaryName<Summary>,
436
+ FeatureSummaryCompositionIdentity<Summary>
437
+ > = CompositionClass<
438
+ FeatureSummaryCompositionProps<Summary>,
439
+ FeatureSummaryContract<Summary>,
440
+ FeatureSummaryStates<Summary>,
441
+ FeatureSummaryName<Summary>,
442
+ FeatureSummaryCompositionIdentity<Summary>
443
+ >,
444
+ > extends FeatureNativeTree<Summary, ROut, DirectRequires, never, readonly [], Composition> {
445
+ readonly eagerModule: FeatureModule<
446
+ ROut,
447
+ DirectRequires,
448
+ FeatureSummaryOpenServices<Summary>,
449
+ false,
450
+ never
451
+ >
452
+ readonly supportModule: undefined
453
+ }
454
+
455
+ export interface LazyFeatureNativeTree<
456
+ Summary extends AnyFeatureGraphSummary,
457
+ ROut,
458
+ DirectRequires extends ReadonlyArray<FeatureRequirement>,
459
+ SupportOut,
460
+ SupportDirectRequires extends ReadonlyArray<FeatureRequirement>,
461
+ Composition extends CompositionClass<
462
+ FeatureSummaryCompositionProps<Summary>,
463
+ FeatureSummaryContract<Summary>,
464
+ FeatureSummaryStates<Summary>,
465
+ FeatureSummaryName<Summary>,
466
+ FeatureSummaryCompositionIdentity<Summary>
467
+ > = CompositionClass<
468
+ FeatureSummaryCompositionProps<Summary>,
469
+ FeatureSummaryContract<Summary>,
470
+ FeatureSummaryStates<Summary>,
471
+ FeatureSummaryName<Summary>,
472
+ FeatureSummaryCompositionIdentity<Summary>
473
+ >,
474
+ > extends FeatureNativeTree<
475
+ Summary,
476
+ ROut,
477
+ DirectRequires,
478
+ SupportOut,
479
+ SupportDirectRequires,
480
+ Composition
481
+ > {
482
+ readonly eagerModule: undefined
483
+ }
484
+
485
+ type FeatureSummaryOf<
486
+ FeatureIdentity,
487
+ MountProps,
488
+ CompositionProps,
489
+ C extends UiContract,
490
+ S extends ReadonlyArray<unknown>,
491
+ N extends string,
492
+ Identity,
493
+ OpenServices,
494
+ SupportOpenServices,
495
+ UsesInput extends boolean,
496
+ Input,
497
+ > = FeatureGraphSummary<
498
+ FeatureIdentity,
499
+ MountProps,
500
+ CompositionProps,
501
+ C,
502
+ S,
503
+ N,
504
+ Identity,
505
+ OpenServices,
506
+ SupportOpenServices,
507
+ UsesInput,
508
+ Input
509
+ >
166
510
 
167
- /**
168
- * A feature *definition*: a reflectable manifest (`kind: 'Feature'`) that carries
169
- * the eager half (composition + placeholders) and an Effect `load` for the heavy
170
- * `.live` half. `Props`/`Contract` flow from the composition; `Requires` is the
171
- * shared-carrier tuple the loaded module declares. `load` is an `Effect`, never a
172
- * Promise — consumers `yield* Feature.load`.
173
- */
174
511
  export interface FeatureClass<
512
+ FeatureIdentity,
175
513
  P,
176
514
  C extends UiContract,
177
515
  ROut,
178
- Requires extends ReadonlyArray<RequireCarrier>,
179
- > {
516
+ DirectRequires extends ReadonlyArray<FeatureRequirement>,
517
+ OpenServices = DefaultFeatureOpenServices<ROut, DirectRequires>,
518
+ SupportOut = never,
519
+ SupportDirectRequires extends ReadonlyArray<FeatureRequirement> = readonly [],
520
+ SupportOpenServices = DefaultFeatureOpenServices<SupportOut, SupportDirectRequires>,
521
+ UsesInput extends boolean = false,
522
+ Input = UsesInput extends true ? P : never,
523
+ S extends ReadonlyArray<unknown> = ReadonlyArray<unknown>,
524
+ N extends string = RootCompositionName<ROut>,
525
+ CompositionProps = P,
526
+ Identity = RootCompositionIdentity<ROut>,
527
+ Composition extends CompositionClass<CompositionProps, C, S, N, Identity> = CompositionClass<
528
+ CompositionProps,
529
+ C,
530
+ S,
531
+ N,
532
+ Identity
533
+ >,
534
+ > extends FeatureDefinition<
535
+ FeatureIdentity,
536
+ FeatureSummaryOf<
537
+ FeatureIdentity,
538
+ P,
539
+ CompositionProps,
540
+ C,
541
+ S,
542
+ N,
543
+ Identity,
544
+ OpenServices,
545
+ SupportOpenServices,
546
+ UsesInput,
547
+ Input
548
+ >,
549
+ FeatureNativeTree<
550
+ FeatureSummaryOf<
551
+ FeatureIdentity,
552
+ P,
553
+ CompositionProps,
554
+ C,
555
+ S,
556
+ N,
557
+ Identity,
558
+ OpenServices,
559
+ SupportOpenServices,
560
+ UsesInput,
561
+ Input
562
+ >,
563
+ ROut,
564
+ DirectRequires,
565
+ SupportOut,
566
+ SupportDirectRequires,
567
+ Composition
568
+ >
569
+ > {}
570
+
571
+ export type EagerFeatureClass<
572
+ FeatureIdentity,
573
+ P,
574
+ C extends UiContract,
575
+ ROut,
576
+ DirectRequires extends ReadonlyArray<FeatureRequirement>,
577
+ OpenServices,
578
+ S extends ReadonlyArray<unknown>,
579
+ N extends string,
580
+ Identity,
581
+ Composition extends CompositionClass<P, C, S, N, Identity> = CompositionClass<
582
+ P,
583
+ C,
584
+ S,
585
+ N,
586
+ Identity
587
+ >,
588
+ > = FeatureDefinition<
589
+ FeatureIdentity,
590
+ FeatureSummaryOf<FeatureIdentity, P, P, C, S, N, Identity, OpenServices, never, false, never>,
591
+ EagerFeatureNativeTree<
592
+ FeatureSummaryOf<FeatureIdentity, P, P, C, S, N, Identity, OpenServices, never, false, never>,
593
+ ROut,
594
+ DirectRequires,
595
+ Composition
596
+ >
597
+ >
598
+
599
+ export type LazyFeatureClass<
600
+ FeatureIdentity,
601
+ P,
602
+ C extends UiContract,
603
+ ROut,
604
+ DirectRequires extends ReadonlyArray<FeatureRequirement>,
605
+ OpenServices,
606
+ SupportOut,
607
+ SupportDirectRequires extends ReadonlyArray<FeatureRequirement>,
608
+ SupportOpenServices,
609
+ UsesInput extends boolean,
610
+ S extends ReadonlyArray<unknown>,
611
+ N extends string,
612
+ CompositionProps,
613
+ Identity,
614
+ Composition extends CompositionClass<CompositionProps, C, S, N, Identity> = CompositionClass<
615
+ CompositionProps,
616
+ C,
617
+ S,
618
+ N,
619
+ Identity
620
+ >,
621
+ > = FeatureDefinition<
622
+ FeatureIdentity,
623
+ FeatureSummaryOf<
624
+ FeatureIdentity,
625
+ P,
626
+ CompositionProps,
627
+ C,
628
+ S,
629
+ N,
630
+ Identity,
631
+ OpenServices,
632
+ SupportOpenServices,
633
+ UsesInput,
634
+ UsesInput extends true ? P : never
635
+ >,
636
+ LazyFeatureNativeTree<
637
+ FeatureSummaryOf<
638
+ FeatureIdentity,
639
+ P,
640
+ CompositionProps,
641
+ C,
642
+ S,
643
+ N,
644
+ Identity,
645
+ OpenServices,
646
+ SupportOpenServices,
647
+ UsesInput,
648
+ UsesInput extends true ? P : never
649
+ >,
650
+ ROut,
651
+ DirectRequires,
652
+ SupportOut,
653
+ SupportDirectRequires,
654
+ Composition
655
+ >
656
+ >
657
+
658
+ interface RuntimeFeatureTree {
659
+ readonly composition: AnyComposition
660
+ readonly load: Effect.Effect<AnyLoadedModule, FeatureLoadFailed>
661
+ readonly binding: AnyFeatureBinding
662
+ readonly eagerModule: AnyLoadedModule | undefined
663
+ readonly supportModule: AnyLoadedModule | undefined
664
+ }
665
+
666
+ interface RuntimeFeature {
180
667
  new (): {}
181
668
  readonly manifest: FeatureManifest
182
- /** Phantom carrying the mounted composition's props type (for `provide` checks). */
183
- readonly Props: P
184
- /** Phantom carrying the composition's UI contract (for typed reflection consumers). */
185
- readonly Contract: C
186
- /** Phantom carrying the declared shared-carrier tuple. */
187
- readonly Requires: Requires
188
- /** Load the feature's module — `yield* Feature.load` → its `FeatureModule`, or `FeatureLoadFailed`. */
189
- readonly load: Effect.Effect<FeatureModule<ROut, Requires>, FeatureLoadFailed>
190
- /** The value `provide(slot, Feature)` binds under the slot tag. */
191
- readonly binding: FeatureBinding
192
- /** The eagerly-available module — present only for `loadingStrategy: 'default'`. */
193
- readonly eagerModule: FeatureModule<ROut, Requires> | undefined
194
- }
195
-
196
- export type AnyFeature = FeatureClass<any, any, any, ReadonlyArray<RequireCarrier>>
197
-
198
- /** Whether a value is a feature definition (its target shape in `provide(slot, Feature)`). */
199
- export const isFeature = (candidate: unknown): candidate is AnyFeature =>
669
+ readonly token: FeatureToken<unknown, RuntimeFeatureTree>
670
+ readonly summary: AnyFeatureGraphSummary
671
+ }
672
+
673
+ const isRuntimeFeatureToken = (
674
+ candidate: unknown,
675
+ ): candidate is FeatureToken<unknown, RuntimeFeatureTree> =>
676
+ typeof candidate === 'object' && candidate !== null && FeatureNativeTypeId in candidate
677
+
678
+ export const isFeature = (candidate: unknown): candidate is RuntimeFeature =>
200
679
  (typeof candidate === 'function' || typeof candidate === 'object') &&
201
680
  candidate !== null &&
202
- 'manifest' in candidate &&
203
- typeof candidate.manifest === 'object' &&
204
- candidate.manifest !== null &&
205
- 'kind' in candidate.manifest &&
206
- candidate.manifest.kind === 'Feature'
207
-
208
- /**
209
- * The placeholders a lazy feature must ship — both eager compositions (their
210
- * `.live` is in the eager bundle, so they paint the load gap). `loading` receives
211
- * the feature's props; `failed` receives `{ error, retry }`. Typed loosely
212
- * (`CompositionClass<any>`) because the host injects these props directly — they
213
- * are not schema-decoded inputs.
214
- */
215
- export interface Placeholders {
216
- readonly loading: CompositionClass<any, UiContract>
217
- readonly failed: CompositionClass<any, UiContract>
218
- }
219
-
220
- type FeatureConfigExternalApi<
681
+ 'token' in candidate &&
682
+ isRuntimeFeatureToken(candidate.token)
683
+
684
+ type PlaceholderComposition<P> = AnyComposition & CompositionPropsReceiver<P>
685
+
686
+ export interface Placeholders<P> {
687
+ readonly loading: PlaceholderComposition<P>
688
+ readonly failed: PlaceholderComposition<FailedProps>
689
+ }
690
+
691
+ export type FeatureMountProps<
692
+ UsesInput extends boolean,
693
+ CompositionProps,
694
+ InputSchema extends Schema.Schema.AnyNoContext,
695
+ > = UsesInput extends true ? Schema.Schema.Type<InputSchema> : CompositionProps
696
+
697
+ type FeatureUsesInput<InputSchema extends Schema.Schema.AnyNoContext> = [InputSchema] extends [
698
+ never,
699
+ ]
700
+ ? false
701
+ : true
702
+
703
+ type FeatureInputValue<InputSchema extends Schema.Schema.AnyNoContext> = [InputSchema] extends [
704
+ never,
705
+ ]
706
+ ? never
707
+ : Schema.Schema.Type<InputSchema>
708
+
709
+ interface FeatureConfigBaseExternalApi<
221
710
  P,
222
711
  C extends UiContract,
223
- ROut,
224
- Requires extends ReadonlyArray<RequireCarrier>,
225
- > = {
226
- readonly composition: CompositionClass<P, C>
712
+ S extends ReadonlyArray<unknown>,
713
+ N extends string,
714
+ Identity,
715
+ > {
716
+ readonly composition: CompositionClass<P, C, S, N, Identity>
227
717
  readonly boot?: ReadonlyArray<Tagged>
228
- } & (
718
+ }
719
+
720
+ interface ExactFeatureCompositionConfig<Composition extends AnyComposition> {
721
+ readonly composition: Composition
722
+ }
723
+
724
+ type IncludesComposition<ROut, N extends string, Identity> =
725
+ HasExactCompositionId<ROut, N, Identity> extends true
726
+ ? object
727
+ : {
728
+ readonly missingCompositionService: 'The feature module must provide its root composition'
729
+ }
730
+
731
+ type EagerFeatureConfigExternalApi<
732
+ P,
733
+ C extends UiContract,
734
+ ROut,
735
+ DirectRequires extends ReadonlyArray<FeatureRequirement>,
736
+ OpenServices,
737
+ S extends ReadonlyArray<unknown>,
738
+ N extends string,
739
+ Identity,
740
+ > = FeatureConfigBaseExternalApi<P, C, S, N, Identity> & {
741
+ readonly loadingStrategy?: 'default'
742
+ readonly module: FeatureModule<ROut, DirectRequires, OpenServices, false, never> &
743
+ IncludesComposition<ROut, N, Identity>
744
+ }
745
+
746
+ type LazyFeatureConfigExternalApi<
747
+ CompositionProps,
748
+ C extends UiContract,
749
+ ROut,
750
+ DirectRequires extends ReadonlyArray<FeatureRequirement>,
751
+ OpenServices,
752
+ SupportOut,
753
+ SupportDirectRequires extends ReadonlyArray<FeatureRequirement>,
754
+ SupportOpenServices,
755
+ S extends ReadonlyArray<unknown>,
756
+ N extends string,
757
+ Identity,
758
+ InputSchema extends Schema.Schema.AnyNoContext,
759
+ Loading extends AnyComposition,
760
+ Failed extends AnyComposition,
761
+ > = FeatureConfigBaseExternalApi<CompositionProps, C, S, N, Identity> & {
762
+ readonly composition: CompositionClass<CompositionProps, C, S, N, Identity> &
763
+ CompositionPropsReceiver<
764
+ NoInfer<FeatureMountProps<FeatureUsesInput<InputSchema>, CompositionProps, InputSchema>>
765
+ >
766
+ readonly loadingStrategy: 'lazy'
767
+ readonly input?: InputSchema
768
+ readonly load: Effect.Effect<
769
+ FeatureModule<
770
+ ROut,
771
+ DirectRequires,
772
+ OpenServices,
773
+ FeatureUsesInput<InputSchema>,
774
+ FeatureInputValue<InputSchema>
775
+ > &
776
+ IncludesComposition<ROut, N, Identity>,
777
+ FeatureLoadFailed
778
+ >
779
+ readonly placeholder: {
780
+ readonly loading: Loading &
781
+ CompositionPropsReceiver<
782
+ NoInfer<FeatureMountProps<FeatureUsesInput<InputSchema>, CompositionProps, InputSchema>>
783
+ >
784
+ readonly failed: Failed & CompositionPropsReceiver<FailedProps>
785
+ }
786
+ readonly eager?: FeatureModule<
787
+ SupportOut,
788
+ SupportDirectRequires,
789
+ SupportOpenServices,
790
+ false,
791
+ never
792
+ >
793
+ } & (FeatureUsesInput<InputSchema> extends true ? { readonly input: InputSchema } : object)
794
+
795
+ type MakeImplementationConfigExternalApi =
229
796
  | {
230
- // EAGER: the module is statically present and merged into the runtime at
231
- // boot. No load gap, so no placeholder.
797
+ readonly composition: AnyComposition
798
+ readonly boot?: ReadonlyArray<Tagged>
232
799
  readonly loadingStrategy?: 'default'
233
- readonly module: FeatureModule<ROut, Requires>
800
+ readonly module: AnyLoadedModule
234
801
  }
235
802
  | {
236
- // LAZY: the module loads on mount via an Effect. Placeholders required.
803
+ readonly composition: AnyComposition
804
+ readonly boot?: ReadonlyArray<Tagged>
237
805
  readonly loadingStrategy: 'lazy'
238
- readonly load: Effect.Effect<FeatureModule<ROut, Requires>, FeatureLoadFailed>
239
- readonly placeholder: Placeholders
806
+ readonly input?: Schema.Schema.Any
807
+ readonly load: Effect.Effect<AnyLoadedModule, FeatureLoadFailed>
808
+ readonly placeholder: {
809
+ readonly loading: AnyComposition
810
+ readonly failed: AnyComposition
811
+ }
812
+ readonly eager?: AnyLoadedModule
240
813
  }
241
- )
242
-
243
- /**
244
- * Define a feature. The whole-app shape (this manifest, the composition, the
245
- * placeholders) stays eager and reflectable; the `.live` layer in `module`/`load`
246
- * is the deferred chunk. Flip `loadingStrategy` from `'default'` to `'lazy'` and
247
- * the discriminated config *requires* `placeholder` — the compiler walks you to a
248
- * complete code-split feature.
249
- */
250
- export const make = <P, C extends UiContract, ROut, Requires extends ReadonlyArray<RequireCarrier>>(
814
+
815
+ const isMakeImplementationConfig = (
816
+ candidate: unknown,
817
+ ): candidate is MakeImplementationConfigExternalApi =>
818
+ typeof candidate === 'object' &&
819
+ candidate !== null &&
820
+ 'composition' in candidate &&
821
+ (!('input' in candidate) || Schema.isSchema(candidate.input)) &&
822
+ ('module' in candidate || ('load' in candidate && 'placeholder' in candidate))
823
+
824
+ type RuntimePlaceholders = {
825
+ readonly loading: AnyComposition
826
+ readonly failed: AnyComposition
827
+ }
828
+
829
+ const makeFeatureManifest = (
251
830
  name: string,
252
- config: FeatureConfigExternalApi<P, C, ROut, Requires>,
253
- ): FeatureClass<P, C, ROut, Requires> => {
254
- const strategy: 'lazy' | 'default' = config.loadingStrategy ?? 'default'
255
- const load: Effect.Effect<FeatureModule<ROut, Requires>, FeatureLoadFailed> =
256
- 'load' in config ? config.load : Effect.succeed(config.module)
257
- const eagerModule = 'module' in config ? config.module : undefined
258
- const placeholder = 'placeholder' in config ? config.placeholder : undefined
259
-
260
- const manifest: FeatureManifest = {
261
- kind: 'Feature',
262
- name,
263
- strategy,
264
- composition: config.composition.manifest,
265
- placeholder: Option.fromNullable(placeholder).pipe(
266
- Option.map((placeholders) => ({
267
- loading: placeholders.loading.manifest,
268
- failed: placeholders.failed.manifest,
269
- })),
270
- ),
271
- }
831
+ strategy: 'lazy' | 'default',
832
+ composition: AnyComposition,
833
+ placeholder: Option.Option<RuntimePlaceholders>,
834
+ ): FeatureManifest => ({
835
+ kind: 'Feature',
836
+ name,
837
+ strategy,
838
+ composition: composition.manifest,
839
+ placeholder: Option.map(placeholder, (placeholders) => ({
840
+ loading: placeholders.loading.manifest,
841
+ failed: placeholders.failed.manifest,
842
+ })),
843
+ })
272
844
 
273
- const binding: FeatureBinding = {
845
+ const makeFeatureInputParser =
846
+ (
847
+ composition: AnyComposition,
848
+ inputSchema: Schema.Schema.AnyNoContext | undefined,
849
+ placeholder: Option.Option<RuntimePlaceholders>,
850
+ ): ((input: unknown) => Option.Option<unknown>) =>
851
+ (input) =>
852
+ Option.flatMap(
853
+ Option.match(Option.fromNullable(inputSchema), {
854
+ onNone: () => Option.some(input),
855
+ onSome: (schema) => Schema.validateOption(schema)(input),
856
+ }),
857
+ (decoded) =>
858
+ Option.flatMap(composition.parseProps(decoded), () =>
859
+ Option.match(placeholder, {
860
+ onNone: () => Option.some(decoded),
861
+ onSome: (placeholders) =>
862
+ Option.map(placeholders.loading.parseProps(decoded), () => decoded),
863
+ }),
864
+ ),
865
+ )
866
+
867
+ const makeFeatureSummary = <
868
+ FeatureIdentity,
869
+ MountProps,
870
+ CompositionProps,
871
+ C extends UiContract,
872
+ S extends ReadonlyArray<unknown>,
873
+ N extends string,
874
+ Identity,
875
+ OpenServices,
876
+ SupportOpenServices,
877
+ UsesInput extends boolean,
878
+ Input,
879
+ >(
880
+ composition: CompositionClass<CompositionProps, C, S, N, Identity>,
881
+ ): FeatureSummaryOf<
882
+ FeatureIdentity,
883
+ MountProps,
884
+ CompositionProps,
885
+ C,
886
+ S,
887
+ N,
888
+ Identity,
889
+ OpenServices,
890
+ SupportOpenServices,
891
+ UsesInput,
892
+ Input
893
+ > => ({
894
+ [SlotBindingSummaryTypeId]: SlotBindingSummaryTypeId,
895
+ [FeatureChildSummaryTypeId]: {
896
+ featureIdentity: [],
897
+ compositionIdentity: [],
898
+ },
899
+ [FeatureGraphSummaryTypeId]: {
900
+ featureIdentity: [],
901
+ mountProps: [],
902
+ compositionProps: [],
903
+ contract: [],
904
+ states: [],
905
+ name: composition.manifest.name,
906
+ compositionIdentity: [],
907
+ openServices: [],
908
+ supportOpenServices: [],
909
+ usesInput: [],
910
+ input: [],
911
+ validated: FeatureModuleGraphValidTypeId,
912
+ },
913
+ })
914
+
915
+ const makeFeatureBinding = <
916
+ Summary extends AnyFeatureGraphSummary,
917
+ ROut,
918
+ DirectRequires extends ReadonlyArray<FeatureRequirement>,
919
+ >(
920
+ identity: symbol,
921
+ manifest: FeatureManifest,
922
+ summary: Summary,
923
+ composition: CompositionClass<
924
+ FeatureSummaryCompositionProps<Summary>,
925
+ FeatureSummaryContract<Summary>,
926
+ FeatureSummaryStates<Summary>,
927
+ FeatureSummaryName<Summary>,
928
+ FeatureSummaryCompositionIdentity<Summary>
929
+ >,
930
+ load: Effect.Effect<
931
+ LoadedModule<
932
+ ROut,
933
+ DirectRequires,
934
+ FeatureSummaryOpenServices<Summary>,
935
+ FeatureSummaryUsesInput<Summary>,
936
+ FeatureSummaryInput<Summary>
937
+ >,
938
+ FeatureLoadFailed
939
+ >,
940
+ placeholder: Option.Option<RuntimePlaceholders>,
941
+ inputSchema: Schema.Schema.AnyNoContext | undefined,
942
+ boot: ReadonlyArray<Tagged>,
943
+ ): FeatureBinding<Summary, ROut, DirectRequires> => {
944
+ const binding: FeatureBinding<Summary, ROut, DirectRequires> = {
274
945
  [FeatureBindingTypeId]: FeatureBindingTypeId,
946
+ [FeatureGraphValidTypeId]: FeatureGraphValidTypeId,
947
+ manifest,
948
+ identity,
949
+ summary,
950
+ composition,
951
+ strategy: manifest.strategy,
952
+ load,
953
+ placeholder,
954
+ parseInput: makeFeatureInputParser(composition, inputSchema, placeholder),
955
+ boot,
956
+ capture: <Result>(visit: FeatureBindingCapture<Result>): Result => visit(binding),
957
+ }
958
+ return binding
959
+ }
960
+
961
+ const makeEagerFeature = <
962
+ P,
963
+ C extends UiContract,
964
+ ROut,
965
+ DirectRequires extends ReadonlyArray<FeatureRequirement>,
966
+ OpenServices,
967
+ S extends ReadonlyArray<unknown>,
968
+ N extends string,
969
+ Identity,
970
+ Composition extends CompositionClass<P, C, S, N, Identity>,
971
+ >(
972
+ name: string,
973
+ config: EagerFeatureConfigExternalApi<P, C, ROut, DirectRequires, OpenServices, S, N, Identity> &
974
+ ExactFeatureCompositionConfig<Composition>,
975
+ ): EagerFeatureClass<
976
+ Identity,
977
+ P,
978
+ C,
979
+ ROut,
980
+ DirectRequires,
981
+ OpenServices,
982
+ S,
983
+ N,
984
+ Identity,
985
+ Composition
986
+ > => {
987
+ const featureIdentity = Symbol(name)
988
+ const placeholder = Option.none<RuntimePlaceholders>()
989
+ const manifest = makeFeatureManifest(name, 'default', config.composition, placeholder)
990
+ const load = Effect.succeed(config.module)
991
+ const summary = makeFeatureSummary<
992
+ Identity,
993
+ P,
994
+ P,
995
+ C,
996
+ S,
997
+ N,
998
+ Identity,
999
+ OpenServices,
1000
+ never,
1001
+ false,
1002
+ never
1003
+ >(config.composition)
1004
+ const binding = makeFeatureBinding<typeof summary, ROut, DirectRequires>(
1005
+ featureIdentity,
275
1006
  manifest,
1007
+ summary,
1008
+ config.composition,
1009
+ load,
1010
+ placeholder,
1011
+ undefined,
1012
+ Option.getOrElse(Option.fromNullable(config.boot), () => []),
1013
+ )
1014
+ const native: EagerFeatureNativeTree<typeof summary, ROut, DirectRequires, Composition> = {
1015
+ identity: featureIdentity,
1016
+ summary,
276
1017
  composition: config.composition,
277
- strategy,
278
- // The host is strategy-uniform: `default`'s `load` resolves immediately. The
279
- // assignment to the erased `LoadedModule` needs no cast — see `LoadedModule`.
280
1018
  load,
281
- ...(placeholder ? { placeholder: { loading: placeholder.loading, failed: placeholder.failed } } : {}),
282
- boot: Option.getOrElse(Option.fromNullable(config.boot), () => []),
1019
+ binding,
1020
+ eagerModule: config.module,
1021
+ supportModule: undefined,
283
1022
  }
1023
+ const token: FeatureToken<Identity, typeof native> = {
1024
+ identity: featureIdentity,
1025
+ [FeatureTokenIdentityTypeId]: [],
1026
+ [FeatureNativeTypeId]: native,
1027
+ }
1028
+ return definitionClass<
1029
+ EagerFeatureClass<
1030
+ Identity,
1031
+ P,
1032
+ C,
1033
+ ROut,
1034
+ DirectRequires,
1035
+ OpenServices,
1036
+ S,
1037
+ N,
1038
+ Identity,
1039
+ Composition
1040
+ >
1041
+ >({
1042
+ manifest,
1043
+ summary,
1044
+ token,
1045
+ })
1046
+ }
284
1047
 
285
- return definitionClass<FeatureClass<P, C, ROut, Requires>>({ manifest, load, binding, eagerModule })
286
- }
287
-
288
- /**
289
- * Mount a feature against a live engine. The renderer-neutral lifecycle, run on a
290
- * per-feature `Scope`: load the module (an Effect — lazy = dynamic import; default
291
- * = immediate), build its `.live` layer against the host's live `engineContext`
292
- * (so its reducers/procedures register into the SHARED registries and its fibers
293
- * fork onto this scope), then dispatch the feature's `boot`. Returns the merged
294
- * context — engine services + the feature's own services — for the host to read
295
- * the feature's composition and stores. Closing the scope disposes exactly this
296
- * feature: its fibers stop and its stores are reclaimed.
297
- *
298
- * The single boundary cast (mirroring `buildRuntime`): `module.layer` is erased to
299
- * `LoadedModule` so the host stays strategy- and type-uniform. The feature's real
300
- * `RIn` was verified cast-free at `provide(slot, Feature)` and scene wiring; here
301
- * we re-attach the concrete `engineContext` it builds against. `R` is the engine
302
- * context's service set, supplied by the host.
303
- */
304
- // oxlint-disable-next-line reform-rules/prefer-effect-fn -- generic export: Effect.fn's inferred type isn't portable under isolatedDeclarations
305
- export const mountFeature = <R>(
306
- binding: FeatureBinding,
307
- engineContext: Context.Context<R>,
308
- ): Effect.Effect<Context.Context<unknown>, FeatureLoadFailed, Scope.Scope> =>
309
- Effect.gen(function* () {
310
- const { layer } = yield* binding.load
311
- // oxlint-disable-next-line reform-rules/no-type-assertion -- documented erasure boundary: re-attach the concrete engine context to the type-erased `LoadedModule` layer (mirrors `buildRuntime`); RIn was verified cast-free at `provide`/scene wiring
312
- const featureLayer = layer as Layer.Layer<unknown, never, R>
313
- const context = yield* Layer.build(
314
- Layer.provideMerge(featureLayer, Layer.succeedContext(engineContext)),
315
- )
316
- yield* Effect.forEach(binding.boot, (event) => publish('High', event), { discard: true }).pipe(
317
- Effect.provide(context),
318
- )
319
- return context
1048
+ const makeLazyFeature = <
1049
+ CompositionProps,
1050
+ C extends UiContract,
1051
+ ROut,
1052
+ DirectRequires extends ReadonlyArray<FeatureRequirement>,
1053
+ OpenServices,
1054
+ SupportOut,
1055
+ SupportDirectRequires extends ReadonlyArray<FeatureRequirement>,
1056
+ SupportOpenServices,
1057
+ S extends ReadonlyArray<unknown>,
1058
+ N extends string,
1059
+ Identity,
1060
+ Composition extends CompositionClass<CompositionProps, C, S, N, Identity>,
1061
+ InputSchema extends Schema.Schema.AnyNoContext,
1062
+ Loading extends AnyComposition,
1063
+ Failed extends AnyComposition,
1064
+ >(
1065
+ name: string,
1066
+ config: LazyFeatureConfigExternalApi<
1067
+ CompositionProps,
1068
+ C,
1069
+ ROut,
1070
+ DirectRequires,
1071
+ OpenServices,
1072
+ SupportOut,
1073
+ SupportDirectRequires,
1074
+ SupportOpenServices,
1075
+ S,
1076
+ N,
1077
+ Identity,
1078
+ InputSchema,
1079
+ Loading,
1080
+ Failed
1081
+ > &
1082
+ ExactFeatureCompositionConfig<Composition>,
1083
+ ): LazyFeatureClass<
1084
+ Identity,
1085
+ FeatureMountProps<FeatureUsesInput<InputSchema>, CompositionProps, InputSchema>,
1086
+ C,
1087
+ ROut,
1088
+ DirectRequires,
1089
+ OpenServices,
1090
+ SupportOut,
1091
+ SupportDirectRequires,
1092
+ SupportOpenServices,
1093
+ FeatureUsesInput<InputSchema>,
1094
+ S,
1095
+ N,
1096
+ CompositionProps,
1097
+ Identity,
1098
+ Composition
1099
+ > => {
1100
+ const featureIdentity = Symbol(name)
1101
+ const placeholder = Option.some<RuntimePlaceholders>(config.placeholder)
1102
+ const manifest = makeFeatureManifest(name, 'lazy', config.composition, placeholder)
1103
+ const inputSchema = 'input' in config ? config.input : undefined
1104
+ const summary = makeFeatureSummary<
1105
+ Identity,
1106
+ FeatureMountProps<FeatureUsesInput<InputSchema>, CompositionProps, InputSchema>,
1107
+ CompositionProps,
1108
+ C,
1109
+ S,
1110
+ N,
1111
+ Identity,
1112
+ OpenServices,
1113
+ SupportOpenServices,
1114
+ FeatureUsesInput<InputSchema>,
1115
+ FeatureInputValue<InputSchema>
1116
+ >(config.composition)
1117
+ const binding = makeFeatureBinding<typeof summary, ROut, DirectRequires>(
1118
+ featureIdentity,
1119
+ manifest,
1120
+ summary,
1121
+ config.composition,
1122
+ config.load,
1123
+ placeholder,
1124
+ inputSchema,
1125
+ Option.getOrElse(Option.fromNullable(config.boot), () => []),
1126
+ )
1127
+ const native: LazyFeatureNativeTree<
1128
+ typeof summary,
1129
+ ROut,
1130
+ DirectRequires,
1131
+ SupportOut,
1132
+ SupportDirectRequires,
1133
+ Composition
1134
+ > = {
1135
+ identity: featureIdentity,
1136
+ summary,
1137
+ composition: config.composition,
1138
+ load: config.load,
1139
+ binding,
1140
+ eagerModule: undefined,
1141
+ supportModule: config.eager,
1142
+ }
1143
+ const token: FeatureToken<Identity, typeof native> = {
1144
+ identity: featureIdentity,
1145
+ [FeatureTokenIdentityTypeId]: [],
1146
+ [FeatureNativeTypeId]: native,
1147
+ }
1148
+ return definitionClass<
1149
+ LazyFeatureClass<
1150
+ Identity,
1151
+ FeatureMountProps<FeatureUsesInput<InputSchema>, CompositionProps, InputSchema>,
1152
+ C,
1153
+ ROut,
1154
+ DirectRequires,
1155
+ OpenServices,
1156
+ SupportOut,
1157
+ SupportDirectRequires,
1158
+ SupportOpenServices,
1159
+ FeatureUsesInput<InputSchema>,
1160
+ S,
1161
+ N,
1162
+ CompositionProps,
1163
+ Identity,
1164
+ Composition
1165
+ >
1166
+ >({
1167
+ manifest,
1168
+ summary,
1169
+ token,
320
1170
  })
1171
+ }
1172
+
1173
+ export function make<
1174
+ P,
1175
+ C extends UiContract,
1176
+ ROut,
1177
+ DirectRequires extends ReadonlyArray<FeatureRequirement>,
1178
+ OpenServices,
1179
+ S extends ReadonlyArray<unknown>,
1180
+ N extends string,
1181
+ Identity,
1182
+ const Composition extends CompositionClass<P, C, S, N, Identity>,
1183
+ >(
1184
+ name: string,
1185
+ config: EagerFeatureConfigExternalApi<
1186
+ P,
1187
+ C,
1188
+ ROut,
1189
+ DirectRequires,
1190
+ OpenServices,
1191
+ S,
1192
+ N,
1193
+ Identity
1194
+ > & { readonly composition: Composition },
1195
+ ): EagerFeatureClass<
1196
+ Identity,
1197
+ P,
1198
+ C,
1199
+ ROut,
1200
+ DirectRequires,
1201
+ OpenServices,
1202
+ S,
1203
+ N,
1204
+ Identity,
1205
+ Composition
1206
+ >
1207
+ export function make<
1208
+ CompositionProps,
1209
+ C extends UiContract,
1210
+ ROut,
1211
+ DirectRequires extends ReadonlyArray<FeatureRequirement>,
1212
+ OpenServices,
1213
+ S extends ReadonlyArray<unknown>,
1214
+ N extends string,
1215
+ Identity,
1216
+ const Composition extends CompositionClass<CompositionProps, C, S, N, Identity>,
1217
+ const Loading extends AnyComposition,
1218
+ const Failed extends AnyComposition,
1219
+ const InputSchema extends Schema.Schema.AnyNoContext = never,
1220
+ SupportOut = never,
1221
+ SupportDirectRequires extends ReadonlyArray<FeatureRequirement> = readonly [],
1222
+ SupportOpenServices = never,
1223
+ >(
1224
+ name: string,
1225
+ config: LazyFeatureConfigExternalApi<
1226
+ CompositionProps,
1227
+ C,
1228
+ ROut,
1229
+ DirectRequires,
1230
+ OpenServices,
1231
+ SupportOut,
1232
+ SupportDirectRequires,
1233
+ SupportOpenServices,
1234
+ S,
1235
+ N,
1236
+ Identity,
1237
+ InputSchema,
1238
+ Loading,
1239
+ Failed
1240
+ > & { readonly composition: Composition },
1241
+ ): LazyFeatureClass<
1242
+ Identity,
1243
+ FeatureMountProps<FeatureUsesInput<InputSchema>, CompositionProps, InputSchema>,
1244
+ C,
1245
+ ROut,
1246
+ DirectRequires,
1247
+ OpenServices,
1248
+ SupportOut,
1249
+ SupportDirectRequires,
1250
+ SupportOpenServices,
1251
+ FeatureUsesInput<InputSchema>,
1252
+ S,
1253
+ N,
1254
+ CompositionProps,
1255
+ Identity,
1256
+ Composition
1257
+ >
1258
+ export function make<
1259
+ CompositionProps,
1260
+ C extends UiContract,
1261
+ ROut,
1262
+ DirectRequires extends ReadonlyArray<FeatureRequirement>,
1263
+ OpenServices,
1264
+ S extends ReadonlyArray<unknown>,
1265
+ N extends string,
1266
+ Identity,
1267
+ Composition extends CompositionClass<CompositionProps, C, S, N, Identity>,
1268
+ Loading extends AnyComposition,
1269
+ Failed extends AnyComposition,
1270
+ InputSchema extends Schema.Schema.AnyNoContext = never,
1271
+ SupportOut = never,
1272
+ SupportDirectRequires extends ReadonlyArray<FeatureRequirement> = readonly [],
1273
+ SupportOpenServices = never,
1274
+ >(
1275
+ name: string,
1276
+ config: (
1277
+ | EagerFeatureConfigExternalApi<
1278
+ CompositionProps,
1279
+ C,
1280
+ ROut,
1281
+ DirectRequires,
1282
+ OpenServices,
1283
+ S,
1284
+ N,
1285
+ Identity
1286
+ >
1287
+ | LazyFeatureConfigExternalApi<
1288
+ CompositionProps,
1289
+ C,
1290
+ ROut,
1291
+ DirectRequires,
1292
+ OpenServices,
1293
+ SupportOut,
1294
+ SupportDirectRequires,
1295
+ SupportOpenServices,
1296
+ S,
1297
+ N,
1298
+ Identity,
1299
+ InputSchema,
1300
+ Loading,
1301
+ Failed
1302
+ >
1303
+ ) &
1304
+ ExactFeatureCompositionConfig<Composition>,
1305
+ ):
1306
+ | EagerFeatureClass<
1307
+ Identity,
1308
+ CompositionProps,
1309
+ C,
1310
+ ROut,
1311
+ DirectRequires,
1312
+ OpenServices,
1313
+ S,
1314
+ N,
1315
+ Identity,
1316
+ Composition
1317
+ >
1318
+ | LazyFeatureClass<
1319
+ Identity,
1320
+ FeatureMountProps<FeatureUsesInput<InputSchema>, CompositionProps, InputSchema>,
1321
+ C,
1322
+ ROut,
1323
+ DirectRequires,
1324
+ OpenServices,
1325
+ SupportOut,
1326
+ SupportDirectRequires,
1327
+ SupportOpenServices,
1328
+ FeatureUsesInput<InputSchema>,
1329
+ S,
1330
+ N,
1331
+ CompositionProps,
1332
+ Identity,
1333
+ Composition
1334
+ > {
1335
+ if (!isMakeImplementationConfig(config)) {
1336
+ return Effect.runSync(Effect.dieMessage(`Invalid Feature config for ${name}`))
1337
+ }
1338
+ if (config.loadingStrategy === 'lazy') {
1339
+ return makeLazyFeature<
1340
+ CompositionProps,
1341
+ C,
1342
+ ROut,
1343
+ DirectRequires,
1344
+ OpenServices,
1345
+ SupportOut,
1346
+ SupportDirectRequires,
1347
+ SupportOpenServices,
1348
+ S,
1349
+ N,
1350
+ Identity,
1351
+ Composition,
1352
+ InputSchema,
1353
+ Loading,
1354
+ Failed
1355
+ >(name, config)
1356
+ }
1357
+ return makeEagerFeature<
1358
+ CompositionProps,
1359
+ C,
1360
+ ROut,
1361
+ DirectRequires,
1362
+ OpenServices,
1363
+ S,
1364
+ N,
1365
+ Identity,
1366
+ Composition
1367
+ >(name, config)
1368
+ }
1369
+
1370
+ type MountFeature = <
1371
+ Summary extends AnyFeatureGraphSummary,
1372
+ ROut,
1373
+ const DirectRequires extends ReadonlyArray<FeatureRequirement>,
1374
+ HostServices,
1375
+ >(
1376
+ binding: FeatureBinding<Summary, ROut, DirectRequires>,
1377
+ engineContext: Context.Context<
1378
+ HostServices | EngineServices | FeatureSummaryOpenServices<Summary>
1379
+ >,
1380
+ props?: FeatureSummaryInput<Summary>,
1381
+ ) => Effect.Effect<
1382
+ Context.Context<HostServices | EngineServices | FeatureSummaryOpenServices<Summary> | ROut>,
1383
+ FeatureLoadFailed,
1384
+ Scope.Scope
1385
+ >
1386
+
1387
+ export const mountFeature: MountFeature = Effect.fn('mountFeature')(function* <
1388
+ Summary extends AnyFeatureGraphSummary,
1389
+ ROut,
1390
+ const DirectRequires extends ReadonlyArray<FeatureRequirement>,
1391
+ HostServices,
1392
+ >(
1393
+ binding: FeatureBinding<Summary, ROut, DirectRequires>,
1394
+ engineContext: Context.Context<
1395
+ HostServices | EngineServices | FeatureSummaryOpenServices<Summary>
1396
+ >,
1397
+ props: FeatureSummaryInput<Summary> | undefined = undefined,
1398
+ ): Effect.fn.Return<
1399
+ Context.Context<HostServices | EngineServices | FeatureSummaryOpenServices<Summary> | ROut>,
1400
+ FeatureLoadFailed,
1401
+ Scope.Scope
1402
+ > {
1403
+ const { layer } = yield* binding.load
1404
+ const bus = Context.get(engineContext, Bus)
1405
+ const featureContext = Context.add(engineContext, FeatureInput, props)
1406
+ const context = yield* Layer.build(Layer.provide(layer, Layer.succeedContext(featureContext)))
1407
+ yield* Effect.forEach(binding.boot, (event) => publish('High', event), {
1408
+ discard: true,
1409
+ }).pipe(Effect.provideService(Bus, bus))
1410
+ return Context.merge(engineContext, context)
1411
+ })