@playfast/reform 1.1.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 (85) hide show
  1. package/README.md +85 -55
  2. package/package.json +17 -17
  3. package/src/boundary/boundary.test.ts +62 -16
  4. package/src/boundary/boundary.ts +141 -32
  5. package/src/calc/asyncCalc.invalidate.test.ts +516 -18
  6. package/src/calc/asyncCalc.test.ts +207 -175
  7. package/src/calc/asyncCalc.ts +168 -39
  8. package/src/calc/calc.test.ts +3 -5
  9. package/src/calc/calc.ts +44 -18
  10. package/src/calc/calcFamily.test.ts +80 -22
  11. package/src/calc/calcFamily.ts +56 -25
  12. package/src/calc/compose.ts +1 -14
  13. package/src/channel/channel.ts +128 -11
  14. package/src/compose/composition.test.ts +19 -0
  15. package/src/compose/composition.ts +346 -62
  16. package/src/compose/props.ts +13 -6
  17. package/src/compose/provide.ts +187 -46
  18. package/src/compose/slot.ts +156 -19
  19. package/src/compose/structure.test.ts +6 -3
  20. package/src/compose/structure.ts +106 -13
  21. package/src/compose/ui.test.ts +19 -3
  22. package/src/compose/ui.ts +147 -54
  23. package/src/compose/ui.typecheck.ts +40 -4
  24. package/src/definition/definition.ts +22 -9
  25. package/src/event/event.fromSource.test.ts +172 -0
  26. package/src/event/event.test.ts +10 -0
  27. package/src/event/event.ts +102 -18
  28. package/src/feature/feature.mount.test.ts +123 -56
  29. package/src/feature/feature.test.ts +67 -63
  30. package/src/feature/feature.ts +1317 -197
  31. package/src/feature/feature.typecheck.ts +253 -61
  32. package/src/graph/closure.ts +403 -0
  33. package/src/index.ts +171 -245
  34. package/src/internal/bucketCache.ts +39 -0
  35. package/src/internal/capture.ts +9 -4
  36. package/src/internal/env.ts +1 -3
  37. package/src/internal/errors.ts +21 -10
  38. package/src/internal/queryDriver.ts +120 -15
  39. package/src/internal/queryDriverStore.ts +8 -5
  40. package/src/internal/queryDriverTypes.ts +3 -1
  41. package/src/internal/reuse.test.ts +10 -3
  42. package/src/internal/reuse.ts +5 -2
  43. package/src/internal/scheduler.ts +4 -1
  44. package/src/internal/sources.ts +25 -11
  45. package/src/internal/track.ts +3 -2
  46. package/src/internal.ts +222 -0
  47. package/src/namespace/namespace.ts +46 -25
  48. package/src/procedure/procedure.ts +38 -11
  49. package/src/reducer/reducer.ts +78 -34
  50. package/src/remote/remoteState.test.ts +515 -339
  51. package/src/remote/remoteState.ts +572 -107
  52. package/src/remote/remoteState.typecheck.ts +47 -23
  53. package/src/runtime/appRuntime.activation.test.ts +100 -0
  54. package/src/runtime/appRuntime.test.ts +157 -105
  55. package/src/runtime/appRuntime.ts +394 -33
  56. package/src/runtime/eventBudget.test.ts +1 -4
  57. package/src/runtime/eventBudget.ts +9 -8
  58. package/src/runtime/hardening.test.ts +4 -9
  59. package/src/runtime/instrumentation.test.ts +174 -192
  60. package/src/runtime/instrumentation.ts +6 -11
  61. package/src/runtime/loop.test.ts +36 -0
  62. package/src/runtime/loop.ts +69 -40
  63. package/src/scene/featureScene.test.ts +4 -2
  64. package/src/scene/scene.ts +234 -64
  65. package/src/scene/seedScene.test.ts +69 -86
  66. package/src/state/state.nominal.typecheck.ts +68 -0
  67. package/src/state/state.test.ts +17 -0
  68. package/src/state/state.ts +79 -26
  69. package/src/state/stateFamily.test.ts +14 -0
  70. package/src/state/stateFamily.ts +55 -22
  71. package/src/state/stateGroup.ts +53 -17
  72. package/src/state/token.ts +40 -10
  73. package/src/synced/syncedStore.ts +46 -23
  74. package/src/testkit/flight.testkit.ts +75 -0
  75. package/src/wire/tree.test.ts +8 -4
  76. package/src/wire/triggers.test.ts +6 -2
  77. package/src/wire/triggers.ts +14 -10
  78. package/src/calc/asyncCalcDefinitions.ts +0 -48
  79. package/src/channel/procedureRegistry.ts +0 -90
  80. package/src/feature/featureBinding.ts +0 -48
  81. package/src/internal/ctx.ts +0 -9
  82. package/src/remote/remoteStateDefinition.ts +0 -135
  83. package/src/remote/remoteStateLayers.ts +0 -118
  84. package/src/remote/remoteStateLiveTypes.ts +0 -59
  85. package/src/remote/remoteStateSend.ts +0 -64
@@ -1,10 +1,6 @@
1
1
  import { Array as Arr, Cause, Chunk, Context, Effect, Layer, Option, PubSub, Queue } from 'effect'
2
- import {
3
- Channels,
4
- channelsLayer,
5
- Procedures,
6
- proceduresLayer,
7
- } from '../channel/channel'
2
+ import { Channels, channelsLayer, Procedures, proceduresLayer } from '../channel/channel'
3
+ import { makeBucketCache } from '../internal/bucketCache'
8
4
  import { notificationsLayer } from '../internal/scheduler'
9
5
  import { Bus, busLayer, type Envelope, type Tagged } from './bus'
10
6
  import { enforceBudget, makeTickCounter, resolveCurrentEventBudget } from './eventBudget'
@@ -19,56 +15,78 @@ export interface ReducerEntry {
19
15
 
20
16
  export interface ReducerRegistry {
21
17
  readonly entries: Array<ReducerEntry>
22
- readonly byTag: Map<string, Array<ReducerEntry>>
18
+ readonly byName: Map<string, Set<ReducerEntry>>
19
+ readonly byTag: Map<string, Set<ReducerEntry>>
20
+ readonly tagged: (tag: string) => ReadonlyArray<ReducerEntry>
23
21
  readonly register: (entry: ReducerEntry) => void
24
22
  readonly unregister: (entry: ReducerEntry) => void
25
23
  }
26
24
 
27
- const ReducersBase: Context.TagClass<Reducers, 'reform/Reducers', ReducerRegistry> =
28
- Context.Tag('reform/Reducers')<Reducers, ReducerRegistry>()
25
+ const ReducersBase: Context.TagClass<Reducers, 'reform/Reducers', ReducerRegistry> = Context.Tag(
26
+ 'reform/Reducers',
27
+ )<Reducers, ReducerRegistry>()
29
28
  export class Reducers extends ReducersBase {}
30
29
 
31
- const dropFromBuckets = <T>(map: Map<string, Array<T>>, key: string, target: T): void => {
30
+ const dropFromBuckets = <T>(map: Map<string, Set<T>>, key: string, target: T): void => {
32
31
  const bucket = map.get(key)
33
32
  if (bucket === undefined) {
34
33
  return
35
34
  }
36
- const next = bucket.filter((entry) => entry !== target)
37
- if (next.length === 0) {
35
+ bucket.delete(target)
36
+ if (bucket.size === 0) {
38
37
  map.delete(key)
39
- } else {
40
- map.set(key, next)
41
38
  }
42
39
  }
43
40
 
44
41
  const makeReducerRegistry = (): ReducerRegistry => {
45
- const byTag = new Map<string, Array<ReducerEntry>>()
46
- const state: { entries: Array<ReducerEntry> } = { entries: [] }
42
+ const entries = new Set<ReducerEntry>()
43
+ const byName = new Map<string, Set<ReducerEntry>>()
44
+ const byTag = new Map<string, Set<ReducerEntry>>()
45
+ const tagged = makeBucketCache(byTag)
46
+ const snapshot: { value: Option.Option<Array<ReducerEntry>> } = {
47
+ value: Option.some([]),
48
+ }
47
49
  return {
48
50
  get entries() {
49
- return state.entries
51
+ return Option.getOrElse(snapshot.value, () => {
52
+ const current = Array.from(entries)
53
+ snapshot.value = Option.some(current)
54
+ return current
55
+ })
50
56
  },
57
+ byName,
51
58
  byTag,
59
+ tagged: tagged.read,
52
60
  register: (entry) => {
53
- state.entries = [...state.entries, entry]
61
+ entries.add(entry)
62
+ snapshot.value = Option.none()
63
+ getOrCreateBucket(byName, entry.name).add(entry)
54
64
  entry.handles.forEach((tag) => {
55
- const bucket = byTag.get(tag)
56
- if (bucket === undefined) {
57
- byTag.set(tag, [entry])
58
- } else {
59
- byTag.set(tag, [...bucket, entry])
60
- }
65
+ getOrCreateBucket(byTag, tag).add(entry)
66
+ tagged.invalidate(tag)
61
67
  })
62
68
  },
63
69
  unregister: (entry) => {
64
- state.entries = state.entries.filter((existing) => existing !== entry)
65
- entry.handles.forEach((tag) => dropFromBuckets(byTag, tag, entry))
70
+ entries.delete(entry)
71
+ snapshot.value = Option.none()
72
+ dropFromBuckets(byName, entry.name, entry)
73
+ entry.handles.forEach((tag) => {
74
+ dropFromBuckets(byTag, tag, entry)
75
+ tagged.invalidate(tag)
76
+ })
66
77
  },
67
78
  }
68
79
  }
69
80
 
70
- const bucketOf = <V>(map: Map<string, Array<V>>, tag: string): Array<V> =>
71
- Option.getOrElse(Option.fromNullable(map.get(tag)), (): Array<V> => [])
81
+ const getOrCreateBucket = <T>(map: Map<string, Set<T>>, key: string): Set<T> => {
82
+ const existing = map.get(key)
83
+ if (existing !== undefined) {
84
+ return existing
85
+ }
86
+ const created = new Set<T>()
87
+ map.set(key, created)
88
+ return created
89
+ }
72
90
 
73
91
  const isolateApply = (
74
92
  reducer: ReducerEntry,
@@ -78,9 +96,10 @@ const isolateApply = (
78
96
  const end = instrumentation.reducerRun(reducer.name, event._tag)
79
97
  return Effect.runSync(
80
98
  Effect.match(
81
- Effect.try({ try: () => reducer.apply(event), catch: (error) => error }).pipe(
82
- Effect.ensuring(Effect.sync(end)),
83
- ),
99
+ Effect.try({
100
+ try: () => reducer.apply(event),
101
+ catch: (error) => error,
102
+ }).pipe(Effect.ensuring(Effect.sync(end))),
84
103
  {
85
104
  onFailure: (error) => Option.some({ event, error }),
86
105
  onSuccess: () => Option.none(),
@@ -91,7 +110,7 @@ const isolateApply = (
91
110
 
92
111
  export const reducersLayer: Layer.Layer<Reducers> = Layer.sync(Reducers, makeReducerRegistry)
93
112
 
94
- // High before Normal; partition preserves intra-priority dispatch order
113
+ // Partitioning keeps dispatch order stable within each priority.
95
114
  const rank = (priority: Envelope['priority']): number => (priority === 'High' ? 0 : 1)
96
115
 
97
116
  const drain = Effect.gen(function* () {
@@ -100,8 +119,8 @@ const drain = Effect.gen(function* () {
100
119
  const { byName: channels } = yield* Channels
101
120
  const procedures = yield* Procedures
102
121
  const instrumentation = yield* resolveInstrumentation
103
- // One counter per engine: feature child runtimes share the app budget
104
122
  const budget = yield* resolveCurrentEventBudget
123
+ // Child feature runtimes share this engine-level counter with the root.
105
124
  const counter = makeTickCounter(budget)
106
125
  const subscription = yield* PubSub.subscribe(bus)
107
126
 
@@ -115,25 +134,25 @@ const drain = Effect.gen(function* () {
115
134
  const [high, normal] = Arr.partition(frame, (envelope) => rank(envelope.priority) !== 0)
116
135
  const ordered = [...high, ...normal]
117
136
 
118
- // Budget before folding EventLoopOverflow is this tick's only typed failure
137
+ // Check before folding so runaway frames stop at the hard limit.
119
138
  const sample = yield* Effect.sync(() => counter.add(ordered.length))
120
139
  yield* enforceBudget(budget, sample)
121
140
 
122
- // Sync block: coalesce reducer writes into one flush; no yield* mid-frame
141
+ // One synchronous block coalesces reducer writes into a single flush.
123
142
  const failures = yield* Effect.sync(() => {
124
143
  const endFrame = instrumentation.frame(ordered.length)
125
144
  ordered.forEach((envelope) =>
126
145
  instrumentation.eventDispatched(envelope.event._tag, envelope.priority),
127
146
  )
128
- // Isolate folds so one throw cannot kill the drain fiber
129
147
  const collected = Arr.flatMap(ordered, (envelope) =>
130
- Arr.filterMap(bucketOf(reducers.byTag, envelope.event._tag), (reducer) =>
148
+ // A reducer defect must not abandon the remaining frame.
149
+ Arr.filterMap(reducers.tagged(envelope.event._tag), (reducer) =>
131
150
  isolateApply(reducer, envelope.event, instrumentation),
132
151
  ),
133
152
  )
134
- // Offer each distinct channel once (per-procedure would double-run shared channels)
135
153
  ordered.forEach((envelope) => {
136
- bucketOf(procedures.channelsByTag, envelope.event._tag).forEach((channelName) => {
154
+ // channelsFor is distinct; per-procedure offers would double-run shared channels.
155
+ procedures.channelsFor(envelope.event._tag).forEach((channelName) => {
137
156
  channels.get(channelName)?.offer(envelope.event)
138
157
  })
139
158
  })
@@ -150,7 +169,7 @@ const drain = Effect.gen(function* () {
150
169
  { discard: true },
151
170
  )
152
171
  }).pipe(
153
- // Only typed failures (overflow) escape; defects restart the tick so the bus never goes deaf
172
+ // Typed overflow halts the loop; defects are logged and the next tick continues.
154
173
  Effect.catchAllCause((cause) =>
155
174
  Option.match(Cause.failureOption(cause), {
156
175
  onSome: () => Effect.failCause(cause),
@@ -180,3 +199,13 @@ export const Engine: Layer.Layer<Bus | Reducers | Channels | Procedures | Querie
180
199
  ),
181
200
  ),
182
201
  )
202
+
203
+ export const EngineRequirements: readonly [
204
+ typeof Bus,
205
+ typeof Reducers,
206
+ typeof Channels,
207
+ typeof Procedures,
208
+ typeof Queries,
209
+ ] = [Bus, Reducers, Channels, Procedures, Queries]
210
+
211
+ export type EngineServices = Layer.Layer.Success<typeof Engine>
@@ -19,11 +19,13 @@ const GreetingBase: Context.TagClass<Greeting, 'featureScene.Greeting', Greeting
19
19
  class Greeting extends GreetingBase {}
20
20
 
21
21
  class Started extends Event.make('featureScene.Started', S.Struct({})) {}
22
- class RootUi extends ui('featureScene.RootUi')<{ props: { readonly greeting: string } }>() {}
22
+ class RootUi extends ui('featureScene.RootUi')<{
23
+ props: { readonly greeting: string }
24
+ }>() {}
23
25
  class RootComp extends Composition.make('featureScene.RootComp', {
24
26
  title: 'Feature root',
25
27
  ui: RootUi,
26
- }) {}
28
+ })<RootComp>() {}
27
29
 
28
30
  const RootLive = Composition.live(RootComp, function* () {
29
31
  const greeting = yield* Greeting
@@ -1,13 +1,14 @@
1
- import { Layer, Struct } from 'effect'
2
- import type { CompositionClass, CompositionService } from '../compose/composition'
1
+ import { Layer, Predicate, Struct } from 'effect'
2
+ import type {
3
+ AnyComposition,
4
+ CompositionClass,
5
+ CompositionId,
6
+ CompositionReflection,
7
+ HasExactCompositionId,
8
+ } from '../compose/composition'
3
9
  import type { UiContract } from '../compose/ui'
4
10
  import type { EventOf } from '../event/event'
5
- import type {
6
- EagerFeatureClass,
7
- EngineServices,
8
- FeatureRequirement,
9
- ProvidedBy,
10
- } from '../feature/feature'
11
+ import { type EagerFeatureClass, type FeatureRequirement, tree } from '../feature/feature'
11
12
  import { CurrentSeedOverrides } from '../internal/seeds'
12
13
  import type { SeedsOf } from '../state/stateGroup'
13
14
  import type { Bus } from '../runtime/bus'
@@ -16,88 +17,257 @@ import {
16
17
  type Instrumentation,
17
18
  noopInstrumentation,
18
19
  } from '../runtime/instrumentation'
20
+ import type { EngineServices } from '../runtime/loop'
19
21
 
20
- export type MountedServices = CompositionService | Bus
22
+ export type MountedServices = CompositionId<string> | Bus
21
23
 
22
- type BootEvent = EventOf<string, unknown>
24
+ export type SceneCapture<C extends UiContract, S extends ReadonlyArray<unknown>, Result> = <
25
+ Services,
26
+ P,
27
+ N extends string,
28
+ Identity,
29
+ >(
30
+ scene: CapturedScene<C, S, Services, P, N, Identity>,
31
+ ) => Result
32
+
33
+ export type AnySceneCapture<Result> = <
34
+ C extends UiContract,
35
+ S extends ReadonlyArray<unknown>,
36
+ Services,
37
+ P,
38
+ N extends string,
39
+ Identity,
40
+ >(
41
+ scene: CapturedScene<C, S, Services, P, N, Identity>,
42
+ ) => Result
43
+
44
+ interface SceneOptionalFields {
45
+ readonly boot: ReadonlyArray<BootEvent>
46
+ // Hosts record render spans outside the scene's layer context.
47
+ readonly instrumentation: Instrumentation
48
+ }
49
+
50
+ export interface AnySceneExternalApi extends Partial<SceneOptionalFields> {
51
+ readonly kind: 'Scene'
52
+ readonly composition: AnyComposition
53
+ readonly captureAny: <Result>(visit: AnySceneCapture<Result>) => Result
54
+ }
55
+
56
+ export type AnyScene = AnySceneExternalApi
23
57
 
24
58
  export interface Scene<
25
59
  C extends UiContract = UiContract,
26
60
  S extends ReadonlyArray<unknown> = ReadonlyArray<unknown>,
27
- > {
61
+ > extends AnySceneExternalApi {
62
+ readonly composition: AnyComposition & CompositionReflection<unknown, C, S>
63
+ readonly capture: <Result>(visit: SceneCapture<C, S, Result>) => Result
64
+ }
65
+
66
+ type BootEvent = EventOf<string, unknown>
67
+
68
+ export interface CapturedScene<
69
+ C extends UiContract,
70
+ S extends ReadonlyArray<unknown>,
71
+ Services,
72
+ P,
73
+ N extends string,
74
+ Identity = unknown,
75
+ > extends Scene<C, S> {
28
76
  readonly kind: 'Scene'
29
- readonly composition: CompositionClass<unknown, C, S>
30
- readonly provide: ReadonlyArray<Layer.Layer<MountedServices, never, never>>
31
- // oxlint-disable-next-line reform-rules/no-optional-fields -- presence-optional boot read as `scene.boot ?? []` across non-batch hosts (drive/proof/remote/react/editor); Option would break them
32
- readonly boot?: ReadonlyArray<BootEvent>
33
- // instrumentation on the scene (not only layers): host render spans live outside layer context
34
- // oxlint-disable-next-line reform-rules/no-optional-fields -- presence-optional carrier like `boot`; absent on every unprofiled scene, so Option would tax all authoring call sites
35
- readonly instrumentation?: Instrumentation
77
+ readonly composition: CompositionClass<P, C, S, N, Identity>
78
+ readonly provide: ReadonlyArray<Layer.Layer<Services, never, never>>
79
+ }
80
+
81
+ interface SceneConfigOptionalFields {
82
+ readonly boot: ReadonlyArray<BootEvent>
83
+ }
84
+
85
+ export interface SceneConfig<Services> extends Partial<SceneConfigOptionalFields> {
86
+ readonly provide: ReadonlyArray<Layer.Layer<Services, never, never>>
87
+ }
88
+
89
+ type SceneConfigIncludingRootComposition<Services, N extends string, Identity> =
90
+ HasExactCompositionId<Services, N, Identity> extends true
91
+ ? SceneConfig<Services>
92
+ : SceneConfig<Services> & {
93
+ readonly missingCompositionService: 'The scene must provide its root composition'
94
+ }
95
+
96
+ interface SceneValueConfigExternalApi<Services>
97
+ extends SceneConfig<Services>, Partial<Pick<SceneOptionalFields, 'instrumentation'>> {}
98
+
99
+ const makeSceneValue = <
100
+ P,
101
+ C extends UiContract,
102
+ S extends ReadonlyArray<unknown>,
103
+ N extends string,
104
+ Identity,
105
+ Services,
106
+ >(
107
+ composition: CompositionClass<P, C, S, N, Identity>,
108
+ config: SceneValueConfigExternalApi<Services>,
109
+ ): CapturedScene<C, S, Services, P, N, Identity> => {
110
+ const sceneValue: CapturedScene<C, S, Services, P, N, Identity> = {
111
+ kind: 'Scene',
112
+ composition,
113
+ provide: config.provide,
114
+ ...(config.boot !== undefined ? { boot: config.boot } : {}),
115
+ ...(config.instrumentation !== undefined ? { instrumentation: config.instrumentation } : {}),
116
+ capture: <Result>(visit: SceneCapture<C, S, Result>): Result => visit(sceneValue),
117
+ captureAny: <Result>(visit: AnySceneCapture<Result>): Result => visit(sceneValue),
118
+ }
119
+ return sceneValue
36
120
  }
37
121
 
38
- export interface SceneConfig {
39
- readonly provide: ReadonlyArray<Layer.Layer<MountedServices, never, never>>
40
- // oxlint-disable-next-line reform-rules/no-optional-fields -- mirrors Scene.boot; presence-optional to keep the authoring call site terse and back-compatible
41
- readonly boot?: ReadonlyArray<BootEvent>
122
+ export const scene = <
123
+ P,
124
+ C extends UiContract,
125
+ S extends ReadonlyArray<unknown>,
126
+ N extends string,
127
+ Identity,
128
+ Services,
129
+ >(
130
+ composition: CompositionClass<P, C, S, N, Identity>,
131
+ config: SceneConfigIncludingRootComposition<Services, N, Identity>,
132
+ ): CapturedScene<C, S, Services, P, N, Identity> => {
133
+ return makeSceneValue(composition, config)
42
134
  }
43
135
 
44
- export const scene = <C extends UiContract, S extends ReadonlyArray<unknown>>(
45
- composition: CompositionClass<unknown, C, S>,
46
- config: SceneConfig,
47
- ): Scene<C, S> => ({
48
- kind: 'Scene',
49
- composition,
50
- provide: config.provide,
51
- ...(config.boot !== undefined ? { boot: config.boot } : {}),
52
- })
53
-
54
- export interface FeatureSceneConfig<
55
- Requires extends ReadonlyArray<FeatureRequirement>,
56
- > {
57
- readonly provide: Layer.Layer<EngineServices | ProvidedBy<Requires>, never, never>
136
+ export interface FeatureSceneConfig<OpenServices> {
137
+ readonly provide: Layer.Layer<EngineServices | OpenServices, never, never>
58
138
  }
59
139
 
60
140
  export const featureScene = <
141
+ FeatureIdentity,
61
142
  P,
62
143
  C extends UiContract,
63
144
  ROut,
64
- Requires extends ReadonlyArray<FeatureRequirement>,
145
+ DirectRequires extends ReadonlyArray<FeatureRequirement>,
146
+ OpenServices,
147
+ S extends ReadonlyArray<unknown>,
148
+ N extends string,
149
+ Identity,
65
150
  >(
66
- feature: EagerFeatureClass<P, C, ROut, Requires>,
67
- config: FeatureSceneConfig<Requires>,
68
- ): Scene<C> => {
69
- const app = feature.eagerModule.layer.pipe(Layer.provideMerge(config.provide))
70
- // oxlint-disable-next-line reform-rules/no-type-assertion -- close the generic root module's erased output to the host-readable Scene subset
71
- const closed = app as Layer.Layer<MountedServices, never, never>
72
- return scene(feature.composition, {
73
- provide: [closed],
74
- boot: feature.binding.boot,
151
+ feature: EagerFeatureClass<
152
+ FeatureIdentity,
153
+ P,
154
+ C,
155
+ ROut,
156
+ DirectRequires,
157
+ OpenServices,
158
+ S,
159
+ N,
160
+ Identity
161
+ >,
162
+ config: FeatureSceneConfig<OpenServices>,
163
+ ): CapturedScene<C, S, ROut | EngineServices | OpenServices, P, N, Identity> => {
164
+ const native = tree<typeof feature>(feature.token)
165
+ const app = native.eagerModule.layer.pipe(Layer.provideMerge(config.provide))
166
+ return makeSceneValue(native.composition, {
167
+ provide: [app],
168
+ boot: native.binding.boot,
75
169
  })
76
170
  }
77
171
 
78
- // Tooling seam: wrap closed layers with seed overrides; keys are member names (not group names)
79
- export const seedScene = <C extends UiContract, S extends ReadonlyArray<unknown>>(
172
+ export function seedScene<
173
+ C extends UiContract,
174
+ S extends ReadonlyArray<unknown>,
175
+ Services,
176
+ P,
177
+ N extends string,
178
+ Identity,
179
+ >(
180
+ base: CapturedScene<C, S, Services, P, N, Identity>,
181
+ seeds: SeedsOf<S>,
182
+ ): CapturedScene<C, S, Services, P, N, Identity>
183
+ export function seedScene<C extends UiContract, S extends ReadonlyArray<unknown>>(
80
184
  base: Scene<C, S>,
81
185
  seeds: SeedsOf<S>,
82
- ): Scene<C, S> =>
83
- Struct.keys(seeds).length === 0
84
- ? base
85
- : { ...base, provide: base.provide.map(Layer.locally(CurrentSeedOverrides, seeds)) }
186
+ ): Scene<C, S>
187
+ export function seedScene<C extends UiContract, S extends ReadonlyArray<unknown>>(
188
+ base: Scene<C, S>,
189
+ seeds: SeedsOf<S>,
190
+ ): Scene<C, S> {
191
+ if (Struct.keys(seeds).length === 0) {
192
+ return base
193
+ }
194
+ // Tooling overlays closed layers; member names, not group names, are the keys.
195
+ return base.capture<Scene<C, S>>(
196
+ (exactScene): Scene<C, S> =>
197
+ makeSceneValue(exactScene.composition, {
198
+ provide: exactScene.provide.map(Layer.locally(CurrentSeedOverrides, seeds)),
199
+ ...(exactScene.boot === undefined ? {} : { boot: exactScene.boot }),
200
+ ...(exactScene.instrumentation === undefined
201
+ ? {}
202
+ : { instrumentation: exactScene.instrumentation }),
203
+ }),
204
+ )
205
+ }
86
206
 
87
- export const profiledScene = <C extends UiContract, S extends ReadonlyArray<unknown>>(
207
+ export const seedAnyScene = (
208
+ base: AnyScene,
209
+ seeds: Readonly<Record<string, unknown>>,
210
+ ): AnyScene => {
211
+ if (Struct.keys(seeds).length === 0) {
212
+ return base
213
+ }
214
+ return base.captureAny<AnyScene>(
215
+ (exactScene): AnyScene =>
216
+ makeSceneValue(exactScene.composition, {
217
+ provide: exactScene.provide.map(Layer.locally(CurrentSeedOverrides, seeds)),
218
+ ...(exactScene.boot === undefined ? {} : { boot: exactScene.boot }),
219
+ ...(exactScene.instrumentation === undefined
220
+ ? {}
221
+ : { instrumentation: exactScene.instrumentation }),
222
+ }),
223
+ )
224
+ }
225
+
226
+ export function profiledScene<
227
+ C extends UiContract,
228
+ S extends ReadonlyArray<unknown>,
229
+ Services,
230
+ P,
231
+ N extends string,
232
+ Identity,
233
+ >(
234
+ base: CapturedScene<C, S, Services, P, N, Identity>,
235
+ instrumentation: Instrumentation,
236
+ ): CapturedScene<C, S, Services, P, N, Identity>
237
+ export function profiledScene<C extends UiContract, S extends ReadonlyArray<unknown>>(
238
+ base: Scene<C, S>,
239
+ instrumentation: Instrumentation,
240
+ ): Scene<C, S>
241
+ export function profiledScene<C extends UiContract, S extends ReadonlyArray<unknown>>(
88
242
  base: Scene<C, S>,
89
243
  instrumentation: Instrumentation,
90
- ): Scene<C, S> => ({
91
- ...base,
92
- instrumentation,
93
- provide: base.provide.map(Layer.locally(CurrentInstrumentation, instrumentation)),
94
- })
244
+ ): Scene<C, S> {
245
+ return base.capture<Scene<C, S>>(
246
+ (exactScene): Scene<C, S> =>
247
+ makeSceneValue(exactScene.composition, {
248
+ provide: exactScene.provide.map(Layer.locally(CurrentInstrumentation, instrumentation)),
249
+ ...(exactScene.boot === undefined ? {} : { boot: exactScene.boot }),
250
+ instrumentation,
251
+ }),
252
+ )
253
+ }
95
254
 
96
- export const sceneInstrumentation = (candidate: Scene): Instrumentation =>
255
+ export const sceneInstrumentation = (candidate: AnyScene): Instrumentation =>
97
256
  candidate.instrumentation ?? noopInstrumentation
98
257
 
99
- export const isScene = (candidate: unknown): candidate is Scene =>
100
- typeof candidate === 'object' &&
101
- candidate !== null &&
102
- 'kind' in candidate &&
103
- candidate.kind === 'Scene'
258
+ const isReflectableComposition = (candidate: unknown): candidate is AnyComposition =>
259
+ typeof candidate === 'function' &&
260
+ Predicate.hasProperty(candidate, 'manifest') &&
261
+ Predicate.isRecord(candidate.manifest) &&
262
+ candidate.manifest['kind'] === 'Composition' &&
263
+ Predicate.hasProperty(candidate, 'parseProps') &&
264
+ typeof candidate.parseProps === 'function' &&
265
+ Predicate.hasProperty(candidate, 'capture') &&
266
+ typeof candidate.capture === 'function'
267
+
268
+ export const isScene = (candidate: unknown): candidate is AnyScene =>
269
+ Predicate.isRecord(candidate) &&
270
+ candidate['kind'] === 'Scene' &&
271
+ isReflectableComposition(candidate['composition']) &&
272
+ typeof candidate['capture'] === 'function' &&
273
+ typeof candidate['captureAny'] === 'function'