@livestore/livestore 0.4.0-dev.22 → 0.4.0-dev.23
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/README.md +0 -1
- package/dist/.tsbuildinfo +1 -1
- package/dist/QueryCache.js +1 -1
- package/dist/QueryCache.js.map +1 -1
- package/dist/SqliteDbWrapper.d.ts +5 -5
- package/dist/SqliteDbWrapper.d.ts.map +1 -1
- package/dist/SqliteDbWrapper.js +8 -8
- package/dist/SqliteDbWrapper.js.map +1 -1
- package/dist/SqliteDbWrapper.test.js +2 -2
- package/dist/SqliteDbWrapper.test.js.map +1 -1
- package/dist/effect/LiveStore.d.ts +14 -7
- package/dist/effect/LiveStore.d.ts.map +1 -1
- package/dist/effect/LiveStore.js +0 -15
- package/dist/effect/LiveStore.js.map +1 -1
- package/dist/effect/LiveStore.test.d.ts +2 -0
- package/dist/effect/LiveStore.test.d.ts.map +1 -0
- package/dist/effect/LiveStore.test.js +42 -0
- package/dist/effect/LiveStore.test.js.map +1 -0
- package/dist/live-queries/base-class.d.ts +3 -3
- package/dist/live-queries/base-class.d.ts.map +1 -1
- package/dist/live-queries/base-class.js +2 -2
- package/dist/live-queries/base-class.js.map +1 -1
- package/dist/live-queries/client-document-get-query.d.ts +1 -1
- package/dist/live-queries/client-document-get-query.d.ts.map +1 -1
- package/dist/live-queries/client-document-get-query.js +1 -1
- package/dist/live-queries/client-document-get-query.js.map +1 -1
- package/dist/live-queries/computed.d.ts.map +1 -1
- package/dist/live-queries/computed.js +2 -2
- package/dist/live-queries/computed.js.map +1 -1
- package/dist/live-queries/db-query.js +14 -14
- package/dist/live-queries/db-query.js.map +1 -1
- package/dist/live-queries/db-query.test.js +2 -2
- package/dist/live-queries/db-query.test.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/mod.d.ts +1 -1
- package/dist/mod.d.ts.map +1 -1
- package/dist/mod.js.map +1 -1
- package/dist/reactive.d.ts +9 -9
- package/dist/reactive.d.ts.map +1 -1
- package/dist/reactive.js +9 -26
- package/dist/reactive.js.map +1 -1
- package/dist/reactive.test.js +2 -2
- package/dist/reactive.test.js.map +1 -1
- package/dist/store/StoreRegistry.d.ts +30 -5
- package/dist/store/StoreRegistry.d.ts.map +1 -1
- package/dist/store/StoreRegistry.js +54 -31
- package/dist/store/StoreRegistry.js.map +1 -1
- package/dist/store/StoreRegistry.test.js +251 -250
- package/dist/store/StoreRegistry.test.js.map +1 -1
- package/dist/store/create-store.d.ts +6 -2
- package/dist/store/create-store.d.ts.map +1 -1
- package/dist/store/create-store.js +13 -7
- package/dist/store/create-store.js.map +1 -1
- package/dist/store/devtools.d.ts +1 -1
- package/dist/store/devtools.d.ts.map +1 -1
- package/dist/store/devtools.js +3 -3
- package/dist/store/devtools.js.map +1 -1
- package/dist/store/store-eventstream.test.js +2 -2
- package/dist/store/store-eventstream.test.js.map +1 -1
- package/dist/store/store-types.d.ts +70 -5
- package/dist/store/store-types.d.ts.map +1 -1
- package/dist/store/store-types.js.map +1 -1
- package/dist/store/store-types.test.js +1 -1
- package/dist/store/store-types.test.js.map +1 -1
- package/dist/store/store.d.ts +81 -2
- package/dist/store/store.d.ts.map +1 -1
- package/dist/store/store.js +128 -45
- package/dist/store/store.js.map +1 -1
- package/dist/utils/dev.js.map +1 -1
- package/dist/utils/stack-info.js +2 -2
- package/dist/utils/stack-info.js.map +1 -1
- package/dist/utils/tests/fixture.d.ts +1 -1
- package/dist/utils/tests/fixture.d.ts.map +1 -1
- package/dist/utils/tests/fixture.js.map +1 -1
- package/dist/utils/tests/otel.d.ts.map +1 -1
- package/dist/utils/tests/otel.js +5 -5
- package/dist/utils/tests/otel.js.map +1 -1
- package/package.json +58 -17
- package/src/QueryCache.ts +1 -1
- package/src/SqliteDbWrapper.test.ts +4 -2
- package/src/SqliteDbWrapper.ts +12 -11
- package/src/ambient.d.ts +0 -7
- package/src/effect/LiveStore.test.ts +61 -0
- package/src/effect/LiveStore.ts +17 -26
- package/src/live-queries/__snapshots__/db-query.test.ts.snap +336 -231
- package/src/live-queries/base-class.ts +7 -6
- package/src/live-queries/client-document-get-query.ts +4 -2
- package/src/live-queries/computed.ts +3 -2
- package/src/live-queries/db-query.test.ts +3 -2
- package/src/live-queries/db-query.ts +15 -15
- package/src/live-queries/signal.test.ts +3 -2
- package/src/mod.ts +1 -0
- package/src/reactive.test.ts +3 -2
- package/src/reactive.ts +22 -23
- package/src/store/StoreRegistry.test.ts +317 -293
- package/src/store/StoreRegistry.ts +63 -38
- package/src/store/create-store.ts +26 -11
- package/src/store/devtools.ts +5 -6
- package/src/store/store-eventstream.test.ts +4 -2
- package/src/store/store-types.test.ts +3 -1
- package/src/store/store-types.ts +47 -8
- package/src/store/store.ts +172 -55
- package/src/utils/dev.ts +2 -2
- package/src/utils/stack-info.ts +2 -2
- package/src/utils/tests/fixture.ts +2 -1
- package/src/utils/tests/otel.ts +8 -7
- package/docs/api/index.md +0 -3
- package/docs/building-with-livestore/complex-ui-state/index.md +0 -3
- package/docs/building-with-livestore/crud/index.md +0 -3
- package/docs/building-with-livestore/data-modeling/index.md +0 -30
- package/docs/building-with-livestore/debugging/index.md +0 -17
- package/docs/building-with-livestore/devtools/index.md +0 -79
- package/docs/building-with-livestore/events/index.md +0 -355
- package/docs/building-with-livestore/examples/ai-agent/index.md +0 -5
- package/docs/building-with-livestore/examples/todo-workspaces/index.md +0 -885
- package/docs/building-with-livestore/examples/turnbased-game/index.md +0 -7
- package/docs/building-with-livestore/opentelemetry/index.md +0 -227
- package/docs/building-with-livestore/production-checklist/index.md +0 -5
- package/docs/building-with-livestore/reactivity-system/index.md +0 -202
- package/docs/building-with-livestore/rules-for-ai-agents/index.md +0 -9
- package/docs/building-with-livestore/state/materializers/index.md +0 -300
- package/docs/building-with-livestore/state/sql-queries/index.md +0 -94
- package/docs/building-with-livestore/state/sqlite/index.md +0 -45
- package/docs/building-with-livestore/state/sqlite-schema/index.md +0 -306
- package/docs/building-with-livestore/state/sqlite-schema-effect/index.md +0 -300
- package/docs/building-with-livestore/store/index.md +0 -625
- package/docs/building-with-livestore/syncing/index.md +0 -136
- package/docs/building-with-livestore/tools/cli/index.md +0 -177
- package/docs/building-with-livestore/tools/mcp/index.md +0 -187
- package/docs/examples/cloudflare-adapter/index.md +0 -44
- package/docs/examples/expo-adapter/index.md +0 -44
- package/docs/examples/index.md +0 -55
- package/docs/examples/node-adapter/index.md +0 -44
- package/docs/examples/web-adapter/index.md +0 -52
- package/docs/framework-integrations/custom-elements/index.md +0 -142
- package/docs/framework-integrations/react-integration/index.md +0 -937
- package/docs/framework-integrations/solid-integration/index.md +0 -293
- package/docs/framework-integrations/svelte-integration/index.md +0 -42
- package/docs/framework-integrations/vue-integration/index.md +0 -294
- package/docs/getting-started/expo/index.md +0 -882
- package/docs/getting-started/node/index.md +0 -115
- package/docs/getting-started/react-web/index.md +0 -626
- package/docs/getting-started/solid/index.md +0 -3
- package/docs/getting-started/vue/index.md +0 -471
- package/docs/index.md +0 -208
- package/docs/llms.txt +0 -146
- package/docs/misc/CODE_OF_CONDUCT/index.md +0 -133
- package/docs/misc/FAQ/index.md +0 -37
- package/docs/misc/community/index.md +0 -88
- package/docs/misc/credits/index.md +0 -14
- package/docs/misc/design-partners/index.md +0 -13
- package/docs/misc/package-management/index.md +0 -21
- package/docs/misc/performance/index.md +0 -25
- package/docs/misc/resources/index.md +0 -46
- package/docs/misc/state-of-the-project/index.md +0 -37
- package/docs/misc/troubleshooting/index.md +0 -82
- package/docs/overview/concepts/index.md +0 -78
- package/docs/overview/how-livestore-works/index.md +0 -56
- package/docs/overview/introduction/index.md +0 -413
- package/docs/overview/technology-comparison/index.md +0 -40
- package/docs/overview/when-livestore/index.md +0 -81
- package/docs/overview/why-livestore/index.md +0 -111
- package/docs/patterns/ai/index.md +0 -15
- package/docs/patterns/anonymous-user-transition/index.md +0 -10
- package/docs/patterns/app-evolution/index.md +0 -72
- package/docs/patterns/auth/index.md +0 -377
- package/docs/patterns/effect/index.md +0 -1505
- package/docs/patterns/encryption/index.md +0 -6
- package/docs/patterns/external-data/index.md +0 -5
- package/docs/patterns/file-management/index.md +0 -11
- package/docs/patterns/file-structure/index.md +0 -14
- package/docs/patterns/list-ordering/index.md +0 -369
- package/docs/patterns/offline/index.md +0 -32
- package/docs/patterns/orm/index.md +0 -18
- package/docs/patterns/presence/index.md +0 -11
- package/docs/patterns/rich-text-editing/index.md +0 -11
- package/docs/patterns/server-side-clients/index.md +0 -97
- package/docs/patterns/side-effects/index.md +0 -11
- package/docs/patterns/state-machines/index.md +0 -11
- package/docs/patterns/storybook/index.md +0 -209
- package/docs/patterns/undo-redo/index.md +0 -9
- package/docs/patterns/version-control/index.md +0 -8
- package/docs/platform-adapters/cloudflare-durable-object-adapter/index.md +0 -453
- package/docs/platform-adapters/electron-adapter/index.md +0 -15
- package/docs/platform-adapters/expo-adapter/index.md +0 -262
- package/docs/platform-adapters/node-adapter/index.md +0 -160
- package/docs/platform-adapters/tauri-adapter/index.md +0 -15
- package/docs/platform-adapters/web-adapter/index.md +0 -287
- package/docs/sustainable-open-source/contributing/docs/index.md +0 -94
- package/docs/sustainable-open-source/contributing/info/index.md +0 -63
- package/docs/sustainable-open-source/contributing/monorepo/index.md +0 -195
- package/docs/sustainable-open-source/sponsoring/index.md +0 -104
- package/docs/sync-providers/cloudflare/index.md +0 -773
- package/docs/sync-providers/custom/index.md +0 -65
- package/docs/sync-providers/electricsql/index.md +0 -159
- package/docs/sync-providers/s2/index.md +0 -230
- package/docs/tutorial/0-welcome/index.md +0 -48
- package/docs/tutorial/1-setup-starter-project/index.md +0 -105
- package/docs/tutorial/2-deploy-to-cloudflare/index.md +0 -195
- package/docs/tutorial/3-read-and-write-todos-via-livestore/index.md +0 -530
- package/docs/tutorial/4-sync-data-via-cloudflare/index.md +0 -210
- package/docs/tutorial/5-expand-business-logic/index.md +0 -174
- package/docs/tutorial/6-persist-ui-state/index.md +0 -453
- package/docs/tutorial/7-next-steps/index.md +0 -22
- package/docs/understanding-livestore/design-decisions/index.md +0 -33
- package/docs/understanding-livestore/event-sourcing/index.md +0 -40
|
@@ -1,11 +1,12 @@
|
|
|
1
|
+
import { describe, expect, it } from '@effect/vitest'
|
|
2
|
+
|
|
1
3
|
import { makeInMemoryAdapter } from '@livestore/adapter-web'
|
|
2
|
-
import { UnknownError } from '@livestore/common'
|
|
3
|
-
import {
|
|
4
|
-
|
|
5
|
-
import { describe, expect, it } from 'vitest'
|
|
4
|
+
import { OtelLiveDummy, UnknownError } from '@livestore/common'
|
|
5
|
+
import { Effect, Fiber, type OtelTracer, type Scope, TestClock } from '@livestore/utils/effect'
|
|
6
|
+
|
|
6
7
|
import { schema } from '../utils/tests/fixture.ts'
|
|
7
|
-
import { type RegistryStoreOptions, StoreRegistry, storeOptions } from './StoreRegistry.ts'
|
|
8
8
|
import { StoreInternalsSymbol } from './store-types.ts'
|
|
9
|
+
import { type RegistryStoreOptions, StoreRegistry, storeOptions } from './StoreRegistry.ts'
|
|
9
10
|
|
|
10
11
|
describe('StoreRegistry', () => {
|
|
11
12
|
it('returns a promise when the store is loading', async () => {
|
|
@@ -128,382 +129,405 @@ describe('StoreRegistry', () => {
|
|
|
128
129
|
expect(error1).toBe(error2)
|
|
129
130
|
})
|
|
130
131
|
|
|
131
|
-
it('
|
|
132
|
-
const
|
|
133
|
-
const storeRegistry = new StoreRegistry({ defaultOptions: { unusedCacheTime } })
|
|
134
|
-
const options = testStoreOptions()
|
|
135
|
-
|
|
136
|
-
const store = await storeRegistry.getOrLoadPromise(options)
|
|
137
|
-
|
|
138
|
-
// Store should be cached
|
|
139
|
-
expect(storeRegistry.getOrLoadPromise(options)).toBe(store)
|
|
140
|
-
|
|
141
|
-
// Wait for disposal
|
|
142
|
-
await sleep(unusedCacheTime + 50)
|
|
132
|
+
it('preload does not throw', async () => {
|
|
133
|
+
const storeRegistry = new StoreRegistry()
|
|
143
134
|
|
|
144
|
-
//
|
|
145
|
-
|
|
146
|
-
|
|
135
|
+
// Create invalid options that would cause an error
|
|
136
|
+
const badOptions = testStoreOptions({
|
|
137
|
+
// @ts-expect-error - intentionally passing invalid adapter to trigger error
|
|
138
|
+
adapter: null,
|
|
139
|
+
})
|
|
147
140
|
|
|
148
|
-
//
|
|
149
|
-
expect(
|
|
150
|
-
expect(nextStore[StoreInternalsSymbol].clientSession.debugInstanceId).toBeDefined()
|
|
141
|
+
// preload should not throw
|
|
142
|
+
await expect(storeRegistry.preload(badOptions)).resolves.toBeUndefined()
|
|
151
143
|
|
|
152
|
-
//
|
|
153
|
-
|
|
144
|
+
// But subsequent getOrLoadStore should throw the cached error
|
|
145
|
+
expect(() => storeRegistry.getOrLoadPromise(badOptions)).toThrow()
|
|
154
146
|
})
|
|
155
147
|
|
|
156
|
-
it('
|
|
157
|
-
const storeRegistry = new StoreRegistry(
|
|
148
|
+
it('warms the cache so subsequent getOrLoadStore is synchronous after preload', async () => {
|
|
149
|
+
const storeRegistry = new StoreRegistry()
|
|
158
150
|
const options = testStoreOptions()
|
|
159
151
|
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
// Store should be cached
|
|
163
|
-
expect(storeRegistry.getOrLoadPromise(options)).toBe(store)
|
|
152
|
+
// Preload the store
|
|
153
|
+
await storeRegistry.preload(options)
|
|
164
154
|
|
|
165
|
-
//
|
|
166
|
-
|
|
155
|
+
// Subsequent getOrLoadStore should return synchronously (not a Promise)
|
|
156
|
+
const store = storeRegistry.getOrLoadPromise(options)
|
|
157
|
+
expect(store).not.toBeInstanceOf(Promise)
|
|
167
158
|
|
|
168
|
-
//
|
|
169
|
-
|
|
159
|
+
// TypeScript doesn't narrow the type, so we need to assert
|
|
160
|
+
if (store instanceof Promise) {
|
|
161
|
+
throw new Error('Expected store, got Promise')
|
|
162
|
+
}
|
|
170
163
|
|
|
171
|
-
// Clean up
|
|
164
|
+
// Clean up
|
|
172
165
|
await store.shutdownPromise()
|
|
173
166
|
})
|
|
174
167
|
|
|
175
|
-
it('
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
// Wait for store to load (no retain registered)
|
|
184
|
-
const store = await storePromise
|
|
185
|
-
|
|
186
|
-
// Since there were no retain when loading completed, disposal should be scheduled
|
|
187
|
-
await sleep(unusedCacheTime + 50)
|
|
188
|
-
|
|
189
|
-
// Store should be disposed
|
|
190
|
-
const nextStore = await storeRegistry.getOrLoadPromise(options)
|
|
191
|
-
expect(nextStore).not.toBe(store)
|
|
192
|
-
|
|
193
|
-
await nextStore.shutdownPromise()
|
|
194
|
-
})
|
|
195
|
-
|
|
196
|
-
// This test is skipped because Effect doesn't yet support different `idleTimeToLive` values for each resource in `RcMap`
|
|
197
|
-
// See https://github.com/livestorejs/livestore/issues/917
|
|
198
|
-
it.skip('allows call-site options to override default options', async () => {
|
|
199
|
-
const storeRegistry = new StoreRegistry({
|
|
200
|
-
defaultOptions: {
|
|
201
|
-
unusedCacheTime: 1000, // Default is long
|
|
202
|
-
},
|
|
203
|
-
})
|
|
204
|
-
|
|
205
|
-
const options = testStoreOptions({
|
|
206
|
-
unusedCacheTime: 10, // Override with shorter time
|
|
207
|
-
})
|
|
208
|
-
|
|
209
|
-
const store = await storeRegistry.getOrLoadPromise(options)
|
|
210
|
-
|
|
211
|
-
// Wait for the override time (10ms)
|
|
212
|
-
await sleep(10)
|
|
213
|
-
|
|
214
|
-
// Should be disposed according to the override time, not default
|
|
215
|
-
const nextStore = await storeRegistry.getOrLoadPromise(options)
|
|
216
|
-
expect(nextStore).not.toBe(store)
|
|
217
|
-
|
|
218
|
-
await nextStore.shutdownPromise()
|
|
219
|
-
})
|
|
220
|
-
|
|
221
|
-
// This test is skipped because we don't yet support dynamic `unusedCacheTime` updates for cached stores.
|
|
222
|
-
// See https://github.com/livestorejs/livestore/issues/918
|
|
223
|
-
it.skip('keeps the longest unusedCacheTime seen for a store when options vary across calls', async () => {
|
|
224
|
-
const storeRegistry = new StoreRegistry()
|
|
225
|
-
|
|
226
|
-
const options = testStoreOptions({ unusedCacheTime: 10 })
|
|
227
|
-
const release = storeRegistry.retain(options)
|
|
228
|
-
|
|
229
|
-
const store = await storeRegistry.getOrLoadPromise(options)
|
|
230
|
-
|
|
231
|
-
// Call with longer unusedCacheTime
|
|
232
|
-
await storeRegistry.getOrLoadPromise(testStoreOptions({ unusedCacheTime: 100 }))
|
|
233
|
-
|
|
234
|
-
release()
|
|
168
|
+
it.layer(OtelLiveDummy)('time-dependent (using TestClock)', (it) => {
|
|
169
|
+
it.scoped('disposes store after unusedCacheTime expires', () =>
|
|
170
|
+
Effect.gen(function* () {
|
|
171
|
+
const unusedCacheTime = 25
|
|
172
|
+
const runtime = yield* Effect.runtime<Scope.Scope | OtelTracer.OtelTracer>()
|
|
173
|
+
const registry = new StoreRegistry({ runtime, defaultOptions: { unusedCacheTime } })
|
|
174
|
+
const options = testStoreOptions()
|
|
235
175
|
|
|
236
|
-
|
|
237
|
-
await sleep(99)
|
|
176
|
+
const store = yield* registry.getOrLoad(options).pipe(Effect.scoped)
|
|
238
177
|
|
|
239
|
-
|
|
240
|
-
|
|
178
|
+
// Store should still be in cache
|
|
179
|
+
const cached = yield* registry.getOrLoad(options).pipe(Effect.scoped)
|
|
180
|
+
expect(cached).toBe(store)
|
|
241
181
|
|
|
242
|
-
|
|
243
|
-
|
|
182
|
+
// Let the idle timer fiber register its sleep with TestClock
|
|
183
|
+
yield* Effect.yieldNow()
|
|
244
184
|
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
expect(nextStore).not.toBe(store)
|
|
185
|
+
// Advance time past unusedCacheTime → idle timer fires → entry evicted
|
|
186
|
+
yield* TestClock.adjust(unusedCacheTime)
|
|
248
187
|
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
188
|
+
// After eviction, a new load should produce a different store
|
|
189
|
+
const nextStore = yield* registry.getOrLoad(options).pipe(Effect.scoped)
|
|
190
|
+
expect(nextStore).not.toBe(store)
|
|
191
|
+
expect(nextStore[StoreInternalsSymbol].clientSession.debugInstanceId).toBeDefined()
|
|
192
|
+
}),
|
|
193
|
+
)
|
|
252
194
|
|
|
253
|
-
|
|
254
|
-
|
|
195
|
+
it.scoped('does not dispose when unusedCacheTime is Infinity', () =>
|
|
196
|
+
Effect.gen(function* () {
|
|
197
|
+
const runtime = yield* Effect.runtime<Scope.Scope | OtelTracer.OtelTracer>()
|
|
198
|
+
const registry = new StoreRegistry({ runtime, defaultOptions: { unusedCacheTime: Number.POSITIVE_INFINITY } })
|
|
199
|
+
const options = testStoreOptions()
|
|
255
200
|
|
|
256
|
-
|
|
257
|
-
const badOptions = testStoreOptions({
|
|
258
|
-
// @ts-expect-error - intentionally passing invalid adapter to trigger error
|
|
259
|
-
adapter: null,
|
|
260
|
-
})
|
|
201
|
+
const store = yield* registry.getOrLoad(options).pipe(Effect.scoped)
|
|
261
202
|
|
|
262
|
-
|
|
263
|
-
|
|
203
|
+
// Advance a large amount of time — no idle timer was started for Infinity
|
|
204
|
+
yield* TestClock.adjust(100_000)
|
|
264
205
|
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
206
|
+
// Store should still be cached
|
|
207
|
+
const cached = yield* registry.getOrLoad(options).pipe(Effect.scoped)
|
|
208
|
+
expect(cached).toBe(store)
|
|
209
|
+
}),
|
|
210
|
+
)
|
|
268
211
|
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
212
|
+
it.scoped('schedules disposal if store becomes unused during loading', () =>
|
|
213
|
+
Effect.gen(function* () {
|
|
214
|
+
const unusedCacheTime = 50
|
|
215
|
+
const runtime = yield* Effect.runtime<Scope.Scope | OtelTracer.OtelTracer>()
|
|
216
|
+
const registry = new StoreRegistry({ runtime, defaultOptions: { unusedCacheTime } })
|
|
217
|
+
const options = testStoreOptions()
|
|
273
218
|
|
|
274
|
-
|
|
219
|
+
// Load without retaining — disposal is scheduled when scope closes
|
|
220
|
+
const store = yield* registry.getOrLoad(options).pipe(Effect.scoped)
|
|
275
221
|
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
const release = storeRegistry.retain(options)
|
|
279
|
-
release()
|
|
280
|
-
}
|
|
222
|
+
yield* Effect.yieldNow()
|
|
223
|
+
yield* TestClock.adjust(unusedCacheTime)
|
|
281
224
|
|
|
282
|
-
|
|
283
|
-
|
|
225
|
+
// Store should be disposed
|
|
226
|
+
const nextStore = yield* registry.getOrLoad(options).pipe(Effect.scoped)
|
|
227
|
+
expect(nextStore).not.toBe(store)
|
|
228
|
+
}),
|
|
229
|
+
)
|
|
284
230
|
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
231
|
+
it.scoped('allows call-site options to override default options', () =>
|
|
232
|
+
Effect.gen(function* () {
|
|
233
|
+
const runtime = yield* Effect.runtime<Scope.Scope | OtelTracer.OtelTracer>()
|
|
234
|
+
const registry = new StoreRegistry({ runtime, defaultOptions: { unusedCacheTime: 10_000 } }) // Long default
|
|
288
235
|
|
|
289
|
-
|
|
290
|
-
|
|
236
|
+
const unusedCacheTimeOverride = 25
|
|
237
|
+
const options = testStoreOptions({ unusedCacheTime: unusedCacheTimeOverride })
|
|
291
238
|
|
|
292
|
-
|
|
293
|
-
const unusedCacheTime = 50
|
|
294
|
-
const storeRegistry = new StoreRegistry({ defaultOptions: { unusedCacheTime } })
|
|
295
|
-
const options = testStoreOptions()
|
|
239
|
+
const store = yield* registry.getOrLoad(options).pipe(Effect.scoped)
|
|
296
240
|
|
|
297
|
-
|
|
241
|
+
yield* Effect.yieldNow()
|
|
242
|
+
yield* TestClock.adjust(unusedCacheTimeOverride)
|
|
298
243
|
|
|
299
|
-
|
|
300
|
-
|
|
244
|
+
// Should be disposed according to the override time, not default
|
|
245
|
+
const nextStore = yield* registry.getOrLoad(options).pipe(Effect.scoped)
|
|
246
|
+
expect(nextStore).not.toBe(store)
|
|
247
|
+
}),
|
|
248
|
+
)
|
|
301
249
|
|
|
302
|
-
|
|
303
|
-
|
|
250
|
+
it.scoped('disposes different stores according to their own unusedCacheTime', () =>
|
|
251
|
+
Effect.gen(function* () {
|
|
252
|
+
const runtime = yield* Effect.runtime<Scope.Scope | OtelTracer.OtelTracer>()
|
|
253
|
+
const registry = new StoreRegistry({ runtime, defaultOptions: { unusedCacheTime: 1000 } })
|
|
304
254
|
|
|
305
|
-
|
|
306
|
-
|
|
255
|
+
const shortOptions = testStoreOptions({ storeId: 'short-lived', unusedCacheTime: 25 })
|
|
256
|
+
const longOptions = testStoreOptions({ storeId: 'long-lived', unusedCacheTime: 10_000 })
|
|
307
257
|
|
|
308
|
-
|
|
309
|
-
|
|
258
|
+
const shortStore = yield* registry.getOrLoad(shortOptions).pipe(Effect.scoped)
|
|
259
|
+
const longStore = yield* registry.getOrLoad(longOptions).pipe(Effect.scoped)
|
|
310
260
|
|
|
311
|
-
|
|
312
|
-
release()
|
|
313
|
-
await sleep(unusedCacheTime + 50)
|
|
261
|
+
yield* Effect.yieldNow()
|
|
314
262
|
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
expect(nextStore).not.toBe(store)
|
|
263
|
+
// Advance past short store's unusedCacheTime only
|
|
264
|
+
yield* TestClock.adjust(25)
|
|
318
265
|
|
|
319
|
-
|
|
320
|
-
|
|
266
|
+
// Short store should be disposed, long store should still be cached
|
|
267
|
+
const nextShortStore = yield* registry.getOrLoad(shortOptions).pipe(Effect.scoped)
|
|
268
|
+
expect(nextShortStore).not.toBe(shortStore)
|
|
321
269
|
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
270
|
+
const cachedLongStore = yield* registry.getOrLoad(longOptions).pipe(Effect.scoped)
|
|
271
|
+
expect(cachedLongStore).toBe(longStore)
|
|
272
|
+
}),
|
|
273
|
+
)
|
|
326
274
|
|
|
327
|
-
//
|
|
328
|
-
|
|
275
|
+
// This test is skipped because we don't yet support dynamic `unusedCacheTime` updates for cached stores.
|
|
276
|
+
// See https://github.com/livestorejs/livestore/issues/918
|
|
277
|
+
it.scoped.skip('keeps the longest unusedCacheTime seen for a store when options vary across calls', () =>
|
|
278
|
+
Effect.gen(function* () {
|
|
279
|
+
const runtime = yield* Effect.runtime<Scope.Scope | OtelTracer.OtelTracer>()
|
|
280
|
+
const registry = new StoreRegistry({ runtime })
|
|
329
281
|
|
|
330
|
-
|
|
331
|
-
|
|
282
|
+
const options = testStoreOptions({ unusedCacheTime: 10 })
|
|
283
|
+
const release = registry.retain(options)
|
|
332
284
|
|
|
333
|
-
|
|
334
|
-
const abortedPromise = (loadPromise as Promise<unknown>).catch(() => {
|
|
335
|
-
// Expected: load was aborted by disposal
|
|
336
|
-
})
|
|
285
|
+
const store = yield* registry.getOrLoad(options).pipe(Effect.scoped)
|
|
337
286
|
|
|
338
|
-
|
|
339
|
-
|
|
287
|
+
// Call with longer unusedCacheTime
|
|
288
|
+
yield* registry.getOrLoad(testStoreOptions({ unusedCacheTime: 100 })).pipe(Effect.scoped)
|
|
340
289
|
|
|
341
|
-
|
|
342
|
-
|
|
290
|
+
release()
|
|
291
|
+
yield* Effect.yieldNow()
|
|
343
292
|
|
|
344
|
-
|
|
345
|
-
|
|
293
|
+
// After 99ms, store should still be alive (100ms unusedCacheTime used)
|
|
294
|
+
yield* TestClock.adjust(99)
|
|
346
295
|
|
|
347
|
-
|
|
348
|
-
|
|
296
|
+
const cached = yield* registry.getOrLoad(options).pipe(Effect.scoped)
|
|
297
|
+
expect(cached).toBe(store)
|
|
349
298
|
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
expect(freshLoadPromise).not.toBe(loadPromise)
|
|
299
|
+
// After 1 more ms, store should be disposed
|
|
300
|
+
yield* TestClock.adjust(1)
|
|
353
301
|
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
302
|
+
const nextStore = yield* registry.getOrLoad(options).pipe(Effect.scoped)
|
|
303
|
+
expect(nextStore).not.toBe(store)
|
|
304
|
+
}),
|
|
305
|
+
)
|
|
357
306
|
|
|
358
|
-
|
|
359
|
-
|
|
307
|
+
it.scoped('handles rapid retain/release cycles without errors', () =>
|
|
308
|
+
Effect.gen(function* () {
|
|
309
|
+
const unusedCacheTime = 50
|
|
310
|
+
const runtime = yield* Effect.runtime<Scope.Scope | OtelTracer.OtelTracer>()
|
|
311
|
+
const registry = new StoreRegistry({ runtime, defaultOptions: { unusedCacheTime } })
|
|
312
|
+
const options = testStoreOptions()
|
|
360
313
|
|
|
361
|
-
|
|
362
|
-
const unusedCacheTime = 50
|
|
363
|
-
const storeRegistry = new StoreRegistry({ defaultOptions: { unusedCacheTime } })
|
|
364
|
-
const options = testStoreOptions()
|
|
314
|
+
const store = yield* registry.getOrLoad(options).pipe(Effect.scoped)
|
|
365
315
|
|
|
366
|
-
|
|
367
|
-
|
|
316
|
+
// Rapidly retain and release multiple times
|
|
317
|
+
for (let i = 0; i < 10; i++) {
|
|
318
|
+
const release = registry.retain(options)
|
|
319
|
+
release()
|
|
320
|
+
}
|
|
368
321
|
|
|
369
|
-
|
|
370
|
-
|
|
322
|
+
yield* Effect.yieldNow()
|
|
323
|
+
yield* TestClock.adjust(unusedCacheTime)
|
|
371
324
|
|
|
372
|
-
|
|
373
|
-
|
|
325
|
+
// Store should be disposed after the last release
|
|
326
|
+
const nextStore = yield* registry.getOrLoad(options).pipe(Effect.scoped)
|
|
327
|
+
expect(nextStore).not.toBe(store)
|
|
328
|
+
}),
|
|
329
|
+
)
|
|
374
330
|
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
331
|
+
it.scoped('cancels disposal when new retain', () =>
|
|
332
|
+
Effect.gen(function* () {
|
|
333
|
+
const unusedCacheTime = 50
|
|
334
|
+
const runtime = yield* Effect.runtime<Scope.Scope | OtelTracer.OtelTracer>()
|
|
335
|
+
const registry = new StoreRegistry({ runtime, defaultOptions: { unusedCacheTime } })
|
|
336
|
+
const options = testStoreOptions()
|
|
378
337
|
|
|
379
|
-
|
|
380
|
-
await store.shutdownPromise()
|
|
381
|
-
})
|
|
338
|
+
const store = yield* registry.getOrLoad(options).pipe(Effect.scoped)
|
|
382
339
|
|
|
383
|
-
|
|
384
|
-
const unusedCacheTime = 50
|
|
385
|
-
const storeRegistry = new StoreRegistry({ defaultOptions: { unusedCacheTime } })
|
|
340
|
+
yield* Effect.yieldNow()
|
|
386
341
|
|
|
387
|
-
|
|
388
|
-
|
|
342
|
+
// Advance almost to disposal threshold
|
|
343
|
+
yield* TestClock.adjust(unusedCacheTime - 5)
|
|
389
344
|
|
|
390
|
-
|
|
391
|
-
|
|
345
|
+
// Add a new retain before disposal triggers
|
|
346
|
+
const release = registry.retain(options)
|
|
392
347
|
|
|
393
|
-
|
|
394
|
-
|
|
348
|
+
// Complete the original unusedCacheTime
|
|
349
|
+
yield* TestClock.adjust(5)
|
|
395
350
|
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
351
|
+
// Store should not have been disposed because retain keeps it alive
|
|
352
|
+
const cached = yield* registry.getOrLoad(options).pipe(Effect.scoped)
|
|
353
|
+
expect(cached).toBe(store)
|
|
399
354
|
|
|
400
|
-
|
|
401
|
-
|
|
355
|
+
// Release retain — new idle timer starts
|
|
356
|
+
release()
|
|
357
|
+
yield* Effect.yieldNow()
|
|
402
358
|
|
|
403
|
-
|
|
404
|
-
const newStore1 = await storeRegistry.getOrLoadPromise(options1)
|
|
405
|
-
expect(newStore1).not.toBe(store1)
|
|
359
|
+
yield* TestClock.adjust(unusedCacheTime)
|
|
406
360
|
|
|
407
|
-
|
|
408
|
-
|
|
361
|
+
// Now it should be disposed
|
|
362
|
+
const nextStore = yield* registry.getOrLoad(options).pipe(Effect.scoped)
|
|
363
|
+
expect(nextStore).not.toBe(store)
|
|
364
|
+
}),
|
|
365
|
+
)
|
|
409
366
|
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
367
|
+
it.scoped('aborts loading when disposal fires while store is still loading', () =>
|
|
368
|
+
Effect.gen(function* () {
|
|
369
|
+
const unusedCacheTime = 10
|
|
370
|
+
const loadDelay = 1000
|
|
371
|
+
const runtime = yield* Effect.runtime<Scope.Scope | OtelTracer.OtelTracer>()
|
|
372
|
+
const registry = new StoreRegistry({ runtime, defaultOptions: { unusedCacheTime } })
|
|
373
|
+
|
|
374
|
+
// Adapter that takes time to load (controlled by TestClock)
|
|
375
|
+
const baseAdapter = makeInMemoryAdapter()
|
|
376
|
+
const options = testStoreOptions({
|
|
377
|
+
adapter: ((args: any) =>
|
|
378
|
+
Effect.gen(function* () {
|
|
379
|
+
yield* Effect.sleep(loadDelay)
|
|
380
|
+
return yield* baseAdapter(args)
|
|
381
|
+
})) as any,
|
|
382
|
+
})
|
|
383
|
+
|
|
384
|
+
// Retain triggers loading (won't complete until clock advances past loadDelay)
|
|
385
|
+
const release = registry.retain(options)
|
|
386
|
+
yield* Effect.yieldNow()
|
|
387
|
+
|
|
388
|
+
// Release immediately — schedules disposal after unusedCacheTime
|
|
389
|
+
release()
|
|
390
|
+
yield* Effect.yieldNow()
|
|
391
|
+
|
|
392
|
+
// Advance past unusedCacheTime but NOT past loadDelay → disposal fires, interrupts loading
|
|
393
|
+
yield* TestClock.adjust(unusedCacheTime)
|
|
394
|
+
|
|
395
|
+
// Start a fresh load — since the first was aborted, this should be a new entry
|
|
396
|
+
const freshLoadFiber = yield* Effect.fork(registry.getOrLoad(options).pipe(Effect.scoped))
|
|
397
|
+
yield* Effect.yieldNow()
|
|
398
|
+
|
|
399
|
+
// Advance enough for the fresh load to complete
|
|
400
|
+
yield* TestClock.adjust(loadDelay)
|
|
401
|
+
const store = yield* Fiber.join(freshLoadFiber)
|
|
402
|
+
|
|
403
|
+
expect(store).toBeDefined()
|
|
404
|
+
}),
|
|
405
|
+
)
|
|
406
|
+
|
|
407
|
+
it.scoped('retain keeps store alive past unusedCacheTime', () =>
|
|
408
|
+
Effect.gen(function* () {
|
|
409
|
+
const unusedCacheTime = 50
|
|
410
|
+
const runtime = yield* Effect.runtime<Scope.Scope | OtelTracer.OtelTracer>()
|
|
411
|
+
const registry = new StoreRegistry({ runtime, defaultOptions: { unusedCacheTime } })
|
|
412
|
+
const options = testStoreOptions()
|
|
414
413
|
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
defaultOptions: {
|
|
418
|
-
unusedCacheTime: 100,
|
|
419
|
-
},
|
|
420
|
-
})
|
|
414
|
+
// Load the store
|
|
415
|
+
const store = yield* registry.getOrLoad(options).pipe(Effect.scoped)
|
|
421
416
|
|
|
422
|
-
|
|
417
|
+
// Retain the store before disposal could fire
|
|
418
|
+
const release = registry.retain(options)
|
|
423
419
|
|
|
424
|
-
|
|
420
|
+
yield* Effect.yieldNow()
|
|
425
421
|
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
expect(store[StoreInternalsSymbol].clientSession.debugInstanceId).toBeDefined()
|
|
422
|
+
// Advance past unusedCacheTime — idle timer fires but refCount > 0, so no eviction
|
|
423
|
+
yield* TestClock.adjust(unusedCacheTime + 50)
|
|
429
424
|
|
|
430
|
-
|
|
431
|
-
|
|
425
|
+
// Store should still be cached because retain keeps it alive
|
|
426
|
+
const cached = yield* registry.getOrLoad(options).pipe(Effect.scoped)
|
|
427
|
+
expect(cached).toBe(store)
|
|
432
428
|
|
|
433
|
-
|
|
434
|
-
|
|
429
|
+
release()
|
|
430
|
+
}),
|
|
431
|
+
)
|
|
435
432
|
|
|
436
|
-
|
|
437
|
-
|
|
433
|
+
it.scoped('manages multiple stores with different IDs independently', () =>
|
|
434
|
+
Effect.gen(function* () {
|
|
435
|
+
const unusedCacheTime = 50
|
|
436
|
+
const runtime = yield* Effect.runtime<Scope.Scope | OtelTracer.OtelTracer>()
|
|
437
|
+
const registry = new StoreRegistry({ runtime, defaultOptions: { unusedCacheTime } })
|
|
438
438
|
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
const storeRegistry = new StoreRegistry({ defaultOptions: { unusedCacheTime } })
|
|
442
|
-
const options = testStoreOptions()
|
|
439
|
+
const options1 = testStoreOptions({ storeId: 'store-1' })
|
|
440
|
+
const options2 = testStoreOptions({ storeId: 'store-2' })
|
|
443
441
|
|
|
444
|
-
|
|
445
|
-
|
|
442
|
+
const store1 = yield* registry.getOrLoad(options1).pipe(Effect.scoped)
|
|
443
|
+
const store2 = yield* registry.getOrLoad(options2).pipe(Effect.scoped)
|
|
446
444
|
|
|
447
|
-
|
|
448
|
-
|
|
445
|
+
// Should be different store instances
|
|
446
|
+
expect(store1).not.toBe(store2)
|
|
449
447
|
|
|
450
|
-
|
|
451
|
-
|
|
448
|
+
// Both should be cached independently
|
|
449
|
+
const cached1 = yield* registry.getOrLoad(options1).pipe(Effect.scoped)
|
|
450
|
+
const cached2 = yield* registry.getOrLoad(options2).pipe(Effect.scoped)
|
|
451
|
+
expect(cached1).toBe(store1)
|
|
452
|
+
expect(cached2).toBe(store2)
|
|
452
453
|
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
const storeOrPromise = storeRegistry.getOrLoadPromise(options)
|
|
454
|
+
yield* Effect.yieldNow()
|
|
455
|
+
yield* TestClock.adjust(unusedCacheTime)
|
|
456
456
|
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
457
|
+
// Both stores should be disposed
|
|
458
|
+
const newStore1 = yield* registry.getOrLoad(options1).pipe(Effect.scoped)
|
|
459
|
+
const newStore2 = yield* registry.getOrLoad(options2).pipe(Effect.scoped)
|
|
460
|
+
expect(newStore1).not.toBe(store1)
|
|
461
|
+
expect(newStore2).not.toBe(store2)
|
|
462
|
+
}),
|
|
463
|
+
)
|
|
460
464
|
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
465
|
+
it.scoped('applies default options from constructor', () =>
|
|
466
|
+
Effect.gen(function* () {
|
|
467
|
+
const runtime = yield* Effect.runtime<Scope.Scope | OtelTracer.OtelTracer>()
|
|
468
|
+
const registry = new StoreRegistry({ runtime, defaultOptions: { unusedCacheTime: 100 } })
|
|
469
|
+
const options = testStoreOptions()
|
|
466
470
|
|
|
467
|
-
|
|
468
|
-
const storeRegistry = new StoreRegistry()
|
|
469
|
-
const options = testStoreOptions()
|
|
471
|
+
const store = yield* registry.getOrLoad(options).pipe(Effect.scoped)
|
|
470
472
|
|
|
471
|
-
|
|
472
|
-
|
|
473
|
+
// Verify the store loads successfully
|
|
474
|
+
expect(store).toBeDefined()
|
|
475
|
+
expect(store[StoreInternalsSymbol].clientSession.debugInstanceId).toBeDefined()
|
|
473
476
|
|
|
474
|
-
|
|
475
|
-
const store = storeRegistry.getOrLoadPromise(options)
|
|
476
|
-
expect(store).not.toBeInstanceOf(Promise)
|
|
477
|
+
yield* Effect.yieldNow()
|
|
477
478
|
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
throw new Error('Expected store, got Promise')
|
|
481
|
-
}
|
|
479
|
+
// After 50ms, store should still be cached (default is 100ms)
|
|
480
|
+
yield* TestClock.adjust(50)
|
|
482
481
|
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
482
|
+
const cached = yield* registry.getOrLoad(options).pipe(Effect.scoped)
|
|
483
|
+
expect(cached).toBe(store)
|
|
484
|
+
}),
|
|
485
|
+
)
|
|
486
486
|
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
487
|
+
it.scoped('does not serve a disposed store from cache', () =>
|
|
488
|
+
Effect.gen(function* () {
|
|
489
|
+
const unusedCacheTime = 25
|
|
490
|
+
const runtime = yield* Effect.runtime<Scope.Scope | OtelTracer.OtelTracer>()
|
|
491
|
+
const registry = new StoreRegistry({ runtime, defaultOptions: { unusedCacheTime } })
|
|
492
|
+
const options = testStoreOptions()
|
|
491
493
|
|
|
492
|
-
|
|
493
|
-
await storeRegistry.preload(options)
|
|
494
|
+
const originalStore = yield* registry.getOrLoad(options).pipe(Effect.scoped)
|
|
494
495
|
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
496
|
+
// Verify store is cached
|
|
497
|
+
const cached = yield* registry.getOrLoad(options).pipe(Effect.scoped)
|
|
498
|
+
expect(cached).toBe(originalStore)
|
|
498
499
|
|
|
499
|
-
|
|
500
|
-
|
|
500
|
+
yield* Effect.yieldNow()
|
|
501
|
+
yield* TestClock.adjust(unusedCacheTime)
|
|
501
502
|
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
503
|
+
// After disposal, calling getOrLoad should produce a fresh store
|
|
504
|
+
const freshStore = yield* registry.getOrLoad(options).pipe(Effect.scoped)
|
|
505
|
+
expect(freshStore).not.toBe(originalStore)
|
|
506
|
+
}),
|
|
507
|
+
)
|
|
505
508
|
|
|
506
|
-
|
|
509
|
+
it.scoped('schedules disposal after preload if no retainers are added', () =>
|
|
510
|
+
Effect.gen(function* () {
|
|
511
|
+
const unusedCacheTime = 50
|
|
512
|
+
const runtime = yield* Effect.runtime<Scope.Scope | OtelTracer.OtelTracer>()
|
|
513
|
+
const registry = new StoreRegistry({ runtime, defaultOptions: { unusedCacheTime } })
|
|
514
|
+
const options = testStoreOptions()
|
|
515
|
+
|
|
516
|
+
// Preload without retaining (load + immediate release)
|
|
517
|
+
const store = yield* registry.getOrLoad(options).pipe(Effect.scoped)
|
|
518
|
+
|
|
519
|
+
// Verify it's cached
|
|
520
|
+
const cached = yield* registry.getOrLoad(options).pipe(Effect.scoped)
|
|
521
|
+
expect(cached).toBe(store)
|
|
522
|
+
|
|
523
|
+
yield* Effect.yieldNow()
|
|
524
|
+
yield* TestClock.adjust(unusedCacheTime)
|
|
525
|
+
|
|
526
|
+
// Store should be disposed since no retainers were added
|
|
527
|
+
const nextStore = yield* registry.getOrLoad(options).pipe(Effect.scoped)
|
|
528
|
+
expect(nextStore).not.toBe(store)
|
|
529
|
+
}),
|
|
530
|
+
)
|
|
507
531
|
})
|
|
508
532
|
})
|
|
509
533
|
|