@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
|
@@ -2,18 +2,17 @@ import { expect, it } from '@effect/vitest'
|
|
|
2
2
|
import { Duration, Effect, Layer, Match, Option, Schema as S } from 'effect'
|
|
3
3
|
import {
|
|
4
4
|
Channel,
|
|
5
|
-
Channels,
|
|
6
5
|
Engine,
|
|
7
6
|
Event,
|
|
8
|
-
Procedures,
|
|
9
|
-
QueryStore,
|
|
10
7
|
Procedure,
|
|
11
8
|
Reducer,
|
|
12
|
-
Reducers,
|
|
13
9
|
RemoteState,
|
|
14
10
|
State,
|
|
15
11
|
StateGroup,
|
|
16
12
|
} from '../index'
|
|
13
|
+
import { Channels, Procedures } from '../channel/channel'
|
|
14
|
+
import { QueryStore } from '../internal/queryStore'
|
|
15
|
+
import { Reducers } from '../runtime/loop'
|
|
17
16
|
|
|
18
17
|
const tick = (ms = 10) => Effect.sleep(Duration.millis(ms))
|
|
19
18
|
|
|
@@ -110,7 +109,10 @@ it.live('settled dispatches after Success is committed to truth', () => {
|
|
|
110
109
|
)
|
|
111
110
|
|
|
112
111
|
return Effect.gen(function* () {
|
|
113
|
-
yield* until(
|
|
112
|
+
yield* until(
|
|
113
|
+
() => observed,
|
|
114
|
+
(values) => values.length === 1,
|
|
115
|
+
)
|
|
114
116
|
expect(observed).toEqual(['Success'])
|
|
115
117
|
}).pipe(Effect.provide(TestLayer))
|
|
116
118
|
})
|
|
@@ -150,7 +152,10 @@ it.live('settled dispatches after Error is committed to truth', () => {
|
|
|
150
152
|
)
|
|
151
153
|
|
|
152
154
|
return Effect.gen(function* () {
|
|
153
|
-
yield* until(
|
|
155
|
+
yield* until(
|
|
156
|
+
() => observed,
|
|
157
|
+
(values) => values.length === 1,
|
|
158
|
+
)
|
|
154
159
|
expect(observed).toEqual(['Error'])
|
|
155
160
|
}).pipe(Effect.provide(TestLayer))
|
|
156
161
|
})
|
|
@@ -179,7 +184,10 @@ it.live('an intent overlays instantly — no fetch fired, truth untouched', () =
|
|
|
179
184
|
const store = yield* Items.store
|
|
180
185
|
const truth = yield* Items.truth.store
|
|
181
186
|
const queue = yield* Items.pending.store
|
|
182
|
-
yield* until(
|
|
187
|
+
yield* until(
|
|
188
|
+
() => store.get(),
|
|
189
|
+
(v) => v._tag === 'Success',
|
|
190
|
+
)
|
|
183
191
|
expect(server.state.fetches).toBe(1)
|
|
184
192
|
|
|
185
193
|
yield* Event.dispatch(Added, { id: 'b', name: 'beta' })
|
|
@@ -198,173 +206,215 @@ it.live('an intent overlays instantly — no fetch fired, truth untouched', () =
|
|
|
198
206
|
}).pipe(Effect.provide(TestLayer))
|
|
199
207
|
})
|
|
200
208
|
|
|
201
|
-
it.live(
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
Layer.provideMerge(StateGroup.live(Inputs, { page: 1 })),
|
|
218
|
-
Layer.provideMerge(Engine),
|
|
219
|
-
)
|
|
220
|
-
|
|
221
|
-
return Effect.gen(function* () {
|
|
222
|
-
const store = yield* Items.store
|
|
223
|
-
const queue = yield* Items.pending.store
|
|
224
|
-
yield* until(() => store.get(), (v) => v._tag === 'Success')
|
|
225
|
-
|
|
226
|
-
// Flicker guard: once optimistic row appears, no later Success frame may lack it
|
|
227
|
-
const frames: Array<number> = []
|
|
228
|
-
const off = store.subscribe(() => {
|
|
229
|
-
const v = store.get()
|
|
230
|
-
if (v._tag === 'Success') frames.push(v.value.length)
|
|
209
|
+
it.live(
|
|
210
|
+
'convergence: the send acks, the refetch lands, the queue drains — and no frame ever lacked the row',
|
|
211
|
+
() => {
|
|
212
|
+
class Page extends State.make('page', S.Number) {}
|
|
213
|
+
class Inputs extends StateGroup.make(Page) {}
|
|
214
|
+
const server = makeServer([{ id: 'a', name: 'alpha' }])
|
|
215
|
+
class Items extends RemoteState.make('Items', {
|
|
216
|
+
inputs: [StateGroup.select(Inputs, 'page')],
|
|
217
|
+
output: S.Array(Item),
|
|
218
|
+
intents: [Added, Renamed],
|
|
219
|
+
alwaysOn: true,
|
|
220
|
+
}) {}
|
|
221
|
+
const ItemsLive = RemoteState.live(Items, {
|
|
222
|
+
query: () => server.fetch(15),
|
|
223
|
+
send: (intent) => server.receive(intent, 15),
|
|
224
|
+
apply: applyIntent,
|
|
231
225
|
})
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
yield* until(() => queue.get(), (q) => q.length === 0)
|
|
236
|
-
off()
|
|
237
|
-
expect(frames.length).toBeGreaterThan(0)
|
|
238
|
-
expect(frames.every((n) => n === 2)).toBe(true)
|
|
239
|
-
|
|
240
|
-
const settled = yield* until(
|
|
241
|
-
() => store.get(),
|
|
242
|
-
(v) => v._tag === 'Success' && v.refetching === false,
|
|
226
|
+
const TestLayer = ItemsLive.pipe(
|
|
227
|
+
Layer.provideMerge(StateGroup.live(Inputs, { page: 1 })),
|
|
228
|
+
Layer.provideMerge(Engine),
|
|
243
229
|
)
|
|
244
|
-
if (settled._tag === 'Success') {
|
|
245
|
-
expect(settled.value).toEqual([
|
|
246
|
-
{ id: 'a', name: 'alpha' },
|
|
247
|
-
{ id: 'b', name: 'beta' },
|
|
248
|
-
])
|
|
249
|
-
}
|
|
250
|
-
expect(server.state.fetches).toBe(2)
|
|
251
|
-
}).pipe(Effect.provide(TestLayer))
|
|
252
|
-
})
|
|
253
|
-
|
|
254
|
-
it.live('THE GENERATION RULE: a refetch that started before the ack can never settle the intent', () => {
|
|
255
|
-
class Page extends State.make('page', S.Number) {}
|
|
256
|
-
class Inputs extends StateGroup.make(Page) {}
|
|
257
|
-
class Poke extends Event.make('Poke', S.Struct({})) {}
|
|
258
|
-
const server = makeServer([{ id: 'a', name: 'alpha' }])
|
|
259
|
-
class Items extends RemoteState.make('Items', {
|
|
260
|
-
inputs: [StateGroup.select(Inputs, 'page')],
|
|
261
|
-
output: S.Array(Item),
|
|
262
|
-
intents: [Added, Renamed],
|
|
263
|
-
alwaysOn: true,
|
|
264
|
-
}) {}
|
|
265
|
-
const ItemsLive = RemoteState.live(Items, {
|
|
266
|
-
query: () => server.fetch(60),
|
|
267
|
-
send: (intent) => server.receive(intent, 15),
|
|
268
|
-
apply: applyIntent,
|
|
269
|
-
invalidateOn: [Poke],
|
|
270
|
-
// Trailing lets pre-ack run land — stale Success must not settle
|
|
271
|
-
coalesce: 'trailing',
|
|
272
|
-
})
|
|
273
|
-
const TestLayer = ItemsLive.pipe(
|
|
274
|
-
Layer.provideMerge(StateGroup.live(Inputs, { page: 1 })),
|
|
275
|
-
Layer.provideMerge(Engine),
|
|
276
|
-
)
|
|
277
|
-
|
|
278
|
-
return Effect.gen(function* () {
|
|
279
|
-
const store = yield* Items.store
|
|
280
|
-
const queue = yield* Items.pending.store
|
|
281
|
-
yield* until(() => store.get(), (v) => v._tag === 'Success')
|
|
282
|
-
expect(server.state.fetches).toBe(1)
|
|
283
230
|
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
const
|
|
287
|
-
|
|
231
|
+
return Effect.gen(function* () {
|
|
232
|
+
const store = yield* Items.store
|
|
233
|
+
const queue = yield* Items.pending.store
|
|
234
|
+
yield* until(
|
|
235
|
+
() => store.get(),
|
|
236
|
+
(v) => v._tag === 'Success',
|
|
237
|
+
)
|
|
238
|
+
|
|
239
|
+
// Flicker guard: once optimistic row appears, no later Success frame may lack it
|
|
240
|
+
const frames: Array<number> = []
|
|
241
|
+
const off = store.subscribe(() => {
|
|
242
|
+
const v = store.get()
|
|
243
|
+
if (v._tag === 'Success') frames.push(v.value.length)
|
|
244
|
+
})
|
|
245
|
+
yield* Event.dispatch(Added, { id: 'b', name: 'beta' })
|
|
246
|
+
// Wait for queue entry to land before waiting for settle (empty would be vacuous)
|
|
247
|
+
yield* until(
|
|
248
|
+
() => queue.get(),
|
|
249
|
+
(q) => q.length === 1,
|
|
250
|
+
)
|
|
251
|
+
yield* until(
|
|
252
|
+
() => queue.get(),
|
|
253
|
+
(q) => q.length === 0,
|
|
254
|
+
)
|
|
255
|
+
off()
|
|
256
|
+
expect(frames.length).toBeGreaterThan(0)
|
|
257
|
+
expect(frames.every((n) => n === 2)).toBe(true)
|
|
258
|
+
|
|
259
|
+
const settled = yield* until(
|
|
260
|
+
() => store.get(),
|
|
261
|
+
(v) => v._tag === 'Success' && v.refetching === false,
|
|
262
|
+
)
|
|
263
|
+
if (settled._tag === 'Success') {
|
|
264
|
+
expect(settled.value).toEqual([
|
|
265
|
+
{ id: 'a', name: 'alpha' },
|
|
266
|
+
{ id: 'b', name: 'beta' },
|
|
267
|
+
])
|
|
268
|
+
}
|
|
269
|
+
expect(server.state.fetches).toBe(2)
|
|
270
|
+
}).pipe(Effect.provide(TestLayer))
|
|
271
|
+
},
|
|
272
|
+
)
|
|
273
|
+
|
|
274
|
+
it.live(
|
|
275
|
+
'THE GENERATION RULE: a refetch that started before the ack can never settle the intent',
|
|
276
|
+
() => {
|
|
277
|
+
class Page extends State.make('page', S.Number) {}
|
|
278
|
+
class Inputs extends StateGroup.make(Page) {}
|
|
279
|
+
class Poke extends Event.make('Poke', S.Struct({})) {}
|
|
280
|
+
const server = makeServer([{ id: 'a', name: 'alpha' }])
|
|
281
|
+
class Items extends RemoteState.make('Items', {
|
|
282
|
+
inputs: [StateGroup.select(Inputs, 'page')],
|
|
283
|
+
output: S.Array(Item),
|
|
284
|
+
intents: [Added, Renamed],
|
|
285
|
+
alwaysOn: true,
|
|
286
|
+
}) {}
|
|
287
|
+
const ItemsLive = RemoteState.live(Items, {
|
|
288
|
+
query: () => server.fetch(60),
|
|
289
|
+
send: (intent) => server.receive(intent, 15),
|
|
290
|
+
apply: applyIntent,
|
|
291
|
+
invalidateOn: [Poke],
|
|
292
|
+
// Trailing lets pre-ack run land — stale Success must not settle
|
|
293
|
+
coalesce: 'trailing',
|
|
288
294
|
})
|
|
295
|
+
const TestLayer = ItemsLive.pipe(
|
|
296
|
+
Layer.provideMerge(StateGroup.live(Inputs, { page: 1 })),
|
|
297
|
+
Layer.provideMerge(Engine),
|
|
298
|
+
)
|
|
289
299
|
|
|
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
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
)
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
const
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
300
|
+
return Effect.gen(function* () {
|
|
301
|
+
const store = yield* Items.store
|
|
302
|
+
const queue = yield* Items.pending.store
|
|
303
|
+
yield* until(
|
|
304
|
+
() => store.get(),
|
|
305
|
+
(v) => v._tag === 'Success',
|
|
306
|
+
)
|
|
307
|
+
expect(server.state.fetches).toBe(1)
|
|
308
|
+
|
|
309
|
+
const frames: Array<number> = []
|
|
310
|
+
const off = store.subscribe(() => {
|
|
311
|
+
const v = store.get()
|
|
312
|
+
if (v._tag === 'Success') frames.push(v.value.length)
|
|
313
|
+
})
|
|
314
|
+
|
|
315
|
+
yield* Event.dispatch(Poke, {})
|
|
316
|
+
yield* tick(5)
|
|
317
|
+
yield* Event.dispatch(Added, { id: 'b', name: 'beta' })
|
|
318
|
+
|
|
319
|
+
// Gen-2 Success settles nobody (generation rule)
|
|
320
|
+
yield* until(
|
|
321
|
+
() => server.state.fetches,
|
|
322
|
+
(n) => n === 2,
|
|
323
|
+
)
|
|
324
|
+
yield* tick(25)
|
|
325
|
+
expect(queue.get()).toHaveLength(1)
|
|
326
|
+
expect(queue.get()[0]).toMatchObject({ status: 'confirmed' })
|
|
327
|
+
const midflight = store.get()
|
|
328
|
+
expect(midflight).toMatchObject({ _tag: 'Success' })
|
|
329
|
+
if (midflight._tag === 'Success') expect(midflight.value).toHaveLength(2)
|
|
330
|
+
|
|
331
|
+
yield* until(
|
|
332
|
+
() => queue.get(),
|
|
333
|
+
(q) => q.length === 0,
|
|
334
|
+
)
|
|
335
|
+
off()
|
|
336
|
+
expect(server.state.fetches).toBe(3)
|
|
337
|
+
const firstOptimistic = frames.indexOf(2)
|
|
338
|
+
expect(firstOptimistic).toBeGreaterThanOrEqual(0)
|
|
339
|
+
expect(frames.slice(firstOptimistic).every((n) => n === 2)).toBe(true)
|
|
340
|
+
const settled = store.get()
|
|
341
|
+
if (settled._tag === 'Success') {
|
|
342
|
+
expect(settled.value).toEqual([
|
|
343
|
+
{ id: 'a', name: 'alpha' },
|
|
344
|
+
{ id: 'b', name: 'beta' },
|
|
345
|
+
])
|
|
346
|
+
}
|
|
347
|
+
}).pipe(Effect.provide(TestLayer))
|
|
348
|
+
},
|
|
349
|
+
)
|
|
350
|
+
|
|
351
|
+
it.live(
|
|
352
|
+
'a failed send settles the intent, reverts the overlay, and Failed reaches a user reducer',
|
|
353
|
+
() => {
|
|
354
|
+
class Page extends State.make('page', S.Number) {}
|
|
355
|
+
class Inputs extends StateGroup.make(Page) {}
|
|
356
|
+
class FailLog extends State.make('failLog', S.Array(S.String)) {}
|
|
357
|
+
const server = makeServer([{ id: 'a', name: 'alpha' }])
|
|
358
|
+
class Items extends RemoteState.make('Items', {
|
|
359
|
+
inputs: [StateGroup.select(Inputs, 'page')],
|
|
360
|
+
output: S.Array(Item),
|
|
361
|
+
intents: [Added, Renamed],
|
|
362
|
+
alwaysOn: true,
|
|
363
|
+
}) {}
|
|
364
|
+
const ItemsLive = RemoteState.live(Items, {
|
|
365
|
+
query: () => server.fetch(5),
|
|
366
|
+
send: () => Effect.fail('rejected' as const).pipe(Effect.delay(Duration.millis(15))),
|
|
367
|
+
apply: applyIntent,
|
|
368
|
+
})
|
|
369
|
+
const LogReducer = Reducer.make('failLog', { states: [FailLog], events: [Items.Failed] })
|
|
370
|
+
const TestLayer = Layer.mergeAll(
|
|
371
|
+
ItemsLive,
|
|
372
|
+
Reducer.live(LogReducer, (log, event) => [
|
|
373
|
+
...log,
|
|
374
|
+
`${event.intent._tag}|${String(event.error)}`,
|
|
375
|
+
]),
|
|
376
|
+
).pipe(
|
|
377
|
+
Layer.provideMerge(State.live(FailLog, [])),
|
|
378
|
+
Layer.provideMerge(StateGroup.live(Inputs, { page: 1 })),
|
|
379
|
+
Layer.provideMerge(Engine),
|
|
359
380
|
)
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
381
|
+
|
|
382
|
+
return Effect.gen(function* () {
|
|
383
|
+
const store = yield* Items.store
|
|
384
|
+
const queue = yield* Items.pending.store
|
|
385
|
+
const log = yield* FailLog.store
|
|
386
|
+
yield* until(
|
|
387
|
+
() => store.get(),
|
|
388
|
+
(v) => v._tag === 'Success',
|
|
389
|
+
)
|
|
390
|
+
|
|
391
|
+
yield* Event.dispatch(Added, { id: 'b', name: 'beta' })
|
|
392
|
+
yield* until(
|
|
393
|
+
() => store.get(),
|
|
394
|
+
(v) => v._tag === 'Success' && v.value.length === 2,
|
|
395
|
+
)
|
|
396
|
+
expect(queue.get()).toHaveLength(1)
|
|
397
|
+
|
|
398
|
+
yield* until(
|
|
399
|
+
() => queue.get(),
|
|
400
|
+
(q) => q.length === 0,
|
|
401
|
+
)
|
|
402
|
+
const reverted = yield* until(
|
|
403
|
+
() => store.get(),
|
|
404
|
+
(v) => v._tag === 'Success' && v.value.length === 1,
|
|
405
|
+
)
|
|
406
|
+
if (reverted._tag === 'Success') {
|
|
407
|
+
expect(reverted.value).toEqual([{ id: 'a', name: 'alpha' }])
|
|
408
|
+
}
|
|
409
|
+
yield* until(
|
|
410
|
+
() => log.get(),
|
|
411
|
+
(l) => l.length === 1,
|
|
412
|
+
)
|
|
413
|
+
expect(log.get()).toEqual(['Added|rejected'])
|
|
414
|
+
expect(server.state.fetches).toBe(1)
|
|
415
|
+
}).pipe(Effect.provide(TestLayer))
|
|
416
|
+
},
|
|
417
|
+
)
|
|
368
418
|
|
|
369
419
|
it.live('two intents apply in dispatch order and settle together on the post-ack run', () => {
|
|
370
420
|
class Page extends State.make('page', S.Number) {}
|
|
@@ -389,19 +439,28 @@ it.live('two intents apply in dispatch order and settle together on the post-ack
|
|
|
389
439
|
return Effect.gen(function* () {
|
|
390
440
|
const store = yield* Items.store
|
|
391
441
|
const queue = yield* Items.pending.store
|
|
392
|
-
yield* until(
|
|
442
|
+
yield* until(
|
|
443
|
+
() => store.get(),
|
|
444
|
+
(v) => v._tag === 'Success',
|
|
445
|
+
)
|
|
393
446
|
|
|
394
447
|
// Apply order = dispatch order
|
|
395
448
|
yield* Event.dispatch(Added, { id: 'b', name: 'beta' })
|
|
396
449
|
yield* Event.dispatch(Renamed, { id: 'b', name: 'BETA' })
|
|
397
|
-
yield* until(
|
|
450
|
+
yield* until(
|
|
451
|
+
() => queue.get(),
|
|
452
|
+
(q) => q.length === 2,
|
|
453
|
+
)
|
|
398
454
|
const optimistic = store.get()
|
|
399
455
|
expect(optimistic).toMatchObject({ _tag: 'Success' })
|
|
400
456
|
if (optimistic._tag === 'Success') {
|
|
401
457
|
expect(optimistic.value[1]).toEqual({ id: 'b', name: 'BETA' })
|
|
402
458
|
}
|
|
403
459
|
|
|
404
|
-
yield* until(
|
|
460
|
+
yield* until(
|
|
461
|
+
() => queue.get(),
|
|
462
|
+
(q) => q.length === 0,
|
|
463
|
+
)
|
|
405
464
|
const settled = yield* until(
|
|
406
465
|
() => store.get(),
|
|
407
466
|
(v) => v._tag === 'Success' && v.refetching === false,
|
|
@@ -441,7 +500,10 @@ it.live('one intent failing leaves the other pending and converging', () => {
|
|
|
441
500
|
return Effect.gen(function* () {
|
|
442
501
|
const store = yield* Items.store
|
|
443
502
|
const queue = yield* Items.pending.store
|
|
444
|
-
yield* until(
|
|
503
|
+
yield* until(
|
|
504
|
+
() => store.get(),
|
|
505
|
+
(v) => v._tag === 'Success',
|
|
506
|
+
)
|
|
445
507
|
|
|
446
508
|
yield* Event.dispatch(Added, { id: 'b', name: 'beta' })
|
|
447
509
|
yield* Event.dispatch(Renamed, { id: 'a', name: 'ALPHA' })
|
|
@@ -465,7 +527,10 @@ it.live('one intent failing leaves the other pending and converging', () => {
|
|
|
465
527
|
])
|
|
466
528
|
}
|
|
467
529
|
|
|
468
|
-
yield* until(
|
|
530
|
+
yield* until(
|
|
531
|
+
() => queue.get(),
|
|
532
|
+
(q) => q.length === 0,
|
|
533
|
+
)
|
|
469
534
|
const settled = yield* until(
|
|
470
535
|
() => store.get(),
|
|
471
536
|
(v) => v._tag === 'Success' && v.refetching === false,
|
|
@@ -502,10 +567,16 @@ it.live("pending exposes the lifecycle: 'sending' -> 'confirmed' -> gone", () =>
|
|
|
502
567
|
return Effect.gen(function* () {
|
|
503
568
|
const store = yield* Items.store
|
|
504
569
|
const queue = yield* Items.pending.store
|
|
505
|
-
yield* until(
|
|
570
|
+
yield* until(
|
|
571
|
+
() => store.get(),
|
|
572
|
+
(v) => v._tag === 'Success',
|
|
573
|
+
)
|
|
506
574
|
|
|
507
575
|
yield* Event.dispatch(Added, { id: 'b', name: 'beta' })
|
|
508
|
-
const sending = yield* until(
|
|
576
|
+
const sending = yield* until(
|
|
577
|
+
() => queue.get(),
|
|
578
|
+
(q) => q.length === 1,
|
|
579
|
+
)
|
|
509
580
|
expect(sending[0]).toMatchObject({ status: 'sending', intent: { _tag: 'Added', id: 'b' } })
|
|
510
581
|
|
|
511
582
|
const confirmed = yield* until(
|
|
@@ -514,7 +585,10 @@ it.live("pending exposes the lifecycle: 'sending' -> 'confirmed' -> gone", () =>
|
|
|
514
585
|
)
|
|
515
586
|
expect(confirmed[0]).toMatchObject({ status: 'confirmed' })
|
|
516
587
|
|
|
517
|
-
yield* until(
|
|
588
|
+
yield* until(
|
|
589
|
+
() => queue.get(),
|
|
590
|
+
(q) => q.length === 0,
|
|
591
|
+
)
|
|
518
592
|
expect(queue.get()).toEqual([])
|
|
519
593
|
}).pipe(Effect.provide(TestLayer))
|
|
520
594
|
})
|
|
@@ -548,14 +622,20 @@ it.live('gated: intents queue and send while Idle; re-enabling converges and set
|
|
|
548
622
|
|
|
549
623
|
// Intents still send while gated Idle; settle waits for re-enable
|
|
550
624
|
yield* Event.dispatch(Added, { id: 'b', name: 'beta' })
|
|
551
|
-
yield* until(
|
|
625
|
+
yield* until(
|
|
626
|
+
() => server.state.items,
|
|
627
|
+
(items) => items.length === 2,
|
|
628
|
+
)
|
|
552
629
|
yield* tick(20)
|
|
553
630
|
expect(store.get()._tag).toBe('Idle')
|
|
554
631
|
expect(queue.get()).toHaveLength(1)
|
|
555
632
|
expect(server.state.fetches).toBe(0)
|
|
556
633
|
|
|
557
634
|
source.set(1)
|
|
558
|
-
yield* until(
|
|
635
|
+
yield* until(
|
|
636
|
+
() => queue.get(),
|
|
637
|
+
(q) => q.length === 0,
|
|
638
|
+
)
|
|
559
639
|
const settled = yield* until(
|
|
560
640
|
() => store.get(),
|
|
561
641
|
(v) => v._tag === 'Success' && v.refetching === false,
|
|
@@ -596,13 +676,22 @@ it.live('reuse: untouched element identities survive the whole optimistic round-
|
|
|
596
676
|
return Effect.gen(function* () {
|
|
597
677
|
const store = yield* Items.store
|
|
598
678
|
const queue = yield* Items.pending.store
|
|
599
|
-
const first = yield* until(
|
|
679
|
+
const first = yield* until(
|
|
680
|
+
() => store.get(),
|
|
681
|
+
(v) => v._tag === 'Success',
|
|
682
|
+
)
|
|
600
683
|
if (first._tag !== 'Success') return
|
|
601
684
|
const untouched = first.value[0]
|
|
602
685
|
|
|
603
686
|
yield* Event.dispatch(Renamed, { id: 'b', name: 'BETA' })
|
|
604
|
-
yield* until(
|
|
605
|
-
|
|
687
|
+
yield* until(
|
|
688
|
+
() => queue.get(),
|
|
689
|
+
(q) => q.length === 1,
|
|
690
|
+
)
|
|
691
|
+
yield* until(
|
|
692
|
+
() => queue.get(),
|
|
693
|
+
(q) => q.length === 0,
|
|
694
|
+
)
|
|
606
695
|
const settled = yield* until(
|
|
607
696
|
() => store.get(),
|
|
608
697
|
(v) => v._tag === 'Success' && v.refetching === false,
|
|
@@ -640,15 +729,27 @@ it.live('invalidateOn + coalesce trailing compose with the mutation lifecycle',
|
|
|
640
729
|
return Effect.gen(function* () {
|
|
641
730
|
const store = yield* Items.store
|
|
642
731
|
const queue = yield* Items.pending.store
|
|
643
|
-
yield* until(
|
|
732
|
+
yield* until(
|
|
733
|
+
() => store.get(),
|
|
734
|
+
(v) => v._tag === 'Success',
|
|
735
|
+
)
|
|
644
736
|
expect(server.state.fetches).toBe(1)
|
|
645
737
|
|
|
646
738
|
yield* Event.dispatch(BoardChanged, {})
|
|
647
|
-
yield* until(
|
|
739
|
+
yield* until(
|
|
740
|
+
() => server.state.fetches,
|
|
741
|
+
(n) => n === 2,
|
|
742
|
+
)
|
|
648
743
|
|
|
649
744
|
yield* Event.dispatch(Added, { id: 'b', name: 'beta' })
|
|
650
|
-
yield* until(
|
|
651
|
-
|
|
745
|
+
yield* until(
|
|
746
|
+
() => queue.get(),
|
|
747
|
+
(q) => q.length === 1,
|
|
748
|
+
)
|
|
749
|
+
yield* until(
|
|
750
|
+
() => queue.get(),
|
|
751
|
+
(q) => q.length === 0,
|
|
752
|
+
)
|
|
652
753
|
const settled = yield* until(
|
|
653
754
|
() => store.get(),
|
|
654
755
|
(v) => v._tag === 'Success' && v.refetching === false,
|
|
@@ -687,56 +788,67 @@ it.live('a query Error passes through by reference with intents still pending',
|
|
|
687
788
|
const store = yield* Items.store
|
|
688
789
|
const truth = yield* Items.truth.store
|
|
689
790
|
const queue = yield* Items.pending.store
|
|
690
|
-
yield* until(
|
|
791
|
+
yield* until(
|
|
792
|
+
() => store.get(),
|
|
793
|
+
(v) => v._tag === 'Error',
|
|
794
|
+
)
|
|
691
795
|
|
|
692
796
|
// Error runs settle nobody; overlay passes Error through without apply
|
|
693
797
|
yield* Event.dispatch(Added, { id: 'b', name: 'beta' })
|
|
694
|
-
yield* until(
|
|
695
|
-
|
|
798
|
+
yield* until(
|
|
799
|
+
() => queue.get(),
|
|
800
|
+
(q) => q.length === 1 && q[0]?.status === 'confirmed',
|
|
801
|
+
)
|
|
802
|
+
yield* until(
|
|
803
|
+
() => store.get(),
|
|
804
|
+
(v) => v._tag === 'Error' && v.refetching === false,
|
|
805
|
+
)
|
|
696
806
|
yield* tick(20)
|
|
697
807
|
expect(queue.get()).toHaveLength(1)
|
|
698
808
|
expect(store.get()).toBe(truth.get())
|
|
699
809
|
}).pipe(Effect.provide(TestLayer))
|
|
700
810
|
})
|
|
701
811
|
|
|
702
|
-
it.live(
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
expect(reducers.entries.some((entry) => entry.name === 'Items/revision')).toBe(false)
|
|
735
|
-
expect(procedures.entries.some((entry) => entry.name === 'Items/send')).toBe(false)
|
|
736
|
-
expect(channels.byName.has('Items/sends')).toBe(false)
|
|
737
|
-
}).pipe(Effect.provide(Engine))
|
|
738
|
-
})
|
|
812
|
+
it.live(
|
|
813
|
+
'teardown: closing the scope unregisters the reducers, the procedure, and the channel',
|
|
814
|
+
() => {
|
|
815
|
+
class Page extends State.make('page', S.Number) {}
|
|
816
|
+
class Inputs extends StateGroup.make(Page) {}
|
|
817
|
+
const server = makeServer([{ id: 'a', name: 'alpha' }])
|
|
818
|
+
class Items extends RemoteState.make('Items', {
|
|
819
|
+
inputs: [StateGroup.select(Inputs, 'page')],
|
|
820
|
+
output: S.Array(Item),
|
|
821
|
+
intents: [Added, Renamed],
|
|
822
|
+
alwaysOn: true,
|
|
823
|
+
}) {}
|
|
824
|
+
const ItemsLive = RemoteState.live(Items, {
|
|
825
|
+
query: () => server.fetch(5),
|
|
826
|
+
send: (intent) => server.receive(intent, 5),
|
|
827
|
+
apply: applyIntent,
|
|
828
|
+
}).pipe(Layer.provideMerge(StateGroup.live(Inputs, { page: 1 })))
|
|
829
|
+
|
|
830
|
+
return Effect.gen(function* () {
|
|
831
|
+
const reducers = yield* Reducers
|
|
832
|
+
const procedures = yield* Procedures
|
|
833
|
+
const channels = yield* Channels
|
|
834
|
+
|
|
835
|
+
yield* Effect.scoped(
|
|
836
|
+
Effect.gen(function* () {
|
|
837
|
+
yield* Layer.build(ItemsLive)
|
|
838
|
+
expect(reducers.entries.some((entry) => entry.name === 'Items/pending')).toBe(true)
|
|
839
|
+
expect(reducers.entries.some((entry) => entry.name === 'Items/revision')).toBe(true)
|
|
840
|
+
expect(procedures.entries.some((entry) => entry.name === 'Items/send')).toBe(true)
|
|
841
|
+
expect(channels.byName.has('Items/sends')).toBe(true)
|
|
842
|
+
}),
|
|
843
|
+
)
|
|
739
844
|
|
|
845
|
+
expect(reducers.entries.some((entry) => entry.name === 'Items/pending')).toBe(false)
|
|
846
|
+
expect(reducers.entries.some((entry) => entry.name === 'Items/revision')).toBe(false)
|
|
847
|
+
expect(procedures.entries.some((entry) => entry.name === 'Items/send')).toBe(false)
|
|
848
|
+
expect(channels.byName.has('Items/sends')).toBe(false)
|
|
849
|
+
}).pipe(Effect.provide(Engine))
|
|
850
|
+
},
|
|
851
|
+
)
|
|
740
852
|
|
|
741
853
|
it.live('persist: hydrates the seeded truth (stale) then writes the fresh truth through', () => {
|
|
742
854
|
class Page extends State.make('page', S.Number) {}
|
|
@@ -765,7 +877,8 @@ it.live('persist: hydrates the seeded truth (stale) then writes the fresh truth
|
|
|
765
877
|
const view = yield* Items.store
|
|
766
878
|
const hydrated = view.get()
|
|
767
879
|
expect(hydrated).toMatchObject({ _tag: 'Success', refetching: true })
|
|
768
|
-
if (hydrated._tag === 'Success')
|
|
880
|
+
if (hydrated._tag === 'Success')
|
|
881
|
+
expect(hydrated.value).toEqual([{ id: 'seed', name: 'seeded' }])
|
|
769
882
|
|
|
770
883
|
const settled = yield* until(
|
|
771
884
|
() => view.get(),
|
|
@@ -780,120 +893,183 @@ it.live('persist: hydrates the seeded truth (stale) then writes the fresh truth
|
|
|
780
893
|
}).pipe(Effect.provide(TestLayer))
|
|
781
894
|
})
|
|
782
895
|
|
|
783
|
-
it.live(
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
return Effect.gen(function* () {
|
|
807
|
-
const view = yield* Items.store
|
|
808
|
-
const queue = yield* Items.pending.store
|
|
809
|
-
yield* until(() => view.get(), (v) => v._tag === 'Success')
|
|
810
|
-
yield* until(() => store.map.get('Items'), (v) => Array.isArray(v) && v.length === 1)
|
|
811
|
-
|
|
812
|
-
yield* Event.dispatch(Added, { id: 'b', name: 'beta' })
|
|
813
|
-
yield* until(() => view.get(), (v) => v._tag === 'Success' && v.value.length === 2)
|
|
814
|
-
yield* until(() => queue.get(), (q) => q.length === 1)
|
|
815
|
-
// Persist never captures overlay — only settled server truth
|
|
816
|
-
expect(store.map.get('Items')).toEqual([{ id: 'a', name: 'alpha' }])
|
|
817
|
-
|
|
818
|
-
yield* until(() => queue.get(), (q) => q.length === 0)
|
|
819
|
-
yield* until(
|
|
820
|
-
() => store.map.get('Items'),
|
|
821
|
-
(v) => Array.isArray(v) && v.length === 2,
|
|
896
|
+
it.live(
|
|
897
|
+
'persist: the optimistic overlay is NEVER written — only the converged server truth is',
|
|
898
|
+
() => {
|
|
899
|
+
class Page extends State.make('page', S.Number) {}
|
|
900
|
+
class Inputs extends StateGroup.make(Page) {}
|
|
901
|
+
const server = makeServer([{ id: 'a', name: 'alpha' }])
|
|
902
|
+
const store = fakeQueryStore()
|
|
903
|
+
class Items extends RemoteState.make('Items', {
|
|
904
|
+
inputs: [StateGroup.select(Inputs, 'page')],
|
|
905
|
+
output: S.Array(Item),
|
|
906
|
+
intents: [Added, Renamed],
|
|
907
|
+
alwaysOn: true,
|
|
908
|
+
}) {}
|
|
909
|
+
const ItemsLive = RemoteState.live(Items, {
|
|
910
|
+
query: () => server.fetch(10),
|
|
911
|
+
send: (intent) => server.receive(intent, 60),
|
|
912
|
+
apply: applyIntent,
|
|
913
|
+
persist: true,
|
|
914
|
+
})
|
|
915
|
+
const TestLayer = ItemsLive.pipe(
|
|
916
|
+
Layer.provideMerge(StateGroup.live(Inputs, { page: 1 })),
|
|
917
|
+
Layer.provideMerge(store.layer),
|
|
918
|
+
Layer.provideMerge(Engine),
|
|
822
919
|
)
|
|
823
|
-
expect(store.map.get('Items')).toEqual([
|
|
824
|
-
{ id: 'a', name: 'alpha' },
|
|
825
|
-
{ id: 'b', name: 'beta' },
|
|
826
|
-
])
|
|
827
|
-
}).pipe(Effect.provide(TestLayer))
|
|
828
|
-
})
|
|
829
920
|
|
|
830
|
-
|
|
831
|
-
|
|
832
|
-
|
|
833
|
-
|
|
834
|
-
|
|
835
|
-
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
|
|
841
|
-
|
|
842
|
-
|
|
843
|
-
|
|
844
|
-
|
|
845
|
-
|
|
846
|
-
|
|
847
|
-
|
|
848
|
-
|
|
849
|
-
|
|
850
|
-
|
|
851
|
-
|
|
852
|
-
|
|
853
|
-
|
|
854
|
-
|
|
855
|
-
|
|
856
|
-
|
|
857
|
-
|
|
858
|
-
|
|
859
|
-
|
|
860
|
-
|
|
861
|
-
|
|
862
|
-
|
|
863
|
-
|
|
864
|
-
}
|
|
921
|
+
return Effect.gen(function* () {
|
|
922
|
+
const view = yield* Items.store
|
|
923
|
+
const queue = yield* Items.pending.store
|
|
924
|
+
yield* until(
|
|
925
|
+
() => view.get(),
|
|
926
|
+
(v) => v._tag === 'Success',
|
|
927
|
+
)
|
|
928
|
+
yield* until(
|
|
929
|
+
() => store.map.get('Items'),
|
|
930
|
+
(v) => Array.isArray(v) && v.length === 1,
|
|
931
|
+
)
|
|
932
|
+
|
|
933
|
+
yield* Event.dispatch(Added, { id: 'b', name: 'beta' })
|
|
934
|
+
yield* until(
|
|
935
|
+
() => view.get(),
|
|
936
|
+
(v) => v._tag === 'Success' && v.value.length === 2,
|
|
937
|
+
)
|
|
938
|
+
yield* until(
|
|
939
|
+
() => queue.get(),
|
|
940
|
+
(q) => q.length === 1,
|
|
941
|
+
)
|
|
942
|
+
// Persist never captures overlay — only settled server truth
|
|
943
|
+
expect(store.map.get('Items')).toEqual([{ id: 'a', name: 'alpha' }])
|
|
944
|
+
|
|
945
|
+
yield* until(
|
|
946
|
+
() => queue.get(),
|
|
947
|
+
(q) => q.length === 0,
|
|
948
|
+
)
|
|
949
|
+
yield* until(
|
|
950
|
+
() => store.map.get('Items'),
|
|
951
|
+
(v) => Array.isArray(v) && v.length === 2,
|
|
952
|
+
)
|
|
953
|
+
expect(store.map.get('Items')).toEqual([
|
|
954
|
+
{ id: 'a', name: 'alpha' },
|
|
955
|
+
{ id: 'b', name: 'beta' },
|
|
956
|
+
])
|
|
957
|
+
}).pipe(Effect.provide(TestLayer))
|
|
958
|
+
},
|
|
959
|
+
)
|
|
960
|
+
|
|
961
|
+
it.live(
|
|
962
|
+
'persist: `true` keys by name; `{ key }` overrides — two definitions keep separate slots',
|
|
963
|
+
() => {
|
|
964
|
+
class Page extends State.make('page', S.Number) {}
|
|
965
|
+
class Inputs extends StateGroup.make(Page) {}
|
|
966
|
+
const serverA = makeServer([{ id: 'a', name: 'alpha' }])
|
|
967
|
+
const serverB = makeServer([{ id: 'b', name: 'beta' }])
|
|
968
|
+
const store = fakeQueryStore()
|
|
969
|
+
class ItemsA extends RemoteState.make('ItemsA', {
|
|
970
|
+
inputs: [StateGroup.select(Inputs, 'page')],
|
|
971
|
+
output: S.Array(Item),
|
|
972
|
+
intents: [Added, Renamed],
|
|
973
|
+
alwaysOn: true,
|
|
974
|
+
}) {}
|
|
975
|
+
class ItemsB extends RemoteState.make('ItemsB', {
|
|
976
|
+
inputs: [StateGroup.select(Inputs, 'page')],
|
|
977
|
+
output: S.Array(Item),
|
|
978
|
+
intents: [Added, Renamed],
|
|
979
|
+
alwaysOn: true,
|
|
980
|
+
}) {}
|
|
981
|
+
const TestLayer = Layer.mergeAll(
|
|
982
|
+
RemoteState.live(ItemsA, {
|
|
983
|
+
query: () => serverA.fetch(10),
|
|
984
|
+
send: (i) => serverA.receive(i, 10),
|
|
985
|
+
apply: applyIntent,
|
|
986
|
+
persist: true,
|
|
987
|
+
}),
|
|
988
|
+
RemoteState.live(ItemsB, {
|
|
989
|
+
query: () => serverB.fetch(10),
|
|
990
|
+
send: (i) => serverB.receive(i, 10),
|
|
991
|
+
apply: applyIntent,
|
|
992
|
+
persist: { key: 'custom-B' },
|
|
993
|
+
}),
|
|
994
|
+
).pipe(
|
|
995
|
+
Layer.provideMerge(StateGroup.live(Inputs, { page: 1 })),
|
|
996
|
+
Layer.provideMerge(store.layer),
|
|
997
|
+
Layer.provideMerge(Engine),
|
|
998
|
+
)
|
|
865
999
|
|
|
866
|
-
|
|
867
|
-
|
|
868
|
-
|
|
869
|
-
|
|
870
|
-
|
|
871
|
-
|
|
872
|
-
|
|
873
|
-
|
|
874
|
-
|
|
875
|
-
|
|
876
|
-
|
|
877
|
-
|
|
878
|
-
|
|
879
|
-
|
|
880
|
-
|
|
881
|
-
|
|
882
|
-
|
|
883
|
-
|
|
884
|
-
|
|
885
|
-
|
|
1000
|
+
return Effect.gen(function* () {
|
|
1001
|
+
const a = yield* ItemsA.store
|
|
1002
|
+
const b = yield* ItemsB.store
|
|
1003
|
+
yield* until(
|
|
1004
|
+
() => a.get(),
|
|
1005
|
+
(v) => v._tag === 'Success',
|
|
1006
|
+
)
|
|
1007
|
+
yield* until(
|
|
1008
|
+
() => b.get(),
|
|
1009
|
+
(v) => v._tag === 'Success',
|
|
1010
|
+
)
|
|
1011
|
+
yield* until(
|
|
1012
|
+
() => store.map.get('ItemsA'),
|
|
1013
|
+
(v) => Array.isArray(v),
|
|
1014
|
+
)
|
|
1015
|
+
yield* until(
|
|
1016
|
+
() => store.map.get('custom-B'),
|
|
1017
|
+
(v) => Array.isArray(v),
|
|
1018
|
+
)
|
|
1019
|
+
expect(store.map.get('ItemsA')).toEqual([{ id: 'a', name: 'alpha' }])
|
|
1020
|
+
expect(store.map.get('custom-B')).toEqual([{ id: 'b', name: 'beta' }])
|
|
1021
|
+
expect(store.map.has('ItemsB')).toBe(false)
|
|
1022
|
+
}).pipe(Effect.provide(TestLayer))
|
|
1023
|
+
},
|
|
1024
|
+
)
|
|
1025
|
+
|
|
1026
|
+
it.live(
|
|
1027
|
+
'persist (00c): a function key keys a family per-input — two input keys, two slots, no collision',
|
|
1028
|
+
() => {
|
|
1029
|
+
class Page extends State.make('page', S.Number) {}
|
|
1030
|
+
class Inputs extends StateGroup.make(Page) {}
|
|
1031
|
+
const inputs = StateGroup.live(Inputs, { page: 1 })
|
|
1032
|
+
const store = fakeQueryStore()
|
|
1033
|
+
const fetchPage = (page: number) =>
|
|
1034
|
+
Effect.as(tick(10), [{ id: `p${page}`, name: `page-${page}` }])
|
|
1035
|
+
class Fam extends RemoteState.make('Fam', {
|
|
1036
|
+
inputs: [StateGroup.select(Inputs, 'page')],
|
|
1037
|
+
output: S.Array(Item),
|
|
1038
|
+
intents: [Added, Renamed],
|
|
1039
|
+
alwaysOn: true,
|
|
1040
|
+
}) {}
|
|
1041
|
+
const FamLive = RemoteState.live(Fam, {
|
|
1042
|
+
query: ({ page }) => fetchPage(page),
|
|
1043
|
+
send: (i) => Effect.sync(() => void i),
|
|
1044
|
+
apply: applyIntent,
|
|
1045
|
+
persist: { key: ({ page }) => `fam:${page}` },
|
|
1046
|
+
})
|
|
1047
|
+
const TestLayer = FamLive.pipe(
|
|
1048
|
+
Layer.provideMerge(inputs),
|
|
1049
|
+
Layer.provideMerge(store.layer),
|
|
1050
|
+
Layer.provideMerge(Engine),
|
|
1051
|
+
)
|
|
886
1052
|
|
|
887
|
-
|
|
888
|
-
|
|
889
|
-
|
|
890
|
-
|
|
891
|
-
|
|
892
|
-
|
|
893
|
-
|
|
894
|
-
|
|
895
|
-
|
|
896
|
-
|
|
897
|
-
|
|
898
|
-
|
|
899
|
-
|
|
1053
|
+
return Effect.gen(function* () {
|
|
1054
|
+
const view = yield* Fam.store
|
|
1055
|
+
const page = yield* Page.store
|
|
1056
|
+
yield* until(
|
|
1057
|
+
() => store.map.get('fam:1'),
|
|
1058
|
+
(v) => Array.isArray(v),
|
|
1059
|
+
)
|
|
1060
|
+
expect(store.map.get('fam:1')).toEqual([{ id: 'p1', name: 'page-1' }])
|
|
1061
|
+
|
|
1062
|
+
page.set(2)
|
|
1063
|
+
yield* until(
|
|
1064
|
+
() => view.get(),
|
|
1065
|
+
(v) => v._tag === 'Success' && v.value[0]?.id === 'p2',
|
|
1066
|
+
)
|
|
1067
|
+
yield* until(
|
|
1068
|
+
() => store.map.get('fam:2'),
|
|
1069
|
+
(v) => Array.isArray(v),
|
|
1070
|
+
)
|
|
1071
|
+
expect(store.map.get('fam:2')).toEqual([{ id: 'p2', name: 'page-2' }])
|
|
1072
|
+
expect(store.map.get('fam:1')).toEqual([{ id: 'p1', name: 'page-1' }])
|
|
1073
|
+
}).pipe(Effect.provide(TestLayer))
|
|
1074
|
+
},
|
|
1075
|
+
)
|