@playfast/reform 1.0.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 +103 -56
- package/package.json +17 -17
- package/src/boundary/boundary.test.ts +63 -49
- package/src/boundary/boundary.ts +144 -113
- package/src/calc/asyncCalc.invalidate.test.ts +518 -32
- package/src/calc/asyncCalc.test.ts +207 -213
- package/src/calc/asyncCalc.ts +131 -154
- package/src/calc/asyncData.ts +0 -37
- package/src/calc/calc.test.ts +7 -33
- package/src/calc/calc.ts +44 -58
- package/src/calc/calcFamily.test.ts +80 -34
- package/src/calc/calcFamily.ts +54 -65
- package/src/calc/compose.test.ts +1 -12
- package/src/calc/compose.ts +1 -36
- package/src/calc/queryState.ts +0 -23
- package/src/channel/channel.ts +126 -111
- package/src/compose/composition.test.ts +19 -0
- package/src/compose/composition.ts +351 -127
- package/src/compose/host.ts +0 -6
- package/src/compose/props.ts +13 -12
- package/src/compose/provide.ts +189 -62
- package/src/compose/slot.ts +158 -49
- package/src/compose/structure.test.ts +6 -9
- package/src/compose/structure.ts +108 -79
- package/src/compose/ui.test.ts +19 -7
- package/src/compose/ui.ts +155 -156
- package/src/compose/ui.typecheck.ts +40 -18
- package/src/definition/definition.ts +22 -41
- package/src/event/event.fromSource.test.ts +172 -0
- package/src/event/event.test.ts +10 -3
- package/src/event/event.ts +102 -27
- package/src/event/eventGroup.ts +0 -1
- package/src/feature/feature.mount.test.ts +122 -43
- package/src/feature/feature.test.ts +46 -21
- package/src/feature/feature.ts +1347 -256
- package/src/feature/feature.typecheck.ts +273 -68
- package/src/graph/closure.ts +403 -0
- package/src/index.ts +68 -126
- package/src/internal/bucketCache.ts +39 -0
- package/src/internal/capture.ts +9 -24
- package/src/internal/env.ts +7 -0
- package/src/internal/errors.test.ts +0 -6
- package/src/internal/errors.ts +37 -60
- package/src/internal/inspect.test.ts +0 -6
- package/src/internal/inspect.ts +0 -12
- package/src/internal/queryDriver.ts +105 -201
- package/src/internal/queryDriverStore.ts +156 -0
- package/src/internal/queryDriverTypes.ts +59 -0
- package/src/internal/queryEvents.ts +0 -12
- package/src/internal/queryStore.ts +0 -14
- package/src/internal/reuse.test.ts +10 -14
- package/src/internal/reuse.ts +5 -30
- package/src/internal/scheduler.ts +4 -44
- package/src/internal/seeds.ts +0 -14
- package/src/internal/sources.ts +26 -49
- package/src/internal/stateRegistry.ts +0 -14
- package/src/internal/store.test.ts +1 -3
- package/src/internal/store.ts +0 -37
- package/src/internal/track.ts +3 -20
- package/src/internal/variance.ts +5 -0
- package/src/internal.ts +222 -0
- package/src/namespace/namespace.test.ts +46 -0
- package/src/namespace/namespace.ts +106 -0
- package/src/procedure/procedure.ts +40 -35
- package/src/reducer/reducer.ts +78 -52
- package/src/remote/remoteState.test.ts +590 -397
- package/src/remote/remoteState.ts +425 -347
- package/src/remote/remoteState.typecheck.ts +47 -56
- package/src/runtime/appRuntime.activation.test.ts +100 -0
- package/src/runtime/appRuntime.test.ts +249 -0
- package/src/runtime/appRuntime.ts +415 -97
- package/src/runtime/bus.ts +2 -15
- package/src/runtime/eventBudget.test.ts +152 -0
- package/src/runtime/eventBudget.ts +120 -0
- package/src/runtime/hardening.test.ts +73 -13
- package/src/runtime/instrumentation.test.ts +55 -52
- package/src/runtime/instrumentation.ts +6 -60
- package/src/runtime/loop.test.ts +36 -17
- package/src/runtime/loop.ts +87 -88
- package/src/runtime/queries.ts +0 -17
- package/src/scene/featureScene.test.ts +61 -0
- package/src/scene/scene.ts +247 -104
- package/src/scene/seedScene.test.ts +42 -79
- package/src/state/state.nominal.typecheck.ts +68 -0
- package/src/state/state.test.ts +17 -0
- package/src/state/state.ts +80 -46
- package/src/state/stateFamily.test.ts +16 -11
- package/src/state/stateFamily.ts +55 -65
- package/src/state/stateGroup.ts +53 -64
- package/src/state/token.ts +40 -23
- package/src/synced/syncedStore.ts +46 -58
- package/src/testkit/flight.testkit.ts +75 -0
- package/src/ui/node.ts +0 -6
- package/src/ui/trigger.ts +0 -5
- package/src/wire/tree.test.ts +8 -7
- package/src/wire/tree.ts +0 -39
- package/src/wire/triggers.test.ts +6 -6
- package/src/wire/triggers.ts +14 -32
- package/src/internal/ctx.ts +0 -16
|
@@ -1,22 +1,7 @@
|
|
|
1
1
|
import { expect, it } from '@effect/vitest'
|
|
2
2
|
import { Data, Duration, Effect, Layer, Schema as S } from 'effect'
|
|
3
3
|
import { AsyncCalc, Engine, Event, State, StateGroup } from '../index'
|
|
4
|
-
|
|
5
|
-
// AsyncCalc is a derived store whose value is the lifecycle of an Effect. It runs
|
|
6
|
-
// the query reactively from its inputs (latest-wins), keeps the last value while
|
|
7
|
-
// re-fetching (stale-while-revalidate), and goes Idle when a gated query disables.
|
|
8
|
-
|
|
9
|
-
const tick = (ms = 10) => Effect.sleep(Duration.millis(ms))
|
|
10
|
-
|
|
11
|
-
// Poll until the predicate holds (bounded) — for assertions that race real
|
|
12
|
-
// timers under parallel-suite host load, where a fixed `tick` budget flakes.
|
|
13
|
-
const until = <A>(read: () => A, pred: (a: A) => boolean, rounds = 200): Effect.Effect<A> =>
|
|
14
|
-
Effect.suspend(() => {
|
|
15
|
-
const a = read()
|
|
16
|
-
return pred(a) || rounds <= 0
|
|
17
|
-
? Effect.succeed(a)
|
|
18
|
-
: Effect.flatMap(tick(10), () => until(read, pred, rounds - 1))
|
|
19
|
-
})
|
|
4
|
+
import { makeFlightGate, tick, until } from '../testkit/flight.testkit'
|
|
20
5
|
|
|
21
6
|
class Boom extends Data.TaggedError('Boom')<{ readonly message: string }> {}
|
|
22
7
|
|
|
@@ -35,7 +20,6 @@ it.live('runs the query and resolves Loading -> Success', () => {
|
|
|
35
20
|
|
|
36
21
|
return Effect.gen(function* () {
|
|
37
22
|
const store = yield* Doubled.store
|
|
38
|
-
// Before the forked query resolves, the value is Loading.
|
|
39
23
|
expect(store.get()._tag).toBe('Loading')
|
|
40
24
|
yield* tick(40)
|
|
41
25
|
const value = store.get()
|
|
@@ -92,7 +76,6 @@ it.live('a dependency change re-fetches, keeping the last value with refetching:
|
|
|
92
76
|
yield* tick(40)
|
|
93
77
|
expect(store.get()).toMatchObject({ _tag: 'Success', value: 10, refetching: false })
|
|
94
78
|
|
|
95
|
-
// Move the input: the prior value is kept while the new fetch is in flight.
|
|
96
79
|
source.set(7)
|
|
97
80
|
yield* tick(5)
|
|
98
81
|
expect(store.get()).toMatchObject({ _tag: 'Success', value: 10, refetching: true })
|
|
@@ -126,9 +109,6 @@ it.live('latest-wins: a rapid second change cancels the in-flight run', () => {
|
|
|
126
109
|
yield* tick(40)
|
|
127
110
|
expect(store.get()).toMatchObject({ _tag: 'Success', value: 2 })
|
|
128
111
|
|
|
129
|
-
// Two changes in quick succession: the first run is switched out before it
|
|
130
|
-
// can write, so only the latest value lands (poll — fixed sleeps flake when
|
|
131
|
-
// a loaded host starves the 25ms query timer).
|
|
132
112
|
source.set(3)
|
|
133
113
|
yield* tick(5)
|
|
134
114
|
source.set(4)
|
|
@@ -181,7 +161,6 @@ it.live('invalidateBy: an input change outside the key does not re-fetch', () =>
|
|
|
181
161
|
runs.n += 1
|
|
182
162
|
return page
|
|
183
163
|
}),
|
|
184
|
-
// Only `page` is part of the key; `status` churn must not re-fetch.
|
|
185
164
|
invalidateBy: ({ page }) => [page],
|
|
186
165
|
})
|
|
187
166
|
const TestLayer = QLive.pipe(
|
|
@@ -195,12 +174,10 @@ it.live('invalidateBy: an input change outside the key does not re-fetch', () =>
|
|
|
195
174
|
yield* tick()
|
|
196
175
|
expect(runs.n).toBe(1)
|
|
197
176
|
|
|
198
|
-
// status moves, key unchanged -> no re-fetch.
|
|
199
177
|
status.set('b')
|
|
200
178
|
yield* tick()
|
|
201
179
|
expect(runs.n).toBe(1)
|
|
202
180
|
|
|
203
|
-
// page moves, key changes -> one re-fetch.
|
|
204
181
|
page.set(2)
|
|
205
182
|
yield* tick()
|
|
206
183
|
expect(runs.n).toBe(2)
|
|
@@ -218,7 +195,6 @@ it.live('reuse: a refetch returning mostly-equal data keeps unchanged element id
|
|
|
218
195
|
alwaysOn: true,
|
|
219
196
|
}) {}
|
|
220
197
|
const RowsLive = AsyncCalc.live(Rows, {
|
|
221
|
-
// Fresh objects every run — only the row matching `page` differs.
|
|
222
198
|
query: ({ page }) =>
|
|
223
199
|
Effect.succeed([
|
|
224
200
|
{ id: 1, name: 'one' },
|
|
@@ -231,7 +207,10 @@ it.live('reuse: a refetch returning mostly-equal data keeps unchanged element id
|
|
|
231
207
|
return Effect.gen(function* () {
|
|
232
208
|
const source = yield* StateGroup.select(Inputs, 'page').store
|
|
233
209
|
const store = yield* Rows.store
|
|
234
|
-
const first = yield* until(
|
|
210
|
+
const first = yield* until(
|
|
211
|
+
() => store.get(),
|
|
212
|
+
(v) => v._tag === 'Success',
|
|
213
|
+
)
|
|
235
214
|
if (first._tag !== 'Success') return
|
|
236
215
|
|
|
237
216
|
source.set(2)
|
|
@@ -240,94 +219,105 @@ it.live('reuse: a refetch returning mostly-equal data keeps unchanged element id
|
|
|
240
219
|
(v) => v._tag === 'Success' && v.refetching === false && v.value !== first.value,
|
|
241
220
|
)
|
|
242
221
|
if (second._tag !== 'Success') return
|
|
243
|
-
// Row 1 didn't move: same identity. Row 2 did: fresh.
|
|
244
222
|
expect(second.value[0]).toBe(first.value[0])
|
|
245
223
|
expect(second.value[1]).not.toBe(first.value[1])
|
|
246
224
|
expect(second.value[1]).toEqual({ id: 2, name: 'two@2' })
|
|
247
225
|
}).pipe(Effect.provide(TestLayer))
|
|
248
226
|
})
|
|
249
227
|
|
|
250
|
-
it.live(
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
const
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
(
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
(
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
228
|
+
it.live(
|
|
229
|
+
'coalesce trailing: a burst during one flight completes it and runs ONE trailing refetch',
|
|
230
|
+
() => {
|
|
231
|
+
class Count extends State.make('count', S.Number) {}
|
|
232
|
+
class Inputs extends StateGroup.make(Count) {}
|
|
233
|
+
const completed: number[] = []
|
|
234
|
+
class Doubled extends AsyncCalc.make('Doubled', {
|
|
235
|
+
inputs: [StateGroup.select(Inputs, 'count')],
|
|
236
|
+
output: S.Number,
|
|
237
|
+
alwaysOn: true,
|
|
238
|
+
}) {}
|
|
239
|
+
const gate = makeFlightGate()
|
|
240
|
+
const DoubledLive = AsyncCalc.live(Doubled, {
|
|
241
|
+
query: ({ count }) =>
|
|
242
|
+
gate.through(
|
|
243
|
+
Effect.sync(() => {
|
|
244
|
+
completed.push(count)
|
|
245
|
+
return count * 2
|
|
246
|
+
}),
|
|
247
|
+
),
|
|
248
|
+
coalesce: 'trailing',
|
|
249
|
+
})
|
|
250
|
+
const TestLayer = DoubledLive.pipe(Layer.provideMerge(StateGroup.live(Inputs, { count: 1 })))
|
|
251
|
+
|
|
252
|
+
return Effect.gen(function* () {
|
|
253
|
+
const source = yield* StateGroup.select(Inputs, 'count').store
|
|
254
|
+
const store = yield* Doubled.store
|
|
255
|
+
yield* until(
|
|
256
|
+
() => store.get(),
|
|
257
|
+
(v) => v._tag === 'Success',
|
|
258
|
+
)
|
|
259
|
+
expect(completed).toEqual([1])
|
|
260
|
+
|
|
261
|
+
// Hold the `2` flight at the gate so 3 and 4 land while it is in flight.
|
|
262
|
+
yield* gate.hold
|
|
263
|
+
source.set(2)
|
|
264
|
+
yield* gate.awaitEntry(2)
|
|
265
|
+
source.set(3)
|
|
266
|
+
yield* tick(5)
|
|
267
|
+
source.set(4)
|
|
268
|
+
yield* tick(5)
|
|
269
|
+
yield* gate.release
|
|
270
|
+
const settled = yield* until(
|
|
271
|
+
() => store.get(),
|
|
272
|
+
(v) => v._tag === 'Success' && v.refetching === false && v.value === 8,
|
|
273
|
+
)
|
|
274
|
+
expect(settled).toMatchObject({ _tag: 'Success', value: 8, refetching: false })
|
|
275
|
+
expect(completed).toEqual([1, 2, 4])
|
|
276
|
+
}).pipe(Effect.provide(TestLayer))
|
|
277
|
+
},
|
|
278
|
+
)
|
|
279
|
+
|
|
280
|
+
it.live(
|
|
281
|
+
'coalesce trailing: the stale settle re-flags refetching until the trailing run lands',
|
|
282
|
+
() => {
|
|
283
|
+
class Count extends State.make('count', S.Number) {}
|
|
284
|
+
class Inputs extends StateGroup.make(Count) {}
|
|
285
|
+
class Doubled extends AsyncCalc.make('Doubled', {
|
|
286
|
+
inputs: [StateGroup.select(Inputs, 'count')],
|
|
287
|
+
output: S.Number,
|
|
288
|
+
alwaysOn: true,
|
|
289
|
+
}) {}
|
|
290
|
+
const DoubledLive = AsyncCalc.live(Doubled, {
|
|
291
|
+
query: ({ count }) => Effect.succeed(count * 2).pipe(Effect.delay(Duration.millis(30))),
|
|
292
|
+
coalesce: 'trailing',
|
|
293
|
+
})
|
|
294
|
+
const TestLayer = DoubledLive.pipe(Layer.provideMerge(StateGroup.live(Inputs, { count: 1 })))
|
|
295
|
+
|
|
296
|
+
return Effect.gen(function* () {
|
|
297
|
+
const source = yield* StateGroup.select(Inputs, 'count').store
|
|
298
|
+
const store = yield* Doubled.store
|
|
299
|
+
yield* until(
|
|
300
|
+
() => store.get(),
|
|
301
|
+
(v) => v._tag === 'Success',
|
|
302
|
+
)
|
|
303
|
+
|
|
304
|
+
source.set(2)
|
|
305
|
+
yield* tick(5)
|
|
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 })
|
|
312
|
+
|
|
313
|
+
const settled = yield* until(
|
|
314
|
+
() => store.get(),
|
|
315
|
+
(v) => v._tag === 'Success' && v.refetching === false,
|
|
316
|
+
)
|
|
317
|
+
expect(settled).toMatchObject({ _tag: 'Success', value: 6 })
|
|
318
|
+
}).pipe(Effect.provide(TestLayer))
|
|
319
|
+
},
|
|
320
|
+
)
|
|
331
321
|
|
|
332
322
|
it.live('coalesce trailing: a disable while a request waits in the slot skips the run', () => {
|
|
333
323
|
class Count extends State.make('count', S.Number) {}
|
|
@@ -337,12 +327,15 @@ it.live('coalesce trailing: a disable while a request waits in the slot skips th
|
|
|
337
327
|
inputs: [StateGroup.select(Inputs, 'count')],
|
|
338
328
|
output: S.Number,
|
|
339
329
|
}) {}
|
|
330
|
+
const gate = makeFlightGate()
|
|
340
331
|
const QLive = AsyncCalc.live(Q, {
|
|
341
332
|
query: ({ count }) =>
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
333
|
+
gate.through(
|
|
334
|
+
Effect.sync(() => {
|
|
335
|
+
completed.push(count)
|
|
336
|
+
return count * 2
|
|
337
|
+
}),
|
|
338
|
+
),
|
|
346
339
|
disabled: ({ count }) => count < 0,
|
|
347
340
|
coalesce: 'trailing',
|
|
348
341
|
})
|
|
@@ -351,30 +344,37 @@ it.live('coalesce trailing: a disable while a request waits in the slot skips th
|
|
|
351
344
|
return Effect.gen(function* () {
|
|
352
345
|
const source = yield* StateGroup.select(Inputs, 'count').store
|
|
353
346
|
const store = yield* Q.store
|
|
354
|
-
yield* until(
|
|
347
|
+
yield* until(
|
|
348
|
+
() => store.get(),
|
|
349
|
+
(v) => v._tag === 'Success',
|
|
350
|
+
)
|
|
355
351
|
|
|
356
|
-
//
|
|
357
|
-
//
|
|
352
|
+
// 3 queues behind the held `2` flight, then the disable overtakes it: the
|
|
353
|
+
// queued run must be dropped, not executed on release.
|
|
354
|
+
yield* gate.hold
|
|
358
355
|
source.set(2)
|
|
359
|
-
yield*
|
|
356
|
+
yield* gate.awaitEntry(2)
|
|
360
357
|
source.set(3)
|
|
361
|
-
yield* tick(
|
|
358
|
+
yield* tick(5)
|
|
362
359
|
source.set(-1)
|
|
360
|
+
yield* tick(5)
|
|
361
|
+
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.
|
|
363
364
|
yield* tick(60)
|
|
364
365
|
expect(store.get()._tag).toBe('Idle')
|
|
365
366
|
expect(completed).toEqual([1, 2])
|
|
366
367
|
|
|
367
|
-
// Re-enabling re-runs (the driver forgot its key when switched off).
|
|
368
368
|
source.set(5)
|
|
369
|
-
const settled = yield* until(
|
|
369
|
+
const settled = yield* until(
|
|
370
|
+
() => store.get(),
|
|
371
|
+
(v) => v._tag === 'Success',
|
|
372
|
+
)
|
|
370
373
|
expect(settled).toMatchObject({ _tag: 'Success', value: 10 })
|
|
371
374
|
expect(completed).toEqual([1, 2, 5])
|
|
372
375
|
}).pipe(Effect.provide(TestLayer))
|
|
373
376
|
})
|
|
374
377
|
|
|
375
|
-
// `invalidateOn` is sugar for a hidden revision state + reducer appended to the
|
|
376
|
-
// key — these proofs need the Engine (the Reducers registry + bus + drain loop).
|
|
377
|
-
|
|
378
378
|
it.live('invalidateOn: dispatching a listed event re-fetches with refetching: true', () => {
|
|
379
379
|
class Count extends State.make('count', S.Number) {}
|
|
380
380
|
class Inputs extends StateGroup.make(Count) {}
|
|
@@ -404,8 +404,6 @@ it.live('invalidateOn: dispatching a listed event re-fetches with refetching: tr
|
|
|
404
404
|
expect(store.get()).toMatchObject({ _tag: 'Success', value: 4, refetching: false })
|
|
405
405
|
expect(runs.n).toBe(1)
|
|
406
406
|
|
|
407
|
-
// The fact lands: the hidden revision bumps, the key moves, the value is
|
|
408
|
-
// kept while the re-fetch is in flight (SWR), then settles.
|
|
409
407
|
yield* Event.dispatch(Poke, {})
|
|
410
408
|
yield* tick(5)
|
|
411
409
|
expect(store.get()).toMatchObject({ _tag: 'Success', value: 4, refetching: true })
|
|
@@ -415,91 +413,92 @@ it.live('invalidateOn: dispatching a listed event re-fetches with refetching: tr
|
|
|
415
413
|
}).pipe(Effect.provide(TestLayer))
|
|
416
414
|
})
|
|
417
415
|
|
|
418
|
-
it.live(
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
const status = yield* StateGroup.select(Inputs, 'status').store
|
|
445
|
-
yield* tick()
|
|
446
|
-
expect(runs.n).toBe(1)
|
|
447
|
-
|
|
448
|
-
// Outside the user key: still no re-fetch (the hidden revision rides along
|
|
449
|
-
// WITHOUT widening the key to every input).
|
|
450
|
-
status.set('b')
|
|
451
|
-
yield* tick()
|
|
452
|
-
expect(runs.n).toBe(1)
|
|
453
|
-
|
|
454
|
-
// The listed event always re-fetches.
|
|
455
|
-
yield* Event.dispatch(Poke, {})
|
|
456
|
-
yield* tick()
|
|
457
|
-
expect(runs.n).toBe(2)
|
|
458
|
-
}).pipe(Effect.provide(TestLayer))
|
|
459
|
-
})
|
|
460
|
-
|
|
461
|
-
it.live('invalidateOn: a listed event while disabled stays Idle, and enabling re-fetches once', () => {
|
|
462
|
-
class Count extends State.make('count', S.Number) {}
|
|
463
|
-
class Inputs extends StateGroup.make(Count) {}
|
|
464
|
-
class Poke extends Event.make('Poke', S.Struct({})) {}
|
|
465
|
-
const runs = { n: 0 }
|
|
466
|
-
class Q extends AsyncCalc.make('Q', {
|
|
467
|
-
inputs: [StateGroup.select(Inputs, 'count')],
|
|
468
|
-
output: S.Number,
|
|
469
|
-
}) {}
|
|
470
|
-
const QLive = AsyncCalc.live(Q, {
|
|
471
|
-
query: ({ count }) =>
|
|
472
|
-
Effect.sync(() => {
|
|
473
|
-
runs.n += 1
|
|
474
|
-
return count * 2
|
|
475
|
-
}),
|
|
476
|
-
disabled: ({ count }) => count < 0,
|
|
477
|
-
invalidateOn: [Poke],
|
|
478
|
-
})
|
|
479
|
-
const TestLayer = QLive.pipe(
|
|
480
|
-
Layer.provideMerge(StateGroup.live(Inputs, { count: -1 })),
|
|
481
|
-
Layer.provideMerge(Engine),
|
|
482
|
-
)
|
|
483
|
-
|
|
484
|
-
return Effect.gen(function* () {
|
|
485
|
-
const source = yield* StateGroup.select(Inputs, 'count').store
|
|
486
|
-
const store = yield* Q.store
|
|
487
|
-
yield* tick()
|
|
488
|
-
expect(store.get()._tag).toBe('Idle')
|
|
416
|
+
it.live(
|
|
417
|
+
'invalidateOn: composes with invalidateBy — non-key churn stays inert, the event re-fetches',
|
|
418
|
+
() => {
|
|
419
|
+
class Status extends State.make('status', S.String) {}
|
|
420
|
+
class Page extends State.make('page', S.Number) {}
|
|
421
|
+
class Inputs extends StateGroup.make(Status, Page) {}
|
|
422
|
+
class Poke extends Event.make('Poke', S.Struct({})) {}
|
|
423
|
+
const runs = { n: 0 }
|
|
424
|
+
class Q extends AsyncCalc.make('Q', {
|
|
425
|
+
inputs: [StateGroup.select(Inputs, 'status'), StateGroup.select(Inputs, 'page')],
|
|
426
|
+
output: S.Number,
|
|
427
|
+
alwaysOn: true,
|
|
428
|
+
}) {}
|
|
429
|
+
const QLive = AsyncCalc.live(Q, {
|
|
430
|
+
query: ({ page }) =>
|
|
431
|
+
Effect.sync(() => {
|
|
432
|
+
runs.n += 1
|
|
433
|
+
return page
|
|
434
|
+
}),
|
|
435
|
+
invalidateBy: ({ page }) => [page],
|
|
436
|
+
invalidateOn: [Poke],
|
|
437
|
+
})
|
|
438
|
+
const TestLayer = QLive.pipe(
|
|
439
|
+
Layer.provideMerge(StateGroup.live(Inputs, { status: 'a', page: 1 })),
|
|
440
|
+
Layer.provideMerge(Engine),
|
|
441
|
+
)
|
|
489
442
|
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
443
|
+
return Effect.gen(function* () {
|
|
444
|
+
const status = yield* StateGroup.select(Inputs, 'status').store
|
|
445
|
+
yield* tick()
|
|
446
|
+
expect(runs.n).toBe(1)
|
|
447
|
+
|
|
448
|
+
status.set('b')
|
|
449
|
+
yield* tick()
|
|
450
|
+
expect(runs.n).toBe(1)
|
|
451
|
+
|
|
452
|
+
yield* Event.dispatch(Poke, {})
|
|
453
|
+
yield* tick()
|
|
454
|
+
expect(runs.n).toBe(2)
|
|
455
|
+
}).pipe(Effect.provide(TestLayer))
|
|
456
|
+
},
|
|
457
|
+
)
|
|
458
|
+
|
|
459
|
+
it.live(
|
|
460
|
+
'invalidateOn: a listed event while disabled stays Idle, and enabling re-fetches once',
|
|
461
|
+
() => {
|
|
462
|
+
class Count extends State.make('count', S.Number) {}
|
|
463
|
+
class Inputs extends StateGroup.make(Count) {}
|
|
464
|
+
class Poke extends Event.make('Poke', S.Struct({})) {}
|
|
465
|
+
const runs = { n: 0 }
|
|
466
|
+
class Q extends AsyncCalc.make('Q', {
|
|
467
|
+
inputs: [StateGroup.select(Inputs, 'count')],
|
|
468
|
+
output: S.Number,
|
|
469
|
+
}) {}
|
|
470
|
+
const QLive = AsyncCalc.live(Q, {
|
|
471
|
+
query: ({ count }) =>
|
|
472
|
+
Effect.sync(() => {
|
|
473
|
+
runs.n += 1
|
|
474
|
+
return count * 2
|
|
475
|
+
}),
|
|
476
|
+
disabled: ({ count }) => count < 0,
|
|
477
|
+
invalidateOn: [Poke],
|
|
478
|
+
})
|
|
479
|
+
const TestLayer = QLive.pipe(
|
|
480
|
+
Layer.provideMerge(StateGroup.live(Inputs, { count: -1 })),
|
|
481
|
+
Layer.provideMerge(Engine),
|
|
482
|
+
)
|
|
496
483
|
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
484
|
+
return Effect.gen(function* () {
|
|
485
|
+
const source = yield* StateGroup.select(Inputs, 'count').store
|
|
486
|
+
const store = yield* Q.store
|
|
487
|
+
yield* tick()
|
|
488
|
+
expect(store.get()._tag).toBe('Idle')
|
|
489
|
+
|
|
490
|
+
yield* Event.dispatch(Poke, {})
|
|
491
|
+
yield* tick()
|
|
492
|
+
expect(store.get()._tag).toBe('Idle')
|
|
493
|
+
expect(runs.n).toBe(0)
|
|
494
|
+
|
|
495
|
+
source.set(5)
|
|
496
|
+
yield* tick()
|
|
497
|
+
expect(store.get()).toMatchObject({ _tag: 'Success', value: 10 })
|
|
498
|
+
expect(runs.n).toBe(1)
|
|
499
|
+
}).pipe(Effect.provide(TestLayer))
|
|
500
|
+
},
|
|
501
|
+
)
|
|
503
502
|
|
|
504
503
|
it.live('invalidateOn: two async calcs keep independent hidden revisions', () => {
|
|
505
504
|
class Count extends State.make('count', S.Number) {}
|
|
@@ -534,17 +533,12 @@ it.live('invalidateOn: two async calcs keep independent hidden revisions', () =>
|
|
|
534
533
|
}),
|
|
535
534
|
invalidateOn: [PokeB],
|
|
536
535
|
}),
|
|
537
|
-
).pipe(
|
|
538
|
-
Layer.provideMerge(StateGroup.live(Inputs, { count: 1 })),
|
|
539
|
-
Layer.provideMerge(Engine),
|
|
540
|
-
)
|
|
536
|
+
).pipe(Layer.provideMerge(StateGroup.live(Inputs, { count: 1 })), Layer.provideMerge(Engine))
|
|
541
537
|
|
|
542
538
|
return Effect.gen(function* () {
|
|
543
539
|
yield* tick()
|
|
544
540
|
expect(runs).toEqual({ a: 1, b: 1 })
|
|
545
541
|
|
|
546
|
-
// Each calc's hidden revision is namespaced by its own name: A's fact moves
|
|
547
|
-
// only A.
|
|
548
542
|
yield* Event.dispatch(PokeA, {})
|
|
549
543
|
yield* tick()
|
|
550
544
|
expect(runs).toEqual({ a: 2, b: 1 })
|