@livestore/react 0.4.0-dev.17 → 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.
- package/dist/.tsbuildinfo +1 -1
- package/dist/LiveStoreProvider.d.ts +2 -2
- package/dist/LiveStoreProvider.d.ts.map +1 -1
- package/dist/LiveStoreProvider.js +2 -2
- package/dist/LiveStoreProvider.js.map +1 -1
- package/dist/LiveStoreProvider.test.js +1 -1
- package/dist/LiveStoreProvider.test.js.map +1 -1
- package/dist/__tests__/fixture.d.ts +6 -6
- package/dist/__tests__/fixture.d.ts.map +1 -1
- package/dist/__tests__/fixture.js.map +1 -1
- package/dist/experimental/multi-store/StoreRegistry.d.ts +11 -12
- package/dist/experimental/multi-store/StoreRegistry.d.ts.map +1 -1
- package/dist/experimental/multi-store/StoreRegistry.js +79 -43
- package/dist/experimental/multi-store/StoreRegistry.js.map +1 -1
- package/dist/experimental/multi-store/StoreRegistry.test.js +140 -49
- package/dist/experimental/multi-store/StoreRegistry.test.js.map +1 -1
- package/dist/experimental/multi-store/StoreRegistryContext.js +1 -1
- package/dist/experimental/multi-store/StoreRegistryContext.js.map +1 -1
- package/dist/experimental/multi-store/types.d.ts +6 -6
- package/dist/experimental/multi-store/types.d.ts.map +1 -1
- package/dist/useClientDocument.test.js +4 -1
- package/dist/useClientDocument.test.js.map +1 -1
- package/package.json +8 -8
- package/src/LiveStoreProvider.test.tsx +1 -1
- package/src/LiveStoreProvider.tsx +4 -4
- package/src/__snapshots__/useQuery.test.tsx.snap +12 -12
- package/src/__tests__/fixture.tsx +2 -2
- package/src/experimental/multi-store/StoreRegistry.test.ts +169 -49
- package/src/experimental/multi-store/StoreRegistry.ts +91 -47
- package/src/experimental/multi-store/StoreRegistryContext.tsx +1 -1
- package/src/experimental/multi-store/types.ts +6 -6
- package/src/useClientDocument.test.tsx +70 -70
|
@@ -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
|
|
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
|
-
|
|
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 {
|
|
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
|
|
77
|
+
it('disposes store after unusedCacheTime expires', async () => {
|
|
78
78
|
vi.useFakeTimers()
|
|
79
79
|
const registry = new StoreRegistry()
|
|
80
|
-
const
|
|
81
|
-
const options = testStoreOptions({
|
|
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
|
|
89
|
-
await vi.advanceTimersByTimeAsync(
|
|
88
|
+
// Advance time to trigger disposal
|
|
89
|
+
await vi.advanceTimersByTimeAsync(unusedCacheTime)
|
|
90
90
|
|
|
91
|
-
// After
|
|
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
|
|
99
|
+
// Clean up the second store (first one was disposed)
|
|
100
100
|
await nextStore.shutdownPromise()
|
|
101
101
|
})
|
|
102
102
|
|
|
103
|
-
it('keeps the longest
|
|
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({
|
|
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
|
|
113
|
-
await registry.getOrLoad(testStoreOptions({
|
|
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
|
|
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
|
|
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
|
|
150
|
+
it('does not dispose when unusedCacheTime is Infinity', async () => {
|
|
151
151
|
vi.useFakeTimers()
|
|
152
152
|
const registry = new StoreRegistry()
|
|
153
|
-
const options = testStoreOptions({
|
|
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
|
|
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
|
|
234
|
-
const options = testStoreOptions({
|
|
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
|
|
245
|
-
await vi.advanceTimersByTimeAsync(
|
|
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
|
|
312
|
+
it('cancels disposal when a new subscription is added', async () => {
|
|
313
313
|
vi.useFakeTimers()
|
|
314
314
|
const registry = new StoreRegistry()
|
|
315
|
-
const
|
|
316
|
-
const options = testStoreOptions({
|
|
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
|
|
321
|
-
await vi.advanceTimersByTimeAsync(
|
|
320
|
+
// Advance time almost to disposal threshold
|
|
321
|
+
await vi.advanceTimersByTimeAsync(unusedCacheTime - 5)
|
|
322
322
|
|
|
323
|
-
// Add a new subscription before
|
|
323
|
+
// Add a new subscription before disposal triggers
|
|
324
324
|
const unsubscribe = registry.subscribe(options.storeId, () => {})
|
|
325
325
|
|
|
326
|
-
// Complete the original
|
|
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(
|
|
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
|
|
343
|
+
it('schedules disposal if store becomes unused during loading', async () => {
|
|
344
344
|
vi.useFakeTimers()
|
|
345
345
|
const registry = new StoreRegistry()
|
|
346
|
-
const
|
|
347
|
-
const options = testStoreOptions({
|
|
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,
|
|
356
|
-
await vi.advanceTimersByTimeAsync(
|
|
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',
|
|
370
|
-
const options2 = testStoreOptions({ storeId: 'store-2',
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
|
428
|
-
await vi.advanceTimersByTimeAsync(
|
|
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
|
|
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
|
-
|
|
512
|
+
unusedCacheTime: 1000, // Default is long
|
|
442
513
|
},
|
|
443
514
|
})
|
|
444
515
|
|
|
445
516
|
const options = testStoreOptions({
|
|
446
|
-
|
|
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
|
|
601
|
+
it('schedules disposal after preload if no subscribers are added', async () => {
|
|
482
602
|
vi.useFakeTimers()
|
|
483
603
|
const registry = new StoreRegistry()
|
|
484
|
-
const
|
|
485
|
-
const options = testStoreOptions({
|
|
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
|
|
495
|
-
await vi.advanceTimersByTimeAsync(
|
|
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)
|