@livestore/livestore 0.4.0 → 0.5.0-dev.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/.tsbuildinfo +1 -1
- package/dist/SqliteDbWrapper.d.ts.map +1 -1
- package/dist/SqliteDbWrapper.js +7 -5
- package/dist/SqliteDbWrapper.js.map +1 -1
- package/dist/SqliteDbWrapper.test.js +20 -3
- package/dist/SqliteDbWrapper.test.js.map +1 -1
- package/dist/effect/LiveStore.d.ts +15 -15
- package/dist/effect/LiveStore.d.ts.map +1 -1
- package/dist/effect/LiveStore.js +17 -17
- package/dist/effect/LiveStore.js.map +1 -1
- package/dist/effect/LiveStore.test.js +5 -6
- package/dist/effect/LiveStore.test.js.map +1 -1
- package/dist/live-queries/base-class.d.ts +19 -2
- package/dist/live-queries/base-class.d.ts.map +1 -1
- package/dist/live-queries/base-class.js +28 -2
- package/dist/live-queries/base-class.js.map +1 -1
- package/dist/live-queries/computed.d.ts.map +1 -1
- package/dist/live-queries/computed.js +4 -4
- package/dist/live-queries/computed.js.map +1 -1
- package/dist/live-queries/db-query.d.ts +2 -2
- package/dist/live-queries/db-query.d.ts.map +1 -1
- package/dist/live-queries/db-query.js +26 -17
- package/dist/live-queries/db-query.js.map +1 -1
- package/dist/live-queries/db-query.test.js +9 -9
- package/dist/live-queries/db-query.test.js.map +1 -1
- package/dist/live-queries/signal.d.ts.map +1 -1
- package/dist/live-queries/signal.js +2 -2
- package/dist/live-queries/signal.js.map +1 -1
- package/dist/live-queries/signal.test.js +2 -2
- package/dist/live-queries/signal.test.js.map +1 -1
- package/dist/reactive.d.ts +1 -1
- package/dist/reactive.d.ts.map +1 -1
- package/dist/reactive.js +20 -1
- package/dist/reactive.js.map +1 -1
- package/dist/store/StoreRegistry.d.ts +12 -12
- package/dist/store/StoreRegistry.d.ts.map +1 -1
- package/dist/store/StoreRegistry.js +24 -24
- package/dist/store/StoreRegistry.js.map +1 -1
- package/dist/store/StoreRegistry.test.js +62 -59
- package/dist/store/StoreRegistry.test.js.map +1 -1
- package/dist/store/create-store.d.ts +12 -15
- package/dist/store/create-store.d.ts.map +1 -1
- package/dist/store/create-store.js +43 -24
- package/dist/store/create-store.js.map +1 -1
- package/dist/store/devtools.d.ts +1 -2
- package/dist/store/devtools.d.ts.map +1 -1
- package/dist/store/devtools.js +3 -2
- package/dist/store/devtools.js.map +1 -1
- package/dist/store/store-eventstream.test.js +5 -5
- package/dist/store/store-eventstream.test.js.map +1 -1
- package/dist/store/store-types.d.ts +10 -28
- package/dist/store/store-types.d.ts.map +1 -1
- package/dist/store/store-types.js +2 -38
- package/dist/store/store-types.js.map +1 -1
- package/dist/store/store-types.test.js +1 -1
- package/dist/store/store.d.ts +2 -2
- package/dist/store/store.d.ts.map +1 -1
- package/dist/store/store.js +114 -56
- package/dist/store/store.js.map +1 -1
- package/dist/utils/dev.js +1 -1
- package/dist/utils/dev.js.map +1 -1
- package/dist/utils/stack-info.d.ts.map +1 -1
- package/dist/utils/stack-info.js +2 -1
- package/dist/utils/stack-info.js.map +1 -1
- package/dist/utils/tests/fixture.d.ts +26 -34
- package/dist/utils/tests/fixture.d.ts.map +1 -1
- package/package.json +14 -24
- package/src/SqliteDbWrapper.test.ts +25 -3
- package/src/SqliteDbWrapper.ts +12 -9
- package/src/effect/LiveStore.test.ts +16 -16
- package/src/effect/LiveStore.ts +79 -50
- package/src/live-queries/__snapshots__/db-query.test.ts.snap +273 -49
- package/src/live-queries/base-class.ts +38 -6
- package/src/live-queries/computed.ts +5 -5
- package/src/live-queries/db-query.test.ts +9 -9
- package/src/live-queries/db-query.ts +42 -30
- package/src/live-queries/signal.test.ts +2 -2
- package/src/live-queries/signal.ts +3 -3
- package/src/reactive.ts +45 -35
- package/src/store/StoreRegistry.test.ts +62 -59
- package/src/store/StoreRegistry.ts +49 -38
- package/src/store/create-store.ts +85 -51
- package/src/store/devtools.ts +283 -283
- package/src/store/store-eventstream.test.ts +16 -7
- package/src/store/store-types.test.ts +1 -1
- package/src/store/store-types.ts +10 -56
- package/src/store/store.ts +148 -71
- package/src/utils/dev.ts +1 -1
- package/src/utils/stack-info.ts +4 -1
|
@@ -9,8 +9,17 @@ import type { ShutdownDeferred, Store } from '@livestore/livestore'
|
|
|
9
9
|
import { createStore, makeShutdownDeferred } from '@livestore/livestore'
|
|
10
10
|
import { omitUndefineds } from '@livestore/utils'
|
|
11
11
|
import { Vitest } from '@livestore/utils-dev/node-vitest'
|
|
12
|
-
import
|
|
13
|
-
|
|
12
|
+
import {
|
|
13
|
+
type OtelTracer,
|
|
14
|
+
type Scope,
|
|
15
|
+
Context,
|
|
16
|
+
Effect,
|
|
17
|
+
FetchHttpClient,
|
|
18
|
+
Layer,
|
|
19
|
+
Queue,
|
|
20
|
+
References,
|
|
21
|
+
Stream,
|
|
22
|
+
} from '@livestore/utils/effect'
|
|
14
23
|
import { nanoid } from '@livestore/utils/nanoid'
|
|
15
24
|
import { PlatformNode } from '@livestore/utils/node'
|
|
16
25
|
|
|
@@ -22,7 +31,7 @@ const withTestCtx = Vitest.makeWithTestCtx({
|
|
|
22
31
|
TestContextLive,
|
|
23
32
|
PlatformNode.NodeFileSystem.layer,
|
|
24
33
|
FetchHttpClient.layer,
|
|
25
|
-
|
|
34
|
+
Layer.succeed(References.MinimumLogLevel, 'Debug'),
|
|
26
35
|
),
|
|
27
36
|
})
|
|
28
37
|
|
|
@@ -32,7 +41,7 @@ const withTestCtx = Vitest.makeWithTestCtx({
|
|
|
32
41
|
* tests/package-common/src/leader-thread/stream-events.test.ts
|
|
33
42
|
*/
|
|
34
43
|
Vitest.describe('Store events API', () => {
|
|
35
|
-
Vitest.
|
|
44
|
+
Vitest.live('should resume when reconnected to sync backend', (test) =>
|
|
36
45
|
Effect.gen(function* () {
|
|
37
46
|
const { makeStore, mockSyncBackend } = yield* TestContext
|
|
38
47
|
const store = yield* makeStore()
|
|
@@ -69,7 +78,7 @@ Vitest.describe('Store events API', () => {
|
|
|
69
78
|
)
|
|
70
79
|
})
|
|
71
80
|
|
|
72
|
-
class TestContext extends Context.
|
|
81
|
+
class TestContext extends Context.Service<
|
|
73
82
|
TestContext,
|
|
74
83
|
{
|
|
75
84
|
makeStore: (args?: {
|
|
@@ -87,9 +96,9 @@ class TestContext extends Context.Tag('TestContext')<
|
|
|
87
96
|
mockSyncBackend: MockSyncBackend
|
|
88
97
|
shutdownDeferred: ShutdownDeferred
|
|
89
98
|
}
|
|
90
|
-
>() {}
|
|
99
|
+
>()('TestContext') {}
|
|
91
100
|
|
|
92
|
-
const TestContextLive = Layer.
|
|
101
|
+
const TestContextLive = Layer.effect(
|
|
93
102
|
TestContext,
|
|
94
103
|
Effect.gen(function* () {
|
|
95
104
|
const mockSyncBackend = yield* makeMockSyncBackend()
|
|
@@ -20,7 +20,7 @@ describe('isQueryable', () => {
|
|
|
20
20
|
it('identifies live query definitions', () => {
|
|
21
21
|
const def = queryDb({
|
|
22
22
|
query: 'select 1 as value',
|
|
23
|
-
schema: Schema.Array(Schema.Struct({ value: Schema.
|
|
23
|
+
schema: Schema.Array(Schema.Struct({ value: Schema.Finite })),
|
|
24
24
|
})
|
|
25
25
|
|
|
26
26
|
expect(isQueryable(def)).toBe(true)
|
package/src/store/store-types.ts
CHANGED
|
@@ -1,21 +1,19 @@
|
|
|
1
1
|
import type * as otel from '@opentelemetry/api'
|
|
2
2
|
|
|
3
3
|
import {
|
|
4
|
+
type BackendIdMismatchError,
|
|
4
5
|
type ClientSession,
|
|
5
6
|
type ClientSessionSyncProcessor,
|
|
6
|
-
type ClientSessionSyncProcessorSimulationParams,
|
|
7
7
|
type IntentionalShutdownCause,
|
|
8
8
|
isQueryBuilder,
|
|
9
9
|
type MaterializeError,
|
|
10
10
|
type QueryBuilder,
|
|
11
11
|
type StoreInterrupted,
|
|
12
|
-
type BackendIdMismatchError,
|
|
13
12
|
type UnknownError,
|
|
14
13
|
} from '@livestore/common'
|
|
15
14
|
import type { StreamEventsOptions } from '@livestore/common/leader-thread'
|
|
16
15
|
import type { LiveStoreEvent, LiveStoreSchema } from '@livestore/common/schema'
|
|
17
|
-
import type
|
|
18
|
-
import { Deferred, Predicate } from '@livestore/utils/effect'
|
|
16
|
+
import { type Context, Deferred, type Effect, Predicate, type Schema, type Scope } from '@livestore/utils/effect'
|
|
19
17
|
|
|
20
18
|
import type {
|
|
21
19
|
LiveQuery,
|
|
@@ -24,7 +22,7 @@ import type {
|
|
|
24
22
|
ReactivityGraphContext,
|
|
25
23
|
SignalDef,
|
|
26
24
|
} from '../live-queries/base-class.ts'
|
|
27
|
-
import { TypeId } from '../live-queries/base-class.ts'
|
|
25
|
+
import { isLiveQueryDef, TypeId } from '../live-queries/base-class.ts'
|
|
28
26
|
import type { DebugRefreshReasonBase, Ref } from '../reactive.ts'
|
|
29
27
|
import type { SqliteDbWrapper } from '../SqliteDbWrapper.ts'
|
|
30
28
|
import type { ReferenceCountedSet } from '../utils/data-structures.ts'
|
|
@@ -98,7 +96,7 @@ export type StoreInternals = {
|
|
|
98
96
|
* Exposed primarily for Devtools (e.g. databrowser) to validate ad‑hoc
|
|
99
97
|
* event payloads. Application code should not depend on it directly.
|
|
100
98
|
*/
|
|
101
|
-
readonly eventSchema: Schema.
|
|
99
|
+
readonly eventSchema: Schema.Codec<LiveStoreEvent.Client.Decoded, LiveStoreEvent.Client.Encoded>
|
|
102
100
|
|
|
103
101
|
/**
|
|
104
102
|
* The active client session backing this Store. Provides access to the
|
|
@@ -115,14 +113,14 @@ export type StoreInternals = {
|
|
|
115
113
|
readonly sqliteDbWrapper: SqliteDbWrapper
|
|
116
114
|
|
|
117
115
|
/**
|
|
118
|
-
* Effect
|
|
116
|
+
* Effect context and scope used to fork background fibers for the Store.
|
|
119
117
|
*
|
|
120
|
-
* - `
|
|
118
|
+
* - `services` provides services when executing effects from imperative Store APIs.
|
|
121
119
|
* - `lifetimeScope` owns forked fibers; closed during Store shutdown.
|
|
122
120
|
*/
|
|
123
121
|
readonly effectContext: {
|
|
124
|
-
/** Effect
|
|
125
|
-
readonly
|
|
122
|
+
/** Effect context used to run Store effects with proper services. */
|
|
123
|
+
readonly services: Context.Context<Scope.Scope>
|
|
126
124
|
/** Scope that owns all long‑lived fibers spawned by the Store. */
|
|
127
125
|
readonly lifetimeScope: Scope.Scope
|
|
128
126
|
}
|
|
@@ -189,7 +187,7 @@ export type StoreConstructorParams<TSchema extends LiveStoreSchema = LiveStoreSc
|
|
|
189
187
|
context: TContext
|
|
190
188
|
otelOptions: OtelOptions
|
|
191
189
|
effectContext: {
|
|
192
|
-
|
|
190
|
+
services: Context.Context<Scope.Scope>
|
|
193
191
|
lifetimeScope: Scope.Scope
|
|
194
192
|
}
|
|
195
193
|
confirmUnsavedChanges: boolean
|
|
@@ -197,9 +195,6 @@ export type StoreConstructorParams<TSchema extends LiveStoreSchema = LiveStoreSc
|
|
|
197
195
|
params: {
|
|
198
196
|
leaderPushBatchSize: number
|
|
199
197
|
eventQueryBatchSize?: number
|
|
200
|
-
simulation?: {
|
|
201
|
-
clientSessionSyncProcessor: typeof ClientSessionSyncProcessorSimulationParams.Type
|
|
202
|
-
}
|
|
203
198
|
}
|
|
204
199
|
__runningInDevtools: boolean
|
|
205
200
|
}
|
|
@@ -335,48 +330,7 @@ export namespace Queryable {
|
|
|
335
330
|
export type Result<TQueryable extends Queryable<any>> = TQueryable extends Queryable<infer TResult> ? TResult : never
|
|
336
331
|
}
|
|
337
332
|
|
|
338
|
-
|
|
339
|
-
* Type guard that checks if a value is a query or signal definition.
|
|
340
|
-
*
|
|
341
|
-
* Use this to distinguish between definitions (blueprints) and instances (live queries).
|
|
342
|
-
* Definitions are created by `queryDb()`, `computed()`, and `signal()`.
|
|
343
|
-
*
|
|
344
|
-
* @example
|
|
345
|
-
* ```ts
|
|
346
|
-
* const todos$ = queryDb(tables.todos.all())
|
|
347
|
-
*
|
|
348
|
-
* if (isLiveQueryDef(todos$)) {
|
|
349
|
-
* console.log('This is a definition:', todos$.label)
|
|
350
|
-
* }
|
|
351
|
-
* ```
|
|
352
|
-
*/
|
|
353
|
-
export const isLiveQueryDef = (value: unknown): value is LiveQueryDef<any> | SignalDef<any> => {
|
|
354
|
-
if (typeof value !== 'object' || value === null) {
|
|
355
|
-
return false
|
|
356
|
-
}
|
|
357
|
-
|
|
358
|
-
if (!('_tag' in value)) {
|
|
359
|
-
return false
|
|
360
|
-
}
|
|
361
|
-
|
|
362
|
-
const tag = (value as LiveQueryDef<any> | SignalDef<any>)._tag
|
|
363
|
-
if (tag !== 'def' && tag !== 'signal-def') {
|
|
364
|
-
return false
|
|
365
|
-
}
|
|
366
|
-
|
|
367
|
-
const candidate = value as LiveQueryDef<any>
|
|
368
|
-
if (typeof candidate.make !== 'function') {
|
|
369
|
-
// The store calls make() to turn the definition into a live query instance.
|
|
370
|
-
return false
|
|
371
|
-
}
|
|
372
|
-
|
|
373
|
-
if (typeof candidate.hash !== 'string' || typeof candidate.label !== 'string') {
|
|
374
|
-
// Both identifiers must be present so the store can cache and log the query.
|
|
375
|
-
return false
|
|
376
|
-
}
|
|
377
|
-
|
|
378
|
-
return true
|
|
379
|
-
}
|
|
333
|
+
export { isLiveQueryDef }
|
|
380
334
|
|
|
381
335
|
/**
|
|
382
336
|
* Type guard that checks if a value is a live query instance.
|
package/src/store/store.ts
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import * as otel from '@opentelemetry/api'
|
|
2
|
+
|
|
1
3
|
import {
|
|
2
4
|
type Bindable,
|
|
3
5
|
type ClientSession,
|
|
@@ -11,10 +13,11 @@ import {
|
|
|
11
13
|
MaterializeError,
|
|
12
14
|
MaterializerHashMismatchError,
|
|
13
15
|
makeClientSessionSyncProcessor,
|
|
14
|
-
type PreparedBindValues,
|
|
15
16
|
prepareBindValues,
|
|
16
17
|
QueryBuilderAstSymbol,
|
|
17
|
-
|
|
18
|
+
resolveSessionIdSymbolInBindValues,
|
|
19
|
+
SqliteDbHelper,
|
|
20
|
+
StateHead,
|
|
18
21
|
type StorageMode,
|
|
19
22
|
type SyncState,
|
|
20
23
|
UnknownError,
|
|
@@ -23,8 +26,8 @@ import type { StreamEventsOptions } from '@livestore/common/leader-thread'
|
|
|
23
26
|
import type { LiveStoreSchema } from '@livestore/common/schema'
|
|
24
27
|
import { EventSequenceNumber, LiveStoreEvent, resolveEventDef, SystemTables } from '@livestore/common/schema'
|
|
25
28
|
import { assertNever, isDevEnv, objectToString, omitUndefineds, shouldNeverHappen } from '@livestore/utils'
|
|
26
|
-
import type { Scope } from '@livestore/utils/effect'
|
|
27
29
|
import {
|
|
30
|
+
type Scope,
|
|
28
31
|
Cause,
|
|
29
32
|
Effect,
|
|
30
33
|
Exit,
|
|
@@ -32,12 +35,12 @@ import {
|
|
|
32
35
|
Inspectable,
|
|
33
36
|
Option,
|
|
34
37
|
OtelTracer,
|
|
35
|
-
|
|
38
|
+
Queue,
|
|
39
|
+
Result,
|
|
36
40
|
Schema,
|
|
37
41
|
Stream,
|
|
38
42
|
} from '@livestore/utils/effect'
|
|
39
43
|
import { nanoid } from '@livestore/utils/nanoid'
|
|
40
|
-
import * as otel from '@opentelemetry/api'
|
|
41
44
|
|
|
42
45
|
import type { LiveQuery, ReactivityGraphContext, SignalDef } from '../live-queries/base-class.ts'
|
|
43
46
|
import { makeReactivityGraph } from '../live-queries/base-class.ts'
|
|
@@ -206,14 +209,15 @@ export class Store<TSchema extends LiveStoreSchema = LiveStoreSchema.Any, TConte
|
|
|
206
209
|
this.storageMode = clientSession.leaderThread.initialState.storageMode
|
|
207
210
|
|
|
208
211
|
const reactivityGraph = makeReactivityGraph()
|
|
212
|
+
const stateHead = StateHead.make({ dbState: clientSession.sqliteDb })
|
|
209
213
|
|
|
210
214
|
const syncProcessor = makeClientSessionSyncProcessor({
|
|
211
215
|
schema,
|
|
212
216
|
clientSession,
|
|
213
217
|
materializeEvent: Effect.fn('client-session-sync-processor:materialize-event')(
|
|
214
|
-
(eventEncoded, {
|
|
218
|
+
(eventEncoded, { materializerHashLeader }) =>
|
|
215
219
|
// We need to use `Effect.gen` (even though we're using `Effect.fn`) so that we can pass `this` to the function
|
|
216
|
-
Effect.gen(this, function* () {
|
|
220
|
+
Effect.gen({ self: this }, function* () {
|
|
217
221
|
const resolution = yield* resolveEventDef(schema, {
|
|
218
222
|
operation: '@livestore/livestore:store:materializeEvent',
|
|
219
223
|
event: eventEncoded,
|
|
@@ -222,6 +226,7 @@ export class Store<TSchema extends LiveStoreSchema = LiveStoreSchema.Any, TConte
|
|
|
222
226
|
if (resolution._tag === 'unknown') {
|
|
223
227
|
// Runtime schema doesn't know this event yet; skip materialization but
|
|
224
228
|
// keep the log entry so upgraded clients can replay it later.
|
|
229
|
+
yield* stateHead.set(eventEncoded.seqNum)
|
|
225
230
|
return {
|
|
226
231
|
writeTables: new Set<string>(),
|
|
227
232
|
sessionChangeset: { _tag: 'no-op' as const },
|
|
@@ -286,18 +291,14 @@ export class Store<TSchema extends LiveStoreSchema = LiveStoreSchema.Any, TConte
|
|
|
286
291
|
}
|
|
287
292
|
}
|
|
288
293
|
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
| { _tag: 'no-op' }
|
|
292
|
-
| { _tag: 'unset' } = { _tag: 'unset' }
|
|
293
|
-
if (withChangeset === true) {
|
|
294
|
-
sessionChangeset = this[StoreInternalsSymbol].sqliteDbWrapper.withChangeset(exec).changeset
|
|
295
|
-
} else {
|
|
296
|
-
exec()
|
|
297
|
-
}
|
|
294
|
+
const sessionChangeset = this[StoreInternalsSymbol].sqliteDbWrapper.withChangeset(exec).changeset
|
|
295
|
+
yield* stateHead.set(eventEncoded.seqNum)
|
|
298
296
|
|
|
299
297
|
return { writeTables: writeTablesForEvent, sessionChangeset, materializerHash }
|
|
300
|
-
}).pipe(
|
|
298
|
+
}).pipe(
|
|
299
|
+
SqliteDbHelper.withSavepoint(clientSession.sqliteDb),
|
|
300
|
+
Effect.mapError((cause) => MaterializeError.make({ cause })),
|
|
301
|
+
),
|
|
301
302
|
),
|
|
302
303
|
rollback: (changeset) => {
|
|
303
304
|
this[StoreInternalsSymbol].sqliteDbWrapper.rollback(changeset)
|
|
@@ -315,12 +316,9 @@ export class Store<TSchema extends LiveStoreSchema = LiveStoreSchema.Any, TConte
|
|
|
315
316
|
...omitUndefineds({
|
|
316
317
|
leaderPushBatchSize: params.leaderPushBatchSize,
|
|
317
318
|
}),
|
|
318
|
-
...(params.simulation?.clientSessionSyncProcessor !== undefined
|
|
319
|
-
? { simulation: params.simulation.clientSessionSyncProcessor }
|
|
320
|
-
: {}),
|
|
321
319
|
},
|
|
322
320
|
confirmUnsavedChanges,
|
|
323
|
-
}).pipe(
|
|
321
|
+
}).pipe(Effect.provideService(StateHead.StateHead, stateHead), Effect.runSyncWith(effectContext.services))
|
|
324
322
|
|
|
325
323
|
// TODO generalize the `tableRefs` concept to allow finer-grained refs
|
|
326
324
|
const tableRefs: { [key: string]: Ref<null, ReactivityGraphContext, RefreshReason> } = {}
|
|
@@ -370,7 +368,7 @@ export class Store<TSchema extends LiveStoreSchema = LiveStoreSchema.Any, TConte
|
|
|
370
368
|
})
|
|
371
369
|
}
|
|
372
370
|
|
|
373
|
-
const boot = Effect.gen(this, function* () {
|
|
371
|
+
const boot = Effect.gen({ self: this }, function* () {
|
|
374
372
|
yield* Effect.addFinalizer(() =>
|
|
375
373
|
Effect.sync(() => {
|
|
376
374
|
// Remove all table refs from the reactivity graph
|
|
@@ -394,7 +392,7 @@ export class Store<TSchema extends LiveStoreSchema = LiveStoreSchema.Any, TConte
|
|
|
394
392
|
|
|
395
393
|
// Initialize internals bag
|
|
396
394
|
this[StoreInternalsSymbol] = {
|
|
397
|
-
eventSchema: LiveStoreEvent.Client.makeSchemaMemo(schema) as Schema.
|
|
395
|
+
eventSchema: LiveStoreEvent.Client.makeSchemaMemo(schema) as Schema.Codec<
|
|
398
396
|
LiveStoreEvent.Client.Decoded,
|
|
399
397
|
LiveStoreEvent.Client.Encoded
|
|
400
398
|
>,
|
|
@@ -570,24 +568,111 @@ export class Store<TSchema extends LiveStoreSchema = LiveStoreSchema.Any, TConte
|
|
|
570
568
|
): AsyncIterable<TResult> => {
|
|
571
569
|
this.checkShutdown('subscribe')
|
|
572
570
|
|
|
573
|
-
|
|
571
|
+
// This used to delegate to `Stream.toAsyncIterable(this.subscribeStream(...))`.
|
|
572
|
+
// With Effect v4, the callback-backed stream producer is forked during stream startup, so a caller could request
|
|
573
|
+
// `next()`, commit an update, and race the actual subscription registration. Register directly here so the first
|
|
574
|
+
// pending `next()` observes post-subscription updates. In the future, this should be refactored back toward a shared
|
|
575
|
+
// stream/async-iterable bridge if Effect exposes a primitive with that synchronous registration guarantee.
|
|
576
|
+
return {
|
|
577
|
+
[Symbol.asyncIterator]: () => {
|
|
578
|
+
const services = this[StoreInternalsSymbol].effectContext.services
|
|
579
|
+
const runSync = Effect.runSyncWith(services)
|
|
580
|
+
const runPromiseExit = Effect.runPromiseExitWith(services)
|
|
581
|
+
let queue: Queue.Queue<TResult, Cause.Done> | undefined
|
|
582
|
+
let isDone = false
|
|
583
|
+
let unsubscribe: Unsubscribe | undefined
|
|
584
|
+
|
|
585
|
+
const done = (): IteratorResult<TResult> => ({ done: true, value: undefined })
|
|
586
|
+
const isClosed = () => isDone
|
|
587
|
+
const isQueueDone = (cause: Cause.Cause<Cause.Done>) => {
|
|
588
|
+
const error = Cause.findError(cause)
|
|
589
|
+
return Result.isSuccess(error) && Cause.isDone(error.success)
|
|
590
|
+
}
|
|
591
|
+
|
|
592
|
+
const ensureQueue = () => {
|
|
593
|
+
// Lazily allocate the queue so creating an async iterator without consuming it has no subscription-side work.
|
|
594
|
+
queue ??= Queue.unbounded<TResult, Cause.Done>().pipe(runSync)
|
|
595
|
+
return queue
|
|
596
|
+
}
|
|
597
|
+
|
|
598
|
+
const emit = (value: TResult) => {
|
|
599
|
+
if (isDone === true) return
|
|
600
|
+
|
|
601
|
+
Queue.offerUnsafe(ensureQueue(), value)
|
|
602
|
+
}
|
|
603
|
+
|
|
604
|
+
const ensureSubscribed = () => {
|
|
605
|
+
// Subscribe from the first `next()` call so callers that request a value before
|
|
606
|
+
// committing updates don't race the asynchronous Stream-to-iterator startup path.
|
|
607
|
+
unsubscribe ??= this.subscribeWithCallback(query, emit, options)
|
|
608
|
+
}
|
|
609
|
+
|
|
610
|
+
const close = (): IteratorResult<TResult> => {
|
|
611
|
+
if (isDone === false) {
|
|
612
|
+
isDone = true
|
|
613
|
+
unsubscribe?.()
|
|
614
|
+
unsubscribe = undefined
|
|
615
|
+
|
|
616
|
+
if (queue !== undefined) {
|
|
617
|
+
// Wake any pending `next()` call and prevent later callback emissions from being buffered.
|
|
618
|
+
Queue.shutdown(queue).pipe(runSync)
|
|
619
|
+
}
|
|
620
|
+
}
|
|
621
|
+
|
|
622
|
+
return done()
|
|
623
|
+
}
|
|
624
|
+
|
|
625
|
+
return {
|
|
626
|
+
next: async () => {
|
|
627
|
+
if (isDone === true) {
|
|
628
|
+
return done()
|
|
629
|
+
}
|
|
630
|
+
|
|
631
|
+
ensureSubscribed()
|
|
632
|
+
|
|
633
|
+
// Delegate buffering and pending-consumer wakeups to Effect's queue implementation.
|
|
634
|
+
const exit = await runPromiseExit(Queue.take(ensureQueue()))
|
|
635
|
+
if (Exit.isSuccess(exit) === true) {
|
|
636
|
+
return { done: false, value: exit.value }
|
|
637
|
+
}
|
|
638
|
+
|
|
639
|
+
if (isClosed() === true || isQueueDone(exit.cause) === true) {
|
|
640
|
+
return done()
|
|
641
|
+
}
|
|
642
|
+
|
|
643
|
+
throw Cause.squash(exit.cause)
|
|
644
|
+
},
|
|
645
|
+
return: () => Promise.resolve(close()),
|
|
646
|
+
throw: (cause) => {
|
|
647
|
+
close()
|
|
648
|
+
return Promise.reject(cause)
|
|
649
|
+
},
|
|
650
|
+
}
|
|
651
|
+
},
|
|
652
|
+
}
|
|
574
653
|
}
|
|
575
654
|
|
|
576
655
|
subscribeStream = <TResult>(query: Queryable<TResult>, options?: SubscribeOptions<TResult>): Stream.Stream<TResult> =>
|
|
577
|
-
Stream.
|
|
578
|
-
Effect.gen(this, function* () {
|
|
656
|
+
Stream.callback<TResult>((emit) =>
|
|
657
|
+
Effect.gen({ self: this }, function* () {
|
|
579
658
|
const otelSpan = yield* OtelTracer.currentOtelSpan.pipe(
|
|
580
|
-
Effect.catchTag('
|
|
659
|
+
Effect.catchTag('NoSuchElementError', () => Effect.void),
|
|
581
660
|
)
|
|
582
661
|
const otelContext =
|
|
583
662
|
otelSpan !== undefined ? otel.trace.setSpan(otel.context.active(), otelSpan) : otel.context.active()
|
|
584
663
|
|
|
585
664
|
yield* Effect.acquireRelease(
|
|
586
665
|
Effect.sync(() =>
|
|
587
|
-
this.subscribe(
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
666
|
+
this.subscribe(
|
|
667
|
+
query,
|
|
668
|
+
(result) => {
|
|
669
|
+
Queue.offerUnsafe(emit, result)
|
|
670
|
+
},
|
|
671
|
+
{
|
|
672
|
+
...options,
|
|
673
|
+
otelContext,
|
|
674
|
+
},
|
|
675
|
+
),
|
|
591
676
|
),
|
|
592
677
|
(unsub) => Effect.sync(() => unsub()),
|
|
593
678
|
)
|
|
@@ -609,7 +694,7 @@ export class Store<TSchema extends LiveStoreSchema = LiveStoreSchema.Any, TConte
|
|
|
609
694
|
* ```
|
|
610
695
|
*/
|
|
611
696
|
query = <TResult>(
|
|
612
|
-
query: Queryable<TResult> | { query: string; bindValues: Bindable; schema?: Schema.
|
|
697
|
+
query: Queryable<TResult> | { query: string; bindValues: Bindable; schema?: Schema.Decoder<TResult> },
|
|
613
698
|
options?: { otelContext?: otel.Context; debugRefreshReason?: RefreshReason },
|
|
614
699
|
): TResult => {
|
|
615
700
|
this.checkShutdown('query')
|
|
@@ -623,7 +708,7 @@ export class Store<TSchema extends LiveStoreSchema = LiveStoreSchema.Any, TConte
|
|
|
623
708
|
},
|
|
624
709
|
) as any
|
|
625
710
|
if (query.schema !== undefined) {
|
|
626
|
-
return Schema.
|
|
711
|
+
return Schema.decodeUnknownSync(query.schema)(res)
|
|
627
712
|
}
|
|
628
713
|
return res
|
|
629
714
|
} else if (isQueryBuilder(query) === true) {
|
|
@@ -640,23 +725,25 @@ export class Store<TSchema extends LiveStoreSchema = LiveStoreSchema.Any, TConte
|
|
|
640
725
|
const sqlRes = query.asSql()
|
|
641
726
|
const schema = getResultSchema(query)
|
|
642
727
|
|
|
643
|
-
//
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
728
|
+
// Query builders preserve SessionIdSymbol so client-document queries can be reused across sessions.
|
|
729
|
+
// SQLite bind values must be concrete primitives, so resolve the symbol only at execution time.
|
|
730
|
+
const resolvedBindValues =
|
|
731
|
+
sqlRes.bindValues === undefined
|
|
732
|
+
? undefined
|
|
733
|
+
: resolveSessionIdSymbolInBindValues(sqlRes.bindValues, this[StoreInternalsSymbol].clientSession.sessionId)
|
|
647
734
|
|
|
648
735
|
const rawRes = this[StoreInternalsSymbol].sqliteDbWrapper.cachedSelect(
|
|
649
736
|
sqlRes.query,
|
|
650
|
-
|
|
737
|
+
resolvedBindValues === undefined ? undefined : prepareBindValues(resolvedBindValues, sqlRes.query),
|
|
651
738
|
{
|
|
652
739
|
...omitUndefineds({ otelContext: options?.otelContext }),
|
|
653
740
|
queriedTables: new Set([query[QueryBuilderAstSymbol].tableDef.sqliteDef.name]),
|
|
654
741
|
},
|
|
655
742
|
)
|
|
656
743
|
|
|
657
|
-
const decodeResult = Schema.
|
|
658
|
-
if (decodeResult
|
|
659
|
-
return decodeResult.
|
|
744
|
+
const decodeResult = Schema.decodeResult(schema)(rawRes)
|
|
745
|
+
if (Result.isSuccess(decodeResult) === true) {
|
|
746
|
+
return decodeResult.success
|
|
660
747
|
} else {
|
|
661
748
|
return shouldNeverHappen(
|
|
662
749
|
'Failed to decode query result with for schema:',
|
|
@@ -664,7 +751,7 @@ export class Store<TSchema extends LiveStoreSchema = LiveStoreSchema.Any, TConte
|
|
|
664
751
|
'raw result:',
|
|
665
752
|
rawRes,
|
|
666
753
|
'decode error:',
|
|
667
|
-
decodeResult.
|
|
754
|
+
decodeResult.failure,
|
|
668
755
|
)
|
|
669
756
|
}
|
|
670
757
|
} else if (query._tag === 'def') {
|
|
@@ -788,29 +875,25 @@ export class Store<TSchema extends LiveStoreSchema = LiveStoreSchema.Any, TConte
|
|
|
788
875
|
|
|
789
876
|
const { events, options } = this.getCommitArgs(firstEventOrTxnFnOrOptions, restEvents)
|
|
790
877
|
|
|
791
|
-
Effect.gen(this, function* () {
|
|
878
|
+
Effect.gen({ self: this }, function* () {
|
|
792
879
|
const commitsSpan = otel.trace.getSpan(this[StoreInternalsSymbol].otel.commitsSpanContext)
|
|
793
880
|
commitsSpan?.addEvent('commit')
|
|
794
881
|
const currentSpan = yield* OtelTracer.currentOtelSpan.pipe(Effect.orDie)
|
|
795
882
|
commitsSpan?.addLink({ context: currentSpan.spanContext() })
|
|
796
883
|
|
|
797
|
-
for (const event of events) {
|
|
798
|
-
replaceSessionIdSymbol(event.args, this[StoreInternalsSymbol].clientSession.sessionId)
|
|
799
|
-
}
|
|
800
|
-
|
|
801
884
|
if (events.length === 0) return
|
|
802
885
|
|
|
803
|
-
const
|
|
886
|
+
const localServices = yield* Effect.context()
|
|
804
887
|
|
|
805
888
|
const encodedEvents = yield* this[StoreInternalsSymbol].syncProcessor.encodeEvents(events)
|
|
806
889
|
|
|
807
890
|
const { writeTables } = yield* Effect.try({
|
|
808
891
|
try: () => {
|
|
809
892
|
const materialize = () =>
|
|
810
|
-
this[StoreInternalsSymbol].syncProcessor
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
|
|
893
|
+
this[StoreInternalsSymbol].syncProcessor
|
|
894
|
+
.materializeEvents(encodedEvents)
|
|
895
|
+
.pipe(Effect.runSyncWith(localServices))
|
|
896
|
+
return events.length > 1 ? this[StoreInternalsSymbol].sqliteDbWrapper.txn(materialize) : materialize()
|
|
814
897
|
},
|
|
815
898
|
catch: (cause) => UnknownError.make({ cause }),
|
|
816
899
|
})
|
|
@@ -854,9 +937,9 @@ export class Store<TSchema extends LiveStoreSchema = LiveStoreSchema.Any, TConte
|
|
|
854
937
|
...(options?.spanLinks?.map(OtelTracer.makeSpanLink) ?? []),
|
|
855
938
|
],
|
|
856
939
|
}),
|
|
857
|
-
Effect.
|
|
858
|
-
Effect.
|
|
859
|
-
|
|
940
|
+
Effect.tapCause(Effect.logError),
|
|
941
|
+
Effect.catchCause((cause) => Effect.forkChild(this.shutdown(cause))),
|
|
942
|
+
Effect.runSyncWith(this[StoreInternalsSymbol].effectContext.services),
|
|
860
943
|
)
|
|
861
944
|
}
|
|
862
945
|
//#endregion commit
|
|
@@ -934,8 +1017,8 @@ export class Store<TSchema extends LiveStoreSchema = LiveStoreSchema.Any, TConte
|
|
|
934
1017
|
}
|
|
935
1018
|
|
|
936
1019
|
return clientSession.leaderThread.events.stream(baseOptions).pipe(
|
|
937
|
-
Stream.
|
|
938
|
-
Stream.catchTag('
|
|
1020
|
+
Stream.mapEffect((event) => Schema.decodeEffect(eventSchema)(event)),
|
|
1021
|
+
Stream.catchTag('SchemaError', (cause) => Stream.fail(UnknownError.make({ cause }))),
|
|
939
1022
|
Stream.tapError((error) => Effect.logError('Error in eventsStream', error)),
|
|
940
1023
|
)
|
|
941
1024
|
}
|
|
@@ -1028,7 +1111,7 @@ export class Store<TSchema extends LiveStoreSchema = LiveStoreSchema.Any, TConte
|
|
|
1028
1111
|
)
|
|
1029
1112
|
|
|
1030
1113
|
return () => {
|
|
1031
|
-
Fiber.interrupt(fiber).pipe(
|
|
1114
|
+
Fiber.interrupt(fiber).pipe(Effect.runForkWith(this[StoreInternalsSymbol].effectContext.services))
|
|
1032
1115
|
}
|
|
1033
1116
|
}
|
|
1034
1117
|
|
|
@@ -1102,7 +1185,7 @@ export class Store<TSchema extends LiveStoreSchema = LiveStoreSchema.Any, TConte
|
|
|
1102
1185
|
*/
|
|
1103
1186
|
_dev = {
|
|
1104
1187
|
downloadDb: (source: 'local' | 'leader' = 'local') => {
|
|
1105
|
-
Effect.gen(this, function* () {
|
|
1188
|
+
Effect.gen({ self: this }, function* () {
|
|
1106
1189
|
const data =
|
|
1107
1190
|
source === 'local'
|
|
1108
1191
|
? this[StoreInternalsSymbol].sqliteDbWrapper.export()
|
|
@@ -1112,14 +1195,14 @@ export class Store<TSchema extends LiveStoreSchema = LiveStoreSchema.Any, TConte
|
|
|
1112
1195
|
},
|
|
1113
1196
|
|
|
1114
1197
|
downloadEventlogDb: () => {
|
|
1115
|
-
Effect.gen(this, function* () {
|
|
1198
|
+
Effect.gen({ self: this }, function* () {
|
|
1116
1199
|
const data = yield* this[StoreInternalsSymbol].clientSession.leaderThread.getEventlogData
|
|
1117
1200
|
downloadBlob(data, `livestore-eventlog-${Date.now()}.db`)
|
|
1118
1201
|
}).pipe(this.runEffectFork)
|
|
1119
1202
|
},
|
|
1120
1203
|
|
|
1121
1204
|
hardReset: (mode: 'all-data' | 'only-app-db' = 'all-data') => {
|
|
1122
|
-
Effect.gen(this, function* () {
|
|
1205
|
+
Effect.gen({ self: this }, function* () {
|
|
1123
1206
|
const clientId = this[StoreInternalsSymbol].clientSession.clientId
|
|
1124
1207
|
yield* this[StoreInternalsSymbol].clientSession.leaderThread.sendDevtoolsMessage(
|
|
1125
1208
|
Devtools.Leader.ResetAllData.Request.make({ liveStoreVersion, mode, requestId: nanoid(), clientId }),
|
|
@@ -1143,14 +1226,14 @@ export class Store<TSchema extends LiveStoreSchema = LiveStoreSchema.Any, TConte
|
|
|
1143
1226
|
|
|
1144
1227
|
// NOTE: Explicit return type needed to avoid TS2742 (inferred type references internal path)
|
|
1145
1228
|
syncStates: (): Promise<{ session: SyncState.SyncState; leader: SyncState.SyncState }> =>
|
|
1146
|
-
Effect.gen(this, function* () {
|
|
1229
|
+
Effect.gen({ self: this }, function* () {
|
|
1147
1230
|
const session = yield* this[StoreInternalsSymbol].syncProcessor.syncState
|
|
1148
1231
|
const leader = yield* this[StoreInternalsSymbol].clientSession.leaderThread.syncState
|
|
1149
1232
|
return { session, leader }
|
|
1150
1233
|
}).pipe(this.runEffectPromise),
|
|
1151
1234
|
|
|
1152
1235
|
printSyncStates: () => {
|
|
1153
|
-
Effect.gen(this, function* () {
|
|
1236
|
+
Effect.gen({ self: this }, function* () {
|
|
1154
1237
|
const session = yield* this[StoreInternalsSymbol].syncProcessor.syncState
|
|
1155
1238
|
yield* Effect.log(
|
|
1156
1239
|
`Session sync state: ${objectToString(session.localHead)} (upstream: ${objectToString(session.upstreamHead)})`,
|
|
@@ -1181,11 +1264,11 @@ export class Store<TSchema extends LiveStoreSchema = LiveStoreSchema.Any, TConte
|
|
|
1181
1264
|
effect.pipe(
|
|
1182
1265
|
Effect.forkIn(this[StoreInternalsSymbol].effectContext.lifetimeScope),
|
|
1183
1266
|
Effect.tapCauseLogPretty,
|
|
1184
|
-
|
|
1267
|
+
Effect.runForkWith(this[StoreInternalsSymbol].effectContext.services),
|
|
1185
1268
|
)
|
|
1186
1269
|
|
|
1187
1270
|
private runEffectPromise = <A, E>(effect: Effect.Effect<A, E, Scope.Scope>) =>
|
|
1188
|
-
effect.pipe(Effect.tapCauseLogPretty,
|
|
1271
|
+
effect.pipe(Effect.tapCauseLogPretty, Effect.runPromiseWith(this[StoreInternalsSymbol].effectContext.services))
|
|
1189
1272
|
|
|
1190
1273
|
private getCommitArgs = (
|
|
1191
1274
|
firstEventOrTxnFnOrOptions: any,
|
|
@@ -1215,12 +1298,6 @@ export class Store<TSchema extends LiveStoreSchema = LiveStoreSchema.Any, TConte
|
|
|
1215
1298
|
events = [firstEventOrTxnFnOrOptions, ...restEvents]
|
|
1216
1299
|
}
|
|
1217
1300
|
|
|
1218
|
-
// for (const event of events) {
|
|
1219
|
-
// if (event.args.id === SessionIdSymbol) {
|
|
1220
|
-
// event.args.id = this.sessionId
|
|
1221
|
-
// }
|
|
1222
|
-
// }
|
|
1223
|
-
|
|
1224
1301
|
return { events, options }
|
|
1225
1302
|
}
|
|
1226
1303
|
}
|
package/src/utils/dev.ts
CHANGED
|
@@ -24,7 +24,7 @@ export const downloadURL = (data: string, fileName: string) => {
|
|
|
24
24
|
const a = document.createElement('a')
|
|
25
25
|
a.href = data
|
|
26
26
|
a.download = fileName
|
|
27
|
-
document.body.
|
|
27
|
+
document.body.appendChild(a)
|
|
28
28
|
a.style.display = 'none'
|
|
29
29
|
a.click()
|
|
30
30
|
a.remove()
|
package/src/utils/stack-info.ts
CHANGED
|
@@ -41,7 +41,10 @@ export const extractStackInfoFromStackTrace = (stackTrace: string): StackInfo =>
|
|
|
41
41
|
// console.debug(name, filePath)
|
|
42
42
|
|
|
43
43
|
// NOTE No idea where this `Module.` comes from - possibly a Vite thing?
|
|
44
|
-
if (
|
|
44
|
+
if (
|
|
45
|
+
(name.startsWith('use') === true || name.startsWith('Module.use') === true) &&
|
|
46
|
+
name.endsWith('QueryRef') === false
|
|
47
|
+
) {
|
|
45
48
|
hasReachedStart = true
|
|
46
49
|
// console.debug('hasReachedStart. adding one more frame.')
|
|
47
50
|
|