@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
package/src/runtime/loop.test.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { expect, it } from '@effect/vitest'
|
|
2
2
|
import { Duration, Effect, Layer, Schema as S, TestClock } from 'effect'
|
|
3
3
|
import { Channel, Engine, Event, Procedure, publish, Reducer, State } from '../index'
|
|
4
|
+
import { makeFlightGate, until } from '../testkit/flight.testkit'
|
|
4
5
|
|
|
5
6
|
it.live('a frame of events drains in one pass with a single notification flush', () => {
|
|
6
7
|
class Counter extends State.make('counter', S.Number) {}
|
|
@@ -22,7 +23,11 @@ it.live('a frame of events drains in one pass with a single notification flush',
|
|
|
22
23
|
yield* publish('Normal', Event.construct(Bumped, { to: 1 }))
|
|
23
24
|
yield* publish('Normal', Event.construct(Bumped, { to: 2 }))
|
|
24
25
|
yield* publish('Normal', Event.construct(Bumped, { to: 3 }))
|
|
25
|
-
|
|
26
|
+
// The flush is the frame's last act, so one notification means the fold is done.
|
|
27
|
+
yield* until(
|
|
28
|
+
() => notifications.n,
|
|
29
|
+
(n) => n >= 1,
|
|
30
|
+
)
|
|
26
31
|
|
|
27
32
|
expect(store.get()).toBe(3)
|
|
28
33
|
expect(notifications.n).toBe(1)
|
|
@@ -34,11 +39,12 @@ it.live('an exclusive channel serializes procedures that share it', () => {
|
|
|
34
39
|
class StartB extends Event.make('StartB', S.Struct({})) {}
|
|
35
40
|
class Exclusive extends Channel.make('Exclusive', { policy: { _tag: 'exclusive' } }) {}
|
|
36
41
|
|
|
42
|
+
const gate = makeFlightGate()
|
|
37
43
|
const concurrency = { active: 0, max: 0 }
|
|
38
44
|
const guarded = function* () {
|
|
39
45
|
concurrency.active += 1
|
|
40
46
|
concurrency.max = Math.max(concurrency.max, concurrency.active)
|
|
41
|
-
yield*
|
|
47
|
+
yield* gate.through(Effect.void)
|
|
42
48
|
concurrency.active -= 1
|
|
43
49
|
}
|
|
44
50
|
|
|
@@ -52,9 +58,20 @@ it.live('an exclusive channel serializes procedures that share it', () => {
|
|
|
52
58
|
)
|
|
53
59
|
|
|
54
60
|
return Effect.gen(function* () {
|
|
61
|
+
// Parking the first run IS the overlap: a shared lane that is not exclusive
|
|
62
|
+
// would start the second procedure while the first sits at the gate.
|
|
63
|
+
yield* gate.hold
|
|
55
64
|
yield* publish('Normal', Event.construct(StartA, {}))
|
|
56
65
|
yield* publish('Normal', Event.construct(StartB, {}))
|
|
57
|
-
yield*
|
|
66
|
+
yield* gate.awaitEntry(1)
|
|
67
|
+
expect(gate.entered()).toBe(1)
|
|
68
|
+
|
|
69
|
+
yield* gate.release
|
|
70
|
+
yield* gate.awaitEntry(2)
|
|
71
|
+
yield* until(
|
|
72
|
+
() => concurrency.active,
|
|
73
|
+
(active) => active === 0,
|
|
74
|
+
)
|
|
58
75
|
expect(concurrency.max).toBe(1)
|
|
59
76
|
}).pipe(Effect.provide(TestLayer))
|
|
60
77
|
})
|
|
@@ -79,7 +96,11 @@ it.live('two procedures sharing a channel each run once for one shared event', (
|
|
|
79
96
|
|
|
80
97
|
return Effect.gen(function* () {
|
|
81
98
|
yield* publish('High', Event.construct(Shared, {}))
|
|
82
|
-
yield*
|
|
99
|
+
yield* until(
|
|
100
|
+
() => runs.a + runs.b,
|
|
101
|
+
(total) => total >= 2,
|
|
102
|
+
)
|
|
103
|
+
// A duplicated offer would land in the same frame and double one of them.
|
|
83
104
|
expect(runs.a).toBe(1)
|
|
84
105
|
expect(runs.b).toBe(1)
|
|
85
106
|
}).pipe(Effect.provide(TestLayer))
|
|
@@ -126,10 +147,12 @@ it.live('a reducer ignores events outside its declared set (no-op)', () => {
|
|
|
126
147
|
return Effect.gen(function* () {
|
|
127
148
|
const store = yield* Counter.store
|
|
128
149
|
yield* publish('Normal', Event.construct(Unrelated, {}))
|
|
129
|
-
yield* Effect.sleep(Duration.millis(10))
|
|
130
|
-
expect(store.get()).toBe(0)
|
|
131
150
|
yield* publish('Normal', Event.construct(Bump, {}))
|
|
132
|
-
yield*
|
|
151
|
+
yield* until(
|
|
152
|
+
() => store.get(),
|
|
153
|
+
(n) => n > 0,
|
|
154
|
+
)
|
|
155
|
+
// Both events fold in one frame: a reducer that also took Unrelated reads 2.
|
|
133
156
|
expect(store.get()).toBe(1)
|
|
134
157
|
}).pipe(Effect.provide(TestLayer))
|
|
135
158
|
})
|
|
@@ -159,3 +182,57 @@ it.effect('a latest channel cancels the in-flight run when a new event arrives',
|
|
|
159
182
|
expect(counts.completed).toBe(1)
|
|
160
183
|
}).pipe(Effect.provide(TestLayer))
|
|
161
184
|
})
|
|
185
|
+
|
|
186
|
+
it.live('a fold returning the previous reference keeps the exact store value', () => {
|
|
187
|
+
const Viewer = S.Union(
|
|
188
|
+
S.TaggedStruct('Closed', {}),
|
|
189
|
+
S.TaggedStruct('Open', { revision: S.Number }),
|
|
190
|
+
)
|
|
191
|
+
class ViewerState extends State.make('viewer', Viewer) {}
|
|
192
|
+
class Marker extends State.make('marker', S.Number) {}
|
|
193
|
+
class Poked extends Event.make('Poked', S.Struct({})) {}
|
|
194
|
+
class PokeReducer extends Reducer.make('PokeReducer', {
|
|
195
|
+
states: [ViewerState],
|
|
196
|
+
events: [Poked],
|
|
197
|
+
}) {}
|
|
198
|
+
// Closed is a fixpoint: the fold hands back the previous reference untouched.
|
|
199
|
+
const PokeReducerLive = Reducer.live(PokeReducer, (viewer) =>
|
|
200
|
+
viewer._tag === 'Open' ? { ...viewer, revision: viewer.revision + 1 } : viewer,
|
|
201
|
+
)
|
|
202
|
+
class MarkReducer extends Reducer.make('MarkReducer', {
|
|
203
|
+
states: [Marker],
|
|
204
|
+
events: [Poked],
|
|
205
|
+
}) {}
|
|
206
|
+
const MarkReducerLive = Reducer.live(MarkReducer, (n) => n + 1)
|
|
207
|
+
|
|
208
|
+
const TestLayer = Layer.mergeAll(PokeReducerLive, MarkReducerLive).pipe(
|
|
209
|
+
Layer.provideMerge(
|
|
210
|
+
Layer.mergeAll(State.live(ViewerState, { _tag: 'Closed' }), State.live(Marker, 0), Engine),
|
|
211
|
+
),
|
|
212
|
+
)
|
|
213
|
+
|
|
214
|
+
return Effect.gen(function* () {
|
|
215
|
+
const store = yield* ViewerState.store
|
|
216
|
+
const marker = yield* Marker.store
|
|
217
|
+
const before = store.get()
|
|
218
|
+
const notifications = { n: 0 }
|
|
219
|
+
const flushed = { n: 0 }
|
|
220
|
+
store.subscribe(() => {
|
|
221
|
+
notifications.n += 1
|
|
222
|
+
})
|
|
223
|
+
marker.subscribe(() => {
|
|
224
|
+
flushed.n += 1
|
|
225
|
+
})
|
|
226
|
+
yield* publish('Normal', Event.construct(Poked, {}))
|
|
227
|
+
// The marker folds on the same event and shares the scheduler's flush: once
|
|
228
|
+
// its listener fires, a viewer notification would have run in that batch too.
|
|
229
|
+
yield* until(
|
|
230
|
+
() => flushed.n,
|
|
231
|
+
(n) => n >= 1,
|
|
232
|
+
)
|
|
233
|
+
// Reference identity is the repaint signal: an unchanged fold must not be
|
|
234
|
+
// re-decoded into a fresh, structurally-equal value that wakes subscribers.
|
|
235
|
+
expect(store.get()).toBe(before)
|
|
236
|
+
expect(notifications.n).toBe(0)
|
|
237
|
+
}).pipe(Effect.provide(TestLayer))
|
|
238
|
+
})
|
package/src/runtime/loop.ts
CHANGED
|
@@ -1,10 +1,6 @@
|
|
|
1
1
|
import { Array as Arr, Cause, Chunk, Context, Effect, Layer, Option, PubSub, Queue } from 'effect'
|
|
2
|
-
import {
|
|
3
|
-
|
|
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
|
|
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
|
-
|
|
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,
|
|
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
|
-
|
|
37
|
-
if (
|
|
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
|
|
46
|
-
const
|
|
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
|
|
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
|
-
|
|
61
|
+
entries.add(entry)
|
|
62
|
+
snapshot.value = Option.none()
|
|
63
|
+
getOrCreateBucket(byName, entry.name).add(entry)
|
|
54
64
|
entry.handles.forEach((tag) => {
|
|
55
|
-
|
|
56
|
-
|
|
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
|
-
|
|
65
|
-
|
|
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
|
|
71
|
-
|
|
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({
|
|
82
|
-
|
|
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
|
-
//
|
|
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
|
-
//
|
|
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
|
-
//
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
//
|
|
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>
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
import type { Context, Effect, Option } from 'effect'
|
|
2
|
+
import type {
|
|
3
|
+
CompositionActivation,
|
|
4
|
+
CompositionId,
|
|
5
|
+
RequiredSlotIdentities,
|
|
6
|
+
} from '../compose/composition'
|
|
7
|
+
import type { UiContract } from '../compose/ui'
|
|
8
|
+
import type {
|
|
9
|
+
AnyFeatureBinding,
|
|
10
|
+
CapturedFeatureBinding,
|
|
11
|
+
FeatureBinding,
|
|
12
|
+
FeatureRequirement,
|
|
13
|
+
} from '../feature/feature'
|
|
14
|
+
import type {
|
|
15
|
+
AnyFeatureGraphSummary,
|
|
16
|
+
FeatureSummaryCompositionIdentity,
|
|
17
|
+
FeatureSummaryContract,
|
|
18
|
+
FeatureSummaryInput,
|
|
19
|
+
FeatureSummaryMountProps,
|
|
20
|
+
FeatureSummaryName,
|
|
21
|
+
FeatureSummaryOpenServices,
|
|
22
|
+
FeatureSummaryUsesInput,
|
|
23
|
+
} from '../graph/closure'
|
|
24
|
+
import type { FeatureLoadFailed } from '../internal/errors'
|
|
25
|
+
import type { Node } from '../ui/node'
|
|
26
|
+
import type { Priority, Tagged } from './bus'
|
|
27
|
+
import type { Instrumentation } from './instrumentation'
|
|
28
|
+
import type { EngineServices } from './loop'
|
|
29
|
+
|
|
30
|
+
export const UnspecifiedRuntimeServicesTypeId: unique symbol = Symbol.for(
|
|
31
|
+
'reform/UnspecifiedRuntimeServices',
|
|
32
|
+
)
|
|
33
|
+
export type UnspecifiedRuntimeServicesTypeId = typeof UnspecifiedRuntimeServicesTypeId
|
|
34
|
+
export interface UnspecifiedRuntimeServices {
|
|
35
|
+
readonly [UnspecifiedRuntimeServicesTypeId]: UnspecifiedRuntimeServicesTypeId
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export type FeatureMountHandlersExternalApi<UsesInput extends boolean, Input, Services> = {
|
|
39
|
+
readonly onLive: (runtime: RuntimeHandle<Services>) => void
|
|
40
|
+
readonly onFailed: (error: FeatureLoadFailed) => void
|
|
41
|
+
} & (UsesInput extends true ? { readonly props: Input } : { readonly props?: never })
|
|
42
|
+
|
|
43
|
+
export type FeatureMountHandlers<
|
|
44
|
+
UsesInput extends boolean,
|
|
45
|
+
Input,
|
|
46
|
+
Services,
|
|
47
|
+
> = FeatureMountHandlersExternalApi<UsesInput, Input, Services>
|
|
48
|
+
|
|
49
|
+
export type RuntimeFeatureAvailability<Services, OpenServices> = [
|
|
50
|
+
EngineServices | OpenServices,
|
|
51
|
+
] extends [Services]
|
|
52
|
+
? object
|
|
53
|
+
: {
|
|
54
|
+
readonly missingFeatureServices: Exclude<EngineServices | OpenServices, Services>
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export interface AnyFeatureMountHandlers {
|
|
58
|
+
readonly props: unknown
|
|
59
|
+
readonly onLive: <Services>(runtime: RuntimeHandle<Services>) => void
|
|
60
|
+
readonly onFailed: (error: FeatureLoadFailed) => void
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
export interface CapturedFeatureMountHandlers<
|
|
64
|
+
Props,
|
|
65
|
+
HostServices,
|
|
66
|
+
FeatureServices,
|
|
67
|
+
C extends UiContract,
|
|
68
|
+
N extends string,
|
|
69
|
+
Identity,
|
|
70
|
+
> {
|
|
71
|
+
readonly props: Props
|
|
72
|
+
readonly onLive: (
|
|
73
|
+
runtime: RuntimeHandle<
|
|
74
|
+
HostServices | FeatureServices | CompositionId<N, Identity, RequiredSlotIdentities<C>>
|
|
75
|
+
>,
|
|
76
|
+
) => void
|
|
77
|
+
readonly onFailed: (error: FeatureLoadFailed) => void
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
export interface RuntimeHandle<Services = UnspecifiedRuntimeServices> {
|
|
81
|
+
read<Identifier extends Services, Service>(tag: Context.Tag<Identifier, Service>): Service
|
|
82
|
+
readOption<Identifier, Service>(tag: Context.Tag<Identifier, Service>): Option.Option<Service>
|
|
83
|
+
runRender(effect: Effect.Effect<Node, never, never>): Node
|
|
84
|
+
dispatch(priority: Priority, event: Tagged): void
|
|
85
|
+
mountScoped(activation: CompositionActivation): () => void
|
|
86
|
+
mountFeature<
|
|
87
|
+
Summary extends AnyFeatureGraphSummary,
|
|
88
|
+
ROut,
|
|
89
|
+
const DirectRequires extends ReadonlyArray<FeatureRequirement>,
|
|
90
|
+
>(
|
|
91
|
+
binding: [EngineServices | FeatureSummaryOpenServices<Summary>] extends [Services]
|
|
92
|
+
? FeatureBinding<Summary, ROut, DirectRequires>
|
|
93
|
+
: never,
|
|
94
|
+
handlers: FeatureMountHandlers<
|
|
95
|
+
FeatureSummaryUsesInput<Summary>,
|
|
96
|
+
FeatureSummaryInput<Summary>,
|
|
97
|
+
Services | ROut
|
|
98
|
+
>,
|
|
99
|
+
): () => void
|
|
100
|
+
mountCapturedFeature<
|
|
101
|
+
Summary extends AnyFeatureGraphSummary,
|
|
102
|
+
ROut,
|
|
103
|
+
const DirectRequires extends ReadonlyArray<FeatureRequirement>,
|
|
104
|
+
>(
|
|
105
|
+
binding: CapturedFeatureBinding<Summary, ROut, DirectRequires> &
|
|
106
|
+
RuntimeFeatureAvailability<Services, FeatureSummaryOpenServices<Summary>>,
|
|
107
|
+
handlers: CapturedFeatureMountHandlers<
|
|
108
|
+
FeatureSummaryMountProps<Summary>,
|
|
109
|
+
Services,
|
|
110
|
+
ROut,
|
|
111
|
+
FeatureSummaryContract<Summary>,
|
|
112
|
+
FeatureSummaryName<Summary>,
|
|
113
|
+
FeatureSummaryCompositionIdentity<Summary>
|
|
114
|
+
>,
|
|
115
|
+
): () => void
|
|
116
|
+
mountAnyFeature(binding: AnyFeatureBinding, handlers: AnyFeatureMountHandlers): () => void
|
|
117
|
+
readonly instrumentation: Instrumentation
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
export interface AppRuntime<Services = UnspecifiedRuntimeServices> extends RuntimeHandle<Services> {
|
|
121
|
+
boot(): void
|
|
122
|
+
dispose(): void
|
|
123
|
+
isDisposed(): boolean
|
|
124
|
+
}
|
|
@@ -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')<{
|
|
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
|