@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.
Files changed (85) hide show
  1. package/README.md +85 -55
  2. package/package.json +17 -17
  3. package/src/boundary/boundary.test.ts +62 -16
  4. package/src/boundary/boundary.ts +141 -32
  5. package/src/calc/asyncCalc.invalidate.test.ts +516 -18
  6. package/src/calc/asyncCalc.test.ts +207 -175
  7. package/src/calc/asyncCalc.ts +168 -39
  8. package/src/calc/calc.test.ts +3 -5
  9. package/src/calc/calc.ts +44 -18
  10. package/src/calc/calcFamily.test.ts +80 -22
  11. package/src/calc/calcFamily.ts +56 -25
  12. package/src/calc/compose.ts +1 -14
  13. package/src/channel/channel.ts +128 -11
  14. package/src/compose/composition.test.ts +19 -0
  15. package/src/compose/composition.ts +346 -62
  16. package/src/compose/props.ts +13 -6
  17. package/src/compose/provide.ts +187 -46
  18. package/src/compose/slot.ts +156 -19
  19. package/src/compose/structure.test.ts +6 -3
  20. package/src/compose/structure.ts +106 -13
  21. package/src/compose/ui.test.ts +19 -3
  22. package/src/compose/ui.ts +147 -54
  23. package/src/compose/ui.typecheck.ts +40 -4
  24. package/src/definition/definition.ts +22 -9
  25. package/src/event/event.fromSource.test.ts +172 -0
  26. package/src/event/event.test.ts +10 -0
  27. package/src/event/event.ts +102 -18
  28. package/src/feature/feature.mount.test.ts +123 -56
  29. package/src/feature/feature.test.ts +67 -63
  30. package/src/feature/feature.ts +1317 -197
  31. package/src/feature/feature.typecheck.ts +253 -61
  32. package/src/graph/closure.ts +403 -0
  33. package/src/index.ts +171 -245
  34. package/src/internal/bucketCache.ts +39 -0
  35. package/src/internal/capture.ts +9 -4
  36. package/src/internal/env.ts +1 -3
  37. package/src/internal/errors.ts +21 -10
  38. package/src/internal/queryDriver.ts +120 -15
  39. package/src/internal/queryDriverStore.ts +8 -5
  40. package/src/internal/queryDriverTypes.ts +3 -1
  41. package/src/internal/reuse.test.ts +10 -3
  42. package/src/internal/reuse.ts +5 -2
  43. package/src/internal/scheduler.ts +4 -1
  44. package/src/internal/sources.ts +25 -11
  45. package/src/internal/track.ts +3 -2
  46. package/src/internal.ts +222 -0
  47. package/src/namespace/namespace.ts +46 -25
  48. package/src/procedure/procedure.ts +38 -11
  49. package/src/reducer/reducer.ts +78 -34
  50. package/src/remote/remoteState.test.ts +515 -339
  51. package/src/remote/remoteState.ts +572 -107
  52. package/src/remote/remoteState.typecheck.ts +47 -23
  53. package/src/runtime/appRuntime.activation.test.ts +100 -0
  54. package/src/runtime/appRuntime.test.ts +157 -105
  55. package/src/runtime/appRuntime.ts +394 -33
  56. package/src/runtime/eventBudget.test.ts +1 -4
  57. package/src/runtime/eventBudget.ts +9 -8
  58. package/src/runtime/hardening.test.ts +4 -9
  59. package/src/runtime/instrumentation.test.ts +174 -192
  60. package/src/runtime/instrumentation.ts +6 -11
  61. package/src/runtime/loop.test.ts +36 -0
  62. package/src/runtime/loop.ts +69 -40
  63. package/src/scene/featureScene.test.ts +4 -2
  64. package/src/scene/scene.ts +234 -64
  65. package/src/scene/seedScene.test.ts +69 -86
  66. package/src/state/state.nominal.typecheck.ts +68 -0
  67. package/src/state/state.test.ts +17 -0
  68. package/src/state/state.ts +79 -26
  69. package/src/state/stateFamily.test.ts +14 -0
  70. package/src/state/stateFamily.ts +55 -22
  71. package/src/state/stateGroup.ts +53 -17
  72. package/src/state/token.ts +40 -10
  73. package/src/synced/syncedStore.ts +46 -23
  74. package/src/testkit/flight.testkit.ts +75 -0
  75. package/src/wire/tree.test.ts +8 -4
  76. package/src/wire/triggers.test.ts +6 -2
  77. package/src/wire/triggers.ts +14 -10
  78. package/src/calc/asyncCalcDefinitions.ts +0 -48
  79. package/src/channel/procedureRegistry.ts +0 -90
  80. package/src/feature/featureBinding.ts +0 -48
  81. package/src/internal/ctx.ts +0 -9
  82. package/src/remote/remoteStateDefinition.ts +0 -135
  83. package/src/remote/remoteStateLayers.ts +0 -118
  84. package/src/remote/remoteStateLiveTypes.ts +0 -59
  85. 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(() => observed, (values) => values.length === 1)
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(() => observed, (values) => values.length === 1)
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(() => store.get(), (v) => v._tag === 'Success')
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('convergence: the send acks, the refetch lands, the queue drains — and no frame ever lacked the row', () => {
202
- class Page extends State.make('page', S.Number) {}
203
- class Inputs extends StateGroup.make(Page) {}
204
- const server = makeServer([{ id: 'a', name: 'alpha' }])
205
- class Items extends RemoteState.make('Items', {
206
- inputs: [StateGroup.select(Inputs, 'page')],
207
- output: S.Array(Item),
208
- intents: [Added, Renamed],
209
- alwaysOn: true,
210
- }) {}
211
- const ItemsLive = RemoteState.live(Items, {
212
- query: () => server.fetch(15),
213
- send: (intent) => server.receive(intent, 15),
214
- apply: applyIntent,
215
- })
216
- const TestLayer = ItemsLive.pipe(
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
- yield* Event.dispatch(Added, { id: 'b', name: 'beta' })
233
- // Wait for queue entry to land before waiting for settle (empty would be vacuous)
234
- yield* until(() => queue.get(), (q) => q.length === 1)
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
- const frames: Array<number> = []
285
- const off = store.subscribe(() => {
286
- const v = store.get()
287
- if (v._tag === 'Success') frames.push(v.value.length)
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
- yield* Event.dispatch(Poke, {})
291
- yield* tick(5)
292
- yield* Event.dispatch(Added, { id: 'b', name: 'beta' })
293
-
294
- // Gen-2 Success settles nobody (generation rule)
295
- yield* until(() => server.state.fetches, (n) => n === 2)
296
- yield* tick(25)
297
- expect(queue.get()).toHaveLength(1)
298
- expect(queue.get()[0]).toMatchObject({ status: 'confirmed' })
299
- const midflight = store.get()
300
- expect(midflight).toMatchObject({ _tag: 'Success' })
301
- if (midflight._tag === 'Success') expect(midflight.value).toHaveLength(2)
302
-
303
- yield* until(() => queue.get(), (q) => q.length === 0)
304
- off()
305
- expect(server.state.fetches).toBe(3)
306
- const firstOptimistic = frames.indexOf(2)
307
- expect(firstOptimistic).toBeGreaterThanOrEqual(0)
308
- expect(frames.slice(firstOptimistic).every((n) => n === 2)).toBe(true)
309
- const settled = store.get()
310
- if (settled._tag === 'Success') {
311
- expect(settled.value).toEqual([
312
- { id: 'a', name: 'alpha' },
313
- { id: 'b', name: 'beta' },
314
- ])
315
- }
316
- }).pipe(Effect.provide(TestLayer))
317
- })
318
-
319
- it.live('a failed send settles the intent, reverts the overlay, and Failed reaches a user reducer', () => {
320
- class Page extends State.make('page', S.Number) {}
321
- class Inputs extends StateGroup.make(Page) {}
322
- class FailLog extends State.make('failLog', S.Array(S.String)) {}
323
- const server = makeServer([{ id: 'a', name: 'alpha' }])
324
- class Items extends RemoteState.make('Items', {
325
- inputs: [StateGroup.select(Inputs, 'page')],
326
- output: S.Array(Item),
327
- intents: [Added, Renamed],
328
- alwaysOn: true,
329
- }) {}
330
- const ItemsLive = RemoteState.live(Items, {
331
- query: () => server.fetch(5),
332
- send: () => Effect.fail('rejected' as const).pipe(Effect.delay(Duration.millis(15))),
333
- apply: applyIntent,
334
- })
335
- const LogReducer = Reducer.make('failLog', { states: [FailLog], events: [Items.Failed] })
336
- const TestLayer = Layer.mergeAll(
337
- ItemsLive,
338
- Reducer.live(LogReducer, (log, event) => [...log, `${event.intent._tag}|${String(event.error)}`]),
339
- ).pipe(
340
- Layer.provideMerge(State.live(FailLog, [])),
341
- Layer.provideMerge(StateGroup.live(Inputs, { page: 1 })),
342
- Layer.provideMerge(Engine),
343
- )
344
-
345
- return Effect.gen(function* () {
346
- const store = yield* Items.store
347
- const queue = yield* Items.pending.store
348
- const log = yield* FailLog.store
349
- yield* until(() => store.get(), (v) => v._tag === 'Success')
350
-
351
- yield* Event.dispatch(Added, { id: 'b', name: 'beta' })
352
- yield* until(() => store.get(), (v) => v._tag === 'Success' && v.value.length === 2)
353
- expect(queue.get()).toHaveLength(1)
354
-
355
- yield* until(() => queue.get(), (q) => q.length === 0)
356
- const reverted = yield* until(
357
- () => store.get(),
358
- (v) => v._tag === 'Success' && v.value.length === 1,
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
- if (reverted._tag === 'Success') {
361
- expect(reverted.value).toEqual([{ id: 'a', name: 'alpha' }])
362
- }
363
- yield* until(() => log.get(), (l) => l.length === 1)
364
- expect(log.get()).toEqual(['Added|rejected'])
365
- expect(server.state.fetches).toBe(1)
366
- }).pipe(Effect.provide(TestLayer))
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(() => store.get(), (v) => v._tag === 'Success')
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(() => queue.get(), (q) => q.length === 2)
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(() => queue.get(), (q) => q.length === 0)
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(() => store.get(), (v) => v._tag === 'Success')
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(() => queue.get(), (q) => q.length === 0)
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(() => store.get(), (v) => v._tag === 'Success')
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(() => queue.get(), (q) => q.length === 1)
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(() => queue.get(), (q) => q.length === 0)
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(() => server.state.items, (items) => items.length === 2)
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(() => queue.get(), (q) => q.length === 0)
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(() => store.get(), (v) => v._tag === 'Success')
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(() => queue.get(), (q) => q.length === 1)
605
- yield* until(() => queue.get(), (q) => q.length === 0)
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(() => store.get(), (v) => v._tag === 'Success')
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(() => server.state.fetches, (n) => n === 2)
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(() => queue.get(), (q) => q.length === 1)
651
- yield* until(() => queue.get(), (q) => q.length === 0)
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(() => store.get(), (v) => v._tag === 'Error')
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(() => queue.get(), (q) => q.length === 1 && q[0]?.status === 'confirmed')
695
- yield* until(() => store.get(), (v) => v._tag === 'Error' && v.refetching === false)
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('teardown: closing the scope unregisters the reducers, the procedure, and the channel', () => {
703
- class Page extends State.make('page', S.Number) {}
704
- class Inputs extends StateGroup.make(Page) {}
705
- const server = makeServer([{ id: 'a', name: 'alpha' }])
706
- class Items extends RemoteState.make('Items', {
707
- inputs: [StateGroup.select(Inputs, 'page')],
708
- output: S.Array(Item),
709
- intents: [Added, Renamed],
710
- alwaysOn: true,
711
- }) {}
712
- const ItemsLive = RemoteState.live(Items, {
713
- query: () => server.fetch(5),
714
- send: (intent) => server.receive(intent, 5),
715
- apply: applyIntent,
716
- }).pipe(Layer.provideMerge(StateGroup.live(Inputs, { page: 1 })))
717
-
718
- return Effect.gen(function* () {
719
- const reducers = yield* Reducers
720
- const procedures = yield* Procedures
721
- const channels = yield* Channels
722
-
723
- yield* Effect.scoped(
724
- Effect.gen(function* () {
725
- yield* Layer.build(ItemsLive)
726
- expect(reducers.entries.some((entry) => entry.name === 'Items/pending')).toBe(true)
727
- expect(reducers.entries.some((entry) => entry.name === 'Items/revision')).toBe(true)
728
- expect(procedures.entries.some((entry) => entry.name === 'Items/send')).toBe(true)
729
- expect(channels.byName.has('Items/sends')).toBe(true)
730
- }),
731
- )
732
-
733
- expect(reducers.entries.some((entry) => entry.name === 'Items/pending')).toBe(false)
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') expect(hydrated.value).toEqual([{ id: 'seed', name: 'seeded' }])
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('persist: the optimistic overlay is NEVER written — only the converged server truth is', () => {
784
- class Page extends State.make('page', S.Number) {}
785
- class Inputs extends StateGroup.make(Page) {}
786
- const server = makeServer([{ id: 'a', name: 'alpha' }])
787
- const store = fakeQueryStore()
788
- class Items extends RemoteState.make('Items', {
789
- inputs: [StateGroup.select(Inputs, 'page')],
790
- output: S.Array(Item),
791
- intents: [Added, Renamed],
792
- alwaysOn: true,
793
- }) {}
794
- const ItemsLive = RemoteState.live(Items, {
795
- query: () => server.fetch(10),
796
- send: (intent) => server.receive(intent, 60),
797
- apply: applyIntent,
798
- persist: true,
799
- })
800
- const TestLayer = ItemsLive.pipe(
801
- Layer.provideMerge(StateGroup.live(Inputs, { page: 1 })),
802
- Layer.provideMerge(store.layer),
803
- Layer.provideMerge(Engine),
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
- it.live('persist: `true` keys by name; `{ key }` overrides — two definitions keep separate slots', () => {
831
- class Page extends State.make('page', S.Number) {}
832
- class Inputs extends StateGroup.make(Page) {}
833
- const serverA = makeServer([{ id: 'a', name: 'alpha' }])
834
- const serverB = makeServer([{ id: 'b', name: 'beta' }])
835
- const store = fakeQueryStore()
836
- class ItemsA extends RemoteState.make('ItemsA', {
837
- inputs: [StateGroup.select(Inputs, 'page')],
838
- output: S.Array(Item),
839
- intents: [Added, Renamed],
840
- alwaysOn: true,
841
- }) {}
842
- class ItemsB extends RemoteState.make('ItemsB', {
843
- inputs: [StateGroup.select(Inputs, 'page')],
844
- output: S.Array(Item),
845
- intents: [Added, Renamed],
846
- alwaysOn: true,
847
- }) {}
848
- const TestLayer = Layer.mergeAll(
849
- RemoteState.live(ItemsA, { query: () => serverA.fetch(10), send: (i) => serverA.receive(i, 10), apply: applyIntent, persist: true }),
850
- RemoteState.live(ItemsB, { query: () => serverB.fetch(10), send: (i) => serverB.receive(i, 10), apply: applyIntent, persist: { key: 'custom-B' } }),
851
- ).pipe(Layer.provideMerge(StateGroup.live(Inputs, { page: 1 })), Layer.provideMerge(store.layer), Layer.provideMerge(Engine))
852
-
853
- return Effect.gen(function* () {
854
- const a = yield* ItemsA.store
855
- const b = yield* ItemsB.store
856
- yield* until(() => a.get(), (v) => v._tag === 'Success')
857
- yield* until(() => b.get(), (v) => v._tag === 'Success')
858
- yield* until(() => store.map.get('ItemsA'), (v) => Array.isArray(v))
859
- yield* until(() => store.map.get('custom-B'), (v) => Array.isArray(v))
860
- expect(store.map.get('ItemsA')).toEqual([{ id: 'a', name: 'alpha' }])
861
- expect(store.map.get('custom-B')).toEqual([{ id: 'b', name: 'beta' }])
862
- expect(store.map.has('ItemsB')).toBe(false)
863
- }).pipe(Effect.provide(TestLayer))
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
- it.live('persist (00c): a function key keys a family per-input — two input keys, two slots, no collision', () => {
867
- class Page extends State.make('page', S.Number) {}
868
- class Inputs extends StateGroup.make(Page) {}
869
- const inputs = StateGroup.live(Inputs, { page: 1 })
870
- const store = fakeQueryStore()
871
- const fetchPage = (page: number) =>
872
- Effect.as(tick(10), [{ id: `p${page}`, name: `page-${page}` }])
873
- class Fam extends RemoteState.make('Fam', {
874
- inputs: [StateGroup.select(Inputs, 'page')],
875
- output: S.Array(Item),
876
- intents: [Added, Renamed],
877
- alwaysOn: true,
878
- }) {}
879
- const FamLive = RemoteState.live(Fam, {
880
- query: ({ page }) => fetchPage(page),
881
- send: (i) => Effect.sync(() => void i),
882
- apply: applyIntent,
883
- persist: { key: ({ page }) => `fam:${page}` },
884
- })
885
- const TestLayer = FamLive.pipe(Layer.provideMerge(inputs), Layer.provideMerge(store.layer), Layer.provideMerge(Engine))
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
- return Effect.gen(function* () {
888
- const view = yield* Fam.store
889
- const page = yield* Page.store
890
- yield* until(() => store.map.get('fam:1'), (v) => Array.isArray(v))
891
- expect(store.map.get('fam:1')).toEqual([{ id: 'p1', name: 'page-1' }])
892
-
893
- page.set(2)
894
- yield* until(() => view.get(), (v) => v._tag === 'Success' && v.value[0]?.id === 'p2')
895
- yield* until(() => store.map.get('fam:2'), (v) => Array.isArray(v))
896
- expect(store.map.get('fam:2')).toEqual([{ id: 'p2', name: 'page-2' }])
897
- expect(store.map.get('fam:1')).toEqual([{ id: 'p1', name: 'page-1' }])
898
- }).pipe(Effect.provide(TestLayer))
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
+ )