@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,16 +1,13 @@
1
- import { expect, it } from "@effect/vitest";
2
- import { Duration, Effect, Layer, Schema as S } from "effect";
1
+ import { expect, it } from '@effect/vitest'
2
+ import { Effect, Layer, Schema as S } from 'effect'
3
3
  import {
4
4
  AsyncCalc,
5
5
  Calc,
6
6
  Channel,
7
7
  Composition,
8
- CurrentInstrumentation,
9
8
  Engine,
10
9
  Event,
11
- type Instrumentation,
12
10
  mount,
13
- noopInstrumentation,
14
11
  type Priority,
15
12
  Procedure,
16
13
  profiledScene,
@@ -23,64 +20,66 @@ import {
23
20
  StateGroup,
24
21
  Ui,
25
22
  ui,
26
- } from "../index";
27
-
28
- const tick = (ms = 20) => Effect.sleep(Duration.millis(ms));
23
+ } from '../index'
24
+ import { makeFlightGate, until } from '../testkit/flight.testkit'
25
+ import {
26
+ CurrentInstrumentation,
27
+ type Instrumentation,
28
+ noopInstrumentation,
29
+ } from './instrumentation'
29
30
 
30
31
  const makeRecorder = () => {
31
- const events: Array<{ readonly tag: string; readonly priority: Priority }> =
32
- [];
33
- const reducers: Array<{ readonly name: string; readonly eventTag: string }> =
34
- [];
35
- const states: Array<string> = [];
36
- const calcs: Array<string> = [];
37
- const queries: Array<string> = [];
32
+ const events: Array<{ readonly tag: string; readonly priority: Priority }> = []
33
+ const reducers: Array<{ readonly name: string; readonly eventTag: string }> = []
34
+ const states: Array<string> = []
35
+ const calcs: Array<string> = []
36
+ const queries: Array<string> = []
38
37
  const procedures: Array<{
39
- readonly name: string;
40
- readonly channel: string;
41
- readonly eventTag: string;
42
- }> = [];
43
- const frames: Array<number> = [];
44
- const ends = { reducer: 0, calc: 0, query: 0, procedure: 0, frame: 0 };
38
+ readonly name: string
39
+ readonly channel: string
40
+ readonly eventTag: string
41
+ }> = []
42
+ const frames: Array<number> = []
43
+ const ends = { reducer: 0, calc: 0, query: 0, procedure: 0, frame: 0 }
45
44
  const instrumentation: Instrumentation = {
46
45
  eventDispatched: (tag, priority) => {
47
- events.push({ tag, priority });
46
+ events.push({ tag, priority })
48
47
  },
49
48
  reducerRun: (name, eventTag) => {
50
- reducers.push({ name, eventTag });
49
+ reducers.push({ name, eventTag })
51
50
  return () => {
52
- ends.reducer += 1;
53
- };
51
+ ends.reducer += 1
52
+ }
54
53
  },
55
54
  stateUpdated: (name) => {
56
- states.push(name);
55
+ states.push(name)
57
56
  },
58
57
  calcRecomputed: (name) => {
59
- calcs.push(name);
58
+ calcs.push(name)
60
59
  return () => {
61
- ends.calc += 1;
62
- };
60
+ ends.calc += 1
61
+ }
63
62
  },
64
63
  queryRun: (name) => {
65
- queries.push(name);
64
+ queries.push(name)
66
65
  return () => {
67
- ends.query += 1;
68
- };
66
+ ends.query += 1
67
+ }
69
68
  },
70
69
  procedureRun: (name, channel, eventTag) => {
71
- procedures.push({ name, channel, eventTag });
70
+ procedures.push({ name, channel, eventTag })
72
71
  return () => {
73
- ends.procedure += 1;
74
- };
72
+ ends.procedure += 1
73
+ }
75
74
  },
76
75
  uiRendered: () => () => {},
77
76
  frame: (eventCount) => {
78
- frames.push(eventCount);
77
+ frames.push(eventCount)
79
78
  return () => {
80
- ends.frame += 1;
81
- };
79
+ ends.frame += 1
80
+ }
82
81
  },
83
- };
82
+ }
84
83
  return {
85
84
  instrumentation,
86
85
  events,
@@ -91,186 +90,207 @@ const makeRecorder = () => {
91
90
  procedures,
92
91
  frames,
93
92
  ends,
94
- };
95
- };
93
+ }
94
+ }
96
95
 
97
- it.live(
98
- "the engine reports frames, events, reducer spans, and state updates",
99
- () => {
100
- const recorder = makeRecorder();
101
- class Counter extends State.make("instr-counter", S.Number) {}
102
- class Bumped extends Event.make(
103
- "InstrBumped",
104
- S.Struct({ to: S.Number }),
105
- ) {}
106
- class Fold extends Reducer.make("InstrFold", {
107
- states: [Counter],
108
- events: [Bumped],
109
- }) {}
110
- const FoldLive = Reducer.live(Fold, (_, event) => event.to);
96
+ it.live('the engine reports frames, events, reducer spans, and state updates', () => {
97
+ const recorder = makeRecorder()
98
+ class Counter extends State.make('instr-counter', S.Number) {}
99
+ class Bumped extends Event.make('InstrBumped', S.Struct({ to: S.Number })) {}
100
+ class Fold extends Reducer.make('InstrFold', {
101
+ states: [Counter],
102
+ events: [Bumped],
103
+ }) {}
104
+ const FoldLive = Reducer.live(Fold, (_, event) => event.to)
111
105
 
112
- const TestLayer = FoldLive.pipe(
113
- Layer.provideMerge(Layer.mergeAll(State.live(Counter, 0), Engine)),
114
- Layer.locally(CurrentInstrumentation, recorder.instrumentation),
115
- );
106
+ const TestLayer = FoldLive.pipe(
107
+ Layer.provideMerge(Layer.mergeAll(State.live(Counter, 0), Engine)),
108
+ Layer.locally(CurrentInstrumentation, recorder.instrumentation),
109
+ )
116
110
 
117
- return Effect.gen(function* () {
118
- yield* publish("High", Event.construct(Bumped, { to: 1 }));
119
- yield* publish("Normal", Event.construct(Bumped, { to: 2 }));
120
- yield* tick();
111
+ return Effect.gen(function* () {
112
+ yield* publish('High', Event.construct(Bumped, { to: 1 }))
113
+ yield* publish('Normal', Event.construct(Bumped, { to: 2 }))
114
+ yield* until(
115
+ () => recorder.ends.frame,
116
+ (n) => n >= 1,
117
+ )
121
118
 
122
- expect(recorder.frames).toEqual([2]);
123
- expect(recorder.ends.frame).toBe(1);
124
- expect(recorder.events).toEqual([
125
- { tag: "InstrBumped", priority: "High" },
126
- { tag: "InstrBumped", priority: "Normal" },
127
- ]);
128
- expect(recorder.reducers).toEqual([
129
- { name: "InstrFold", eventTag: "InstrBumped" },
130
- { name: "InstrFold", eventTag: "InstrBumped" },
131
- ]);
132
- expect(recorder.ends.reducer).toBe(2);
133
- expect(recorder.states).toEqual(["instr-counter", "instr-counter"]);
134
- }).pipe(Effect.provide(TestLayer));
135
- },
136
- );
119
+ expect(recorder.frames).toEqual([2])
120
+ expect(recorder.ends.frame).toBe(1)
121
+ expect(recorder.events).toEqual([
122
+ { tag: 'InstrBumped', priority: 'High' },
123
+ { tag: 'InstrBumped', priority: 'Normal' },
124
+ ])
125
+ expect(recorder.reducers).toEqual([
126
+ { name: 'InstrFold', eventTag: 'InstrBumped' },
127
+ { name: 'InstrFold', eventTag: 'InstrBumped' },
128
+ ])
129
+ expect(recorder.ends.reducer).toBe(2)
130
+ expect(recorder.states).toEqual(['instr-counter', 'instr-counter'])
131
+ }).pipe(Effect.provide(TestLayer))
132
+ })
137
133
 
138
- it.live("an equal-value write is not reported as a state update", () => {
139
- const recorder = makeRecorder();
140
- class Cell extends State.make("instr-cell", S.Number) {}
141
- class Wrote extends Event.make("InstrWrote", S.Struct({ to: S.Number })) {}
142
- class Write extends Reducer.make("InstrWrite", {
134
+ it.live('an equal-value write is not reported as a state update', () => {
135
+ const recorder = makeRecorder()
136
+ class Cell extends State.make('instr-cell', S.Number) {}
137
+ class Wrote extends Event.make('InstrWrote', S.Struct({ to: S.Number })) {}
138
+ class Write extends Reducer.make('InstrWrite', {
143
139
  states: [Cell],
144
140
  events: [Wrote],
145
141
  }) {}
146
- const WriteLive = Reducer.live(Write, (_, event) => event.to);
142
+ const WriteLive = Reducer.live(Write, (_, event) => event.to)
147
143
 
148
144
  const TestLayer = WriteLive.pipe(
149
145
  Layer.provideMerge(Layer.mergeAll(State.live(Cell, 7), Engine)),
150
146
  Layer.locally(CurrentInstrumentation, recorder.instrumentation),
151
- );
147
+ )
152
148
 
153
149
  return Effect.gen(function* () {
154
- yield* publish("High", Event.construct(Wrote, { to: 7 }));
155
- yield* tick();
156
- expect(recorder.reducers.length).toBe(1);
157
- expect(recorder.states).toEqual([]);
158
- }).pipe(Effect.provide(TestLayer));
159
- });
150
+ yield* publish('High', Event.construct(Wrote, { to: 7 }))
151
+ // The state update, if any, is reported inside the reducer's span.
152
+ yield* until(
153
+ () => recorder.ends.reducer,
154
+ (n) => n >= 1,
155
+ )
156
+ expect(recorder.reducers.length).toBe(1)
157
+ expect(recorder.states).toEqual([])
158
+ }).pipe(Effect.provide(TestLayer))
159
+ })
160
160
 
161
- it.live("calc recomputes are spanned; memo hits are not counted", () => {
162
- const recorder = makeRecorder();
163
- class Count extends State.make("instr-calc-count", S.Number) {}
161
+ it.live('calc recomputes are spanned; memo hits are not counted', () => {
162
+ const recorder = makeRecorder()
163
+ class Count extends State.make('instr-calc-count', S.Number) {}
164
164
  class Inputs extends StateGroup.make(Count) {}
165
- class Doubled extends Calc.make("InstrDoubled", {
166
- inputs: [StateGroup.select(Inputs, "instr-calc-count")],
165
+ class Doubled extends Calc.make('InstrDoubled', {
166
+ inputs: [StateGroup.select(Inputs, 'instr-calc-count')],
167
167
  output: S.Number,
168
168
  }) {}
169
- const DoubledLive = Calc.live(
170
- Doubled,
171
- (inputs) => inputs["instr-calc-count"] * 2,
172
- );
169
+ const DoubledLive = Calc.live(Doubled, (inputs) => inputs['instr-calc-count'] * 2)
173
170
 
174
171
  const TestLayer = DoubledLive.pipe(
175
- Layer.provideMerge(StateGroup.live(Inputs, { "instr-calc-count": 5 })),
172
+ Layer.provideMerge(StateGroup.live(Inputs, { 'instr-calc-count': 5 })),
176
173
  Layer.locally(CurrentInstrumentation, recorder.instrumentation),
177
- );
174
+ )
178
175
 
179
176
  return Effect.gen(function* () {
180
- const derived = yield* Doubled.store;
181
- const builds = recorder.calcs.length;
182
- expect(derived.get()).toBe(10);
183
- expect(derived.get()).toBe(10);
184
- expect(recorder.calcs.length).toBe(builds);
177
+ const derived = yield* Doubled.store
178
+ const builds = recorder.calcs.length
179
+ expect(derived.get()).toBe(10)
180
+ expect(derived.get()).toBe(10)
181
+ expect(recorder.calcs.length).toBe(builds)
185
182
 
186
- const source = yield* StateGroup.select(Inputs, "instr-calc-count").store;
187
- source.set(6);
188
- yield* tick();
189
- expect(derived.get()).toBe(12);
190
- expect(recorder.calcs.length).toBe(builds + 1);
191
- expect(recorder.calcs.every((name) => name === "InstrDoubled")).toBe(true);
192
- expect(recorder.ends.calc).toBe(recorder.calcs.length);
193
- expect(recorder.states).toContain("InstrDoubled");
194
- }).pipe(Effect.provide(TestLayer));
195
- });
183
+ const source = yield* StateGroup.select(Inputs, 'instr-calc-count').store
184
+ source.set(6)
185
+ // Wait on the recompute itself — pulling `get()` would recompute under the
186
+ // poller and hide the propagation that reports the derived state update.
187
+ yield* until(
188
+ () => recorder.calcs.length,
189
+ (n) => n === builds + 1,
190
+ )
191
+ expect(derived.get()).toBe(12)
192
+ expect(recorder.calcs.length).toBe(builds + 1)
193
+ expect(recorder.calcs.every((name) => name === 'InstrDoubled')).toBe(true)
194
+ expect(recorder.ends.calc).toBe(recorder.calcs.length)
195
+ expect(recorder.states).toContain('InstrDoubled')
196
+ }).pipe(Effect.provide(TestLayer))
197
+ })
196
198
 
197
- it.live("async calc query runs are spanned start-to-settle", () => {
198
- const recorder = makeRecorder();
199
- class Count extends State.make("instr-query-count", S.Number) {}
199
+ it.live('async calc query runs are spanned start-to-settle', () => {
200
+ const recorder = makeRecorder()
201
+ class Count extends State.make('instr-query-count', S.Number) {}
200
202
  class Inputs extends StateGroup.make(Count) {}
201
- class Fetched extends AsyncCalc.make("InstrFetched", {
202
- inputs: [StateGroup.select(Inputs, "instr-query-count")],
203
+ class Fetched extends AsyncCalc.make('InstrFetched', {
204
+ inputs: [StateGroup.select(Inputs, 'instr-query-count')],
203
205
  output: S.Number,
204
206
  alwaysOn: true,
205
207
  }) {}
208
+ const gate = makeFlightGate()
206
209
  const FetchedLive = AsyncCalc.live(Fetched, {
207
- query: (inputs) =>
208
- Effect.succeed(inputs["instr-query-count"] * 2).pipe(
209
- Effect.delay(Duration.millis(5)),
210
- ),
211
- });
210
+ query: (inputs) => gate.through(Effect.succeed(inputs['instr-query-count'] * 2)),
211
+ })
212
212
 
213
213
  const TestLayer = FetchedLive.pipe(
214
- Layer.provideMerge(StateGroup.live(Inputs, { "instr-query-count": 3 })),
214
+ Layer.provideMerge(StateGroup.live(Inputs, { 'instr-query-count': 3 })),
215
215
  Layer.locally(CurrentInstrumentation, recorder.instrumentation),
216
- );
216
+ )
217
217
 
218
218
  return Effect.gen(function* () {
219
- const store = yield* Fetched.store;
220
- yield* tick(40);
221
- expect(store.get()._tag).toBe("Success");
222
- expect(recorder.queries).toEqual(["InstrFetched"]);
223
- expect(recorder.ends.query).toBe(1);
224
- }).pipe(Effect.provide(TestLayer));
225
- });
219
+ yield* gate.hold
220
+ const store = yield* Fetched.store
221
+ // Parked mid-flight: the span is open, so its end must not have run yet.
222
+ yield* gate.awaitEntry(1)
223
+ expect(recorder.queries).toEqual(['InstrFetched'])
224
+ expect(recorder.ends.query).toBe(0)
226
225
 
227
- it.live("procedure runs are spanned with their channel and trigger tag", () => {
228
- const recorder = makeRecorder();
229
- class Kicked extends Event.make("InstrKicked", S.Struct({})) {}
230
- class Lane extends Channel.make("InstrLane", { policy: { _tag: "merge" } }) {}
231
- class Work extends Procedure.make("InstrWork", {
226
+ yield* gate.release
227
+ yield* until(
228
+ () => store.get()._tag,
229
+ (tag) => tag === 'Success',
230
+ )
231
+ expect(store.get()._tag).toBe('Success')
232
+ expect(recorder.queries).toEqual(['InstrFetched'])
233
+ expect(recorder.ends.query).toBe(1)
234
+ }).pipe(Effect.provide(TestLayer))
235
+ })
236
+
237
+ it.live('procedure runs are spanned with their channel and trigger tag', () => {
238
+ const recorder = makeRecorder()
239
+ class Kicked extends Event.make('InstrKicked', S.Struct({})) {}
240
+ class Lane extends Channel.make('InstrLane', { policy: { _tag: 'merge' } }) {}
241
+ class Work extends Procedure.make('InstrWork', {
232
242
  events: [Kicked],
233
243
  channel: Lane,
234
244
  }) {}
245
+ const gate = makeFlightGate()
235
246
  const WorkLive = Procedure.live(Work, function* () {
236
- yield* tick(5);
237
- });
247
+ yield* gate.through(Effect.void)
248
+ })
238
249
 
239
250
  const TestLayer = Layer.mergeAll(Channel.live(Lane), WorkLive).pipe(
240
251
  Layer.provideMerge(Engine),
241
252
  Layer.locally(CurrentInstrumentation, recorder.instrumentation),
242
- );
253
+ )
243
254
 
244
255
  return Effect.gen(function* () {
245
- yield* publish("Normal", Event.construct(Kicked, {}));
246
- yield* tick(40);
256
+ yield* gate.hold
257
+ yield* publish('Normal', Event.construct(Kicked, {}))
258
+ // Parked mid-run: the span opened with its channel and trigger, and stays open.
259
+ yield* gate.awaitEntry(1)
247
260
  expect(recorder.procedures).toEqual([
248
- { name: "InstrWork", channel: "InstrLane", eventTag: "InstrKicked" },
249
- ]);
250
- expect(recorder.ends.procedure).toBe(1);
251
- }).pipe(Effect.provide(TestLayer));
252
- });
261
+ { name: 'InstrWork', channel: 'InstrLane', eventTag: 'InstrKicked' },
262
+ ])
263
+ expect(recorder.ends.procedure).toBe(0)
264
+
265
+ yield* gate.release
266
+ yield* until(
267
+ () => recorder.ends.procedure,
268
+ (n) => n >= 1,
269
+ )
270
+ expect(recorder.ends.procedure).toBe(1)
271
+ }).pipe(Effect.provide(TestLayer))
272
+ })
253
273
 
254
- class SceneCount extends State.make("instr-scene-count", S.Number) {}
274
+ class SceneCount extends State.make('instr-scene-count', S.Number) {}
255
275
  class SceneStates extends StateGroup.make(SceneCount) {}
256
- class SceneBumped extends Event.make("InstrSceneBumped", S.Struct({})) {}
257
- class SceneBump extends Reducer.make("InstrSceneBump", {
276
+ class SceneBumped extends Event.make('InstrSceneBumped', S.Struct({})) {}
277
+ class SceneBump extends Reducer.make('InstrSceneBump', {
258
278
  states: [SceneCount],
259
279
  events: [SceneBumped],
260
280
  }) {}
261
- const SceneBumpLive = Reducer.live(SceneBump, (n) => n + 1);
281
+ const SceneBumpLive = Reducer.live(SceneBump, (n) => n + 1)
262
282
 
263
- class SceneUi extends ui("InstrScene")<{ props: { count: number } }>() {}
264
- class SceneRoot extends Composition.make("InstrScene", {
265
- title: "InstrScene",
283
+ class SceneUi extends ui('InstrScene')<{ props: { count: number } }>() {}
284
+ class SceneRoot extends Composition.make('InstrScene', {
285
+ title: 'InstrScene',
266
286
  states: [SceneStates],
267
287
  events: [SceneBumped],
268
288
  ui: SceneUi,
269
- }) {}
289
+ })<SceneRoot>() {}
270
290
  const SceneRootLive = Composition.live(SceneRoot, function* () {
271
- const count = yield* StateGroup.select(SceneStates, "instr-scene-count");
272
- return mount({ props: { count }, slots: {} });
273
- });
291
+ const count = yield* StateGroup.select(SceneStates, 'instr-scene-count')
292
+ return mount({ props: { count }, slots: {} })
293
+ })
274
294
 
275
295
  const SceneApp = SceneRootLive.pipe(
276
296
  Layer.provideMerge(
@@ -282,43 +302,42 @@ const SceneApp = SceneRootLive.pipe(
282
302
  Layer.provideMerge(
283
303
  Layer.mergeAll(SceneBumpLive).pipe(
284
304
  Layer.provideMerge(
285
- Layer.mergeAll(
286
- Engine,
287
- StateGroup.live(SceneStates, { "instr-scene-count": 0 }),
288
- ),
305
+ Layer.mergeAll(Engine, StateGroup.live(SceneStates, { 'instr-scene-count': 0 })),
289
306
  ),
290
307
  ),
291
308
  ),
292
- );
293
- const InstrScene = scene(SceneRoot, { provide: [SceneApp] });
309
+ )
310
+ const InstrScene = scene(SceneRoot, { provide: [SceneApp] })
294
311
 
295
312
  const runScene = (target: typeof InstrScene) => {
296
- const layer = target.provide.reduce((a, b) => Layer.merge(a, b));
313
+ const layer = target.provide.reduce((a, b) => Layer.merge(a, b))
297
314
  const drive = Effect.gen(function* () {
298
- yield* publish("High", Event.construct(SceneBumped, {}));
299
- yield* tick();
300
- });
301
- return { layer, drive };
302
- };
315
+ // Each run builds its own state store, so the fold itself is the barrier —
316
+ // it settles whether or not the run is being recorded.
317
+ const count = yield* StateGroup.select(SceneStates, 'instr-scene-count').store
318
+ yield* publish('High', Event.construct(SceneBumped, {}))
319
+ yield* until(
320
+ () => count.get(),
321
+ (n) => n === 1,
322
+ )
323
+ })
324
+ return { layer, drive }
325
+ }
303
326
 
304
- it.scopedLive(
305
- "profiledScene reaches nested layers and carries the handle",
306
- () =>
307
- Effect.gen(function* () {
308
- const recorder = makeRecorder();
309
- const profiled = profiledScene(InstrScene, recorder.instrumentation);
310
- expect(sceneInstrumentation(profiled)).toBe(recorder.instrumentation);
327
+ it.scopedLive('profiledScene reaches nested layers and carries the handle', () =>
328
+ Effect.gen(function* () {
329
+ const recorder = makeRecorder()
330
+ const profiled = profiledScene(InstrScene, recorder.instrumentation)
331
+ expect(sceneInstrumentation(profiled)).toBe(recorder.instrumentation)
311
332
 
312
- const profiledHarness = runScene(profiled);
313
- yield* profiledHarness.drive.pipe(Effect.provide(profiledHarness.layer));
314
- expect(recorder.reducers).toEqual([
315
- { name: "InstrSceneBump", eventTag: "InstrSceneBumped" },
316
- ]);
317
- expect(recorder.states).toEqual(["instr-scene-count"]);
318
- const before = recorder.reducers.length;
319
- expect(sceneInstrumentation(InstrScene)).toBe(noopInstrumentation);
320
- const original = runScene(InstrScene);
321
- yield* original.drive.pipe(Effect.provide(original.layer));
322
- expect(recorder.reducers.length).toBe(before);
323
- }),
324
- );
333
+ const profiledHarness = runScene(profiled)
334
+ yield* profiledHarness.drive.pipe(Effect.provide(profiledHarness.layer))
335
+ expect(recorder.reducers).toEqual([{ name: 'InstrSceneBump', eventTag: 'InstrSceneBumped' }])
336
+ expect(recorder.states).toEqual(['instr-scene-count'])
337
+ const before = recorder.reducers.length
338
+ expect(sceneInstrumentation(InstrScene)).toBe(noopInstrumentation)
339
+ const original = runScene(InstrScene)
340
+ yield* original.drive.pipe(Effect.provide(original.layer))
341
+ expect(recorder.reducers.length).toBe(before)
342
+ }),
343
+ )
@@ -27,18 +27,13 @@ export const noopInstrumentation: Instrumentation = {
27
27
  frame: () => noopSpanEnd,
28
28
  }
29
29
 
30
- export const CurrentInstrumentation: FiberRef.FiberRef<Instrumentation> =
31
- GlobalValue.globalValue(Symbol.for('reform/CurrentInstrumentation'), () =>
32
- FiberRef.unsafeMake<Instrumentation>(noopInstrumentation),
33
- )
30
+ export const CurrentInstrumentation: FiberRef.FiberRef<Instrumentation> = GlobalValue.globalValue(
31
+ Symbol.for('reform/CurrentInstrumentation'),
32
+ () => FiberRef.unsafeMake<Instrumentation>(noopInstrumentation),
33
+ )
34
34
 
35
35
  export const resolveInstrumentation: Effect.Effect<Instrumentation> =
36
36
  FiberRef.get(CurrentInstrumentation)
37
37
 
38
- export const stateUpdateHook = (
39
- instrumentation: Instrumentation,
40
- name: string,
41
- ): (() => void) =>
42
- instrumentation === noopInstrumentation
43
- ? noopSpanEnd
44
- : () => instrumentation.stateUpdated(name)
38
+ export const stateUpdateHook = (instrumentation: Instrumentation, name: string): (() => void) =>
39
+ instrumentation === noopInstrumentation ? noopSpanEnd : () => instrumentation.stateUpdated(name)