@livestore/react 0.4.0-dev.18 → 0.4.0-dev.19

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 (29) hide show
  1. package/dist/.tsbuildinfo +1 -1
  2. package/dist/LiveStoreProvider.d.ts +2 -2
  3. package/dist/LiveStoreProvider.d.ts.map +1 -1
  4. package/dist/LiveStoreProvider.js +2 -2
  5. package/dist/LiveStoreProvider.js.map +1 -1
  6. package/dist/LiveStoreProvider.test.js +1 -1
  7. package/dist/LiveStoreProvider.test.js.map +1 -1
  8. package/dist/__tests__/fixture.d.ts +6 -6
  9. package/dist/__tests__/fixture.d.ts.map +1 -1
  10. package/dist/__tests__/fixture.js.map +1 -1
  11. package/dist/experimental/multi-store/StoreRegistry.d.ts +11 -12
  12. package/dist/experimental/multi-store/StoreRegistry.d.ts.map +1 -1
  13. package/dist/experimental/multi-store/StoreRegistry.js +79 -43
  14. package/dist/experimental/multi-store/StoreRegistry.js.map +1 -1
  15. package/dist/experimental/multi-store/StoreRegistry.test.js +140 -49
  16. package/dist/experimental/multi-store/StoreRegistry.test.js.map +1 -1
  17. package/dist/experimental/multi-store/StoreRegistryContext.js +1 -1
  18. package/dist/experimental/multi-store/StoreRegistryContext.js.map +1 -1
  19. package/dist/experimental/multi-store/types.d.ts +6 -6
  20. package/dist/experimental/multi-store/types.d.ts.map +1 -1
  21. package/package.json +8 -8
  22. package/src/LiveStoreProvider.test.tsx +1 -1
  23. package/src/LiveStoreProvider.tsx +4 -4
  24. package/src/__snapshots__/useQuery.test.tsx.snap +12 -12
  25. package/src/__tests__/fixture.tsx +2 -2
  26. package/src/experimental/multi-store/StoreRegistry.test.ts +169 -49
  27. package/src/experimental/multi-store/StoreRegistry.ts +91 -47
  28. package/src/experimental/multi-store/StoreRegistryContext.tsx +1 -1
  29. package/src/experimental/multi-store/types.ts +6 -6
@@ -101,7 +101,7 @@ exports[`useQuery (strictMode={ strictMode: false }) > filtered dependency query
101
101
  },
102
102
  "previousResult": {
103
103
  "_tag": "Some",
104
- "value": "{"query":"SELECT * FROM 'todos' WHERE id = ?","bindValues":["t1"],"queriedTables":{}}",
104
+ "value": "{"query":"SELECT * FROM 'todos' WHERE \\"id\\" = ?","bindValues":["t1"],"queriedTables":{}}",
105
105
  },
106
106
  "recomputations": 1,
107
107
  "sub": [
@@ -158,7 +158,7 @@ exports[`useQuery (strictMode={ strictMode: false }) > filtered dependency query
158
158
  "id": "node-9",
159
159
  "invocations": 1,
160
160
  "isDestroyed": false,
161
- "label": "subscribe:SELECT * FROM 'todos' WHERE id = ?",
161
+ "label": "subscribe:SELECT * FROM 'todos' WHERE "id" = ?",
162
162
  "sub": [
163
163
  "node-7",
164
164
  ],
@@ -268,7 +268,7 @@ exports[`useQuery (strictMode={ strictMode: false }) > filtered dependency query
268
268
  },
269
269
  "previousResult": {
270
270
  "_tag": "Some",
271
- "value": "{"query":"SELECT * FROM 'todos' WHERE id = ?","bindValues":["t1"],"queriedTables":{}}",
271
+ "value": "{"query":"SELECT * FROM 'todos' WHERE \\"id\\" = ?","bindValues":["t1"],"queriedTables":{}}",
272
272
  },
273
273
  "recomputations": 1,
274
274
  "sub": [
@@ -325,7 +325,7 @@ exports[`useQuery (strictMode={ strictMode: false }) > filtered dependency query
325
325
  "id": "node-9",
326
326
  "invocations": 2,
327
327
  "isDestroyed": false,
328
- "label": "subscribe:SELECT * FROM 'todos' WHERE id = ?",
328
+ "label": "subscribe:SELECT * FROM 'todos' WHERE "id" = ?",
329
329
  "sub": [
330
330
  "node-7",
331
331
  ],
@@ -435,7 +435,7 @@ exports[`useQuery (strictMode={ strictMode: false }) > filtered dependency query
435
435
  },
436
436
  "previousResult": {
437
437
  "_tag": "Some",
438
- "value": "{"query":"SELECT * FROM 'todos' WHERE id = ?","bindValues":["t2"],"queriedTables":{}}",
438
+ "value": "{"query":"SELECT * FROM 'todos' WHERE \\"id\\" = ?","bindValues":["t2"],"queriedTables":{}}",
439
439
  },
440
440
  "recomputations": 2,
441
441
  "sub": [
@@ -492,7 +492,7 @@ exports[`useQuery (strictMode={ strictMode: false }) > filtered dependency query
492
492
  "id": "node-9",
493
493
  "invocations": 3,
494
494
  "isDestroyed": false,
495
- "label": "subscribe:SELECT * FROM 'todos' WHERE id = ?",
495
+ "label": "subscribe:SELECT * FROM 'todos' WHERE "id" = ?",
496
496
  "sub": [
497
497
  "node-7",
498
498
  ],
@@ -1242,7 +1242,7 @@ exports[`useQuery (strictMode={ strictMode: true }) > filtered dependency query
1242
1242
  },
1243
1243
  "previousResult": {
1244
1244
  "_tag": "Some",
1245
- "value": "{"query":"SELECT * FROM 'todos' WHERE id = ?","bindValues":["t1"],"queriedTables":{}}",
1245
+ "value": "{"query":"SELECT * FROM 'todos' WHERE \\"id\\" = ?","bindValues":["t1"],"queriedTables":{}}",
1246
1246
  },
1247
1247
  "recomputations": 1,
1248
1248
  "sub": [
@@ -1299,7 +1299,7 @@ exports[`useQuery (strictMode={ strictMode: true }) > filtered dependency query
1299
1299
  "id": "node-9",
1300
1300
  "invocations": 1,
1301
1301
  "isDestroyed": false,
1302
- "label": "subscribe:SELECT * FROM 'todos' WHERE id = ?",
1302
+ "label": "subscribe:SELECT * FROM 'todos' WHERE "id" = ?",
1303
1303
  "sub": [
1304
1304
  "node-7",
1305
1305
  ],
@@ -1409,7 +1409,7 @@ exports[`useQuery (strictMode={ strictMode: true }) > filtered dependency query
1409
1409
  },
1410
1410
  "previousResult": {
1411
1411
  "_tag": "Some",
1412
- "value": "{"query":"SELECT * FROM 'todos' WHERE id = ?","bindValues":["t1"],"queriedTables":{}}",
1412
+ "value": "{"query":"SELECT * FROM 'todos' WHERE \\"id\\" = ?","bindValues":["t1"],"queriedTables":{}}",
1413
1413
  },
1414
1414
  "recomputations": 1,
1415
1415
  "sub": [
@@ -1466,7 +1466,7 @@ exports[`useQuery (strictMode={ strictMode: true }) > filtered dependency query
1466
1466
  "id": "node-9",
1467
1467
  "invocations": 2,
1468
1468
  "isDestroyed": false,
1469
- "label": "subscribe:SELECT * FROM 'todos' WHERE id = ?",
1469
+ "label": "subscribe:SELECT * FROM 'todos' WHERE "id" = ?",
1470
1470
  "sub": [
1471
1471
  "node-7",
1472
1472
  ],
@@ -1576,7 +1576,7 @@ exports[`useQuery (strictMode={ strictMode: true }) > filtered dependency query
1576
1576
  },
1577
1577
  "previousResult": {
1578
1578
  "_tag": "Some",
1579
- "value": "{"query":"SELECT * FROM 'todos' WHERE id = ?","bindValues":["t2"],"queriedTables":{}}",
1579
+ "value": "{"query":"SELECT * FROM 'todos' WHERE \\"id\\" = ?","bindValues":["t2"],"queriedTables":{}}",
1580
1580
  },
1581
1581
  "recomputations": 2,
1582
1582
  "sub": [
@@ -1633,7 +1633,7 @@ exports[`useQuery (strictMode={ strictMode: true }) > filtered dependency query
1633
1633
  "id": "node-9",
1634
1634
  "invocations": 3,
1635
1635
  "isDestroyed": false,
1636
- "label": "subscribe:SELECT * FROM 'todos' WHERE id = ?",
1636
+ "label": "subscribe:SELECT * FROM 'todos' WHERE "id" = ?",
1637
1637
  "sub": [
1638
1638
  "node-7",
1639
1639
  ],
@@ -1,5 +1,5 @@
1
1
  import { makeInMemoryAdapter } from '@livestore/adapter-web'
2
- import { provideOtel, type UnexpectedError } from '@livestore/common'
2
+ import { provideOtel, type UnknownError } from '@livestore/common'
3
3
  import { Events, makeSchema, State } from '@livestore/common/schema'
4
4
  import type { LiveStoreSchema, SqliteDsl, Store } from '@livestore/livestore'
5
5
  import { createStore } from '@livestore/livestore'
@@ -107,7 +107,7 @@ export const makeTodoMvcReact: (opts?: MakeTodoMvcReactOptions) => Effect.Effect
107
107
  store: Store<LiveStoreSchema<SqliteDsl.DbSchema, State.SQLite.EventDefRecord>, {}> & LiveStoreReact.ReactApi
108
108
  renderCount: { readonly val: number; inc: () => void }
109
109
  },
110
- UnexpectedError,
110
+ UnknownError,
111
111
  Scope.Scope
112
112
  > = (opts: MakeTodoMvcReactOptions = {}) =>
113
113
  Effect.gen(function* () {
@@ -2,7 +2,7 @@ import { makeInMemoryAdapter } from '@livestore/adapter-web'
2
2
  import { StoreInternalsSymbol } from '@livestore/livestore'
3
3
  import { afterEach, describe, expect, it, vi } from 'vitest'
4
4
  import { schema } from '../../__tests__/fixture.tsx'
5
- import { DEFAULT_GC_TIME, StoreRegistry } from './StoreRegistry.ts'
5
+ import { DEFAULT_UNUSED_CACHE_TIME, StoreRegistry } from './StoreRegistry.ts'
6
6
  import { storeOptions } from './storeOptions.ts'
7
7
  import type { CachedStoreOptions } from './types.ts'
8
8
 
@@ -74,21 +74,21 @@ describe('StoreRegistry', () => {
74
74
  expect(() => registry.getOrLoad(badOptions)).toThrow()
75
75
  })
76
76
 
77
- it('disposes store after gc timeout expires', async () => {
77
+ it('disposes store after unusedCacheTime expires', async () => {
78
78
  vi.useFakeTimers()
79
79
  const registry = new StoreRegistry()
80
- const gcTime = 25
81
- const options = testStoreOptions({ gcTime })
80
+ const unusedCacheTime = 25
81
+ const options = testStoreOptions({ unusedCacheTime })
82
82
 
83
83
  const store = await registry.getOrLoad(options)
84
84
 
85
85
  // Store should be cached
86
86
  expect(registry.getOrLoad(options)).toBe(store)
87
87
 
88
- // Advance time to trigger GC
89
- await vi.advanceTimersByTimeAsync(gcTime)
88
+ // Advance time to trigger disposal
89
+ await vi.advanceTimersByTimeAsync(unusedCacheTime)
90
90
 
91
- // After GC, store should be disposed
91
+ // After disposal, store should be removed
92
92
  // The store is removed from cache, so next getOrLoad creates a new one
93
93
  const nextStore = await registry.getOrLoad(options)
94
94
 
@@ -96,25 +96,25 @@ describe('StoreRegistry', () => {
96
96
  expect(nextStore).not.toBe(store)
97
97
  expect(nextStore[StoreInternalsSymbol].clientSession.debugInstanceId).toBeDefined()
98
98
 
99
- // Clean up the second store (first one was cleaned up by GC)
99
+ // Clean up the second store (first one was disposed)
100
100
  await nextStore.shutdownPromise()
101
101
  })
102
102
 
103
- it('keeps the longest gcTime seen for a store when options vary across calls', async () => {
103
+ it('keeps the longest unusedCacheTime seen for a store when options vary across calls', async () => {
104
104
  vi.useFakeTimers()
105
105
  const registry = new StoreRegistry()
106
106
 
107
- const options = testStoreOptions({ gcTime: 10 })
107
+ const options = testStoreOptions({ unusedCacheTime: 10 })
108
108
  const unsubscribe = registry.subscribe(options.storeId, () => {})
109
109
 
110
110
  const store = await registry.getOrLoad(options)
111
111
 
112
- // Call with longer gcTime
113
- await registry.getOrLoad(testStoreOptions({ gcTime: 100 }))
112
+ // Call with longer unusedCacheTime
113
+ await registry.getOrLoad(testStoreOptions({ unusedCacheTime: 100 }))
114
114
 
115
115
  unsubscribe()
116
116
 
117
- // After 99ms, store should still be alive (100ms gcTime used)
117
+ // After 99ms, store should still be alive (100ms unusedCacheTime used)
118
118
  await vi.advanceTimersByTimeAsync(99)
119
119
 
120
120
  // Store should still be cached
@@ -127,7 +127,7 @@ describe('StoreRegistry', () => {
127
127
  const nextStore = await registry.getOrLoad(options)
128
128
  expect(nextStore).not.toBe(store)
129
129
 
130
- // Clean up the second store (first one was cleaned up by GC)
130
+ // Clean up the second store (first one was disposed)
131
131
  await nextStore.shutdownPromise()
132
132
  })
133
133
 
@@ -147,10 +147,10 @@ describe('StoreRegistry', () => {
147
147
  expect(() => registry.getOrLoad(badOptions)).toThrow()
148
148
  })
149
149
 
150
- it('does not garbage collect when gcTime is Infinity', async () => {
150
+ it('does not dispose when unusedCacheTime is Infinity', async () => {
151
151
  vi.useFakeTimers()
152
152
  const registry = new StoreRegistry()
153
- const options = testStoreOptions({ gcTime: Number.POSITIVE_INFINITY })
153
+ const options = testStoreOptions({ unusedCacheTime: Number.POSITIVE_INFINITY })
154
154
 
155
155
  const store = await registry.getOrLoad(options)
156
156
 
@@ -160,7 +160,7 @@ describe('StoreRegistry', () => {
160
160
  // Advance time by a very long duration
161
161
  await vi.advanceTimersByTimeAsync(1000000)
162
162
 
163
- // Store should still be cached (not garbage collected)
163
+ // Store should still be cached (not disposed)
164
164
  expect(registry.getOrLoad(options)).toBe(store)
165
165
 
166
166
  // Clean up manually
@@ -230,8 +230,8 @@ describe('StoreRegistry', () => {
230
230
  it('handles rapid subscribe/unsubscribe cycles without errors', async () => {
231
231
  vi.useFakeTimers()
232
232
  const registry = new StoreRegistry()
233
- const gcTime = 50
234
- const options = testStoreOptions({ gcTime })
233
+ const unusedCacheTime = 50
234
+ const options = testStoreOptions({ unusedCacheTime })
235
235
 
236
236
  const store = await registry.getOrLoad(options)
237
237
 
@@ -241,8 +241,8 @@ describe('StoreRegistry', () => {
241
241
  unsubscribe()
242
242
  }
243
243
 
244
- // Advance time to check if GC is scheduled correctly
245
- await vi.advanceTimersByTimeAsync(gcTime)
244
+ // Advance time to check if disposal is scheduled correctly
245
+ await vi.advanceTimersByTimeAsync(unusedCacheTime)
246
246
 
247
247
  // Store should be disposed after the last unsubscribe
248
248
  const nextStore = await registry.getOrLoad(options)
@@ -309,21 +309,21 @@ describe('StoreRegistry', () => {
309
309
  await store.shutdownPromise()
310
310
  })
311
311
 
312
- it('cancels GC when a new subscription is added', async () => {
312
+ it('cancels disposal when a new subscription is added', async () => {
313
313
  vi.useFakeTimers()
314
314
  const registry = new StoreRegistry()
315
- const gcTime = 50
316
- const options = testStoreOptions({ gcTime })
315
+ const unusedCacheTime = 50
316
+ const options = testStoreOptions({ unusedCacheTime })
317
317
 
318
318
  const store = await registry.getOrLoad(options)
319
319
 
320
- // Advance time almost to GC threshold
321
- await vi.advanceTimersByTimeAsync(gcTime - 5)
320
+ // Advance time almost to disposal threshold
321
+ await vi.advanceTimersByTimeAsync(unusedCacheTime - 5)
322
322
 
323
- // Add a new subscription before GC triggers
323
+ // Add a new subscription before disposal triggers
324
324
  const unsubscribe = registry.subscribe(options.storeId, () => {})
325
325
 
326
- // Complete the original GC time
326
+ // Complete the original unusedCacheTime
327
327
  await vi.advanceTimersByTimeAsync(5)
328
328
 
329
329
  // Store should not have been disposed because we added a subscription
@@ -331,7 +331,7 @@ describe('StoreRegistry', () => {
331
331
 
332
332
  // Clean up
333
333
  unsubscribe()
334
- await vi.advanceTimersByTimeAsync(gcTime)
334
+ await vi.advanceTimersByTimeAsync(unusedCacheTime)
335
335
 
336
336
  // Now it should be disposed
337
337
  const nextStore = await registry.getOrLoad(options)
@@ -340,11 +340,11 @@ describe('StoreRegistry', () => {
340
340
  await nextStore.shutdownPromise()
341
341
  })
342
342
 
343
- it('schedules GC if store becomes inactive during loading', async () => {
343
+ it('schedules disposal if store becomes unused during loading', async () => {
344
344
  vi.useFakeTimers()
345
345
  const registry = new StoreRegistry()
346
- const gcTime = 50
347
- const options = testStoreOptions({ gcTime })
346
+ const unusedCacheTime = 50
347
+ const options = testStoreOptions({ unusedCacheTime })
348
348
 
349
349
  // Start loading without any subscription
350
350
  const storePromise = registry.getOrLoad(options)
@@ -352,8 +352,8 @@ describe('StoreRegistry', () => {
352
352
  // Wait for store to load (no subscribers registered)
353
353
  const store = await storePromise
354
354
 
355
- // Since there were no subscribers when loading completed, GC should be scheduled
356
- await vi.advanceTimersByTimeAsync(gcTime)
355
+ // Since there were no subscribers when loading completed, disposal should be scheduled
356
+ await vi.advanceTimersByTimeAsync(unusedCacheTime)
357
357
 
358
358
  // Store should be disposed
359
359
  const nextStore = await registry.getOrLoad(options)
@@ -362,12 +362,83 @@ describe('StoreRegistry', () => {
362
362
  await nextStore.shutdownPromise()
363
363
  })
364
364
 
365
+ it('aborts loading when disposal fires while store is still loading', async () => {
366
+ vi.useFakeTimers()
367
+ const registry = new StoreRegistry()
368
+ const unusedCacheTime = 10
369
+ const options = testStoreOptions({ unusedCacheTime })
370
+
371
+ // Subscribe briefly to trigger getOrLoad and then unsubscribe
372
+ const unsubscribe = registry.subscribe(options.storeId, () => {})
373
+
374
+ // Start loading - this will be slow due to fake timers
375
+ const loadPromise = registry.getOrLoad(options)
376
+
377
+ // Attach a catch handler to prevent unhandled rejection when the load is aborted
378
+ const abortedPromise = (loadPromise as Promise<unknown>).catch(() => {
379
+ // Expected: load was aborted by disposal
380
+ })
381
+
382
+ // Unsubscribe immediately, which schedules disposal
383
+ unsubscribe()
384
+
385
+ // Advance time to trigger disposal while still loading
386
+ await vi.advanceTimersByTimeAsync(unusedCacheTime)
387
+
388
+ // Wait for the abort to complete
389
+ await abortedPromise
390
+
391
+ // After abort, a new getOrLoad should start a fresh load
392
+ const freshLoadPromise = registry.getOrLoad(options)
393
+
394
+ // This should be a new promise (not the aborted one)
395
+ expect(freshLoadPromise).toBeInstanceOf(Promise)
396
+ expect(freshLoadPromise).not.toBe(loadPromise)
397
+
398
+ // Wait for fresh load to complete
399
+ const store = await freshLoadPromise
400
+ expect(store).toBeDefined()
401
+
402
+ await store.shutdownPromise()
403
+ })
404
+
405
+ it('does not abort loading when new subscription arrives before disposal fires', async () => {
406
+ vi.useFakeTimers()
407
+ const registry = new StoreRegistry()
408
+ const unusedCacheTime = 50
409
+ const options = testStoreOptions({ unusedCacheTime })
410
+
411
+ // Start loading and immediately unsubscribe to schedule disposal
412
+ const unsub1 = registry.subscribe(options.storeId, () => {})
413
+ const loadPromise = registry.getOrLoad(options)
414
+ unsub1()
415
+
416
+ // Advance time partially (before disposal fires)
417
+ await vi.advanceTimersByTimeAsync(unusedCacheTime - 10)
418
+
419
+ // Add a new subscription - this should cancel the pending disposal
420
+ const unsub2 = registry.subscribe(options.storeId, () => {})
421
+
422
+ // Advance past the original unusedCacheTime
423
+ await vi.advanceTimersByTimeAsync(20)
424
+
425
+ // The load should complete normally (not be aborted)
426
+ const store = await loadPromise
427
+
428
+ // And should be the same instance when retrieved again
429
+ const cachedStore = registry.getOrLoad(options)
430
+ expect(cachedStore).toBe(store)
431
+
432
+ unsub2()
433
+ await store.shutdownPromise()
434
+ })
435
+
365
436
  it('manages multiple stores with different IDs independently', async () => {
366
437
  vi.useFakeTimers()
367
438
  const registry = new StoreRegistry()
368
439
 
369
- const options1 = testStoreOptions({ storeId: 'store-1', gcTime: 50 })
370
- const options2 = testStoreOptions({ storeId: 'store-2', gcTime: 100 })
440
+ const options1 = testStoreOptions({ storeId: 'store-1', unusedCacheTime: 50 })
441
+ const options2 = testStoreOptions({ storeId: 'store-2', unusedCacheTime: 100 })
371
442
 
372
443
  const store1 = await registry.getOrLoad(options1)
373
444
  const store2 = await registry.getOrLoad(options2)
@@ -387,7 +458,7 @@ describe('StoreRegistry', () => {
387
458
  expect(newStore1).not.toBe(store1)
388
459
  expect(registry.getOrLoad(options2)).toBe(store2)
389
460
 
390
- // Subscribe to prevent GC of newStore1
461
+ // Subscribe to prevent disposal of newStore1
391
462
  const unsub1 = registry.subscribe(options1.storeId, () => {})
392
463
 
393
464
  // Advance remaining time to dispose store2
@@ -397,7 +468,7 @@ describe('StoreRegistry', () => {
397
468
  const newStore2 = await registry.getOrLoad(options2)
398
469
  expect(newStore2).not.toBe(store2)
399
470
 
400
- // Subscribe to prevent GC of newStore2
471
+ // Subscribe to prevent disposal of newStore2
401
472
  const unsub2 = registry.subscribe(options2.storeId, () => {})
402
473
 
403
474
  // Clean up
@@ -412,7 +483,7 @@ describe('StoreRegistry', () => {
412
483
 
413
484
  const registry = new StoreRegistry({
414
485
  defaultOptions: {
415
- gcTime: DEFAULT_GC_TIME * 2,
486
+ unusedCacheTime: DEFAULT_UNUSED_CACHE_TIME * 2,
416
487
  },
417
488
  })
418
489
 
@@ -424,10 +495,10 @@ describe('StoreRegistry', () => {
424
495
  expect(store).toBeDefined()
425
496
  expect(store[StoreInternalsSymbol].clientSession.debugInstanceId).toBeDefined()
426
497
 
427
- // Verify configured default gcTime is applied by checking GC doesn't happen at library's default gc time
428
- await vi.advanceTimersByTimeAsync(DEFAULT_GC_TIME)
498
+ // Verify configured default unusedCacheTime is applied by checking disposal doesn't happen at library's default time
499
+ await vi.advanceTimersByTimeAsync(DEFAULT_UNUSED_CACHE_TIME)
429
500
 
430
- // Store should still be cached after default gc time
501
+ // Store should still be cached after default unusedCacheTime
431
502
  expect(registry.getOrLoad(options)).toBe(store)
432
503
 
433
504
  await store.shutdownPromise()
@@ -438,12 +509,12 @@ describe('StoreRegistry', () => {
438
509
 
439
510
  const registry = new StoreRegistry({
440
511
  defaultOptions: {
441
- gcTime: 1000, // Default is long
512
+ unusedCacheTime: 1000, // Default is long
442
513
  },
443
514
  })
444
515
 
445
516
  const options = testStoreOptions({
446
- gcTime: 10, // Override with shorter time
517
+ unusedCacheTime: 10, // Override with shorter time
447
518
  })
448
519
 
449
520
  const store = await registry.getOrLoad(options)
@@ -458,6 +529,55 @@ describe('StoreRegistry', () => {
458
529
  await nextStore.shutdownPromise()
459
530
  })
460
531
 
532
+ it('prevents subscriptions to stores that are shutting down', async () => {
533
+ vi.useFakeTimers()
534
+ const registry = new StoreRegistry()
535
+ const unusedCacheTime = 10
536
+ const options = testStoreOptions({ unusedCacheTime })
537
+
538
+ // Load the store and wait for it to be ready
539
+ const originalStore = await registry.getOrLoad(options)
540
+
541
+ // Verify store is cached
542
+ expect(registry.getOrLoad(options)).toBe(originalStore)
543
+
544
+ // Spy on shutdownPromise to detect when shutdown starts
545
+ let shutdownStarted = false
546
+ let shutdownCompleted = false
547
+ const originalShutdownPromise = originalStore.shutdownPromise.bind(originalStore)
548
+ originalStore.shutdownPromise = () => {
549
+ shutdownStarted = true
550
+ return originalShutdownPromise().finally(() => {
551
+ shutdownCompleted = true
552
+ })
553
+ }
554
+
555
+ // Use vi.advanceTimersToNextTimer to advance ONLY to the disposal timer firing,
556
+ // then immediately (before microtasks resolve) try to get the store
557
+ vi.advanceTimersToNextTimer()
558
+
559
+ // The disposal callback has now executed synchronously, which means:
560
+ // 1. Subscriber check passed (no subscribers)
561
+ // 2. shutdown() was called (but it's async, hasn't resolved yet)
562
+ // 3. Cache entry SHOULD have been removed
563
+
564
+ // Verify shutdown was initiated
565
+ expect(shutdownStarted).toBe(true)
566
+ // Shutdown is async, so it shouldn't have completed yet in the same tick
567
+ expect(shutdownCompleted).toBe(false)
568
+
569
+ const storeOrPromise = registry.getOrLoad(options)
570
+
571
+ if (!(storeOrPromise instanceof Promise)) {
572
+ expect.fail('getOrLoad returned dying store synchronously instead of starting fresh load')
573
+ }
574
+
575
+ const freshStore = await storeOrPromise
576
+ // A fresh load was triggered because cache was cleared
577
+ expect(freshStore).not.toBe(originalStore)
578
+ await freshStore.shutdownPromise()
579
+ })
580
+
461
581
  it('warms the cache so subsequent getOrLoad is synchronous after preload', async () => {
462
582
  const registry = new StoreRegistry()
463
583
  const options = testStoreOptions()
@@ -478,11 +598,11 @@ describe('StoreRegistry', () => {
478
598
  await store.shutdownPromise()
479
599
  })
480
600
 
481
- it('schedules GC after preload if no subscribers are added', async () => {
601
+ it('schedules disposal after preload if no subscribers are added', async () => {
482
602
  vi.useFakeTimers()
483
603
  const registry = new StoreRegistry()
484
- const gcTime = 50
485
- const options = testStoreOptions({ gcTime })
604
+ const unusedCacheTime = 50
605
+ const options = testStoreOptions({ unusedCacheTime })
486
606
 
487
607
  // Preload without subscribing
488
608
  await registry.preload(options)
@@ -491,8 +611,8 @@ describe('StoreRegistry', () => {
491
611
  const store = registry.getOrLoad(options)
492
612
  expect(store).not.toBeInstanceOf(Promise)
493
613
 
494
- // Advance time to trigger GC
495
- await vi.advanceTimersByTimeAsync(gcTime)
614
+ // Advance time to trigger disposal
615
+ await vi.advanceTimersByTimeAsync(unusedCacheTime)
496
616
 
497
617
  // Store should be disposed since no subscribers were added
498
618
  const nextStore = await registry.getOrLoad(options)