@playfast/reform 1.1.1 → 1.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +85 -55
- package/package.json +17 -17
- package/src/boundary/boundary.test.ts +62 -16
- package/src/boundary/boundary.ts +141 -32
- package/src/calc/asyncCalc.invalidate.test.ts +516 -18
- package/src/calc/asyncCalc.test.ts +207 -175
- package/src/calc/asyncCalc.ts +168 -39
- package/src/calc/calc.test.ts +3 -5
- package/src/calc/calc.ts +44 -18
- package/src/calc/calcFamily.test.ts +80 -22
- package/src/calc/calcFamily.ts +56 -25
- package/src/calc/compose.ts +1 -14
- package/src/channel/channel.ts +128 -11
- package/src/compose/composition.test.ts +19 -0
- package/src/compose/composition.ts +346 -62
- 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 +1317 -197
- package/src/feature/feature.typecheck.ts +253 -61
- package/src/graph/closure.ts +403 -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 +120 -15
- package/src/internal/queryDriverStore.ts +8 -5
- package/src/internal/queryDriverTypes.ts +3 -1
- 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/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/remoteState.test.ts +515 -339
- package/src/remote/remoteState.ts +572 -107
- package/src/remote/remoteState.typecheck.ts +47 -23
- package/src/runtime/appRuntime.activation.test.ts +100 -0
- package/src/runtime/appRuntime.test.ts +157 -105
- package/src/runtime/appRuntime.ts +394 -33
- package/src/runtime/eventBudget.test.ts +1 -4
- package/src/runtime/eventBudget.ts +9 -8
- package/src/runtime/hardening.test.ts +4 -9
- package/src/runtime/instrumentation.test.ts +174 -192
- package/src/runtime/instrumentation.ts +6 -11
- package/src/runtime/loop.test.ts +36 -0
- package/src/runtime/loop.ts +69 -40
- 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 +14 -0
- 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 +75 -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/channel/procedureRegistry.ts +0 -90
- package/src/feature/featureBinding.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/remote/remoteStateSend.ts +0 -64
|
@@ -1,16 +1,13 @@
|
|
|
1
|
-
import { expect, it } from
|
|
2
|
-
import { Duration, Effect, Layer, Schema as S } from
|
|
1
|
+
import { expect, it } from '@effect/vitest'
|
|
2
|
+
import { Duration, 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,67 @@ import {
|
|
|
23
20
|
StateGroup,
|
|
24
21
|
Ui,
|
|
25
22
|
ui,
|
|
26
|
-
} from
|
|
23
|
+
} from '../index'
|
|
24
|
+
import {
|
|
25
|
+
CurrentInstrumentation,
|
|
26
|
+
type Instrumentation,
|
|
27
|
+
noopInstrumentation,
|
|
28
|
+
} from './instrumentation'
|
|
27
29
|
|
|
28
|
-
const tick = (ms = 20) => Effect.sleep(Duration.millis(ms))
|
|
30
|
+
const tick = (ms = 20) => Effect.sleep(Duration.millis(ms))
|
|
29
31
|
|
|
30
32
|
const makeRecorder = () => {
|
|
31
|
-
const events: Array<{ readonly tag: string; readonly priority: Priority }> =
|
|
32
|
-
|
|
33
|
-
const
|
|
34
|
-
|
|
35
|
-
const
|
|
36
|
-
const calcs: Array<string> = [];
|
|
37
|
-
const queries: Array<string> = [];
|
|
33
|
+
const events: Array<{ readonly tag: string; readonly priority: Priority }> = []
|
|
34
|
+
const reducers: Array<{ readonly name: string; readonly eventTag: string }> = []
|
|
35
|
+
const states: Array<string> = []
|
|
36
|
+
const calcs: Array<string> = []
|
|
37
|
+
const queries: Array<string> = []
|
|
38
38
|
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 }
|
|
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 }
|
|
45
45
|
const instrumentation: Instrumentation = {
|
|
46
46
|
eventDispatched: (tag, priority) => {
|
|
47
|
-
events.push({ tag, priority })
|
|
47
|
+
events.push({ tag, priority })
|
|
48
48
|
},
|
|
49
49
|
reducerRun: (name, eventTag) => {
|
|
50
|
-
reducers.push({ name, eventTag })
|
|
50
|
+
reducers.push({ name, eventTag })
|
|
51
51
|
return () => {
|
|
52
|
-
ends.reducer += 1
|
|
53
|
-
}
|
|
52
|
+
ends.reducer += 1
|
|
53
|
+
}
|
|
54
54
|
},
|
|
55
55
|
stateUpdated: (name) => {
|
|
56
|
-
states.push(name)
|
|
56
|
+
states.push(name)
|
|
57
57
|
},
|
|
58
58
|
calcRecomputed: (name) => {
|
|
59
|
-
calcs.push(name)
|
|
59
|
+
calcs.push(name)
|
|
60
60
|
return () => {
|
|
61
|
-
ends.calc += 1
|
|
62
|
-
}
|
|
61
|
+
ends.calc += 1
|
|
62
|
+
}
|
|
63
63
|
},
|
|
64
64
|
queryRun: (name) => {
|
|
65
|
-
queries.push(name)
|
|
65
|
+
queries.push(name)
|
|
66
66
|
return () => {
|
|
67
|
-
ends.query += 1
|
|
68
|
-
}
|
|
67
|
+
ends.query += 1
|
|
68
|
+
}
|
|
69
69
|
},
|
|
70
70
|
procedureRun: (name, channel, eventTag) => {
|
|
71
|
-
procedures.push({ name, channel, eventTag })
|
|
71
|
+
procedures.push({ name, channel, eventTag })
|
|
72
72
|
return () => {
|
|
73
|
-
ends.procedure += 1
|
|
74
|
-
}
|
|
73
|
+
ends.procedure += 1
|
|
74
|
+
}
|
|
75
75
|
},
|
|
76
76
|
uiRendered: () => () => {},
|
|
77
77
|
frame: (eventCount) => {
|
|
78
|
-
frames.push(eventCount)
|
|
78
|
+
frames.push(eventCount)
|
|
79
79
|
return () => {
|
|
80
|
-
ends.frame += 1
|
|
81
|
-
}
|
|
80
|
+
ends.frame += 1
|
|
81
|
+
}
|
|
82
82
|
},
|
|
83
|
-
}
|
|
83
|
+
}
|
|
84
84
|
return {
|
|
85
85
|
instrumentation,
|
|
86
86
|
events,
|
|
@@ -91,186 +91,175 @@ const makeRecorder = () => {
|
|
|
91
91
|
procedures,
|
|
92
92
|
frames,
|
|
93
93
|
ends,
|
|
94
|
-
}
|
|
95
|
-
}
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
96
|
|
|
97
|
-
it.live(
|
|
98
|
-
|
|
99
|
-
()
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
class Fold extends Reducer.make("InstrFold", {
|
|
107
|
-
states: [Counter],
|
|
108
|
-
events: [Bumped],
|
|
109
|
-
}) {}
|
|
110
|
-
const FoldLive = Reducer.live(Fold, (_, event) => event.to);
|
|
97
|
+
it.live('the engine reports frames, events, reducer spans, and state updates', () => {
|
|
98
|
+
const recorder = makeRecorder()
|
|
99
|
+
class Counter extends State.make('instr-counter', S.Number) {}
|
|
100
|
+
class Bumped extends Event.make('InstrBumped', S.Struct({ to: S.Number })) {}
|
|
101
|
+
class Fold extends Reducer.make('InstrFold', {
|
|
102
|
+
states: [Counter],
|
|
103
|
+
events: [Bumped],
|
|
104
|
+
}) {}
|
|
105
|
+
const FoldLive = Reducer.live(Fold, (_, event) => event.to)
|
|
111
106
|
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
107
|
+
const TestLayer = FoldLive.pipe(
|
|
108
|
+
Layer.provideMerge(Layer.mergeAll(State.live(Counter, 0), Engine)),
|
|
109
|
+
Layer.locally(CurrentInstrumentation, recorder.instrumentation),
|
|
110
|
+
)
|
|
116
111
|
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
112
|
+
return Effect.gen(function* () {
|
|
113
|
+
yield* publish('High', Event.construct(Bumped, { to: 1 }))
|
|
114
|
+
yield* publish('Normal', Event.construct(Bumped, { to: 2 }))
|
|
115
|
+
yield* tick()
|
|
121
116
|
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
);
|
|
117
|
+
expect(recorder.frames).toEqual([2])
|
|
118
|
+
expect(recorder.ends.frame).toBe(1)
|
|
119
|
+
expect(recorder.events).toEqual([
|
|
120
|
+
{ tag: 'InstrBumped', priority: 'High' },
|
|
121
|
+
{ tag: 'InstrBumped', priority: 'Normal' },
|
|
122
|
+
])
|
|
123
|
+
expect(recorder.reducers).toEqual([
|
|
124
|
+
{ name: 'InstrFold', eventTag: 'InstrBumped' },
|
|
125
|
+
{ name: 'InstrFold', eventTag: 'InstrBumped' },
|
|
126
|
+
])
|
|
127
|
+
expect(recorder.ends.reducer).toBe(2)
|
|
128
|
+
expect(recorder.states).toEqual(['instr-counter', 'instr-counter'])
|
|
129
|
+
}).pipe(Effect.provide(TestLayer))
|
|
130
|
+
})
|
|
137
131
|
|
|
138
|
-
it.live(
|
|
139
|
-
const recorder = makeRecorder()
|
|
140
|
-
class Cell extends State.make(
|
|
141
|
-
class Wrote extends Event.make(
|
|
142
|
-
class Write extends Reducer.make(
|
|
132
|
+
it.live('an equal-value write is not reported as a state update', () => {
|
|
133
|
+
const recorder = makeRecorder()
|
|
134
|
+
class Cell extends State.make('instr-cell', S.Number) {}
|
|
135
|
+
class Wrote extends Event.make('InstrWrote', S.Struct({ to: S.Number })) {}
|
|
136
|
+
class Write extends Reducer.make('InstrWrite', {
|
|
143
137
|
states: [Cell],
|
|
144
138
|
events: [Wrote],
|
|
145
139
|
}) {}
|
|
146
|
-
const WriteLive = Reducer.live(Write, (_, event) => event.to)
|
|
140
|
+
const WriteLive = Reducer.live(Write, (_, event) => event.to)
|
|
147
141
|
|
|
148
142
|
const TestLayer = WriteLive.pipe(
|
|
149
143
|
Layer.provideMerge(Layer.mergeAll(State.live(Cell, 7), Engine)),
|
|
150
144
|
Layer.locally(CurrentInstrumentation, recorder.instrumentation),
|
|
151
|
-
)
|
|
145
|
+
)
|
|
152
146
|
|
|
153
147
|
return Effect.gen(function* () {
|
|
154
|
-
yield* publish(
|
|
155
|
-
yield* tick()
|
|
156
|
-
expect(recorder.reducers.length).toBe(1)
|
|
157
|
-
expect(recorder.states).toEqual([])
|
|
158
|
-
}).pipe(Effect.provide(TestLayer))
|
|
159
|
-
})
|
|
148
|
+
yield* publish('High', Event.construct(Wrote, { to: 7 }))
|
|
149
|
+
yield* tick()
|
|
150
|
+
expect(recorder.reducers.length).toBe(1)
|
|
151
|
+
expect(recorder.states).toEqual([])
|
|
152
|
+
}).pipe(Effect.provide(TestLayer))
|
|
153
|
+
})
|
|
160
154
|
|
|
161
|
-
it.live(
|
|
162
|
-
const recorder = makeRecorder()
|
|
163
|
-
class Count extends State.make(
|
|
155
|
+
it.live('calc recomputes are spanned; memo hits are not counted', () => {
|
|
156
|
+
const recorder = makeRecorder()
|
|
157
|
+
class Count extends State.make('instr-calc-count', S.Number) {}
|
|
164
158
|
class Inputs extends StateGroup.make(Count) {}
|
|
165
|
-
class Doubled extends Calc.make(
|
|
166
|
-
inputs: [StateGroup.select(Inputs,
|
|
159
|
+
class Doubled extends Calc.make('InstrDoubled', {
|
|
160
|
+
inputs: [StateGroup.select(Inputs, 'instr-calc-count')],
|
|
167
161
|
output: S.Number,
|
|
168
162
|
}) {}
|
|
169
|
-
const DoubledLive = Calc.live(
|
|
170
|
-
Doubled,
|
|
171
|
-
(inputs) => inputs["instr-calc-count"] * 2,
|
|
172
|
-
);
|
|
163
|
+
const DoubledLive = Calc.live(Doubled, (inputs) => inputs['instr-calc-count'] * 2)
|
|
173
164
|
|
|
174
165
|
const TestLayer = DoubledLive.pipe(
|
|
175
|
-
Layer.provideMerge(StateGroup.live(Inputs, {
|
|
166
|
+
Layer.provideMerge(StateGroup.live(Inputs, { 'instr-calc-count': 5 })),
|
|
176
167
|
Layer.locally(CurrentInstrumentation, recorder.instrumentation),
|
|
177
|
-
)
|
|
168
|
+
)
|
|
178
169
|
|
|
179
170
|
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)
|
|
171
|
+
const derived = yield* Doubled.store
|
|
172
|
+
const builds = recorder.calcs.length
|
|
173
|
+
expect(derived.get()).toBe(10)
|
|
174
|
+
expect(derived.get()).toBe(10)
|
|
175
|
+
expect(recorder.calcs.length).toBe(builds)
|
|
185
176
|
|
|
186
|
-
const source = yield* StateGroup.select(Inputs,
|
|
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 ===
|
|
192
|
-
expect(recorder.ends.calc).toBe(recorder.calcs.length)
|
|
193
|
-
expect(recorder.states).toContain(
|
|
194
|
-
}).pipe(Effect.provide(TestLayer))
|
|
195
|
-
})
|
|
177
|
+
const source = yield* StateGroup.select(Inputs, 'instr-calc-count').store
|
|
178
|
+
source.set(6)
|
|
179
|
+
yield* tick()
|
|
180
|
+
expect(derived.get()).toBe(12)
|
|
181
|
+
expect(recorder.calcs.length).toBe(builds + 1)
|
|
182
|
+
expect(recorder.calcs.every((name) => name === 'InstrDoubled')).toBe(true)
|
|
183
|
+
expect(recorder.ends.calc).toBe(recorder.calcs.length)
|
|
184
|
+
expect(recorder.states).toContain('InstrDoubled')
|
|
185
|
+
}).pipe(Effect.provide(TestLayer))
|
|
186
|
+
})
|
|
196
187
|
|
|
197
|
-
it.live(
|
|
198
|
-
const recorder = makeRecorder()
|
|
199
|
-
class Count extends State.make(
|
|
188
|
+
it.live('async calc query runs are spanned start-to-settle', () => {
|
|
189
|
+
const recorder = makeRecorder()
|
|
190
|
+
class Count extends State.make('instr-query-count', S.Number) {}
|
|
200
191
|
class Inputs extends StateGroup.make(Count) {}
|
|
201
|
-
class Fetched extends AsyncCalc.make(
|
|
202
|
-
inputs: [StateGroup.select(Inputs,
|
|
192
|
+
class Fetched extends AsyncCalc.make('InstrFetched', {
|
|
193
|
+
inputs: [StateGroup.select(Inputs, 'instr-query-count')],
|
|
203
194
|
output: S.Number,
|
|
204
195
|
alwaysOn: true,
|
|
205
196
|
}) {}
|
|
206
197
|
const FetchedLive = AsyncCalc.live(Fetched, {
|
|
207
198
|
query: (inputs) =>
|
|
208
|
-
Effect.succeed(inputs[
|
|
209
|
-
|
|
210
|
-
),
|
|
211
|
-
});
|
|
199
|
+
Effect.succeed(inputs['instr-query-count'] * 2).pipe(Effect.delay(Duration.millis(5))),
|
|
200
|
+
})
|
|
212
201
|
|
|
213
202
|
const TestLayer = FetchedLive.pipe(
|
|
214
|
-
Layer.provideMerge(StateGroup.live(Inputs, {
|
|
203
|
+
Layer.provideMerge(StateGroup.live(Inputs, { 'instr-query-count': 3 })),
|
|
215
204
|
Layer.locally(CurrentInstrumentation, recorder.instrumentation),
|
|
216
|
-
)
|
|
205
|
+
)
|
|
217
206
|
|
|
218
207
|
return Effect.gen(function* () {
|
|
219
|
-
const store = yield* Fetched.store
|
|
220
|
-
yield* tick(40)
|
|
221
|
-
expect(store.get()._tag).toBe(
|
|
222
|
-
expect(recorder.queries).toEqual([
|
|
223
|
-
expect(recorder.ends.query).toBe(1)
|
|
224
|
-
}).pipe(Effect.provide(TestLayer))
|
|
225
|
-
})
|
|
208
|
+
const store = yield* Fetched.store
|
|
209
|
+
yield* tick(40)
|
|
210
|
+
expect(store.get()._tag).toBe('Success')
|
|
211
|
+
expect(recorder.queries).toEqual(['InstrFetched'])
|
|
212
|
+
expect(recorder.ends.query).toBe(1)
|
|
213
|
+
}).pipe(Effect.provide(TestLayer))
|
|
214
|
+
})
|
|
226
215
|
|
|
227
|
-
it.live(
|
|
228
|
-
const recorder = makeRecorder()
|
|
229
|
-
class Kicked extends Event.make(
|
|
230
|
-
class Lane extends Channel.make(
|
|
231
|
-
class Work extends Procedure.make(
|
|
216
|
+
it.live('procedure runs are spanned with their channel and trigger tag', () => {
|
|
217
|
+
const recorder = makeRecorder()
|
|
218
|
+
class Kicked extends Event.make('InstrKicked', S.Struct({})) {}
|
|
219
|
+
class Lane extends Channel.make('InstrLane', { policy: { _tag: 'merge' } }) {}
|
|
220
|
+
class Work extends Procedure.make('InstrWork', {
|
|
232
221
|
events: [Kicked],
|
|
233
222
|
channel: Lane,
|
|
234
223
|
}) {}
|
|
235
224
|
const WorkLive = Procedure.live(Work, function* () {
|
|
236
|
-
yield* tick(5)
|
|
237
|
-
})
|
|
225
|
+
yield* tick(5)
|
|
226
|
+
})
|
|
238
227
|
|
|
239
228
|
const TestLayer = Layer.mergeAll(Channel.live(Lane), WorkLive).pipe(
|
|
240
229
|
Layer.provideMerge(Engine),
|
|
241
230
|
Layer.locally(CurrentInstrumentation, recorder.instrumentation),
|
|
242
|
-
)
|
|
231
|
+
)
|
|
243
232
|
|
|
244
233
|
return Effect.gen(function* () {
|
|
245
|
-
yield* publish(
|
|
246
|
-
yield* tick(40)
|
|
234
|
+
yield* publish('Normal', Event.construct(Kicked, {}))
|
|
235
|
+
yield* tick(40)
|
|
247
236
|
expect(recorder.procedures).toEqual([
|
|
248
|
-
{ name:
|
|
249
|
-
])
|
|
250
|
-
expect(recorder.ends.procedure).toBe(1)
|
|
251
|
-
}).pipe(Effect.provide(TestLayer))
|
|
252
|
-
})
|
|
237
|
+
{ name: 'InstrWork', channel: 'InstrLane', eventTag: 'InstrKicked' },
|
|
238
|
+
])
|
|
239
|
+
expect(recorder.ends.procedure).toBe(1)
|
|
240
|
+
}).pipe(Effect.provide(TestLayer))
|
|
241
|
+
})
|
|
253
242
|
|
|
254
|
-
class SceneCount extends State.make(
|
|
243
|
+
class SceneCount extends State.make('instr-scene-count', S.Number) {}
|
|
255
244
|
class SceneStates extends StateGroup.make(SceneCount) {}
|
|
256
|
-
class SceneBumped extends Event.make(
|
|
257
|
-
class SceneBump extends Reducer.make(
|
|
245
|
+
class SceneBumped extends Event.make('InstrSceneBumped', S.Struct({})) {}
|
|
246
|
+
class SceneBump extends Reducer.make('InstrSceneBump', {
|
|
258
247
|
states: [SceneCount],
|
|
259
248
|
events: [SceneBumped],
|
|
260
249
|
}) {}
|
|
261
|
-
const SceneBumpLive = Reducer.live(SceneBump, (n) => n + 1)
|
|
250
|
+
const SceneBumpLive = Reducer.live(SceneBump, (n) => n + 1)
|
|
262
251
|
|
|
263
|
-
class SceneUi extends ui(
|
|
264
|
-
class SceneRoot extends Composition.make(
|
|
265
|
-
title:
|
|
252
|
+
class SceneUi extends ui('InstrScene')<{ props: { count: number } }>() {}
|
|
253
|
+
class SceneRoot extends Composition.make('InstrScene', {
|
|
254
|
+
title: 'InstrScene',
|
|
266
255
|
states: [SceneStates],
|
|
267
256
|
events: [SceneBumped],
|
|
268
257
|
ui: SceneUi,
|
|
269
|
-
}) {}
|
|
258
|
+
})<SceneRoot>() {}
|
|
270
259
|
const SceneRootLive = Composition.live(SceneRoot, function* () {
|
|
271
|
-
const count = yield* StateGroup.select(SceneStates,
|
|
272
|
-
return mount({ props: { count }, slots: {} })
|
|
273
|
-
})
|
|
260
|
+
const count = yield* StateGroup.select(SceneStates, 'instr-scene-count')
|
|
261
|
+
return mount({ props: { count }, slots: {} })
|
|
262
|
+
})
|
|
274
263
|
|
|
275
264
|
const SceneApp = SceneRootLive.pipe(
|
|
276
265
|
Layer.provideMerge(
|
|
@@ -282,43 +271,36 @@ const SceneApp = SceneRootLive.pipe(
|
|
|
282
271
|
Layer.provideMerge(
|
|
283
272
|
Layer.mergeAll(SceneBumpLive).pipe(
|
|
284
273
|
Layer.provideMerge(
|
|
285
|
-
Layer.mergeAll(
|
|
286
|
-
Engine,
|
|
287
|
-
StateGroup.live(SceneStates, { "instr-scene-count": 0 }),
|
|
288
|
-
),
|
|
274
|
+
Layer.mergeAll(Engine, StateGroup.live(SceneStates, { 'instr-scene-count': 0 })),
|
|
289
275
|
),
|
|
290
276
|
),
|
|
291
277
|
),
|
|
292
|
-
)
|
|
293
|
-
const InstrScene = scene(SceneRoot, { provide: [SceneApp] })
|
|
278
|
+
)
|
|
279
|
+
const InstrScene = scene(SceneRoot, { provide: [SceneApp] })
|
|
294
280
|
|
|
295
281
|
const runScene = (target: typeof InstrScene) => {
|
|
296
|
-
const layer = target.provide.reduce((a, b) => Layer.merge(a, b))
|
|
282
|
+
const layer = target.provide.reduce((a, b) => Layer.merge(a, b))
|
|
297
283
|
const drive = Effect.gen(function* () {
|
|
298
|
-
yield* publish(
|
|
299
|
-
yield* tick()
|
|
300
|
-
})
|
|
301
|
-
return { layer, drive }
|
|
302
|
-
}
|
|
284
|
+
yield* publish('High', Event.construct(SceneBumped, {}))
|
|
285
|
+
yield* tick()
|
|
286
|
+
})
|
|
287
|
+
return { layer, drive }
|
|
288
|
+
}
|
|
303
289
|
|
|
304
|
-
it.scopedLive(
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
const profiled = profiledScene(InstrScene, recorder.instrumentation);
|
|
310
|
-
expect(sceneInstrumentation(profiled)).toBe(recorder.instrumentation);
|
|
290
|
+
it.scopedLive('profiledScene reaches nested layers and carries the handle', () =>
|
|
291
|
+
Effect.gen(function* () {
|
|
292
|
+
const recorder = makeRecorder()
|
|
293
|
+
const profiled = profiledScene(InstrScene, recorder.instrumentation)
|
|
294
|
+
expect(sceneInstrumentation(profiled)).toBe(recorder.instrumentation)
|
|
311
295
|
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
}),
|
|
324
|
-
);
|
|
296
|
+
const profiledHarness = runScene(profiled)
|
|
297
|
+
yield* profiledHarness.drive.pipe(Effect.provide(profiledHarness.layer))
|
|
298
|
+
expect(recorder.reducers).toEqual([{ name: 'InstrSceneBump', eventTag: 'InstrSceneBumped' }])
|
|
299
|
+
expect(recorder.states).toEqual(['instr-scene-count'])
|
|
300
|
+
const before = recorder.reducers.length
|
|
301
|
+
expect(sceneInstrumentation(InstrScene)).toBe(noopInstrumentation)
|
|
302
|
+
const original = runScene(InstrScene)
|
|
303
|
+
yield* original.drive.pipe(Effect.provide(original.layer))
|
|
304
|
+
expect(recorder.reducers.length).toBe(before)
|
|
305
|
+
}),
|
|
306
|
+
)
|
|
@@ -27,18 +27,13 @@ export const noopInstrumentation: Instrumentation = {
|
|
|
27
27
|
frame: () => noopSpanEnd,
|
|
28
28
|
}
|
|
29
29
|
|
|
30
|
-
export const CurrentInstrumentation: FiberRef.FiberRef<Instrumentation> =
|
|
31
|
-
|
|
32
|
-
|
|
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:
|
|
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)
|
package/src/runtime/loop.test.ts
CHANGED
|
@@ -159,3 +159,39 @@ it.effect('a latest channel cancels the in-flight run when a new event arrives',
|
|
|
159
159
|
expect(counts.completed).toBe(1)
|
|
160
160
|
}).pipe(Effect.provide(TestLayer))
|
|
161
161
|
})
|
|
162
|
+
|
|
163
|
+
it.live('a fold returning the previous reference keeps the exact store value', () => {
|
|
164
|
+
const Viewer = S.Union(
|
|
165
|
+
S.TaggedStruct('Closed', {}),
|
|
166
|
+
S.TaggedStruct('Open', { revision: S.Number }),
|
|
167
|
+
)
|
|
168
|
+
class ViewerState extends State.make('viewer', Viewer) {}
|
|
169
|
+
class Poked extends Event.make('Poked', S.Struct({})) {}
|
|
170
|
+
class PokeReducer extends Reducer.make('PokeReducer', {
|
|
171
|
+
states: [ViewerState],
|
|
172
|
+
events: [Poked],
|
|
173
|
+
}) {}
|
|
174
|
+
// Closed is a fixpoint: the fold hands back the previous reference untouched.
|
|
175
|
+
const PokeReducerLive = Reducer.live(PokeReducer, (viewer) =>
|
|
176
|
+
viewer._tag === 'Open' ? { ...viewer, revision: viewer.revision + 1 } : viewer,
|
|
177
|
+
)
|
|
178
|
+
|
|
179
|
+
const TestLayer = PokeReducerLive.pipe(
|
|
180
|
+
Layer.provideMerge(Layer.mergeAll(State.live(ViewerState, { _tag: 'Closed' }), Engine)),
|
|
181
|
+
)
|
|
182
|
+
|
|
183
|
+
return Effect.gen(function* () {
|
|
184
|
+
const store = yield* ViewerState.store
|
|
185
|
+
const before = store.get()
|
|
186
|
+
const notifications = { n: 0 }
|
|
187
|
+
store.subscribe(() => {
|
|
188
|
+
notifications.n += 1
|
|
189
|
+
})
|
|
190
|
+
yield* publish('Normal', Event.construct(Poked, {}))
|
|
191
|
+
yield* Effect.sleep(Duration.millis(20))
|
|
192
|
+
// Reference identity is the repaint signal: an unchanged fold must not be
|
|
193
|
+
// re-decoded into a fresh, structurally-equal value that wakes subscribers.
|
|
194
|
+
expect(store.get()).toBe(before)
|
|
195
|
+
expect(notifications.n).toBe(0)
|
|
196
|
+
}).pipe(Effect.provide(TestLayer))
|
|
197
|
+
})
|