@livestore/livestore 0.4.0 → 0.5.0-dev.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 (89) hide show
  1. package/dist/.tsbuildinfo +1 -1
  2. package/dist/SqliteDbWrapper.d.ts.map +1 -1
  3. package/dist/SqliteDbWrapper.js +7 -5
  4. package/dist/SqliteDbWrapper.js.map +1 -1
  5. package/dist/SqliteDbWrapper.test.js +20 -3
  6. package/dist/SqliteDbWrapper.test.js.map +1 -1
  7. package/dist/effect/LiveStore.d.ts +15 -15
  8. package/dist/effect/LiveStore.d.ts.map +1 -1
  9. package/dist/effect/LiveStore.js +17 -17
  10. package/dist/effect/LiveStore.js.map +1 -1
  11. package/dist/effect/LiveStore.test.js +5 -6
  12. package/dist/effect/LiveStore.test.js.map +1 -1
  13. package/dist/live-queries/base-class.d.ts +19 -2
  14. package/dist/live-queries/base-class.d.ts.map +1 -1
  15. package/dist/live-queries/base-class.js +28 -2
  16. package/dist/live-queries/base-class.js.map +1 -1
  17. package/dist/live-queries/computed.d.ts.map +1 -1
  18. package/dist/live-queries/computed.js +4 -4
  19. package/dist/live-queries/computed.js.map +1 -1
  20. package/dist/live-queries/db-query.d.ts +2 -2
  21. package/dist/live-queries/db-query.d.ts.map +1 -1
  22. package/dist/live-queries/db-query.js +26 -17
  23. package/dist/live-queries/db-query.js.map +1 -1
  24. package/dist/live-queries/db-query.test.js +9 -9
  25. package/dist/live-queries/db-query.test.js.map +1 -1
  26. package/dist/live-queries/signal.d.ts.map +1 -1
  27. package/dist/live-queries/signal.js +2 -2
  28. package/dist/live-queries/signal.js.map +1 -1
  29. package/dist/live-queries/signal.test.js +2 -2
  30. package/dist/live-queries/signal.test.js.map +1 -1
  31. package/dist/reactive.d.ts +1 -1
  32. package/dist/reactive.d.ts.map +1 -1
  33. package/dist/reactive.js +20 -1
  34. package/dist/reactive.js.map +1 -1
  35. package/dist/store/StoreRegistry.d.ts +12 -12
  36. package/dist/store/StoreRegistry.d.ts.map +1 -1
  37. package/dist/store/StoreRegistry.js +24 -24
  38. package/dist/store/StoreRegistry.js.map +1 -1
  39. package/dist/store/StoreRegistry.test.js +62 -59
  40. package/dist/store/StoreRegistry.test.js.map +1 -1
  41. package/dist/store/create-store.d.ts +12 -15
  42. package/dist/store/create-store.d.ts.map +1 -1
  43. package/dist/store/create-store.js +43 -24
  44. package/dist/store/create-store.js.map +1 -1
  45. package/dist/store/devtools.d.ts +1 -2
  46. package/dist/store/devtools.d.ts.map +1 -1
  47. package/dist/store/devtools.js +3 -2
  48. package/dist/store/devtools.js.map +1 -1
  49. package/dist/store/store-eventstream.test.js +5 -5
  50. package/dist/store/store-eventstream.test.js.map +1 -1
  51. package/dist/store/store-types.d.ts +10 -28
  52. package/dist/store/store-types.d.ts.map +1 -1
  53. package/dist/store/store-types.js +2 -38
  54. package/dist/store/store-types.js.map +1 -1
  55. package/dist/store/store-types.test.js +1 -1
  56. package/dist/store/store.d.ts +2 -2
  57. package/dist/store/store.d.ts.map +1 -1
  58. package/dist/store/store.js +114 -56
  59. package/dist/store/store.js.map +1 -1
  60. package/dist/utils/dev.js +1 -1
  61. package/dist/utils/dev.js.map +1 -1
  62. package/dist/utils/stack-info.d.ts.map +1 -1
  63. package/dist/utils/stack-info.js +2 -1
  64. package/dist/utils/stack-info.js.map +1 -1
  65. package/dist/utils/tests/fixture.d.ts +26 -34
  66. package/dist/utils/tests/fixture.d.ts.map +1 -1
  67. package/package.json +14 -24
  68. package/src/SqliteDbWrapper.test.ts +25 -3
  69. package/src/SqliteDbWrapper.ts +12 -9
  70. package/src/effect/LiveStore.test.ts +16 -16
  71. package/src/effect/LiveStore.ts +79 -50
  72. package/src/live-queries/__snapshots__/db-query.test.ts.snap +273 -49
  73. package/src/live-queries/base-class.ts +38 -6
  74. package/src/live-queries/computed.ts +5 -5
  75. package/src/live-queries/db-query.test.ts +9 -9
  76. package/src/live-queries/db-query.ts +42 -30
  77. package/src/live-queries/signal.test.ts +2 -2
  78. package/src/live-queries/signal.ts +3 -3
  79. package/src/reactive.ts +45 -35
  80. package/src/store/StoreRegistry.test.ts +62 -59
  81. package/src/store/StoreRegistry.ts +49 -38
  82. package/src/store/create-store.ts +85 -51
  83. package/src/store/devtools.ts +283 -283
  84. package/src/store/store-eventstream.test.ts +16 -7
  85. package/src/store/store-types.test.ts +1 -1
  86. package/src/store/store-types.ts +10 -56
  87. package/src/store/store.ts +148 -71
  88. package/src/utils/dev.ts +1 -1
  89. package/src/utils/stack-info.ts +4 -1
@@ -166,11 +166,11 @@ describe('StoreRegistry', () => {
166
166
  })
167
167
 
168
168
  it.layer(OtelLiveDummy)('time-dependent (using TestClock)', (it) => {
169
- it.scoped('disposes store after unusedCacheTime expires', () =>
169
+ it.effect('disposes store after unusedCacheTime expires', () =>
170
170
  Effect.gen(function* () {
171
171
  const unusedCacheTime = 25
172
- const runtime = yield* Effect.runtime<Scope.Scope | OtelTracer.OtelTracer>()
173
- const registry = new StoreRegistry({ runtime, defaultOptions: { unusedCacheTime } })
172
+ const services = yield* Effect.context<Scope.Scope | OtelTracer.OtelTracer>()
173
+ const registry = new StoreRegistry({ context: services, defaultOptions: { unusedCacheTime } })
174
174
  const options = testStoreOptions()
175
175
 
176
176
  const store = yield* registry.getOrLoad(options).pipe(Effect.scoped)
@@ -180,7 +180,7 @@ describe('StoreRegistry', () => {
180
180
  expect(cached).toBe(store)
181
181
 
182
182
  // Let the idle timer fiber register its sleep with TestClock
183
- yield* Effect.yieldNow()
183
+ yield* Effect.yieldNow
184
184
 
185
185
  // Advance time past unusedCacheTime → idle timer fires → entry evicted
186
186
  yield* TestClock.adjust(unusedCacheTime)
@@ -192,10 +192,13 @@ describe('StoreRegistry', () => {
192
192
  }),
193
193
  )
194
194
 
195
- it.scoped('does not dispose when unusedCacheTime is Infinity', () =>
195
+ it.effect('does not dispose when unusedCacheTime is Infinity', () =>
196
196
  Effect.gen(function* () {
197
- const runtime = yield* Effect.runtime<Scope.Scope | OtelTracer.OtelTracer>()
198
- const registry = new StoreRegistry({ runtime, defaultOptions: { unusedCacheTime: Number.POSITIVE_INFINITY } })
197
+ const services = yield* Effect.context<Scope.Scope | OtelTracer.OtelTracer>()
198
+ const registry = new StoreRegistry({
199
+ context: services,
200
+ defaultOptions: { unusedCacheTime: Number.POSITIVE_INFINITY },
201
+ })
199
202
  const options = testStoreOptions()
200
203
 
201
204
  const store = yield* registry.getOrLoad(options).pipe(Effect.scoped)
@@ -209,17 +212,17 @@ describe('StoreRegistry', () => {
209
212
  }),
210
213
  )
211
214
 
212
- it.scoped('schedules disposal if store becomes unused during loading', () =>
215
+ it.effect('schedules disposal if store becomes unused during loading', () =>
213
216
  Effect.gen(function* () {
214
217
  const unusedCacheTime = 50
215
- const runtime = yield* Effect.runtime<Scope.Scope | OtelTracer.OtelTracer>()
216
- const registry = new StoreRegistry({ runtime, defaultOptions: { unusedCacheTime } })
218
+ const services = yield* Effect.context<Scope.Scope | OtelTracer.OtelTracer>()
219
+ const registry = new StoreRegistry({ context: services, defaultOptions: { unusedCacheTime } })
217
220
  const options = testStoreOptions()
218
221
 
219
222
  // Load without retaining — disposal is scheduled when scope closes
220
223
  const store = yield* registry.getOrLoad(options).pipe(Effect.scoped)
221
224
 
222
- yield* Effect.yieldNow()
225
+ yield* Effect.yieldNow
223
226
  yield* TestClock.adjust(unusedCacheTime)
224
227
 
225
228
  // Store should be disposed
@@ -228,17 +231,17 @@ describe('StoreRegistry', () => {
228
231
  }),
229
232
  )
230
233
 
231
- it.scoped('allows call-site options to override default options', () =>
234
+ it.effect('allows call-site options to override default options', () =>
232
235
  Effect.gen(function* () {
233
- const runtime = yield* Effect.runtime<Scope.Scope | OtelTracer.OtelTracer>()
234
- const registry = new StoreRegistry({ runtime, defaultOptions: { unusedCacheTime: 10_000 } }) // Long default
236
+ const services = yield* Effect.context<Scope.Scope | OtelTracer.OtelTracer>()
237
+ const registry = new StoreRegistry({ context: services, defaultOptions: { unusedCacheTime: 10_000 } }) // Long default
235
238
 
236
239
  const unusedCacheTimeOverride = 25
237
240
  const options = testStoreOptions({ unusedCacheTime: unusedCacheTimeOverride })
238
241
 
239
242
  const store = yield* registry.getOrLoad(options).pipe(Effect.scoped)
240
243
 
241
- yield* Effect.yieldNow()
244
+ yield* Effect.yieldNow
242
245
  yield* TestClock.adjust(unusedCacheTimeOverride)
243
246
 
244
247
  // Should be disposed according to the override time, not default
@@ -247,10 +250,10 @@ describe('StoreRegistry', () => {
247
250
  }),
248
251
  )
249
252
 
250
- it.scoped('disposes different stores according to their own unusedCacheTime', () =>
253
+ it.effect('disposes different stores according to their own unusedCacheTime', () =>
251
254
  Effect.gen(function* () {
252
- const runtime = yield* Effect.runtime<Scope.Scope | OtelTracer.OtelTracer>()
253
- const registry = new StoreRegistry({ runtime, defaultOptions: { unusedCacheTime: 1000 } })
255
+ const services = yield* Effect.context<Scope.Scope | OtelTracer.OtelTracer>()
256
+ const registry = new StoreRegistry({ context: services, defaultOptions: { unusedCacheTime: 1000 } })
254
257
 
255
258
  const shortOptions = testStoreOptions({ storeId: 'short-lived', unusedCacheTime: 25 })
256
259
  const longOptions = testStoreOptions({ storeId: 'long-lived', unusedCacheTime: 10_000 })
@@ -258,7 +261,7 @@ describe('StoreRegistry', () => {
258
261
  const shortStore = yield* registry.getOrLoad(shortOptions).pipe(Effect.scoped)
259
262
  const longStore = yield* registry.getOrLoad(longOptions).pipe(Effect.scoped)
260
263
 
261
- yield* Effect.yieldNow()
264
+ yield* Effect.yieldNow
262
265
 
263
266
  // Advance past short store's unusedCacheTime only
264
267
  yield* TestClock.adjust(25)
@@ -274,10 +277,10 @@ describe('StoreRegistry', () => {
274
277
 
275
278
  // This test is skipped because we don't yet support dynamic `unusedCacheTime` updates for cached stores.
276
279
  // See https://github.com/livestorejs/livestore/issues/918
277
- it.scoped.skip('keeps the longest unusedCacheTime seen for a store when options vary across calls', () =>
280
+ it.effect.skip('keeps the longest unusedCacheTime seen for a store when options vary across calls', () =>
278
281
  Effect.gen(function* () {
279
- const runtime = yield* Effect.runtime<Scope.Scope | OtelTracer.OtelTracer>()
280
- const registry = new StoreRegistry({ runtime })
282
+ const services = yield* Effect.context<Scope.Scope | OtelTracer.OtelTracer>()
283
+ const registry = new StoreRegistry({ context: services })
281
284
 
282
285
  const options = testStoreOptions({ unusedCacheTime: 10 })
283
286
  const release = registry.retain(options)
@@ -288,7 +291,7 @@ describe('StoreRegistry', () => {
288
291
  yield* registry.getOrLoad(testStoreOptions({ unusedCacheTime: 100 })).pipe(Effect.scoped)
289
292
 
290
293
  release()
291
- yield* Effect.yieldNow()
294
+ yield* Effect.yieldNow
292
295
 
293
296
  // After 99ms, store should still be alive (100ms unusedCacheTime used)
294
297
  yield* TestClock.adjust(99)
@@ -304,11 +307,11 @@ describe('StoreRegistry', () => {
304
307
  }),
305
308
  )
306
309
 
307
- it.scoped('handles rapid retain/release cycles without errors', () =>
310
+ it.effect('handles rapid retain/release cycles without errors', () =>
308
311
  Effect.gen(function* () {
309
312
  const unusedCacheTime = 50
310
- const runtime = yield* Effect.runtime<Scope.Scope | OtelTracer.OtelTracer>()
311
- const registry = new StoreRegistry({ runtime, defaultOptions: { unusedCacheTime } })
313
+ const services = yield* Effect.context<Scope.Scope | OtelTracer.OtelTracer>()
314
+ const registry = new StoreRegistry({ context: services, defaultOptions: { unusedCacheTime } })
312
315
  const options = testStoreOptions()
313
316
 
314
317
  const store = yield* registry.getOrLoad(options).pipe(Effect.scoped)
@@ -319,7 +322,7 @@ describe('StoreRegistry', () => {
319
322
  release()
320
323
  }
321
324
 
322
- yield* Effect.yieldNow()
325
+ yield* Effect.yieldNow
323
326
  yield* TestClock.adjust(unusedCacheTime)
324
327
 
325
328
  // Store should be disposed after the last release
@@ -328,16 +331,16 @@ describe('StoreRegistry', () => {
328
331
  }),
329
332
  )
330
333
 
331
- it.scoped('cancels disposal when new retain', () =>
334
+ it.effect('cancels disposal when new retain', () =>
332
335
  Effect.gen(function* () {
333
336
  const unusedCacheTime = 50
334
- const runtime = yield* Effect.runtime<Scope.Scope | OtelTracer.OtelTracer>()
335
- const registry = new StoreRegistry({ runtime, defaultOptions: { unusedCacheTime } })
337
+ const services = yield* Effect.context<Scope.Scope | OtelTracer.OtelTracer>()
338
+ const registry = new StoreRegistry({ context: services, defaultOptions: { unusedCacheTime } })
336
339
  const options = testStoreOptions()
337
340
 
338
341
  const store = yield* registry.getOrLoad(options).pipe(Effect.scoped)
339
342
 
340
- yield* Effect.yieldNow()
343
+ yield* Effect.yieldNow
341
344
 
342
345
  // Advance almost to disposal threshold
343
346
  yield* TestClock.adjust(unusedCacheTime - 5)
@@ -354,7 +357,7 @@ describe('StoreRegistry', () => {
354
357
 
355
358
  // Release retain — new idle timer starts
356
359
  release()
357
- yield* Effect.yieldNow()
360
+ yield* Effect.yieldNow
358
361
 
359
362
  yield* TestClock.adjust(unusedCacheTime)
360
363
 
@@ -364,12 +367,12 @@ describe('StoreRegistry', () => {
364
367
  }),
365
368
  )
366
369
 
367
- it.scoped('aborts loading when disposal fires while store is still loading', () =>
370
+ it.effect('aborts loading when disposal fires while store is still loading', () =>
368
371
  Effect.gen(function* () {
369
372
  const unusedCacheTime = 10
370
373
  const loadDelay = 1000
371
- const runtime = yield* Effect.runtime<Scope.Scope | OtelTracer.OtelTracer>()
372
- const registry = new StoreRegistry({ runtime, defaultOptions: { unusedCacheTime } })
374
+ const services = yield* Effect.context<Scope.Scope | OtelTracer.OtelTracer>()
375
+ const registry = new StoreRegistry({ context: services, defaultOptions: { unusedCacheTime } })
373
376
 
374
377
  // Adapter that takes time to load (controlled by TestClock)
375
378
  const baseAdapter = makeInMemoryAdapter()
@@ -383,18 +386,18 @@ describe('StoreRegistry', () => {
383
386
 
384
387
  // Retain triggers loading (won't complete until clock advances past loadDelay)
385
388
  const release = registry.retain(options)
386
- yield* Effect.yieldNow()
389
+ yield* Effect.yieldNow
387
390
 
388
391
  // Release immediately — schedules disposal after unusedCacheTime
389
392
  release()
390
- yield* Effect.yieldNow()
393
+ yield* Effect.yieldNow
391
394
 
392
395
  // Advance past unusedCacheTime but NOT past loadDelay → disposal fires, interrupts loading
393
396
  yield* TestClock.adjust(unusedCacheTime)
394
397
 
395
398
  // Start a fresh load — since the first was aborted, this should be a new entry
396
- const freshLoadFiber = yield* Effect.fork(registry.getOrLoad(options).pipe(Effect.scoped))
397
- yield* Effect.yieldNow()
399
+ const freshLoadFiber = yield* Effect.forkChild(registry.getOrLoad(options).pipe(Effect.scoped))
400
+ yield* Effect.yieldNow
398
401
 
399
402
  // Advance enough for the fresh load to complete
400
403
  yield* TestClock.adjust(loadDelay)
@@ -404,11 +407,11 @@ describe('StoreRegistry', () => {
404
407
  }),
405
408
  )
406
409
 
407
- it.scoped('retain keeps store alive past unusedCacheTime', () =>
410
+ it.effect('retain keeps store alive past unusedCacheTime', () =>
408
411
  Effect.gen(function* () {
409
412
  const unusedCacheTime = 50
410
- const runtime = yield* Effect.runtime<Scope.Scope | OtelTracer.OtelTracer>()
411
- const registry = new StoreRegistry({ runtime, defaultOptions: { unusedCacheTime } })
413
+ const services = yield* Effect.context<Scope.Scope | OtelTracer.OtelTracer>()
414
+ const registry = new StoreRegistry({ context: services, defaultOptions: { unusedCacheTime } })
412
415
  const options = testStoreOptions()
413
416
 
414
417
  // Load the store
@@ -417,7 +420,7 @@ describe('StoreRegistry', () => {
417
420
  // Retain the store before disposal could fire
418
421
  const release = registry.retain(options)
419
422
 
420
- yield* Effect.yieldNow()
423
+ yield* Effect.yieldNow
421
424
 
422
425
  // Advance past unusedCacheTime — idle timer fires but refCount > 0, so no eviction
423
426
  yield* TestClock.adjust(unusedCacheTime + 50)
@@ -430,11 +433,11 @@ describe('StoreRegistry', () => {
430
433
  }),
431
434
  )
432
435
 
433
- it.scoped('manages multiple stores with different IDs independently', () =>
436
+ it.effect('manages multiple stores with different IDs independently', () =>
434
437
  Effect.gen(function* () {
435
438
  const unusedCacheTime = 50
436
- const runtime = yield* Effect.runtime<Scope.Scope | OtelTracer.OtelTracer>()
437
- const registry = new StoreRegistry({ runtime, defaultOptions: { unusedCacheTime } })
439
+ const services = yield* Effect.context<Scope.Scope | OtelTracer.OtelTracer>()
440
+ const registry = new StoreRegistry({ context: services, defaultOptions: { unusedCacheTime } })
438
441
 
439
442
  const options1 = testStoreOptions({ storeId: 'store-1' })
440
443
  const options2 = testStoreOptions({ storeId: 'store-2' })
@@ -451,7 +454,7 @@ describe('StoreRegistry', () => {
451
454
  expect(cached1).toBe(store1)
452
455
  expect(cached2).toBe(store2)
453
456
 
454
- yield* Effect.yieldNow()
457
+ yield* Effect.yieldNow
455
458
  yield* TestClock.adjust(unusedCacheTime)
456
459
 
457
460
  // Both stores should be disposed
@@ -462,10 +465,10 @@ describe('StoreRegistry', () => {
462
465
  }),
463
466
  )
464
467
 
465
- it.scoped('applies default options from constructor', () =>
468
+ it.effect('applies default options from constructor', () =>
466
469
  Effect.gen(function* () {
467
- const runtime = yield* Effect.runtime<Scope.Scope | OtelTracer.OtelTracer>()
468
- const registry = new StoreRegistry({ runtime, defaultOptions: { unusedCacheTime: 100 } })
470
+ const services = yield* Effect.context<Scope.Scope | OtelTracer.OtelTracer>()
471
+ const registry = new StoreRegistry({ context: services, defaultOptions: { unusedCacheTime: 100 } })
469
472
  const options = testStoreOptions()
470
473
 
471
474
  const store = yield* registry.getOrLoad(options).pipe(Effect.scoped)
@@ -474,7 +477,7 @@ describe('StoreRegistry', () => {
474
477
  expect(store).toBeDefined()
475
478
  expect(store[StoreInternalsSymbol].clientSession.debugInstanceId).toBeDefined()
476
479
 
477
- yield* Effect.yieldNow()
480
+ yield* Effect.yieldNow
478
481
 
479
482
  // After 50ms, store should still be cached (default is 100ms)
480
483
  yield* TestClock.adjust(50)
@@ -484,11 +487,11 @@ describe('StoreRegistry', () => {
484
487
  }),
485
488
  )
486
489
 
487
- it.scoped('does not serve a disposed store from cache', () =>
490
+ it.effect('does not serve a disposed store from cache', () =>
488
491
  Effect.gen(function* () {
489
492
  const unusedCacheTime = 25
490
- const runtime = yield* Effect.runtime<Scope.Scope | OtelTracer.OtelTracer>()
491
- const registry = new StoreRegistry({ runtime, defaultOptions: { unusedCacheTime } })
493
+ const services = yield* Effect.context<Scope.Scope | OtelTracer.OtelTracer>()
494
+ const registry = new StoreRegistry({ context: services, defaultOptions: { unusedCacheTime } })
492
495
  const options = testStoreOptions()
493
496
 
494
497
  const originalStore = yield* registry.getOrLoad(options).pipe(Effect.scoped)
@@ -497,7 +500,7 @@ describe('StoreRegistry', () => {
497
500
  const cached = yield* registry.getOrLoad(options).pipe(Effect.scoped)
498
501
  expect(cached).toBe(originalStore)
499
502
 
500
- yield* Effect.yieldNow()
503
+ yield* Effect.yieldNow
501
504
  yield* TestClock.adjust(unusedCacheTime)
502
505
 
503
506
  // After disposal, calling getOrLoad should produce a fresh store
@@ -506,11 +509,11 @@ describe('StoreRegistry', () => {
506
509
  }),
507
510
  )
508
511
 
509
- it.scoped('schedules disposal after preload if no retainers are added', () =>
512
+ it.effect('schedules disposal after preload if no retainers are added', () =>
510
513
  Effect.gen(function* () {
511
514
  const unusedCacheTime = 50
512
- const runtime = yield* Effect.runtime<Scope.Scope | OtelTracer.OtelTracer>()
513
- const registry = new StoreRegistry({ runtime, defaultOptions: { unusedCacheTime } })
515
+ const services = yield* Effect.context<Scope.Scope | OtelTracer.OtelTracer>()
516
+ const registry = new StoreRegistry({ context: services, defaultOptions: { unusedCacheTime } })
514
517
  const options = testStoreOptions()
515
518
 
516
519
  // Preload without retaining (load + immediate release)
@@ -520,7 +523,7 @@ describe('StoreRegistry', () => {
520
523
  const cached = yield* registry.getOrLoad(options).pipe(Effect.scoped)
521
524
  expect(cached).toBe(store)
522
525
 
523
- yield* Effect.yieldNow()
526
+ yield* Effect.yieldNow
524
527
  yield* TestClock.adjust(unusedCacheTime)
525
528
 
526
529
  // Store should be disposed since no retainers were added
@@ -1,8 +1,9 @@
1
- import { LogConfig, OtelLiveDummy, provideOtel, UnknownError } from '@livestore/common'
1
+ import { OtelLiveDummy, provideOtel, UnknownError } from '@livestore/common'
2
2
  import type { LiveStoreSchema } from '@livestore/common/schema'
3
- import { omitUndefineds } from '@livestore/utils'
3
+ import { isDevEnv, omitUndefineds } from '@livestore/utils'
4
4
  import {
5
5
  Cause,
6
+ type Context,
6
7
  Effect,
7
8
  Equal,
8
9
  Exit,
@@ -10,16 +11,17 @@ import {
10
11
  Hash,
11
12
  Layer,
12
13
  ManagedRuntime,
14
+ Result,
13
15
  type OtelTracer,
14
16
  RcMap,
15
- Runtime,
17
+ References,
16
18
  type Schema,
17
- type Scope,
19
+ Scope,
18
20
  } from '@livestore/utils/effect'
19
21
 
20
- import { createStore, type CreateStoreOptions } from './create-store.ts'
21
- import type { Store } from './store.ts'
22
+ import { type CreateStoreOptions, createStore } from './create-store.ts'
22
23
  import type { OtelOptions } from './store-types.ts'
24
+ import type { Store } from './store.ts'
23
25
 
24
26
  /**
25
27
  * Default time to keep unused stores in cache.
@@ -47,7 +49,7 @@ export const DEFAULT_UNUSED_CACHE_TIME = typeof window === 'undefined' ? Number.
47
49
  export interface RegistryStoreOptions<
48
50
  TSchema extends LiveStoreSchema = LiveStoreSchema.Any,
49
51
  TContext = {},
50
- TSyncPayloadSchema extends Schema.Schema<any> = typeof Schema.JsonValue,
52
+ TSyncPayloadSchema extends Schema.Codec<Schema.Json, Schema.Json> = typeof Schema.Json,
51
53
  > extends CreateStoreOptions<TSchema, TContext, TSyncPayloadSchema> {
52
54
  /**
53
55
  * OpenTelemetry configuration for tracing store operations.
@@ -99,10 +101,10 @@ type StoreRegistryConfig = {
99
101
  >
100
102
  >
101
103
  /**
102
- * Custom Effect runtime for all registry operations (loading, caching, etc.).
103
- * When the runtime's scope closes, all managed stores are automatically shut down.
104
+ * Custom Effect context for all registry operations (loading, caching, etc.).
105
+ * When the context's scope closes, all managed stores are automatically shut down.
104
106
  */
105
- runtime?: Runtime.Runtime<Scope.Scope | OtelTracer.OtelTracer>
107
+ context?: Context.Context<Scope.Scope | OtelTracer.OtelTracer>
106
108
  }
107
109
 
108
110
  /**
@@ -150,14 +152,14 @@ export class StoreRegistry {
150
152
  readonly #rcMap: RcMap.RcMap<StoreCacheKey, Store<any, any>, UnknownError>
151
153
 
152
154
  /**
153
- * Effect runtime providing Scope and OtelTracer for all registry operations.
154
- * When the runtime's scope closes, all managed stores are automatically shut down.
155
+ * Effect context providing Scope and OtelTracer for all registry operations.
156
+ * When the context's scope closes, all managed stores are automatically shut down.
155
157
  */
156
- readonly #runtime: Runtime.Runtime<Scope.Scope | OtelTracer.OtelTracer>
158
+ readonly #context: Context.Context<Scope.Scope | OtelTracer.OtelTracer>
157
159
 
158
160
  /**
159
161
  * Disposal callback for the runtime created by the registry.
160
- * Undefined when caller provided their own runtime (caller owns cleanup in that case).
162
+ * Undefined when caller provided their own services (caller owns cleanup in that case).
161
163
  */
162
164
  readonly #disposeOwnedRuntime: (() => Promise<void>) | undefined
163
165
 
@@ -181,11 +183,11 @@ export class StoreRegistry {
181
183
  * ```
182
184
  */
183
185
  constructor(config: StoreRegistryConfig = {}) {
184
- if (config.runtime !== undefined) {
185
- this.#runtime = config.runtime
186
+ if (config.context !== undefined) {
187
+ this.#context = config.context
186
188
  } else {
187
- const ownedRuntime = ManagedRuntime.make(Layer.mergeAll(Layer.scope, OtelLiveDummy))
188
- this.#runtime = ownedRuntime.runtimeEffect.pipe(Effect.runSync)
189
+ const ownedRuntime = ManagedRuntime.make(Layer.mergeAll(Layer.effect(Scope.Scope, Effect.scope), OtelLiveDummy))
190
+ this.#context = ownedRuntime.contextEffect.pipe(Effect.runSync)
189
191
  this.#disposeOwnedRuntime = () => ownedRuntime.dispose()
190
192
  }
191
193
 
@@ -194,9 +196,17 @@ export class StoreRegistry {
194
196
  // Merge registry defaults with call-site options (call-site takes precedence)
195
197
  const mergedOptions = { ...config.defaultOptions, ...options }
196
198
  return createStore(mergedOptions).pipe(
197
- Effect.catchAllDefect((cause) => UnknownError.make({ cause })),
199
+ Effect.catchDefect((cause) => UnknownError.make({ cause })),
198
200
  Effect.withSpan(`StoreRegistry.lookup:${mergedOptions.storeId}`),
199
- LogConfig.withLoggerConfig(mergedOptions, { threadName: 'window' }),
201
+ Effect.provide(
202
+ Layer.mergeAll(
203
+ mergedOptions.logger ?? Layer.empty,
204
+ Layer.succeed(
205
+ References.MinimumLogLevel,
206
+ mergedOptions.logLevel ?? (isDevEnv() === true ? 'Debug' : 'Info'),
207
+ ),
208
+ ),
209
+ ),
200
210
  provideOtel(
201
211
  omitUndefineds({
202
212
  parentSpanContext: mergedOptions.otelOptions?.rootSpanContext,
@@ -205,8 +215,9 @@ export class StoreRegistry {
205
215
  ),
206
216
  )
207
217
  },
208
- idleTimeToLive: ({ options }: StoreCacheKey) => options.unusedCacheTime ?? config.defaultOptions?.unusedCacheTime ?? DEFAULT_UNUSED_CACHE_TIME,
209
- }).pipe(Runtime.runSync(this.#runtime))
218
+ idleTimeToLive: ({ options }: StoreCacheKey) =>
219
+ options.unusedCacheTime ?? config.defaultOptions?.unusedCacheTime ?? DEFAULT_UNUSED_CACHE_TIME,
220
+ }).pipe(Effect.runSyncWith(this.#context))
210
221
  }
211
222
 
212
223
  /**
@@ -226,11 +237,11 @@ export class StoreRegistry {
226
237
  getOrLoad = <
227
238
  TSchema extends LiveStoreSchema,
228
239
  TContext = {},
229
- TSyncPayloadSchema extends Schema.Schema<any> = typeof Schema.JsonValue,
240
+ TSyncPayloadSchema extends Schema.Codec<Schema.Json, Schema.Json> = typeof Schema.Json,
230
241
  >(
231
242
  options: RegistryStoreOptions<TSchema, TContext, TSyncPayloadSchema>,
232
243
  ): Effect.Effect<Store<TSchema, TContext>, UnknownError, Scope.Scope> =>
233
- Effect.gen(this, function* () {
244
+ Effect.gen({ self: this }, function* () {
234
245
  // Cast options to satisfy StoreCacheKey's wider type (type safety enforced at API boundary)
235
246
  const key = new StoreCacheKey(options)
236
247
  const store = yield* RcMap.get(this.#rcMap, key)
@@ -257,22 +268,22 @@ export class StoreRegistry {
257
268
  getOrLoadPromise = <
258
269
  TSchema extends LiveStoreSchema,
259
270
  TContext = {},
260
- TSyncPayloadSchema extends Schema.Schema<any> = typeof Schema.JsonValue,
271
+ TSyncPayloadSchema extends Schema.Codec<Schema.Json, Schema.Json> = typeof Schema.Json,
261
272
  >(
262
273
  options: RegistryStoreOptions<TSchema, TContext, TSyncPayloadSchema>,
263
274
  ): Store<TSchema, TContext> | Promise<Store<TSchema, TContext>> => {
264
- const exit = this.getOrLoad(options).pipe(Effect.scoped, Runtime.runSyncExit(this.#runtime))
275
+ const exit = this.getOrLoad(options).pipe(Effect.scoped, Effect.runSyncExitWith(this.#context))
265
276
 
266
277
  if (Exit.isSuccess(exit) === true) return exit.value
267
278
 
268
279
  // Check if the failure is due to async work
269
- const defect = Cause.dieOption(exit.cause)
270
- if (defect._tag !== 'Some') {
280
+ const defect = Cause.findDefect(exit.cause)
281
+ if (Result.isFailure(defect) === true) {
271
282
  // Handle synchronous failure
272
283
  throw Cause.squash(exit.cause)
273
284
  }
274
285
 
275
- if (Runtime.isAsyncFiberException(defect.value) === false) {
286
+ if (Cause.isAsyncFiberError(defect.success) === false) {
276
287
  // Handle synchronous failure
277
288
  throw Cause.squash(exit.cause)
278
289
  }
@@ -284,9 +295,9 @@ export class StoreRegistry {
284
295
  if (cached !== undefined) return cached as Promise<Store<TSchema, TContext>>
285
296
 
286
297
  // Create and cache the promise
287
- const fiber = defect.value.fiber as Fiber.RuntimeFiber<Store<TSchema, TContext>>
298
+ const fiber = defect.success.fiber as Fiber.Fiber<Store<TSchema, TContext>>
288
299
  const promise = Fiber.join(fiber)
289
- .pipe(Runtime.runPromise(this.#runtime))
300
+ .pipe(Effect.runPromiseWith(this.#context))
290
301
  .finally(() => this.#loadingPromises.delete(storeId))
291
302
 
292
303
  this.#loadingPromises.set(storeId, promise)
@@ -309,19 +320,19 @@ export class StoreRegistry {
309
320
  retain = <
310
321
  TSchema extends LiveStoreSchema,
311
322
  TContext = {},
312
- TSyncPayloadSchema extends Schema.Schema<any> = typeof Schema.JsonValue,
323
+ TSyncPayloadSchema extends Schema.Codec<Schema.Json, Schema.Json> = typeof Schema.Json,
313
324
  >(
314
325
  options: RegistryStoreOptions<TSchema, TContext, TSyncPayloadSchema>,
315
326
  ): (() => void) => {
316
- const release = Effect.gen(this, function* () {
327
+ const release = Effect.gen({ self: this }, function* () {
317
328
  // Cast options to satisfy StoreCacheKey's wider type (type safety enforced at API boundary)
318
329
  const key = new StoreCacheKey(options)
319
330
  yield* RcMap.get(this.#rcMap, key)
320
331
  // Effect.never suspends indefinitely, keeping the RcMap reference alive.
321
332
  // When `release()` is called, the fiber is interrupted, closing the scope
322
333
  // and releasing the RcMap entry (which may trigger disposal after idleTimeToLive).
323
- yield* Effect.never
324
- }).pipe(Effect.scoped, Runtime.runCallback(this.#runtime))
334
+ return yield* Effect.never
335
+ }).pipe(Effect.scoped, Effect.runCallbackWith(this.#context))
325
336
 
326
337
  return () => release()
327
338
  }
@@ -342,7 +353,7 @@ export class StoreRegistry {
342
353
  preload = async <
343
354
  TSchema extends LiveStoreSchema,
344
355
  TContext = {},
345
- TSyncPayloadSchema extends Schema.Schema<any> = typeof Schema.JsonValue,
356
+ TSyncPayloadSchema extends Schema.Codec<Schema.Json, Schema.Json> = typeof Schema.Json,
346
357
  >(
347
358
  options: RegistryStoreOptions<TSchema, TContext, TSyncPayloadSchema>,
348
359
  ): Promise<void> => {
@@ -364,7 +375,7 @@ export class StoreRegistry {
364
375
  * @returns A promise that resolves when disposal is complete
365
376
  *
366
377
  * @remarks
367
- * - No-op if a custom `runtime` was provided to the constructor (caller owns cleanup)
378
+ * - No-op if a custom `context` was provided to the constructor (caller owns cleanup)
368
379
  * - Idempotent: safe to call multiple times
369
380
  * - After disposal, the registry should not be used
370
381
  */
@@ -412,7 +423,7 @@ export class StoreRegistry {
412
423
  export const storeOptions = <
413
424
  TSchema extends LiveStoreSchema,
414
425
  TContext = {},
415
- TSyncPayloadSchema extends Schema.Schema<any> = typeof Schema.JsonValue,
426
+ TSyncPayloadSchema extends Schema.Codec<Schema.Json, Schema.Json> = typeof Schema.Json,
416
427
  >(
417
428
  options: RegistryStoreOptions<TSchema, TContext, TSyncPayloadSchema>,
418
429
  ): RegistryStoreOptions<TSchema, TContext, TSyncPayloadSchema> => options