@livestore/react 0.4.0-dev.16 → 0.4.0-dev.17
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.test.js +2 -2
- package/dist/LiveStoreProvider.test.js.map +1 -1
- package/dist/experimental/multi-store/StoreRegistry.d.ts.map +1 -1
- package/dist/experimental/multi-store/StoreRegistry.js +58 -58
- package/dist/experimental/multi-store/StoreRegistry.js.map +1 -1
- package/dist/experimental/multi-store/StoreRegistry.test.d.ts +2 -0
- package/dist/experimental/multi-store/StoreRegistry.test.d.ts.map +1 -0
- package/dist/experimental/multi-store/StoreRegistry.test.js +373 -0
- package/dist/experimental/multi-store/StoreRegistry.test.js.map +1 -0
- package/dist/experimental/multi-store/useStore.d.ts.map +1 -1
- package/dist/experimental/multi-store/useStore.js +7 -3
- package/dist/experimental/multi-store/useStore.js.map +1 -1
- package/dist/experimental/multi-store/useStore.test.d.ts +2 -0
- package/dist/experimental/multi-store/useStore.test.d.ts.map +1 -0
- package/dist/experimental/multi-store/useStore.test.js +144 -0
- package/dist/experimental/multi-store/useStore.test.js.map +1 -0
- package/dist/useClientDocument.js +1 -1
- package/dist/useClientDocument.js.map +1 -1
- package/dist/useClientDocument.test.js +3 -2
- package/dist/useClientDocument.test.js.map +1 -1
- package/dist/useQuery.d.ts.map +1 -1
- package/dist/useQuery.js +3 -3
- package/dist/useQuery.js.map +1 -1
- package/dist/useQuery.test.js +9 -9
- package/dist/useQuery.test.js.map +1 -1
- package/package.json +6 -6
- package/src/LiveStoreProvider.test.tsx +2 -2
- package/src/experimental/multi-store/StoreRegistry.test.ts +511 -0
- package/src/experimental/multi-store/StoreRegistry.ts +63 -64
- package/src/experimental/multi-store/useStore.test.tsx +197 -0
- package/src/experimental/multi-store/useStore.ts +7 -3
- package/src/useClientDocument.test.tsx +3 -2
- package/src/useClientDocument.ts +1 -1
- package/src/useQuery.test.tsx +15 -9
- package/src/useQuery.ts +4 -3
|
@@ -0,0 +1,511 @@
|
|
|
1
|
+
import { makeInMemoryAdapter } from '@livestore/adapter-web'
|
|
2
|
+
import { StoreInternalsSymbol } from '@livestore/livestore'
|
|
3
|
+
import { afterEach, describe, expect, it, vi } from 'vitest'
|
|
4
|
+
import { schema } from '../../__tests__/fixture.tsx'
|
|
5
|
+
import { DEFAULT_GC_TIME, StoreRegistry } from './StoreRegistry.ts'
|
|
6
|
+
import { storeOptions } from './storeOptions.ts'
|
|
7
|
+
import type { CachedStoreOptions } from './types.ts'
|
|
8
|
+
|
|
9
|
+
describe('StoreRegistry', () => {
|
|
10
|
+
afterEach(() => {
|
|
11
|
+
vi.clearAllTimers()
|
|
12
|
+
vi.useRealTimers()
|
|
13
|
+
})
|
|
14
|
+
|
|
15
|
+
it('returns a Promise when the store is loading', async () => {
|
|
16
|
+
const registry = new StoreRegistry()
|
|
17
|
+
const result = registry.getOrLoad(testStoreOptions())
|
|
18
|
+
|
|
19
|
+
expect(result).toBeInstanceOf(Promise)
|
|
20
|
+
|
|
21
|
+
// Clean up
|
|
22
|
+
const store = await result
|
|
23
|
+
await store.shutdownPromise()
|
|
24
|
+
})
|
|
25
|
+
|
|
26
|
+
it('returns cached store synchronously after first load resolves', async () => {
|
|
27
|
+
const registry = new StoreRegistry()
|
|
28
|
+
|
|
29
|
+
const initial = registry.getOrLoad(testStoreOptions())
|
|
30
|
+
expect(initial).toBeInstanceOf(Promise)
|
|
31
|
+
|
|
32
|
+
const store = await initial
|
|
33
|
+
|
|
34
|
+
const cached = registry.getOrLoad(testStoreOptions())
|
|
35
|
+
expect(cached).toBe(store)
|
|
36
|
+
expect(cached).not.toBeInstanceOf(Promise)
|
|
37
|
+
|
|
38
|
+
// Clean up
|
|
39
|
+
await store.shutdownPromise()
|
|
40
|
+
})
|
|
41
|
+
|
|
42
|
+
it('reuses the same promise for concurrent getOrLoad calls while loading', async () => {
|
|
43
|
+
const registry = new StoreRegistry()
|
|
44
|
+
const options = testStoreOptions()
|
|
45
|
+
|
|
46
|
+
const first = registry.getOrLoad(options)
|
|
47
|
+
const second = registry.getOrLoad(options)
|
|
48
|
+
|
|
49
|
+
// Both should be the same promise
|
|
50
|
+
expect(first).toBe(second)
|
|
51
|
+
expect(first).toBeInstanceOf(Promise)
|
|
52
|
+
|
|
53
|
+
const store = await first
|
|
54
|
+
|
|
55
|
+
// Both promises should resolve to the same store
|
|
56
|
+
expect(await second).toBe(store)
|
|
57
|
+
|
|
58
|
+
// Clean up
|
|
59
|
+
await store.shutdownPromise()
|
|
60
|
+
})
|
|
61
|
+
|
|
62
|
+
it('stores and rethrows the rejection on subsequent getOrLoad calls after a failure', async () => {
|
|
63
|
+
const registry = new StoreRegistry()
|
|
64
|
+
|
|
65
|
+
// Create an invalid adapter that will cause an error
|
|
66
|
+
const badOptions = testStoreOptions({
|
|
67
|
+
// @ts-expect-error - intentionally passing invalid adapter to trigger error
|
|
68
|
+
adapter: null,
|
|
69
|
+
})
|
|
70
|
+
|
|
71
|
+
await expect(registry.getOrLoad(badOptions)).rejects.toThrow()
|
|
72
|
+
|
|
73
|
+
// Subsequent call should throw the cached error synchronously
|
|
74
|
+
expect(() => registry.getOrLoad(badOptions)).toThrow()
|
|
75
|
+
})
|
|
76
|
+
|
|
77
|
+
it('disposes store after gc timeout expires', async () => {
|
|
78
|
+
vi.useFakeTimers()
|
|
79
|
+
const registry = new StoreRegistry()
|
|
80
|
+
const gcTime = 25
|
|
81
|
+
const options = testStoreOptions({ gcTime })
|
|
82
|
+
|
|
83
|
+
const store = await registry.getOrLoad(options)
|
|
84
|
+
|
|
85
|
+
// Store should be cached
|
|
86
|
+
expect(registry.getOrLoad(options)).toBe(store)
|
|
87
|
+
|
|
88
|
+
// Advance time to trigger GC
|
|
89
|
+
await vi.advanceTimersByTimeAsync(gcTime)
|
|
90
|
+
|
|
91
|
+
// After GC, store should be disposed
|
|
92
|
+
// The store is removed from cache, so next getOrLoad creates a new one
|
|
93
|
+
const nextStore = await registry.getOrLoad(options)
|
|
94
|
+
|
|
95
|
+
// Should be a different store instance
|
|
96
|
+
expect(nextStore).not.toBe(store)
|
|
97
|
+
expect(nextStore[StoreInternalsSymbol].clientSession.debugInstanceId).toBeDefined()
|
|
98
|
+
|
|
99
|
+
// Clean up the second store (first one was cleaned up by GC)
|
|
100
|
+
await nextStore.shutdownPromise()
|
|
101
|
+
})
|
|
102
|
+
|
|
103
|
+
it('keeps the longest gcTime seen for a store when options vary across calls', async () => {
|
|
104
|
+
vi.useFakeTimers()
|
|
105
|
+
const registry = new StoreRegistry()
|
|
106
|
+
|
|
107
|
+
const options = testStoreOptions({ gcTime: 10 })
|
|
108
|
+
const unsubscribe = registry.subscribe(options.storeId, () => {})
|
|
109
|
+
|
|
110
|
+
const store = await registry.getOrLoad(options)
|
|
111
|
+
|
|
112
|
+
// Call with longer gcTime
|
|
113
|
+
await registry.getOrLoad(testStoreOptions({ gcTime: 100 }))
|
|
114
|
+
|
|
115
|
+
unsubscribe()
|
|
116
|
+
|
|
117
|
+
// After 99ms, store should still be alive (100ms gcTime used)
|
|
118
|
+
await vi.advanceTimersByTimeAsync(99)
|
|
119
|
+
|
|
120
|
+
// Store should still be cached
|
|
121
|
+
expect(registry.getOrLoad(options)).toBe(store)
|
|
122
|
+
|
|
123
|
+
// After the full 100ms, store should be disposed
|
|
124
|
+
await vi.advanceTimersByTimeAsync(1)
|
|
125
|
+
|
|
126
|
+
// Next getOrLoad should create a new store
|
|
127
|
+
const nextStore = await registry.getOrLoad(options)
|
|
128
|
+
expect(nextStore).not.toBe(store)
|
|
129
|
+
|
|
130
|
+
// Clean up the second store (first one was cleaned up by GC)
|
|
131
|
+
await nextStore.shutdownPromise()
|
|
132
|
+
})
|
|
133
|
+
|
|
134
|
+
it('preload does not throw', async () => {
|
|
135
|
+
const registry = new StoreRegistry()
|
|
136
|
+
|
|
137
|
+
// Create invalid options that would cause an error
|
|
138
|
+
const badOptions = testStoreOptions({
|
|
139
|
+
// @ts-expect-error - intentionally passing invalid adapter to trigger error
|
|
140
|
+
adapter: null,
|
|
141
|
+
})
|
|
142
|
+
|
|
143
|
+
// preload should not throw
|
|
144
|
+
await expect(registry.preload(badOptions)).resolves.toBeUndefined()
|
|
145
|
+
|
|
146
|
+
// But subsequent getOrLoad should throw the cached error
|
|
147
|
+
expect(() => registry.getOrLoad(badOptions)).toThrow()
|
|
148
|
+
})
|
|
149
|
+
|
|
150
|
+
it('does not garbage collect when gcTime is Infinity', async () => {
|
|
151
|
+
vi.useFakeTimers()
|
|
152
|
+
const registry = new StoreRegistry()
|
|
153
|
+
const options = testStoreOptions({ gcTime: Number.POSITIVE_INFINITY })
|
|
154
|
+
|
|
155
|
+
const store = await registry.getOrLoad(options)
|
|
156
|
+
|
|
157
|
+
// Store should be cached
|
|
158
|
+
expect(registry.getOrLoad(options)).toBe(store)
|
|
159
|
+
|
|
160
|
+
// Advance time by a very long duration
|
|
161
|
+
await vi.advanceTimersByTimeAsync(1000000)
|
|
162
|
+
|
|
163
|
+
// Store should still be cached (not garbage collected)
|
|
164
|
+
expect(registry.getOrLoad(options)).toBe(store)
|
|
165
|
+
|
|
166
|
+
// Clean up manually
|
|
167
|
+
await store.shutdownPromise()
|
|
168
|
+
})
|
|
169
|
+
|
|
170
|
+
it('throws the same error instance on multiple synchronous calls after failure', async () => {
|
|
171
|
+
const registry = new StoreRegistry()
|
|
172
|
+
|
|
173
|
+
const badOptions = testStoreOptions({
|
|
174
|
+
// @ts-expect-error - intentionally passing invalid adapter to trigger error
|
|
175
|
+
adapter: null,
|
|
176
|
+
})
|
|
177
|
+
|
|
178
|
+
// Wait for the first failure
|
|
179
|
+
await expect(registry.getOrLoad(badOptions)).rejects.toThrow()
|
|
180
|
+
|
|
181
|
+
// Capture the errors from subsequent synchronous calls
|
|
182
|
+
let error1: unknown
|
|
183
|
+
let error2: unknown
|
|
184
|
+
|
|
185
|
+
try {
|
|
186
|
+
registry.getOrLoad(badOptions)
|
|
187
|
+
} catch (err) {
|
|
188
|
+
error1 = err
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
try {
|
|
192
|
+
registry.getOrLoad(badOptions)
|
|
193
|
+
} catch (err) {
|
|
194
|
+
error2 = err
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
// Both should be the exact same error instance (cached)
|
|
198
|
+
expect(error1).toBeDefined()
|
|
199
|
+
expect(error1).toBe(error2)
|
|
200
|
+
})
|
|
201
|
+
|
|
202
|
+
it('notifies subscribers when store state changes', async () => {
|
|
203
|
+
const registry = new StoreRegistry()
|
|
204
|
+
const options = testStoreOptions()
|
|
205
|
+
|
|
206
|
+
let notificationCount = 0
|
|
207
|
+
const listener = () => {
|
|
208
|
+
notificationCount++
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
const unsubscribe = registry.subscribe(options.storeId, listener)
|
|
212
|
+
|
|
213
|
+
// Start loading the store
|
|
214
|
+
const storePromise = registry.getOrLoad(options)
|
|
215
|
+
|
|
216
|
+
// Listener should be called when store starts loading
|
|
217
|
+
expect(notificationCount).toBe(1)
|
|
218
|
+
|
|
219
|
+
const store = await storePromise
|
|
220
|
+
|
|
221
|
+
// Listener should be called when store loads successfully
|
|
222
|
+
expect(notificationCount).toBe(2)
|
|
223
|
+
|
|
224
|
+
unsubscribe()
|
|
225
|
+
|
|
226
|
+
// Clean up
|
|
227
|
+
await store.shutdownPromise()
|
|
228
|
+
})
|
|
229
|
+
|
|
230
|
+
it('handles rapid subscribe/unsubscribe cycles without errors', async () => {
|
|
231
|
+
vi.useFakeTimers()
|
|
232
|
+
const registry = new StoreRegistry()
|
|
233
|
+
const gcTime = 50
|
|
234
|
+
const options = testStoreOptions({ gcTime })
|
|
235
|
+
|
|
236
|
+
const store = await registry.getOrLoad(options)
|
|
237
|
+
|
|
238
|
+
// Rapidly subscribe and unsubscribe multiple times
|
|
239
|
+
for (let i = 0; i < 10; i++) {
|
|
240
|
+
const unsubscribe = registry.subscribe(options.storeId, () => {})
|
|
241
|
+
unsubscribe()
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
// Advance time to check if GC is scheduled correctly
|
|
245
|
+
await vi.advanceTimersByTimeAsync(gcTime)
|
|
246
|
+
|
|
247
|
+
// Store should be disposed after the last unsubscribe
|
|
248
|
+
const nextStore = await registry.getOrLoad(options)
|
|
249
|
+
expect(nextStore).not.toBe(store)
|
|
250
|
+
|
|
251
|
+
await nextStore.shutdownPromise()
|
|
252
|
+
})
|
|
253
|
+
|
|
254
|
+
it('swallows errors thrown by subscribers during notification', async () => {
|
|
255
|
+
const registry = new StoreRegistry()
|
|
256
|
+
const options = testStoreOptions()
|
|
257
|
+
|
|
258
|
+
let errorListenerCalled = false
|
|
259
|
+
let goodListenerCalled = false
|
|
260
|
+
|
|
261
|
+
const errorListener = () => {
|
|
262
|
+
errorListenerCalled = true
|
|
263
|
+
throw new Error('Listener error')
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
const goodListener = () => {
|
|
267
|
+
goodListenerCalled = true
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
registry.subscribe(options.storeId, errorListener)
|
|
271
|
+
registry.subscribe(options.storeId, goodListener)
|
|
272
|
+
|
|
273
|
+
// Should not throw despite errorListener throwing
|
|
274
|
+
const store = await registry.getOrLoad(options)
|
|
275
|
+
|
|
276
|
+
// Both listeners should have been called
|
|
277
|
+
expect(errorListenerCalled).toBe(true)
|
|
278
|
+
expect(goodListenerCalled).toBe(true)
|
|
279
|
+
|
|
280
|
+
await store.shutdownPromise()
|
|
281
|
+
})
|
|
282
|
+
|
|
283
|
+
it('supports concurrent load and subscribe operations', async () => {
|
|
284
|
+
const registry = new StoreRegistry()
|
|
285
|
+
const options = testStoreOptions()
|
|
286
|
+
|
|
287
|
+
let notificationCount = 0
|
|
288
|
+
const listener = () => {
|
|
289
|
+
notificationCount++
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
// Subscribe before loading starts
|
|
293
|
+
const unsubscribe = registry.subscribe(options.storeId, listener)
|
|
294
|
+
|
|
295
|
+
// Start loading
|
|
296
|
+
const storePromise = registry.getOrLoad(options)
|
|
297
|
+
|
|
298
|
+
// Listener should be notified when loading starts
|
|
299
|
+
expect(notificationCount).toBeGreaterThan(0)
|
|
300
|
+
|
|
301
|
+
const store = await storePromise
|
|
302
|
+
|
|
303
|
+
// Listener should be notified when loading completes
|
|
304
|
+
expect(notificationCount).toBe(2)
|
|
305
|
+
|
|
306
|
+
unsubscribe()
|
|
307
|
+
|
|
308
|
+
// Clean up
|
|
309
|
+
await store.shutdownPromise()
|
|
310
|
+
})
|
|
311
|
+
|
|
312
|
+
it('cancels GC when a new subscription is added', async () => {
|
|
313
|
+
vi.useFakeTimers()
|
|
314
|
+
const registry = new StoreRegistry()
|
|
315
|
+
const gcTime = 50
|
|
316
|
+
const options = testStoreOptions({ gcTime })
|
|
317
|
+
|
|
318
|
+
const store = await registry.getOrLoad(options)
|
|
319
|
+
|
|
320
|
+
// Advance time almost to GC threshold
|
|
321
|
+
await vi.advanceTimersByTimeAsync(gcTime - 5)
|
|
322
|
+
|
|
323
|
+
// Add a new subscription before GC triggers
|
|
324
|
+
const unsubscribe = registry.subscribe(options.storeId, () => {})
|
|
325
|
+
|
|
326
|
+
// Complete the original GC time
|
|
327
|
+
await vi.advanceTimersByTimeAsync(5)
|
|
328
|
+
|
|
329
|
+
// Store should not have been disposed because we added a subscription
|
|
330
|
+
expect(registry.getOrLoad(options)).toBe(store)
|
|
331
|
+
|
|
332
|
+
// Clean up
|
|
333
|
+
unsubscribe()
|
|
334
|
+
await vi.advanceTimersByTimeAsync(gcTime)
|
|
335
|
+
|
|
336
|
+
// Now it should be disposed
|
|
337
|
+
const nextStore = await registry.getOrLoad(options)
|
|
338
|
+
expect(nextStore).not.toBe(store)
|
|
339
|
+
|
|
340
|
+
await nextStore.shutdownPromise()
|
|
341
|
+
})
|
|
342
|
+
|
|
343
|
+
it('schedules GC if store becomes inactive during loading', async () => {
|
|
344
|
+
vi.useFakeTimers()
|
|
345
|
+
const registry = new StoreRegistry()
|
|
346
|
+
const gcTime = 50
|
|
347
|
+
const options = testStoreOptions({ gcTime })
|
|
348
|
+
|
|
349
|
+
// Start loading without any subscription
|
|
350
|
+
const storePromise = registry.getOrLoad(options)
|
|
351
|
+
|
|
352
|
+
// Wait for store to load (no subscribers registered)
|
|
353
|
+
const store = await storePromise
|
|
354
|
+
|
|
355
|
+
// Since there were no subscribers when loading completed, GC should be scheduled
|
|
356
|
+
await vi.advanceTimersByTimeAsync(gcTime)
|
|
357
|
+
|
|
358
|
+
// Store should be disposed
|
|
359
|
+
const nextStore = await registry.getOrLoad(options)
|
|
360
|
+
expect(nextStore).not.toBe(store)
|
|
361
|
+
|
|
362
|
+
await nextStore.shutdownPromise()
|
|
363
|
+
})
|
|
364
|
+
|
|
365
|
+
it('manages multiple stores with different IDs independently', async () => {
|
|
366
|
+
vi.useFakeTimers()
|
|
367
|
+
const registry = new StoreRegistry()
|
|
368
|
+
|
|
369
|
+
const options1 = testStoreOptions({ storeId: 'store-1', gcTime: 50 })
|
|
370
|
+
const options2 = testStoreOptions({ storeId: 'store-2', gcTime: 100 })
|
|
371
|
+
|
|
372
|
+
const store1 = await registry.getOrLoad(options1)
|
|
373
|
+
const store2 = await registry.getOrLoad(options2)
|
|
374
|
+
|
|
375
|
+
// Should be different store instances
|
|
376
|
+
expect(store1).not.toBe(store2)
|
|
377
|
+
|
|
378
|
+
// Both should be cached independently
|
|
379
|
+
expect(registry.getOrLoad(options1)).toBe(store1)
|
|
380
|
+
expect(registry.getOrLoad(options2)).toBe(store2)
|
|
381
|
+
|
|
382
|
+
// Advance time to dispose store1 only
|
|
383
|
+
await vi.advanceTimersByTimeAsync(50)
|
|
384
|
+
|
|
385
|
+
// store1 should be disposed, store2 should still be cached
|
|
386
|
+
const newStore1 = await registry.getOrLoad(options1)
|
|
387
|
+
expect(newStore1).not.toBe(store1)
|
|
388
|
+
expect(registry.getOrLoad(options2)).toBe(store2)
|
|
389
|
+
|
|
390
|
+
// Subscribe to prevent GC of newStore1
|
|
391
|
+
const unsub1 = registry.subscribe(options1.storeId, () => {})
|
|
392
|
+
|
|
393
|
+
// Advance remaining time to dispose store2
|
|
394
|
+
await vi.advanceTimersByTimeAsync(50)
|
|
395
|
+
|
|
396
|
+
// store2 should be disposed
|
|
397
|
+
const newStore2 = await registry.getOrLoad(options2)
|
|
398
|
+
expect(newStore2).not.toBe(store2)
|
|
399
|
+
|
|
400
|
+
// Subscribe to prevent GC of newStore2
|
|
401
|
+
const unsub2 = registry.subscribe(options2.storeId, () => {})
|
|
402
|
+
|
|
403
|
+
// Clean up
|
|
404
|
+
unsub1()
|
|
405
|
+
unsub2()
|
|
406
|
+
await newStore1.shutdownPromise()
|
|
407
|
+
await newStore2.shutdownPromise()
|
|
408
|
+
})
|
|
409
|
+
|
|
410
|
+
it('applies default options from constructor', async () => {
|
|
411
|
+
vi.useFakeTimers()
|
|
412
|
+
|
|
413
|
+
const registry = new StoreRegistry({
|
|
414
|
+
defaultOptions: {
|
|
415
|
+
gcTime: DEFAULT_GC_TIME * 2,
|
|
416
|
+
},
|
|
417
|
+
})
|
|
418
|
+
|
|
419
|
+
const options = testStoreOptions()
|
|
420
|
+
|
|
421
|
+
const store = await registry.getOrLoad(options)
|
|
422
|
+
|
|
423
|
+
// Verify the store loads successfully
|
|
424
|
+
expect(store).toBeDefined()
|
|
425
|
+
expect(store[StoreInternalsSymbol].clientSession.debugInstanceId).toBeDefined()
|
|
426
|
+
|
|
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)
|
|
429
|
+
|
|
430
|
+
// Store should still be cached after default gc time
|
|
431
|
+
expect(registry.getOrLoad(options)).toBe(store)
|
|
432
|
+
|
|
433
|
+
await store.shutdownPromise()
|
|
434
|
+
})
|
|
435
|
+
|
|
436
|
+
it('allows call-site options to override default options', async () => {
|
|
437
|
+
vi.useFakeTimers()
|
|
438
|
+
|
|
439
|
+
const registry = new StoreRegistry({
|
|
440
|
+
defaultOptions: {
|
|
441
|
+
gcTime: 1000, // Default is long
|
|
442
|
+
},
|
|
443
|
+
})
|
|
444
|
+
|
|
445
|
+
const options = testStoreOptions({
|
|
446
|
+
gcTime: 10, // Override with shorter time
|
|
447
|
+
})
|
|
448
|
+
|
|
449
|
+
const store = await registry.getOrLoad(options)
|
|
450
|
+
|
|
451
|
+
// Advance by the override time (10ms)
|
|
452
|
+
await vi.advanceTimersByTimeAsync(10)
|
|
453
|
+
|
|
454
|
+
// Should be disposed according to the override time, not default
|
|
455
|
+
const nextStore = await registry.getOrLoad(options)
|
|
456
|
+
expect(nextStore).not.toBe(store)
|
|
457
|
+
|
|
458
|
+
await nextStore.shutdownPromise()
|
|
459
|
+
})
|
|
460
|
+
|
|
461
|
+
it('warms the cache so subsequent getOrLoad is synchronous after preload', async () => {
|
|
462
|
+
const registry = new StoreRegistry()
|
|
463
|
+
const options = testStoreOptions()
|
|
464
|
+
|
|
465
|
+
// Preload the store
|
|
466
|
+
await registry.preload(options)
|
|
467
|
+
|
|
468
|
+
// Subsequent getOrLoad should return synchronously (not a Promise)
|
|
469
|
+
const store = registry.getOrLoad(options)
|
|
470
|
+
expect(store).not.toBeInstanceOf(Promise)
|
|
471
|
+
|
|
472
|
+
// TypeScript doesn't narrow the type, so we need to assert
|
|
473
|
+
if (store instanceof Promise) {
|
|
474
|
+
throw new Error('Expected store, got Promise')
|
|
475
|
+
}
|
|
476
|
+
|
|
477
|
+
// Clean up
|
|
478
|
+
await store.shutdownPromise()
|
|
479
|
+
})
|
|
480
|
+
|
|
481
|
+
it('schedules GC after preload if no subscribers are added', async () => {
|
|
482
|
+
vi.useFakeTimers()
|
|
483
|
+
const registry = new StoreRegistry()
|
|
484
|
+
const gcTime = 50
|
|
485
|
+
const options = testStoreOptions({ gcTime })
|
|
486
|
+
|
|
487
|
+
// Preload without subscribing
|
|
488
|
+
await registry.preload(options)
|
|
489
|
+
|
|
490
|
+
// Get the store
|
|
491
|
+
const store = registry.getOrLoad(options)
|
|
492
|
+
expect(store).not.toBeInstanceOf(Promise)
|
|
493
|
+
|
|
494
|
+
// Advance time to trigger GC
|
|
495
|
+
await vi.advanceTimersByTimeAsync(gcTime)
|
|
496
|
+
|
|
497
|
+
// Store should be disposed since no subscribers were added
|
|
498
|
+
const nextStore = await registry.getOrLoad(options)
|
|
499
|
+
expect(nextStore).not.toBe(store)
|
|
500
|
+
|
|
501
|
+
await nextStore.shutdownPromise()
|
|
502
|
+
})
|
|
503
|
+
})
|
|
504
|
+
|
|
505
|
+
const testStoreOptions = (overrides: Partial<CachedStoreOptions<typeof schema>> = {}) =>
|
|
506
|
+
storeOptions({
|
|
507
|
+
storeId: 'test-store',
|
|
508
|
+
schema,
|
|
509
|
+
adapter: makeInMemoryAdapter(),
|
|
510
|
+
...overrides,
|
|
511
|
+
})
|