@playfast/reform 1.2.0 → 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/package.json +1 -1
- package/src/boundary/boundary.test.ts +26 -23
- package/src/calc/asyncCalc.invalidate.test.ts +258 -55
- package/src/calc/asyncCalc.test.ts +225 -85
- package/src/calc/asyncCalc.ts +27 -141
- package/src/calc/asyncCalcTypes.ts +147 -0
- package/src/calc/calc.test.ts +10 -11
- package/src/calc/calcFamily.test.ts +12 -12
- package/src/channel/channel.ts +8 -106
- package/src/channel/procedures.ts +108 -0
- package/src/compose/composition.ts +49 -246
- package/src/compose/compositionTypes.ts +249 -0
- package/src/event/event.fromSource.test.ts +12 -12
- package/src/feature/feature.ts +50 -1210
- package/src/feature/feature.typecheck.ts +2 -61
- package/src/feature/featureBinding.ts +158 -0
- 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 +45 -226
- package/src/graph/services.ts +95 -0
- package/src/graph/summary.ts +134 -0
- package/src/internal/queryDriver.ts +20 -89
- package/src/internal/queryDriverHydrate.ts +45 -0
- package/src/internal/queryDriverTypes.ts +8 -10
- package/src/internal/store.test.ts +27 -6
- package/src/remote/pendingQueue.ts +145 -0
- package/src/remote/remoteState.test.ts +28 -22
- package/src/remote/remoteState.ts +63 -489
- package/src/remote/remoteStateMake.ts +97 -0
- package/src/remote/remoteStateSend.ts +122 -0
- package/src/remote/remoteStateTypes.ts +155 -0
- package/src/runtime/appRuntime.activation.test.ts +9 -5
- package/src/runtime/appRuntime.test.ts +40 -16
- package/src/runtime/appRuntime.ts +17 -472
- package/src/runtime/capturedAppRuntime.ts +274 -0
- package/src/runtime/eventBudget.test.ts +38 -8
- package/src/runtime/featureMount.ts +94 -0
- package/src/runtime/hardening.test.ts +18 -4
- package/src/runtime/instrumentation.test.ts +49 -12
- package/src/runtime/loop.test.ts +51 -10
- package/src/runtime/runtimeHandle.ts +124 -0
- package/src/state/stateFamily.test.ts +25 -8
- package/src/testkit/flight.testkit.ts +9 -0
|
@@ -1,10 +1,36 @@
|
|
|
1
1
|
import { expect, it } from '@effect/vitest'
|
|
2
|
-
import { Data,
|
|
3
|
-
import { AsyncCalc, Engine, Event, State, StateGroup } from '../index'
|
|
4
|
-
import { makeFlightGate,
|
|
2
|
+
import { Data, Effect, Layer, MutableRef, Schema as S } from 'effect'
|
|
3
|
+
import { AsyncCalc, Engine, Event, Reducer, State, StateGroup } from '../index'
|
|
4
|
+
import { makeFlightGate, until } from '../testkit/flight.testkit'
|
|
5
5
|
|
|
6
6
|
class Boom extends Data.TaggedError('Boom')<{ readonly message: string }> {}
|
|
7
7
|
|
|
8
|
+
interface Notifier {
|
|
9
|
+
readonly subscribe: (listener: () => void) => () => void
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
// The notification scheduler drains every listener of a write in one pass, and a
|
|
13
|
+
// driver subscribes before the test does: our own notification proves its
|
|
14
|
+
// trigger already ran, so an inert change can be asserted without waiting.
|
|
15
|
+
const afterNotify = <A, E, R>(
|
|
16
|
+
source: Notifier,
|
|
17
|
+
write: Effect.Effect<A, E, R>,
|
|
18
|
+
): Effect.Effect<void, E, R> =>
|
|
19
|
+
Effect.suspend(() => {
|
|
20
|
+
const fired = MutableRef.make(false)
|
|
21
|
+
const off = source.subscribe(() => MutableRef.set(fired, true))
|
|
22
|
+
return write.pipe(
|
|
23
|
+
Effect.zipRight(
|
|
24
|
+
until(
|
|
25
|
+
() => MutableRef.get(fired),
|
|
26
|
+
(value) => value,
|
|
27
|
+
),
|
|
28
|
+
),
|
|
29
|
+
Effect.ensuring(Effect.sync(off)),
|
|
30
|
+
Effect.asVoid,
|
|
31
|
+
)
|
|
32
|
+
})
|
|
33
|
+
|
|
8
34
|
it.live('runs the query and resolves Loading -> Success', () => {
|
|
9
35
|
class Count extends State.make('count', S.Number) {}
|
|
10
36
|
class Inputs extends StateGroup.make(Count) {}
|
|
@@ -13,16 +39,24 @@ it.live('runs the query and resolves Loading -> Success', () => {
|
|
|
13
39
|
output: S.Number,
|
|
14
40
|
alwaysOn: true,
|
|
15
41
|
}) {}
|
|
42
|
+
const gate = makeFlightGate()
|
|
43
|
+
// Armed before the layer builds, so the very first flight parks at the gate.
|
|
44
|
+
Effect.runSync(gate.hold)
|
|
16
45
|
const DoubledLive = AsyncCalc.live(Doubled, {
|
|
17
|
-
query: ({ count }) => Effect.succeed(count * 2)
|
|
46
|
+
query: ({ count }) => gate.through(Effect.succeed(count * 2)),
|
|
18
47
|
})
|
|
19
48
|
const TestLayer = DoubledLive.pipe(Layer.provideMerge(StateGroup.live(Inputs, { count: 5 })))
|
|
20
49
|
|
|
21
50
|
return Effect.gen(function* () {
|
|
22
51
|
const store = yield* Doubled.store
|
|
52
|
+
yield* gate.awaitEntry(1)
|
|
23
53
|
expect(store.get()._tag).toBe('Loading')
|
|
24
|
-
|
|
25
|
-
|
|
54
|
+
|
|
55
|
+
yield* gate.release
|
|
56
|
+
const value = yield* until(
|
|
57
|
+
() => store.get(),
|
|
58
|
+
(state) => state._tag === 'Success',
|
|
59
|
+
)
|
|
26
60
|
expect(value._tag).toBe('Success')
|
|
27
61
|
if (value._tag === 'Success') {
|
|
28
62
|
expect(value.value).toBe(10)
|
|
@@ -47,8 +81,10 @@ it.live('a failing query resolves to Error carrying the failure', () => {
|
|
|
47
81
|
|
|
48
82
|
return Effect.gen(function* () {
|
|
49
83
|
const store = yield* Q.store
|
|
50
|
-
yield*
|
|
51
|
-
|
|
84
|
+
const value = yield* until(
|
|
85
|
+
() => store.get(),
|
|
86
|
+
(state) => state._tag === 'Error',
|
|
87
|
+
)
|
|
52
88
|
expect(value._tag).toBe('Error')
|
|
53
89
|
if (value._tag === 'Error') {
|
|
54
90
|
expect(value.error.message).toBe('nope')
|
|
@@ -65,23 +101,33 @@ it.live('a dependency change re-fetches, keeping the last value with refetching:
|
|
|
65
101
|
output: S.Number,
|
|
66
102
|
alwaysOn: true,
|
|
67
103
|
}) {}
|
|
104
|
+
const gate = makeFlightGate()
|
|
68
105
|
const DoubledLive = AsyncCalc.live(Doubled, {
|
|
69
|
-
query: ({ count }) => Effect.succeed(count * 2)
|
|
106
|
+
query: ({ count }) => gate.through(Effect.succeed(count * 2)),
|
|
70
107
|
})
|
|
71
108
|
const TestLayer = DoubledLive.pipe(Layer.provideMerge(StateGroup.live(Inputs, { count: 5 })))
|
|
72
109
|
|
|
73
110
|
return Effect.gen(function* () {
|
|
74
111
|
const source = yield* StateGroup.select(Inputs, 'count').store
|
|
75
112
|
const store = yield* Doubled.store
|
|
76
|
-
yield*
|
|
113
|
+
yield* until(
|
|
114
|
+
() => store.get(),
|
|
115
|
+
(v) => v._tag === 'Success',
|
|
116
|
+
)
|
|
77
117
|
expect(store.get()).toMatchObject({ _tag: 'Success', value: 10, refetching: false })
|
|
78
118
|
|
|
119
|
+
// Hold the re-fetch at the gate: the previous value stays visible under it.
|
|
120
|
+
yield* gate.hold
|
|
79
121
|
source.set(7)
|
|
80
|
-
yield*
|
|
122
|
+
yield* gate.awaitEntry(2)
|
|
81
123
|
expect(store.get()).toMatchObject({ _tag: 'Success', value: 10, refetching: true })
|
|
82
124
|
|
|
83
|
-
yield*
|
|
84
|
-
|
|
125
|
+
yield* gate.release
|
|
126
|
+
const settled = yield* until(
|
|
127
|
+
() => store.get(),
|
|
128
|
+
(v) => v._tag === 'Success' && v.refetching === false,
|
|
129
|
+
)
|
|
130
|
+
expect(settled).toMatchObject({ _tag: 'Success', value: 14, refetching: false })
|
|
85
131
|
}).pipe(Effect.provide(TestLayer))
|
|
86
132
|
})
|
|
87
133
|
|
|
@@ -94,24 +140,35 @@ it.live('latest-wins: a rapid second change cancels the in-flight run', () => {
|
|
|
94
140
|
output: S.Number,
|
|
95
141
|
alwaysOn: true,
|
|
96
142
|
}) {}
|
|
143
|
+
const gate = makeFlightGate()
|
|
97
144
|
const DoubledLive = AsyncCalc.live(Doubled, {
|
|
98
145
|
query: ({ count }) =>
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
146
|
+
gate.through(
|
|
147
|
+
Effect.sync(() => {
|
|
148
|
+
runs.n += 1
|
|
149
|
+
return count * 2
|
|
150
|
+
}),
|
|
151
|
+
),
|
|
103
152
|
})
|
|
104
153
|
const TestLayer = DoubledLive.pipe(Layer.provideMerge(StateGroup.live(Inputs, { count: 1 })))
|
|
105
154
|
|
|
106
155
|
return Effect.gen(function* () {
|
|
107
156
|
const source = yield* StateGroup.select(Inputs, 'count').store
|
|
108
157
|
const store = yield* Doubled.store
|
|
109
|
-
yield*
|
|
158
|
+
yield* until(
|
|
159
|
+
() => store.get(),
|
|
160
|
+
(v) => v._tag === 'Success',
|
|
161
|
+
)
|
|
110
162
|
expect(store.get()).toMatchObject({ _tag: 'Success', value: 2 })
|
|
111
163
|
|
|
164
|
+
// `4` lands while `3` is still parked at the gate, so it can only win by
|
|
165
|
+
// cancelling it.
|
|
166
|
+
yield* gate.hold
|
|
112
167
|
source.set(3)
|
|
113
|
-
yield*
|
|
168
|
+
yield* gate.awaitEntry(2)
|
|
114
169
|
source.set(4)
|
|
170
|
+
yield* gate.awaitEntry(3)
|
|
171
|
+
yield* gate.release
|
|
115
172
|
const settled = yield* until(
|
|
116
173
|
() => store.get(),
|
|
117
174
|
(v) => v._tag === 'Success' && v.refetching === false && v.value !== 2,
|
|
@@ -127,8 +184,9 @@ it.live('a gated query starts Idle while disabled and runs once enabled', () =>
|
|
|
127
184
|
inputs: [StateGroup.select(Inputs, 'count')],
|
|
128
185
|
output: S.Number,
|
|
129
186
|
}) {}
|
|
187
|
+
const gate = makeFlightGate()
|
|
130
188
|
const DoubledLive = AsyncCalc.live(Doubled, {
|
|
131
|
-
query: ({ count }) => Effect.succeed(count * 2),
|
|
189
|
+
query: ({ count }) => gate.through(Effect.succeed(count * 2)),
|
|
132
190
|
disabled: ({ count }) => count < 0,
|
|
133
191
|
})
|
|
134
192
|
const TestLayer = DoubledLive.pipe(Layer.provideMerge(StateGroup.live(Inputs, { count: -1 })))
|
|
@@ -136,12 +194,16 @@ it.live('a gated query starts Idle while disabled and runs once enabled', () =>
|
|
|
136
194
|
return Effect.gen(function* () {
|
|
137
195
|
const source = yield* StateGroup.select(Inputs, 'count').store
|
|
138
196
|
const store = yield* Doubled.store
|
|
139
|
-
yield* tick()
|
|
140
197
|
expect(store.get()._tag).toBe('Idle')
|
|
141
198
|
|
|
142
199
|
source.set(5)
|
|
143
|
-
yield*
|
|
144
|
-
|
|
200
|
+
const settled = yield* until(
|
|
201
|
+
() => store.get(),
|
|
202
|
+
(v) => v._tag === 'Success',
|
|
203
|
+
)
|
|
204
|
+
expect(settled).toMatchObject({ _tag: 'Success', value: 10 })
|
|
205
|
+
// Exactly one flight ever ran: the disabled window produced none.
|
|
206
|
+
expect(gate.entered()).toBe(1)
|
|
145
207
|
}).pipe(Effect.provide(TestLayer))
|
|
146
208
|
})
|
|
147
209
|
|
|
@@ -155,12 +217,15 @@ it.live('invalidateBy: an input change outside the key does not re-fetch', () =>
|
|
|
155
217
|
output: S.Number,
|
|
156
218
|
alwaysOn: true,
|
|
157
219
|
}) {}
|
|
220
|
+
const gate = makeFlightGate()
|
|
158
221
|
const QLive = AsyncCalc.live(Q, {
|
|
159
222
|
query: ({ page }) =>
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
223
|
+
gate.through(
|
|
224
|
+
Effect.sync(() => {
|
|
225
|
+
runs.n += 1
|
|
226
|
+
return page
|
|
227
|
+
}),
|
|
228
|
+
),
|
|
164
229
|
invalidateBy: ({ page }) => [page],
|
|
165
230
|
})
|
|
166
231
|
const TestLayer = QLive.pipe(
|
|
@@ -171,17 +236,27 @@ it.live('invalidateBy: an input change outside the key does not re-fetch', () =>
|
|
|
171
236
|
const status = yield* StateGroup.select(Inputs, 'status').store
|
|
172
237
|
const page = yield* StateGroup.select(Inputs, 'page').store
|
|
173
238
|
const store = yield* Q.store
|
|
174
|
-
yield*
|
|
239
|
+
yield* until(
|
|
240
|
+
() => store.get(),
|
|
241
|
+
(v) => v._tag === 'Success',
|
|
242
|
+
)
|
|
175
243
|
expect(runs.n).toBe(1)
|
|
176
244
|
|
|
177
|
-
status
|
|
178
|
-
|
|
245
|
+
// Holding the gate means any flight the status churn triggered would still
|
|
246
|
+
// be marked fetching here, and could not have settled back out of view.
|
|
247
|
+
yield* gate.hold
|
|
248
|
+
yield* afterNotify(status, Effect.sync(() => status.set('b')))
|
|
249
|
+
expect(store.get()).toMatchObject({ _tag: 'Success', refetching: false })
|
|
179
250
|
expect(runs.n).toBe(1)
|
|
251
|
+
yield* gate.release
|
|
180
252
|
|
|
181
253
|
page.set(2)
|
|
182
|
-
yield*
|
|
254
|
+
const settled = yield* until(
|
|
255
|
+
() => store.get(),
|
|
256
|
+
(v) => v._tag === 'Success' && v.refetching === false && v.value === 2,
|
|
257
|
+
)
|
|
183
258
|
expect(runs.n).toBe(2)
|
|
184
|
-
expect(
|
|
259
|
+
expect(settled).toMatchObject({ _tag: 'Success', value: 2 })
|
|
185
260
|
}).pipe(Effect.provide(TestLayer))
|
|
186
261
|
})
|
|
187
262
|
|
|
@@ -258,14 +333,13 @@ it.live(
|
|
|
258
333
|
)
|
|
259
334
|
expect(completed).toEqual([1])
|
|
260
335
|
|
|
261
|
-
// Hold the `2` flight at the gate so 3 and 4 land
|
|
336
|
+
// Hold the `2` flight at the gate so 3 and 4 land, in that order, while it
|
|
337
|
+
// is in flight.
|
|
262
338
|
yield* gate.hold
|
|
263
339
|
source.set(2)
|
|
264
340
|
yield* gate.awaitEntry(2)
|
|
265
|
-
source.set(3)
|
|
266
|
-
yield*
|
|
267
|
-
source.set(4)
|
|
268
|
-
yield* tick(5)
|
|
341
|
+
yield* afterNotify(source, Effect.sync(() => source.set(3)))
|
|
342
|
+
yield* afterNotify(source, Effect.sync(() => source.set(4)))
|
|
269
343
|
yield* gate.release
|
|
270
344
|
const settled = yield* until(
|
|
271
345
|
() => store.get(),
|
|
@@ -287,8 +361,15 @@ it.live(
|
|
|
287
361
|
output: S.Number,
|
|
288
362
|
alwaysOn: true,
|
|
289
363
|
}) {}
|
|
364
|
+
// Two gates: one parks the flight the burst interrupts, the other parks the
|
|
365
|
+
// trailing run, so the stale settle in between is a stable state, not a race.
|
|
366
|
+
const firstGate = makeFlightGate()
|
|
367
|
+
const trailingGate = makeFlightGate()
|
|
290
368
|
const DoubledLive = AsyncCalc.live(Doubled, {
|
|
291
|
-
query: ({ count }) =>
|
|
369
|
+
query: ({ count }) =>
|
|
370
|
+
count === 3
|
|
371
|
+
? trailingGate.through(Effect.succeed(count * 2))
|
|
372
|
+
: firstGate.through(Effect.succeed(count * 2)),
|
|
292
373
|
coalesce: 'trailing',
|
|
293
374
|
})
|
|
294
375
|
const TestLayer = DoubledLive.pipe(Layer.provideMerge(StateGroup.live(Inputs, { count: 1 })))
|
|
@@ -301,15 +382,17 @@ it.live(
|
|
|
301
382
|
(v) => v._tag === 'Success',
|
|
302
383
|
)
|
|
303
384
|
|
|
385
|
+
yield* firstGate.hold
|
|
386
|
+
yield* trailingGate.hold
|
|
304
387
|
source.set(2)
|
|
305
|
-
yield*
|
|
306
|
-
source.set(3)
|
|
307
|
-
const midflight = yield* until(
|
|
308
|
-
() => store.get(),
|
|
309
|
-
(v) => v._tag === 'Success' && v.value === 4,
|
|
310
|
-
)
|
|
311
|
-
expect(midflight).toMatchObject({ _tag: 'Success', value: 4, refetching: true })
|
|
388
|
+
yield* firstGate.awaitEntry(2)
|
|
389
|
+
yield* afterNotify(source, Effect.sync(() => source.set(3)))
|
|
312
390
|
|
|
391
|
+
yield* firstGate.release
|
|
392
|
+
yield* trailingGate.awaitEntry(1)
|
|
393
|
+
expect(store.get()).toMatchObject({ _tag: 'Success', value: 4, refetching: true })
|
|
394
|
+
|
|
395
|
+
yield* trailingGate.release
|
|
313
396
|
const settled = yield* until(
|
|
314
397
|
() => store.get(),
|
|
315
398
|
(v) => v._tag === 'Success' && v.refetching === false,
|
|
@@ -354,17 +437,14 @@ it.live('coalesce trailing: a disable while a request waits in the slot skips th
|
|
|
354
437
|
yield* gate.hold
|
|
355
438
|
source.set(2)
|
|
356
439
|
yield* gate.awaitEntry(2)
|
|
357
|
-
source.set(3)
|
|
358
|
-
yield*
|
|
359
|
-
source.set(-1)
|
|
360
|
-
yield* tick(5)
|
|
440
|
+
yield* afterNotify(source, Effect.sync(() => source.set(3)))
|
|
441
|
+
yield* afterNotify(source, Effect.sync(() => source.set(-1)))
|
|
361
442
|
yield* gate.release
|
|
362
|
-
// Asserting an absence: give the consumer a real window to wake and (wrongly)
|
|
363
|
-
// run the queued key. A slow machine only widens the window, never narrows it.
|
|
364
|
-
yield* tick(60)
|
|
365
443
|
expect(store.get()._tag).toBe('Idle')
|
|
366
444
|
expect(completed).toEqual([1, 2])
|
|
367
445
|
|
|
446
|
+
// The queue is FIFO: once 5 has landed, 3's fate is settled, and it must not
|
|
447
|
+
// appear between them.
|
|
368
448
|
source.set(5)
|
|
369
449
|
const settled = yield* until(
|
|
370
450
|
() => store.get(),
|
|
@@ -385,12 +465,15 @@ it.live('invalidateOn: dispatching a listed event re-fetches with refetching: tr
|
|
|
385
465
|
output: S.Number,
|
|
386
466
|
alwaysOn: true,
|
|
387
467
|
}) {}
|
|
468
|
+
const gate = makeFlightGate()
|
|
388
469
|
const QLive = AsyncCalc.live(Q, {
|
|
389
470
|
query: ({ count }) =>
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
471
|
+
gate.through(
|
|
472
|
+
Effect.sync(() => {
|
|
473
|
+
runs.n += 1
|
|
474
|
+
return count * 2
|
|
475
|
+
}),
|
|
476
|
+
),
|
|
394
477
|
invalidateOn: [Poke],
|
|
395
478
|
})
|
|
396
479
|
const TestLayer = QLive.pipe(
|
|
@@ -400,15 +483,24 @@ it.live('invalidateOn: dispatching a listed event re-fetches with refetching: tr
|
|
|
400
483
|
|
|
401
484
|
return Effect.gen(function* () {
|
|
402
485
|
const store = yield* Q.store
|
|
403
|
-
yield*
|
|
486
|
+
yield* until(
|
|
487
|
+
() => store.get(),
|
|
488
|
+
(v) => v._tag === 'Success' && v.refetching === false,
|
|
489
|
+
)
|
|
404
490
|
expect(store.get()).toMatchObject({ _tag: 'Success', value: 4, refetching: false })
|
|
405
491
|
expect(runs.n).toBe(1)
|
|
406
492
|
|
|
493
|
+
yield* gate.hold
|
|
407
494
|
yield* Event.dispatch(Poke, {})
|
|
408
|
-
yield*
|
|
495
|
+
yield* gate.awaitEntry(2)
|
|
409
496
|
expect(store.get()).toMatchObject({ _tag: 'Success', value: 4, refetching: true })
|
|
410
|
-
|
|
411
|
-
|
|
497
|
+
|
|
498
|
+
yield* gate.release
|
|
499
|
+
const settled = yield* until(
|
|
500
|
+
() => store.get(),
|
|
501
|
+
(v) => v._tag === 'Success' && v.refetching === false,
|
|
502
|
+
)
|
|
503
|
+
expect(settled).toMatchObject({ _tag: 'Success', value: 4, refetching: false })
|
|
412
504
|
expect(runs.n).toBe(2)
|
|
413
505
|
}).pipe(Effect.provide(TestLayer))
|
|
414
506
|
})
|
|
@@ -426,12 +518,15 @@ it.live(
|
|
|
426
518
|
output: S.Number,
|
|
427
519
|
alwaysOn: true,
|
|
428
520
|
}) {}
|
|
521
|
+
const gate = makeFlightGate()
|
|
429
522
|
const QLive = AsyncCalc.live(Q, {
|
|
430
523
|
query: ({ page }) =>
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
524
|
+
gate.through(
|
|
525
|
+
Effect.sync(() => {
|
|
526
|
+
runs.n += 1
|
|
527
|
+
return page
|
|
528
|
+
}),
|
|
529
|
+
),
|
|
435
530
|
invalidateBy: ({ page }) => [page],
|
|
436
531
|
invalidateOn: [Poke],
|
|
437
532
|
})
|
|
@@ -442,15 +537,21 @@ it.live(
|
|
|
442
537
|
|
|
443
538
|
return Effect.gen(function* () {
|
|
444
539
|
const status = yield* StateGroup.select(Inputs, 'status').store
|
|
445
|
-
yield*
|
|
540
|
+
const store = yield* Q.store
|
|
541
|
+
yield* until(
|
|
542
|
+
() => store.get(),
|
|
543
|
+
(v) => v._tag === 'Success' && v.refetching === false,
|
|
544
|
+
)
|
|
446
545
|
expect(runs.n).toBe(1)
|
|
447
546
|
|
|
448
|
-
|
|
449
|
-
yield*
|
|
547
|
+
yield* gate.hold
|
|
548
|
+
yield* afterNotify(status, Effect.sync(() => status.set('b')))
|
|
549
|
+
expect(store.get()).toMatchObject({ _tag: 'Success', refetching: false })
|
|
450
550
|
expect(runs.n).toBe(1)
|
|
551
|
+
yield* gate.release
|
|
451
552
|
|
|
452
553
|
yield* Event.dispatch(Poke, {})
|
|
453
|
-
yield*
|
|
554
|
+
yield* gate.awaitEntry(2)
|
|
454
555
|
expect(runs.n).toBe(2)
|
|
455
556
|
}).pipe(Effect.provide(TestLayer))
|
|
456
557
|
},
|
|
@@ -462,6 +563,10 @@ it.live(
|
|
|
462
563
|
class Count extends State.make('count', S.Number) {}
|
|
463
564
|
class Inputs extends StateGroup.make(Count) {}
|
|
464
565
|
class Poke extends Event.make('Poke', S.Struct({})) {}
|
|
566
|
+
// A second reducer on the same event: reducers of one frame apply in a single
|
|
567
|
+
// pass, so this state landing proves the query's revision write landed too.
|
|
568
|
+
class Pokes extends State.make('pokes', S.Number) {}
|
|
569
|
+
const PokesReducer = Reducer.make('pokes', { states: [Pokes], events: [Poke] })
|
|
465
570
|
const runs = { n: 0 }
|
|
466
571
|
class Q extends AsyncCalc.make('Q', {
|
|
467
572
|
inputs: [StateGroup.select(Inputs, 'count')],
|
|
@@ -476,25 +581,33 @@ it.live(
|
|
|
476
581
|
disabled: ({ count }) => count < 0,
|
|
477
582
|
invalidateOn: [Poke],
|
|
478
583
|
})
|
|
479
|
-
const TestLayer =
|
|
584
|
+
const TestLayer = Layer.mergeAll(
|
|
585
|
+
QLive,
|
|
586
|
+
Reducer.live(PokesReducer, (seen) => seen + 1),
|
|
587
|
+
).pipe(
|
|
480
588
|
Layer.provideMerge(StateGroup.live(Inputs, { count: -1 })),
|
|
589
|
+
Layer.provideMerge(State.live(Pokes, 0)),
|
|
481
590
|
Layer.provideMerge(Engine),
|
|
482
591
|
)
|
|
483
592
|
|
|
484
593
|
return Effect.gen(function* () {
|
|
485
594
|
const source = yield* StateGroup.select(Inputs, 'count').store
|
|
595
|
+
const pokes = yield* Pokes.store
|
|
486
596
|
const store = yield* Q.store
|
|
487
|
-
yield* tick()
|
|
488
597
|
expect(store.get()._tag).toBe('Idle')
|
|
489
598
|
|
|
490
|
-
|
|
491
|
-
|
|
599
|
+
// An enqueued run marks the state fetching before it can run, so staying
|
|
600
|
+
// Idle across the frame is proof that nothing was enqueued.
|
|
601
|
+
yield* afterNotify(pokes, Event.dispatch(Poke, {}))
|
|
492
602
|
expect(store.get()._tag).toBe('Idle')
|
|
493
603
|
expect(runs.n).toBe(0)
|
|
494
604
|
|
|
495
605
|
source.set(5)
|
|
496
|
-
yield*
|
|
497
|
-
|
|
606
|
+
const settled = yield* until(
|
|
607
|
+
() => store.get(),
|
|
608
|
+
(v) => v._tag === 'Success',
|
|
609
|
+
)
|
|
610
|
+
expect(settled).toMatchObject({ _tag: 'Success', value: 10 })
|
|
498
611
|
expect(runs.n).toBe(1)
|
|
499
612
|
}).pipe(Effect.provide(TestLayer))
|
|
500
613
|
},
|
|
@@ -516,35 +629,62 @@ it.live('invalidateOn: two async calcs keep independent hidden revisions', () =>
|
|
|
516
629
|
output: S.Number,
|
|
517
630
|
alwaysOn: true,
|
|
518
631
|
}) {}
|
|
632
|
+
const gateA = makeFlightGate()
|
|
633
|
+
const gateB = makeFlightGate()
|
|
519
634
|
const TestLayer = Layer.mergeAll(
|
|
520
635
|
AsyncCalc.live(QA, {
|
|
521
636
|
query: ({ count }) =>
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
637
|
+
gateA.through(
|
|
638
|
+
Effect.sync(() => {
|
|
639
|
+
runs.a += 1
|
|
640
|
+
return count
|
|
641
|
+
}),
|
|
642
|
+
),
|
|
526
643
|
invalidateOn: [PokeA],
|
|
527
644
|
}),
|
|
528
645
|
AsyncCalc.live(QB, {
|
|
529
646
|
query: ({ count }) =>
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
647
|
+
gateB.through(
|
|
648
|
+
Effect.sync(() => {
|
|
649
|
+
runs.b += 1
|
|
650
|
+
return count
|
|
651
|
+
}),
|
|
652
|
+
),
|
|
534
653
|
invalidateOn: [PokeB],
|
|
535
654
|
}),
|
|
536
655
|
).pipe(Layer.provideMerge(StateGroup.live(Inputs, { count: 1 })), Layer.provideMerge(Engine))
|
|
537
656
|
|
|
538
657
|
return Effect.gen(function* () {
|
|
539
|
-
yield*
|
|
658
|
+
const storeA = yield* QA.store
|
|
659
|
+
const storeB = yield* QB.store
|
|
660
|
+
yield* until(
|
|
661
|
+
() => storeA.get()._tag === 'Success' && storeB.get()._tag === 'Success',
|
|
662
|
+
(both) => both,
|
|
663
|
+
)
|
|
540
664
|
expect(runs).toEqual({ a: 1, b: 1 })
|
|
541
665
|
|
|
666
|
+
// B's gate is held, so a revision leak from PokeA would leave B visibly
|
|
667
|
+
// fetching instead of settling back out of sight.
|
|
668
|
+
yield* gateB.hold
|
|
542
669
|
yield* Event.dispatch(PokeA, {})
|
|
543
|
-
yield*
|
|
670
|
+
yield* gateA.awaitEntry(2)
|
|
671
|
+
yield* until(
|
|
672
|
+
() => storeA.get(),
|
|
673
|
+
(v) => v._tag === 'Success' && v.refetching === false,
|
|
674
|
+
)
|
|
675
|
+
expect(storeB.get()).toMatchObject({ _tag: 'Success', refetching: false })
|
|
544
676
|
expect(runs).toEqual({ a: 2, b: 1 })
|
|
677
|
+
yield* gateB.release
|
|
545
678
|
|
|
679
|
+
yield* gateA.hold
|
|
546
680
|
yield* Event.dispatch(PokeB, {})
|
|
547
|
-
yield*
|
|
681
|
+
yield* gateB.awaitEntry(2)
|
|
682
|
+
yield* until(
|
|
683
|
+
() => storeB.get(),
|
|
684
|
+
(v) => v._tag === 'Success' && v.refetching === false,
|
|
685
|
+
)
|
|
686
|
+
expect(storeA.get()).toMatchObject({ _tag: 'Success', refetching: false })
|
|
548
687
|
expect(runs).toEqual({ a: 2, b: 2 })
|
|
688
|
+
yield* gateA.release
|
|
549
689
|
}).pipe(Effect.provide(TestLayer))
|
|
550
690
|
})
|