@playfast/reform 1.1.1 → 1.2.1

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 (106) hide show
  1. package/README.md +85 -55
  2. package/package.json +17 -17
  3. package/src/boundary/boundary.test.ts +82 -33
  4. package/src/boundary/boundary.ts +141 -32
  5. package/src/calc/asyncCalc.invalidate.test.ts +733 -32
  6. package/src/calc/asyncCalc.test.ts +397 -225
  7. package/src/calc/asyncCalc.ts +115 -100
  8. package/src/calc/asyncCalcTypes.ts +147 -0
  9. package/src/calc/calc.test.ts +13 -16
  10. package/src/calc/calc.ts +44 -18
  11. package/src/calc/calcFamily.test.ts +90 -32
  12. package/src/calc/calcFamily.ts +56 -25
  13. package/src/calc/compose.ts +1 -14
  14. package/src/channel/channel.ts +25 -6
  15. package/src/channel/{procedureRegistry.ts → procedures.ts} +41 -23
  16. package/src/compose/composition.test.ts +19 -0
  17. package/src/compose/composition.ts +168 -81
  18. package/src/compose/compositionTypes.ts +249 -0
  19. package/src/compose/props.ts +13 -6
  20. package/src/compose/provide.ts +187 -46
  21. package/src/compose/slot.ts +156 -19
  22. package/src/compose/structure.test.ts +6 -3
  23. package/src/compose/structure.ts +106 -13
  24. package/src/compose/ui.test.ts +19 -3
  25. package/src/compose/ui.ts +147 -54
  26. package/src/compose/ui.typecheck.ts +40 -4
  27. package/src/definition/definition.ts +22 -9
  28. package/src/event/event.fromSource.test.ts +172 -0
  29. package/src/event/event.test.ts +10 -0
  30. package/src/event/event.ts +102 -18
  31. package/src/feature/feature.mount.test.ts +123 -56
  32. package/src/feature/feature.test.ts +67 -63
  33. package/src/feature/feature.ts +222 -262
  34. package/src/feature/feature.typecheck.ts +224 -91
  35. package/src/feature/featureBinding.ts +130 -20
  36. package/src/feature/featureClass.ts +162 -0
  37. package/src/feature/featureConfig.ts +192 -0
  38. package/src/feature/featureFactory.ts +163 -0
  39. package/src/feature/featureModule.ts +136 -0
  40. package/src/feature/featureModule.typecheck.ts +63 -0
  41. package/src/feature/featureNativeTree.ts +152 -0
  42. package/src/feature/featureRequirement.ts +82 -0
  43. package/src/feature/featureVariants.ts +234 -0
  44. package/src/feature/mountFeature.ts +54 -0
  45. package/src/graph/closure.ts +222 -0
  46. package/src/graph/services.ts +95 -0
  47. package/src/graph/summary.ts +134 -0
  48. package/src/index.ts +171 -245
  49. package/src/internal/bucketCache.ts +39 -0
  50. package/src/internal/capture.ts +9 -4
  51. package/src/internal/env.ts +1 -3
  52. package/src/internal/errors.ts +21 -10
  53. package/src/internal/queryDriver.ts +61 -25
  54. package/src/internal/queryDriverHydrate.ts +45 -0
  55. package/src/internal/queryDriverStore.ts +8 -5
  56. package/src/internal/queryDriverTypes.ts +11 -11
  57. package/src/internal/reuse.test.ts +10 -3
  58. package/src/internal/reuse.ts +5 -2
  59. package/src/internal/scheduler.ts +4 -1
  60. package/src/internal/sources.ts +25 -11
  61. package/src/internal/store.test.ts +27 -6
  62. package/src/internal/track.ts +3 -2
  63. package/src/internal.ts +222 -0
  64. package/src/namespace/namespace.ts +46 -25
  65. package/src/procedure/procedure.ts +38 -11
  66. package/src/reducer/reducer.ts +78 -34
  67. package/src/remote/pendingQueue.ts +145 -0
  68. package/src/remote/remoteState.test.ts +536 -354
  69. package/src/remote/remoteState.ts +145 -106
  70. package/src/remote/remoteState.typecheck.ts +47 -23
  71. package/src/remote/remoteStateMake.ts +97 -0
  72. package/src/remote/remoteStateSend.ts +95 -37
  73. package/src/remote/remoteStateTypes.ts +155 -0
  74. package/src/runtime/appRuntime.activation.test.ts +104 -0
  75. package/src/runtime/appRuntime.test.ts +187 -111
  76. package/src/runtime/appRuntime.ts +36 -130
  77. package/src/runtime/capturedAppRuntime.ts +274 -0
  78. package/src/runtime/eventBudget.test.ts +39 -12
  79. package/src/runtime/eventBudget.ts +9 -8
  80. package/src/runtime/featureMount.ts +94 -0
  81. package/src/runtime/hardening.test.ts +22 -13
  82. package/src/runtime/instrumentation.test.ts +213 -194
  83. package/src/runtime/instrumentation.ts +6 -11
  84. package/src/runtime/loop.test.ts +84 -7
  85. package/src/runtime/loop.ts +69 -40
  86. package/src/runtime/runtimeHandle.ts +124 -0
  87. package/src/scene/featureScene.test.ts +4 -2
  88. package/src/scene/scene.ts +234 -64
  89. package/src/scene/seedScene.test.ts +69 -86
  90. package/src/state/state.nominal.typecheck.ts +68 -0
  91. package/src/state/state.test.ts +17 -0
  92. package/src/state/state.ts +79 -26
  93. package/src/state/stateFamily.test.ts +39 -8
  94. package/src/state/stateFamily.ts +55 -22
  95. package/src/state/stateGroup.ts +53 -17
  96. package/src/state/token.ts +40 -10
  97. package/src/synced/syncedStore.ts +46 -23
  98. package/src/testkit/flight.testkit.ts +84 -0
  99. package/src/wire/tree.test.ts +8 -4
  100. package/src/wire/triggers.test.ts +6 -2
  101. package/src/wire/triggers.ts +14 -10
  102. package/src/calc/asyncCalcDefinitions.ts +0 -48
  103. package/src/internal/ctx.ts +0 -9
  104. package/src/remote/remoteStateDefinition.ts +0 -135
  105. package/src/remote/remoteStateLayers.ts +0 -118
  106. package/src/remote/remoteStateLiveTypes.ts +0 -59
@@ -1,18 +1,19 @@
1
- import { Context, Duration, Effect, Layer, Schema as S } from "effect";
2
- import { expect, it } from "@effect/vitest";
3
- import * as Composition from "../compose/composition";
4
- import { provide } from "../compose/provide";
5
- import { mount } from "../compose/structure";
6
- import * as Ui from "../compose/ui";
7
- import { ui } from "../compose/ui";
8
- import * as Event from "../event/event";
9
- import * as Feature from "../feature/feature";
10
- import { featureModule } from "../feature/feature";
11
- import * as Reducer from "../reducer/reducer";
12
- import { scene } from "../scene/scene";
13
- import * as State from "../state/state";
14
- import { Engine, Reducers } from "./loop";
15
- import { makeAppRuntime } from "./appRuntime";
1
+ import { Context, Effect, Layer, Option, Schema as S } from 'effect'
2
+ import { expect, it } from '@effect/vitest'
3
+ import * as Composition from '../compose/composition'
4
+ import { provide } from '../compose/provide'
5
+ import { mount } from '../compose/structure'
6
+ import * as Ui from '../compose/ui'
7
+ import { ui } from '../compose/ui'
8
+ import * as Event from '../event/event'
9
+ import * as Feature from '../feature/feature'
10
+ import { featureModule } from '../feature/feature'
11
+ import * as Reducer from '../reducer/reducer'
12
+ import { scene } from '../scene/scene'
13
+ import * as State from '../state/state'
14
+ import { until } from '../testkit/flight.testkit'
15
+ import { Engine, Reducers } from './loop'
16
+ import { makeAppRuntime } from './appRuntime'
16
17
 
17
18
  // The host-level mount lifecycle: `RuntimeHandle.mountFeature` forks the mount,
18
19
  // and its disposer must CANCEL a mount whose fiber has not run yet (StrictMode
@@ -20,38 +21,42 @@ import { makeAppRuntime } from "./appRuntime";
20
21
  // stale mount would still register reducers and dispatch `binding.boot` onto the
21
22
  // shared bus.
22
23
 
23
- class Tick extends Event.make("appRuntime.Tick", S.Struct({})) {}
24
- class Count extends State.make("appRuntime.count", S.Number) {}
25
- class CountReducer extends Reducer.make("appRuntime.CountReducer", {
24
+ class Tick extends Event.make('appRuntime.Tick', S.Struct({})) {}
25
+ class Count extends State.make('appRuntime.count', S.Number) {}
26
+ class CountReducer extends Reducer.make('appRuntime.CountReducer', {
26
27
  states: [Count],
27
28
  events: [Tick],
28
29
  }) {}
29
30
 
30
- class CtrUi extends ui("appRuntime.CtrUi")<{ props: {} }>() {}
31
- class CtrComp extends Composition.make("appRuntime.CtrComp", {
32
- title: "Counter",
31
+ class CtrUi extends ui('appRuntime.CtrUi')<{ props: {} }>() {}
32
+ class CtrComp extends Composition.make('appRuntime.CtrComp', {
33
+ title: 'Counter',
33
34
  ui: CtrUi,
34
- }) {}
35
+ })<CtrComp>() {}
36
+ const CtrLive = Composition.live(CtrComp, function* () {
37
+ return mount({ props: {}, slots: {}, events: {} })
38
+ })
35
39
 
36
- class CounterFeature extends Feature.make("appRuntime.counter", {
40
+ class CounterFeature extends Feature.make('appRuntime.counter', {
37
41
  composition: CtrComp,
38
42
  module: featureModule(
39
43
  [],
40
- Reducer.live(CountReducer, (n) => n + 1).pipe(
41
- Layer.provideMerge(State.live(Count, 0)),
44
+ Layer.merge(
45
+ CtrLive,
46
+ Reducer.live(CountReducer, (n) => n + 1).pipe(Layer.provideMerge(State.live(Count, 0))),
42
47
  ),
43
48
  ),
44
49
  boot: [Event.construct(Tick, {})],
45
50
  }) {}
46
51
 
47
- class RootUi extends ui("appRuntime.RootUi")<{ props: {} }>() {}
48
- class RootComp extends Composition.make("appRuntime.RootComp", {
49
- title: "Root",
52
+ class RootUi extends ui('appRuntime.RootUi')<{ props: {} }>() {}
53
+ class RootComp extends Composition.make('appRuntime.RootComp', {
54
+ title: 'Root',
50
55
  ui: RootUi,
51
- }) {}
56
+ })<RootComp>() {}
52
57
  const RootLive = Composition.live(RootComp, function* () {
53
- return mount({ props: {}, slots: {}, events: {} });
54
- });
58
+ return mount({ props: {}, slots: {}, events: {} })
59
+ })
55
60
 
56
61
  const rootScene = () =>
57
62
  scene(RootComp, {
@@ -66,132 +71,203 @@ const rootScene = () =>
66
71
  Layer.provideMerge(Engine),
67
72
  ),
68
73
  ],
69
- });
74
+ })
70
75
 
71
- const settle = Effect.sleep(Duration.millis(50));
72
76
  const appRuntime = Effect.acquireRelease(
73
77
  Effect.sync(() => makeAppRuntime(rootScene())),
74
78
  (app) => Effect.sync(() => app.dispose()),
75
- );
79
+ )
76
80
 
77
81
  interface ParentValueService {
78
- readonly value: string;
82
+ readonly value: string
79
83
  }
80
- const ParentValueBase: Context.TagClass<
81
- ParentValue,
82
- "appRuntime.ParentValue",
83
- ParentValueService
84
- > = Context.Tag("appRuntime.ParentValue")<ParentValue, ParentValueService>();
84
+ const ParentValueBase: Context.TagClass<ParentValue, 'appRuntime.ParentValue', ParentValueService> =
85
+ Context.Tag('appRuntime.ParentValue')<ParentValue, ParentValueService>()
85
86
  class ParentValue extends ParentValueBase {}
86
87
 
87
88
  interface ChildValueService {
88
- readonly value: string;
89
+ readonly value: string
89
90
  }
90
- const ChildValueBase: Context.TagClass<
91
- ChildValue,
92
- "appRuntime.ChildValue",
93
- ChildValueService
94
- > = Context.Tag("appRuntime.ChildValue")<ChildValue, ChildValueService>();
91
+ const ChildValueBase: Context.TagClass<ChildValue, 'appRuntime.ChildValue', ChildValueService> =
92
+ Context.Tag('appRuntime.ChildValue')<ChildValue, ChildValueService>()
95
93
  class ChildValue extends ChildValueBase {}
96
94
 
97
- class ParentFeature extends Feature.make("appRuntime.parent", {
95
+ class ParentFeature extends Feature.make('appRuntime.parent', {
98
96
  composition: CtrComp,
99
97
  module: featureModule(
100
98
  [],
101
- Layer.succeed(ParentValue, { value: "from-parent" }),
99
+ Layer.merge(CtrLive, Layer.succeed(ParentValue, { value: 'from-parent' })),
102
100
  ),
103
101
  }) {}
104
102
 
105
- class ChildFeature extends Feature.make("appRuntime.child", {
103
+ class ChildFeature extends Feature.make('appRuntime.child', {
106
104
  composition: CtrComp,
107
105
  module: featureModule(
108
106
  [ParentValue],
109
- Layer.effect(
110
- ChildValue,
111
- Effect.map(ParentValue, ({ value }) => ({ value })),
107
+ Layer.merge(
108
+ CtrLive,
109
+ Layer.effect(
110
+ ChildValue,
111
+ Effect.map(ParentValue, ({ value }) => ({ value })),
112
+ ),
112
113
  ),
113
114
  ),
114
115
  }) {}
115
116
 
117
+ const CounterNative = Feature.tree<typeof CounterFeature>(CounterFeature.token)
118
+ const ParentNative = Feature.tree<typeof ParentFeature>(ParentFeature.token)
119
+ const ChildNative = Feature.tree<typeof ChildFeature>(ChildFeature.token)
120
+
116
121
  it.scopedLive(
117
- "disposing a mount before its fiber runs cancels it — no registration, no boot, no handler calls",
122
+ 'disposing a mount before its fiber runs cancels it — no registration, no boot, no handler calls',
118
123
  () =>
119
124
  Effect.gen(function* () {
120
- const app = yield* appRuntime;
121
- const baseline = app.read(Reducers).entries.length;
122
- const calls = { live: 0, failed: 0 };
123
- const dispose = app.mountFeature(CounterFeature.binding, {
125
+ const app = yield* appRuntime
126
+ const baseline = app.read(Reducers).entries.length
127
+ const calls = { live: 0, failed: 0 }
128
+ const dispose = app.mountFeature(CounterNative.binding, {
124
129
  onLive: () => {
125
- calls.live += 1;
130
+ calls.live += 1
126
131
  },
127
132
  onFailed: () => {
128
- calls.failed += 1;
133
+ calls.failed += 1
129
134
  },
130
- });
135
+ })
131
136
  // Same-tick dispose (the StrictMode cleanup shape): the fork has not run yet.
132
- dispose();
133
- yield* settle;
134
- expect(calls).toEqual({ live: 0, failed: 0 });
135
- expect(app.read(Reducers).entries.length).toBe(baseline);
136
- expect(app.read(Reducers).byTag.get("appRuntime.Tick")).toBeUndefined();
137
+ dispose()
138
+ // A probe mount forked behind the cancelled one: once it is live, the
139
+ // cancelled fork's turn has provably passed.
140
+ const probe = { live: 0, failed: 0 }
141
+ const disposeProbe = app.mountFeature(CounterNative.binding, {
142
+ onLive: () => {
143
+ probe.live += 1
144
+ },
145
+ onFailed: () => {
146
+ probe.failed += 1
147
+ },
148
+ })
149
+ yield* until(
150
+ () => probe.live + probe.failed,
151
+ (n) => n > 0,
152
+ )
153
+ expect(calls).toEqual({ live: 0, failed: 0 })
154
+ expect(app.read(Reducers).entries.length).toBe(baseline + 1)
155
+
156
+ disposeProbe()
157
+ yield* until(
158
+ () => app.read(Reducers).entries.length,
159
+ (n) => n === baseline,
160
+ )
161
+ expect(app.read(Reducers).entries.length).toBe(baseline)
162
+ expect(app.read(Reducers).byTag.get('appRuntime.Tick')).toBeUndefined()
137
163
  }),
138
- );
164
+ )
139
165
 
140
166
  it.scopedLive(
141
- "an undisposed mount still goes live, and disposing it after live reclaims the registration",
167
+ 'an undisposed mount still goes live, and disposing it after live reclaims the registration',
142
168
  () =>
143
169
  Effect.gen(function* () {
144
- const app = yield* appRuntime;
145
- const baseline = app.read(Reducers).entries.length;
146
- const calls = { live: 0, failed: 0 };
147
- const dispose = app.mountFeature(CounterFeature.binding, {
170
+ const app = yield* appRuntime
171
+ const baseline = app.read(Reducers).entries.length
172
+ const calls = { live: 0, failed: 0 }
173
+ const dispose = app.mountFeature(CounterNative.binding, {
148
174
  onLive: () => {
149
- calls.live += 1;
175
+ calls.live += 1
150
176
  },
151
177
  onFailed: () => {
152
- calls.failed += 1;
178
+ calls.failed += 1
179
+ },
180
+ })
181
+ yield* until(
182
+ () => calls.live,
183
+ (n) => n === 1,
184
+ )
185
+ expect(calls).toEqual({ live: 1, failed: 0 })
186
+ expect(app.read(Reducers).entries.length).toBe(baseline + 1)
187
+ dispose()
188
+ yield* until(
189
+ () => app.read(Reducers).entries.length,
190
+ (n) => n === baseline,
191
+ )
192
+ expect(app.read(Reducers).entries.length).toBe(baseline)
193
+ }),
194
+ )
195
+
196
+ it.scopedLive('a nested Feature mounts against its immediate parent context', () =>
197
+ Effect.gen(function* () {
198
+ const app = yield* appRuntime
199
+ const observed: Array<string> = []
200
+ const disposers: Array<() => void> = []
201
+ yield* Effect.addFinalizer(() =>
202
+ Effect.sync(() => disposers.reverse().forEach((dispose) => dispose())),
203
+ )
204
+ disposers.push(
205
+ app.mountFeature(ParentNative.binding, {
206
+ onFailed: () => observed.push('parent-failed'),
207
+ onLive: (parent) => {
208
+ disposers.push(
209
+ parent.mountFeature(ChildNative.binding, {
210
+ onFailed: () => observed.push('child-failed'),
211
+ onLive: (child) => observed.push(child.read(ChildValue).value),
212
+ }),
213
+ )
214
+ },
215
+ }),
216
+ )
217
+ yield* until(
218
+ () => observed.length,
219
+ (n) => n > 0,
220
+ )
221
+ expect(observed).toEqual(['from-parent'])
222
+ }),
223
+ )
224
+
225
+ it.scopedLive(
226
+ 'an existential Feature binding validates its graph and exposes only safe optional reads',
227
+ () =>
228
+ Effect.gen(function* () {
229
+ const app = yield* appRuntime
230
+ const observed: Array<string> = []
231
+ const binding: Feature.AnyFeatureBinding = ParentNative.binding
232
+ const dispose = app.mountAnyFeature(binding, {
233
+ props: {},
234
+ onFailed: () => observed.push('failed'),
235
+ onLive: (child) => {
236
+ const service = child.readOption(ParentValue)
237
+ observed.push(
238
+ Option.match(service, {
239
+ onNone: () => 'missing',
240
+ onSome: ({ value }) => value,
241
+ }),
242
+ )
153
243
  },
154
- });
155
- yield* Effect.sleep(Duration.millis(1)).pipe(
156
- Effect.repeat({ until: () => calls.live === 1 }),
157
- Effect.timeout(Duration.seconds(2)),
158
- );
159
- expect(calls).toEqual({ live: 1, failed: 0 });
160
- expect(app.read(Reducers).entries.length).toBe(baseline + 1);
161
- dispose();
162
- yield* settle;
163
- expect(app.read(Reducers).entries.length).toBe(baseline);
244
+ })
245
+ yield* Effect.addFinalizer(() => Effect.sync(dispose))
246
+ yield* until(
247
+ () => observed.length,
248
+ (n) => n > 0,
249
+ )
250
+ expect(observed).toEqual(['from-parent'])
164
251
  }),
165
- );
252
+ )
166
253
 
167
254
  it.scopedLive(
168
- "a nested Feature mounts against its immediate parent context",
255
+ 'an existential Feature binding fails before build when a requirement is absent',
169
256
  () =>
170
257
  Effect.gen(function* () {
171
- const app = yield* appRuntime;
172
- const observed: Array<string> = [];
173
- const disposers: Array<() => void> = [];
174
- yield* Effect.addFinalizer(() =>
175
- Effect.sync(() => disposers.reverse().forEach((dispose) => dispose())),
176
- );
177
- disposers.push(
178
- app.mountFeature(ParentFeature.binding, {
179
- onFailed: () => observed.push("parent-failed"),
180
- onLive: (parent) => {
181
- disposers.push(
182
- parent.mountFeature(ChildFeature.binding, {
183
- onFailed: () => observed.push("child-failed"),
184
- onLive: (child) =>
185
- observed.push(child.read(ChildValue).value),
186
- }),
187
- );
188
- },
189
- }),
190
- );
191
- yield* Effect.sleep(Duration.millis(1)).pipe(
192
- Effect.repeat({ until: () => observed.length > 0 }),
193
- Effect.timeout(Duration.seconds(2)),
194
- );
195
- expect(observed).toEqual(["from-parent"]);
258
+ const app = yield* appRuntime
259
+ const observed: Array<string> = []
260
+ const binding: Feature.AnyFeatureBinding = ChildNative.binding
261
+ const dispose = app.mountAnyFeature(binding, {
262
+ props: {},
263
+ onFailed: () => observed.push('failed'),
264
+ onLive: () => observed.push('live'),
265
+ })
266
+ yield* Effect.addFinalizer(() => Effect.sync(dispose))
267
+ yield* until(
268
+ () => observed.length,
269
+ (n) => n > 0,
270
+ )
271
+ expect(observed).toEqual(['failed'])
196
272
  }),
197
- );
273
+ )
@@ -1,131 +1,37 @@
1
- import { Cause, Context, Effect, Exit, Fiber, Layer, ManagedRuntime, Option, Scope } from 'effect'
2
- import { FeatureLoadFailed, forceSync } from '../internal/errors'
3
- import { type FeatureBinding, mountFeature } from '../feature/feature'
4
- import { type Node } from '../ui/node'
5
- import { type MountedServices, type Scene, sceneInstrumentation } from '../scene/scene'
6
- import { publish, type Priority, type Tagged } from './bus'
7
- import { CurrentEventBudget, type ReformOptions, resolveEventBudget } from './eventBudget'
8
- import { type Instrumentation } from './instrumentation'
9
-
10
- export interface FeatureMountHandlersExternalApi {
11
- readonly props?: unknown
12
- readonly onLive: (runtime: RuntimeHandle) => void
13
- readonly onFailed: (error: FeatureLoadFailed) => void
14
- }
15
- export type FeatureMountHandlers = FeatureMountHandlersExternalApi
16
-
17
- export interface RuntimeHandle {
18
- read<Identifier, Service>(tag: Context.Tag<Identifier, Service>): Service
19
- runRender(effect: Effect.Effect<Node, never, never>): Node
20
- dispatch(priority: Priority, event: Tagged): void
21
- mountFeature(binding: FeatureBinding, handlers: FeatureMountHandlers): () => void
22
- readonly instrumentation: Instrumentation
23
- }
24
-
25
- export interface AppRuntime extends RuntimeHandle {
26
- boot(): void
27
- dispose(): void
28
- isDisposed(): boolean
29
- }
30
-
31
- export const makeAppRuntime = (scene: Scene, options: ReformOptions = {}): AppRuntime => {
32
- const instrumentation = sceneInstrumentation(scene)
33
- const provide = scene.provide.map(Layer.locally(CurrentEventBudget, resolveEventBudget(options)))
34
- const layer = provide.reduce((accLayer, nextLayer) => Layer.merge(accLayer, nextLayer))
35
- const runtime = ManagedRuntime.make(layer)
36
- const rootContext = forceSync(() => runtime.runSync(Effect.context<MountedServices>()))
37
-
38
- // Inert past dispose: host may still render tearing-down trees; forked fibers outlive cleanup
39
- const status = { disposed: false, booted: false }
40
-
41
- // Self-contained Effect runs on default runtime after dispose (never throw mid-render)
42
- const runRender = (effect: Effect.Effect<Node, never, never>): Node =>
43
- status.disposed ? Effect.runSync(effect) : runtime.runSync(effect)
44
-
45
- const dispatch = (priority: Priority, event: Tagged): void => {
46
- if (status.disposed) {
47
- return
48
- }
49
- runtime.runFork(publish(priority, event))
50
- }
51
-
52
- const makeChildRuntime = <R>(context: Context.Context<R>): RuntimeHandle => ({
53
- read: <Identifier, Service>(tag: Context.Tag<Identifier, Service>): Service =>
54
- Context.unsafeGet(context, tag),
55
- runRender,
56
- dispatch,
57
- mountFeature: (binding, handlers) => mountFeatureOnto(context, binding, handlers),
58
- instrumentation,
59
- })
60
-
61
- const mountFeatureOnto = <R>(
62
- hostContext: Context.Context<R>,
63
- binding: FeatureBinding,
64
- handlers: FeatureMountHandlers,
65
- ): (() => void) => {
66
- if (status.disposed) {
67
- return () => {}
68
- }
69
- const scope = Effect.runSync(Scope.make())
70
- const fiber = runtime.runFork(
71
- mountFeature(binding, hostContext, handlers.props).pipe(
72
- Effect.provideService(Scope.Scope, scope),
73
- Effect.matchCause({
74
- onFailure: (cause) => {
75
- // Disposer-cancelled mounts are not failures
76
- if (!Cause.isInterruptedOnly(cause)) {
77
- handlers.onFailed(
78
- Option.getOrElse(
79
- Cause.failureOption(cause),
80
- () => new FeatureLoadFailed({ cause }),
81
- ),
82
- )
83
- }
84
- },
85
- onSuccess: (context) => handlers.onLive(makeChildRuntime(context)),
86
- }),
87
- ),
88
- )
89
- return () => {
90
- // Interrupt sync before scheduler: Scope.close alone does not interrupt (StrictMode)
91
- fiber.unsafeInterruptAsFork(fiber.id())
92
- // Await mount so mid-build finalizers register before scope reclaim; close on default runtime if disposed
93
- const close = Fiber.await(fiber).pipe(
94
- Effect.zipRight(Scope.close(scope, Exit.succeed(undefined))),
95
- )
96
- if (status.disposed) {
97
- Effect.runFork(close)
98
- } else {
99
- runtime.runFork(close)
100
- }
101
- }
102
- }
103
-
104
- const app: AppRuntime = {
105
- // Read captured context so tearing-down trees never hit ManagedRuntime disposed
106
- read: <Identifier, Service>(tag: Context.Tag<Identifier, Service>): Service =>
107
- Context.unsafeGet(rootContext, tag),
108
- runRender,
109
- dispatch,
110
- mountFeature: (binding, handlers) => mountFeatureOnto(rootContext, binding, handlers),
111
- boot: () => {
112
- if (status.disposed || status.booted) {
113
- return
114
- }
115
- status.booted = true
116
- Option.fromNullable(scene.boot)
117
- .pipe(Option.getOrElse((): ReadonlyArray<Tagged> => []))
118
- .forEach((event) => dispatch('High', event))
119
- },
120
- dispose: () => {
121
- if (status.disposed) {
122
- return
123
- }
124
- status.disposed = true
125
- void runtime.dispose()
126
- },
127
- isDisposed: () => status.disposed,
128
- instrumentation,
129
- }
130
- return app
1
+ import type { CompositionId, RequiredSlotIdentities } from '../compose/composition'
2
+ import type { UiContract } from '../compose/ui'
3
+ import type { AnyScene, CapturedScene, Scene } from '../scene/scene'
4
+ import { makeCapturedAppRuntime } from './capturedAppRuntime'
5
+ import type { ReformOptions } from './eventBudget'
6
+ import type { AppRuntime, UnspecifiedRuntimeServices } from './runtimeHandle'
7
+
8
+ export { UnspecifiedRuntimeServicesTypeId } from './runtimeHandle'
9
+ export type {
10
+ AnyFeatureMountHandlers,
11
+ AppRuntime,
12
+ CapturedFeatureMountHandlers,
13
+ FeatureMountHandlers,
14
+ FeatureMountHandlersExternalApi,
15
+ RuntimeFeatureAvailability,
16
+ RuntimeHandle,
17
+ UnspecifiedRuntimeServices,
18
+ } from './runtimeHandle'
19
+
20
+ export function makeAppRuntime<
21
+ C extends UiContract,
22
+ S extends ReadonlyArray<unknown>,
23
+ Services,
24
+ P,
25
+ N extends string,
26
+ Identity,
27
+ >(
28
+ scene: CapturedScene<C, S, Services, P, N, Identity>,
29
+ options?: ReformOptions,
30
+ ): AppRuntime<Services | CompositionId<N, Identity, RequiredSlotIdentities<C>>>
31
+ export function makeAppRuntime<C extends UiContract, S extends ReadonlyArray<unknown>>(
32
+ scene: Scene<C, S>,
33
+ options?: ReformOptions,
34
+ ): AppRuntime<UnspecifiedRuntimeServices>
35
+ export function makeAppRuntime(scene: AnyScene, options: ReformOptions = {}): unknown {
36
+ return scene.captureAny<unknown>((exactScene) => makeCapturedAppRuntime(exactScene, options))
131
37
  }