@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.
- package/README.md +85 -55
- package/package.json +17 -17
- package/src/boundary/boundary.test.ts +82 -33
- package/src/boundary/boundary.ts +141 -32
- package/src/calc/asyncCalc.invalidate.test.ts +733 -32
- package/src/calc/asyncCalc.test.ts +397 -225
- package/src/calc/asyncCalc.ts +115 -100
- package/src/calc/asyncCalcTypes.ts +147 -0
- package/src/calc/calc.test.ts +13 -16
- package/src/calc/calc.ts +44 -18
- package/src/calc/calcFamily.test.ts +90 -32
- package/src/calc/calcFamily.ts +56 -25
- package/src/calc/compose.ts +1 -14
- package/src/channel/channel.ts +25 -6
- package/src/channel/{procedureRegistry.ts → procedures.ts} +41 -23
- package/src/compose/composition.test.ts +19 -0
- package/src/compose/composition.ts +168 -81
- package/src/compose/compositionTypes.ts +249 -0
- package/src/compose/props.ts +13 -6
- package/src/compose/provide.ts +187 -46
- package/src/compose/slot.ts +156 -19
- package/src/compose/structure.test.ts +6 -3
- package/src/compose/structure.ts +106 -13
- package/src/compose/ui.test.ts +19 -3
- package/src/compose/ui.ts +147 -54
- package/src/compose/ui.typecheck.ts +40 -4
- package/src/definition/definition.ts +22 -9
- package/src/event/event.fromSource.test.ts +172 -0
- package/src/event/event.test.ts +10 -0
- package/src/event/event.ts +102 -18
- package/src/feature/feature.mount.test.ts +123 -56
- package/src/feature/feature.test.ts +67 -63
- package/src/feature/feature.ts +222 -262
- package/src/feature/feature.typecheck.ts +224 -91
- package/src/feature/featureBinding.ts +130 -20
- package/src/feature/featureClass.ts +162 -0
- package/src/feature/featureConfig.ts +192 -0
- package/src/feature/featureFactory.ts +163 -0
- package/src/feature/featureModule.ts +136 -0
- package/src/feature/featureModule.typecheck.ts +63 -0
- package/src/feature/featureNativeTree.ts +152 -0
- package/src/feature/featureRequirement.ts +82 -0
- package/src/feature/featureVariants.ts +234 -0
- package/src/feature/mountFeature.ts +54 -0
- package/src/graph/closure.ts +222 -0
- package/src/graph/services.ts +95 -0
- package/src/graph/summary.ts +134 -0
- package/src/index.ts +171 -245
- package/src/internal/bucketCache.ts +39 -0
- package/src/internal/capture.ts +9 -4
- package/src/internal/env.ts +1 -3
- package/src/internal/errors.ts +21 -10
- package/src/internal/queryDriver.ts +61 -25
- package/src/internal/queryDriverHydrate.ts +45 -0
- package/src/internal/queryDriverStore.ts +8 -5
- package/src/internal/queryDriverTypes.ts +11 -11
- package/src/internal/reuse.test.ts +10 -3
- package/src/internal/reuse.ts +5 -2
- package/src/internal/scheduler.ts +4 -1
- package/src/internal/sources.ts +25 -11
- package/src/internal/store.test.ts +27 -6
- package/src/internal/track.ts +3 -2
- package/src/internal.ts +222 -0
- package/src/namespace/namespace.ts +46 -25
- package/src/procedure/procedure.ts +38 -11
- package/src/reducer/reducer.ts +78 -34
- package/src/remote/pendingQueue.ts +145 -0
- package/src/remote/remoteState.test.ts +536 -354
- package/src/remote/remoteState.ts +145 -106
- package/src/remote/remoteState.typecheck.ts +47 -23
- package/src/remote/remoteStateMake.ts +97 -0
- package/src/remote/remoteStateSend.ts +95 -37
- package/src/remote/remoteStateTypes.ts +155 -0
- package/src/runtime/appRuntime.activation.test.ts +104 -0
- package/src/runtime/appRuntime.test.ts +187 -111
- package/src/runtime/appRuntime.ts +36 -130
- package/src/runtime/capturedAppRuntime.ts +274 -0
- package/src/runtime/eventBudget.test.ts +39 -12
- package/src/runtime/eventBudget.ts +9 -8
- package/src/runtime/featureMount.ts +94 -0
- package/src/runtime/hardening.test.ts +22 -13
- package/src/runtime/instrumentation.test.ts +213 -194
- package/src/runtime/instrumentation.ts +6 -11
- package/src/runtime/loop.test.ts +84 -7
- package/src/runtime/loop.ts +69 -40
- package/src/runtime/runtimeHandle.ts +124 -0
- package/src/scene/featureScene.test.ts +4 -2
- package/src/scene/scene.ts +234 -64
- package/src/scene/seedScene.test.ts +69 -86
- package/src/state/state.nominal.typecheck.ts +68 -0
- package/src/state/state.test.ts +17 -0
- package/src/state/state.ts +79 -26
- package/src/state/stateFamily.test.ts +39 -8
- package/src/state/stateFamily.ts +55 -22
- package/src/state/stateGroup.ts +53 -17
- package/src/state/token.ts +40 -10
- package/src/synced/syncedStore.ts +46 -23
- package/src/testkit/flight.testkit.ts +84 -0
- package/src/wire/tree.test.ts +8 -4
- package/src/wire/triggers.test.ts +6 -2
- package/src/wire/triggers.ts +14 -10
- package/src/calc/asyncCalcDefinitions.ts +0 -48
- package/src/internal/ctx.ts +0 -9
- package/src/remote/remoteStateDefinition.ts +0 -135
- package/src/remote/remoteStateLayers.ts +0 -118
- package/src/remote/remoteStateLiveTypes.ts +0 -59
|
@@ -1,18 +1,19 @@
|
|
|
1
|
-
import { Context,
|
|
2
|
-
import { expect, it } from
|
|
3
|
-
import * as Composition from
|
|
4
|
-
import { provide } from
|
|
5
|
-
import { mount } from
|
|
6
|
-
import * as Ui from
|
|
7
|
-
import { ui } from
|
|
8
|
-
import * as Event from
|
|
9
|
-
import * as Feature from
|
|
10
|
-
import { featureModule } from
|
|
11
|
-
import * as Reducer from
|
|
12
|
-
import { scene } from
|
|
13
|
-
import * as State from
|
|
14
|
-
import {
|
|
15
|
-
import {
|
|
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(
|
|
24
|
-
class Count extends State.make(
|
|
25
|
-
class CountReducer extends Reducer.make(
|
|
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(
|
|
31
|
-
class CtrComp extends Composition.make(
|
|
32
|
-
title:
|
|
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(
|
|
40
|
+
class CounterFeature extends Feature.make('appRuntime.counter', {
|
|
37
41
|
composition: CtrComp,
|
|
38
42
|
module: featureModule(
|
|
39
43
|
[],
|
|
40
|
-
|
|
41
|
-
|
|
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(
|
|
48
|
-
class RootComp extends Composition.make(
|
|
49
|
-
title:
|
|
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(
|
|
95
|
+
class ParentFeature extends Feature.make('appRuntime.parent', {
|
|
98
96
|
composition: CtrComp,
|
|
99
97
|
module: featureModule(
|
|
100
98
|
[],
|
|
101
|
-
Layer.succeed(ParentValue, { value:
|
|
99
|
+
Layer.merge(CtrLive, Layer.succeed(ParentValue, { value: 'from-parent' })),
|
|
102
100
|
),
|
|
103
101
|
}) {}
|
|
104
102
|
|
|
105
|
-
class ChildFeature extends Feature.make(
|
|
103
|
+
class ChildFeature extends Feature.make('appRuntime.child', {
|
|
106
104
|
composition: CtrComp,
|
|
107
105
|
module: featureModule(
|
|
108
106
|
[ParentValue],
|
|
109
|
-
Layer.
|
|
110
|
-
|
|
111
|
-
|
|
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
|
-
|
|
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(
|
|
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
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
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
|
-
|
|
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(
|
|
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.
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
expect(
|
|
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
|
-
|
|
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
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
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 {
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
4
|
-
import {
|
|
5
|
-
import
|
|
6
|
-
import {
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
export
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
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
|
}
|