@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.
- package/dist/.tsbuildinfo +1 -1
- package/dist/SqliteDbWrapper.d.ts.map +1 -1
- package/dist/SqliteDbWrapper.js +7 -5
- package/dist/SqliteDbWrapper.js.map +1 -1
- package/dist/SqliteDbWrapper.test.js +20 -3
- package/dist/SqliteDbWrapper.test.js.map +1 -1
- package/dist/effect/LiveStore.d.ts +15 -15
- package/dist/effect/LiveStore.d.ts.map +1 -1
- package/dist/effect/LiveStore.js +17 -17
- package/dist/effect/LiveStore.js.map +1 -1
- package/dist/effect/LiveStore.test.js +5 -6
- package/dist/effect/LiveStore.test.js.map +1 -1
- package/dist/live-queries/base-class.d.ts +19 -2
- package/dist/live-queries/base-class.d.ts.map +1 -1
- package/dist/live-queries/base-class.js +28 -2
- package/dist/live-queries/base-class.js.map +1 -1
- package/dist/live-queries/computed.d.ts.map +1 -1
- package/dist/live-queries/computed.js +4 -4
- package/dist/live-queries/computed.js.map +1 -1
- package/dist/live-queries/db-query.d.ts +2 -2
- package/dist/live-queries/db-query.d.ts.map +1 -1
- package/dist/live-queries/db-query.js +26 -17
- package/dist/live-queries/db-query.js.map +1 -1
- package/dist/live-queries/db-query.test.js +9 -9
- package/dist/live-queries/db-query.test.js.map +1 -1
- package/dist/live-queries/signal.d.ts.map +1 -1
- package/dist/live-queries/signal.js +2 -2
- package/dist/live-queries/signal.js.map +1 -1
- package/dist/live-queries/signal.test.js +2 -2
- package/dist/live-queries/signal.test.js.map +1 -1
- package/dist/reactive.d.ts +1 -1
- package/dist/reactive.d.ts.map +1 -1
- package/dist/reactive.js +20 -1
- package/dist/reactive.js.map +1 -1
- package/dist/store/StoreRegistry.d.ts +12 -12
- package/dist/store/StoreRegistry.d.ts.map +1 -1
- package/dist/store/StoreRegistry.js +24 -24
- package/dist/store/StoreRegistry.js.map +1 -1
- package/dist/store/StoreRegistry.test.js +62 -59
- package/dist/store/StoreRegistry.test.js.map +1 -1
- package/dist/store/create-store.d.ts +12 -15
- package/dist/store/create-store.d.ts.map +1 -1
- package/dist/store/create-store.js +43 -24
- package/dist/store/create-store.js.map +1 -1
- package/dist/store/devtools.d.ts +1 -2
- package/dist/store/devtools.d.ts.map +1 -1
- package/dist/store/devtools.js +3 -2
- package/dist/store/devtools.js.map +1 -1
- package/dist/store/store-eventstream.test.js +5 -5
- package/dist/store/store-eventstream.test.js.map +1 -1
- package/dist/store/store-types.d.ts +10 -28
- package/dist/store/store-types.d.ts.map +1 -1
- package/dist/store/store-types.js +2 -38
- package/dist/store/store-types.js.map +1 -1
- package/dist/store/store-types.test.js +1 -1
- package/dist/store/store.d.ts +2 -2
- package/dist/store/store.d.ts.map +1 -1
- package/dist/store/store.js +114 -56
- package/dist/store/store.js.map +1 -1
- package/dist/utils/dev.js +1 -1
- package/dist/utils/dev.js.map +1 -1
- package/dist/utils/stack-info.d.ts.map +1 -1
- package/dist/utils/stack-info.js +2 -1
- package/dist/utils/stack-info.js.map +1 -1
- package/dist/utils/tests/fixture.d.ts +26 -34
- package/dist/utils/tests/fixture.d.ts.map +1 -1
- package/package.json +14 -24
- package/src/SqliteDbWrapper.test.ts +25 -3
- package/src/SqliteDbWrapper.ts +12 -9
- package/src/effect/LiveStore.test.ts +16 -16
- package/src/effect/LiveStore.ts +79 -50
- package/src/live-queries/__snapshots__/db-query.test.ts.snap +273 -49
- package/src/live-queries/base-class.ts +38 -6
- package/src/live-queries/computed.ts +5 -5
- package/src/live-queries/db-query.test.ts +9 -9
- package/src/live-queries/db-query.ts +42 -30
- package/src/live-queries/signal.test.ts +2 -2
- package/src/live-queries/signal.ts +3 -3
- package/src/reactive.ts +45 -35
- package/src/store/StoreRegistry.test.ts +62 -59
- package/src/store/StoreRegistry.ts +49 -38
- package/src/store/create-store.ts +85 -51
- package/src/store/devtools.ts +283 -283
- package/src/store/store-eventstream.test.ts +16 -7
- package/src/store/store-types.test.ts +1 -1
- package/src/store/store-types.ts +10 -56
- package/src/store/store.ts +148 -71
- package/src/utils/dev.ts +1 -1
- 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.
|
|
169
|
+
it.effect('disposes store after unusedCacheTime expires', () =>
|
|
170
170
|
Effect.gen(function* () {
|
|
171
171
|
const unusedCacheTime = 25
|
|
172
|
-
const
|
|
173
|
-
const registry = new StoreRegistry({
|
|
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.
|
|
195
|
+
it.effect('does not dispose when unusedCacheTime is Infinity', () =>
|
|
196
196
|
Effect.gen(function* () {
|
|
197
|
-
const
|
|
198
|
-
const registry = new StoreRegistry({
|
|
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.
|
|
215
|
+
it.effect('schedules disposal if store becomes unused during loading', () =>
|
|
213
216
|
Effect.gen(function* () {
|
|
214
217
|
const unusedCacheTime = 50
|
|
215
|
-
const
|
|
216
|
-
const registry = new StoreRegistry({
|
|
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.
|
|
234
|
+
it.effect('allows call-site options to override default options', () =>
|
|
232
235
|
Effect.gen(function* () {
|
|
233
|
-
const
|
|
234
|
-
const registry = new StoreRegistry({
|
|
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.
|
|
253
|
+
it.effect('disposes different stores according to their own unusedCacheTime', () =>
|
|
251
254
|
Effect.gen(function* () {
|
|
252
|
-
const
|
|
253
|
-
const registry = new StoreRegistry({
|
|
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.
|
|
280
|
+
it.effect.skip('keeps the longest unusedCacheTime seen for a store when options vary across calls', () =>
|
|
278
281
|
Effect.gen(function* () {
|
|
279
|
-
const
|
|
280
|
-
const registry = new StoreRegistry({
|
|
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.
|
|
310
|
+
it.effect('handles rapid retain/release cycles without errors', () =>
|
|
308
311
|
Effect.gen(function* () {
|
|
309
312
|
const unusedCacheTime = 50
|
|
310
|
-
const
|
|
311
|
-
const registry = new StoreRegistry({
|
|
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.
|
|
334
|
+
it.effect('cancels disposal when new retain', () =>
|
|
332
335
|
Effect.gen(function* () {
|
|
333
336
|
const unusedCacheTime = 50
|
|
334
|
-
const
|
|
335
|
-
const registry = new StoreRegistry({
|
|
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.
|
|
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
|
|
372
|
-
const registry = new StoreRegistry({
|
|
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.
|
|
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.
|
|
410
|
+
it.effect('retain keeps store alive past unusedCacheTime', () =>
|
|
408
411
|
Effect.gen(function* () {
|
|
409
412
|
const unusedCacheTime = 50
|
|
410
|
-
const
|
|
411
|
-
const registry = new StoreRegistry({
|
|
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.
|
|
436
|
+
it.effect('manages multiple stores with different IDs independently', () =>
|
|
434
437
|
Effect.gen(function* () {
|
|
435
438
|
const unusedCacheTime = 50
|
|
436
|
-
const
|
|
437
|
-
const registry = new StoreRegistry({
|
|
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.
|
|
468
|
+
it.effect('applies default options from constructor', () =>
|
|
466
469
|
Effect.gen(function* () {
|
|
467
|
-
const
|
|
468
|
-
const registry = new StoreRegistry({
|
|
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.
|
|
490
|
+
it.effect('does not serve a disposed store from cache', () =>
|
|
488
491
|
Effect.gen(function* () {
|
|
489
492
|
const unusedCacheTime = 25
|
|
490
|
-
const
|
|
491
|
-
const registry = new StoreRegistry({
|
|
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.
|
|
512
|
+
it.effect('schedules disposal after preload if no retainers are added', () =>
|
|
510
513
|
Effect.gen(function* () {
|
|
511
514
|
const unusedCacheTime = 50
|
|
512
|
-
const
|
|
513
|
-
const registry = new StoreRegistry({
|
|
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 {
|
|
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
|
-
|
|
17
|
+
References,
|
|
16
18
|
type Schema,
|
|
17
|
-
|
|
19
|
+
Scope,
|
|
18
20
|
} from '@livestore/utils/effect'
|
|
19
21
|
|
|
20
|
-
import {
|
|
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
|
|
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
|
|
103
|
-
* When the
|
|
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
|
-
|
|
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
|
|
154
|
-
* When the
|
|
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 #
|
|
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
|
|
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.
|
|
185
|
-
this.#
|
|
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.#
|
|
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.
|
|
199
|
+
Effect.catchDefect((cause) => UnknownError.make({ cause })),
|
|
198
200
|
Effect.withSpan(`StoreRegistry.lookup:${mergedOptions.storeId}`),
|
|
199
|
-
|
|
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) =>
|
|
209
|
-
|
|
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
|
|
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
|
|
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,
|
|
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.
|
|
270
|
-
if (defect
|
|
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 (
|
|
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.
|
|
298
|
+
const fiber = defect.success.fiber as Fiber.Fiber<Store<TSchema, TContext>>
|
|
288
299
|
const promise = Fiber.join(fiber)
|
|
289
|
-
.pipe(
|
|
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
|
|
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,
|
|
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
|
|
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 `
|
|
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
|
|
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
|