@livestore/livestore 0.4.0-dev.9 → 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/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 +15 -13
- package/dist/SqliteDbWrapper.js.map +1 -1
- package/dist/SqliteDbWrapper.test.js +24 -6
- package/dist/SqliteDbWrapper.test.js.map +1 -1
- package/dist/effect/LiveStore.d.ts +134 -6
- package/dist/effect/LiveStore.d.ts.map +1 -1
- package/dist/effect/LiveStore.js +190 -11
- 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 +41 -0
- package/dist/effect/LiveStore.test.js.map +1 -0
- package/dist/effect/mod.d.ts +1 -1
- package/dist/effect/mod.d.ts.map +1 -1
- package/dist/effect/mod.js +3 -1
- package/dist/effect/mod.js.map +1 -1
- package/dist/live-queries/base-class.d.ts +129 -9
- package/dist/live-queries/base-class.d.ts.map +1 -1
- package/dist/live-queries/base-class.js +30 -4
- 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 +4 -3
- package/dist/live-queries/client-document-get-query.js.map +1 -1
- package/dist/live-queries/computed.d.ts +56 -0
- package/dist/live-queries/computed.d.ts.map +1 -1
- package/dist/live-queries/computed.js +62 -6
- 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 +41 -30
- package/dist/live-queries/db-query.js.map +1 -1
- package/dist/live-queries/db-query.test.js +112 -29
- package/dist/live-queries/db-query.test.js.map +1 -1
- package/dist/live-queries/signal.d.ts +49 -0
- package/dist/live-queries/signal.d.ts.map +1 -1
- package/dist/live-queries/signal.js +51 -2
- package/dist/live-queries/signal.js.map +1 -1
- package/dist/live-queries/signal.test.js +4 -4
- package/dist/live-queries/signal.test.js.map +1 -1
- package/dist/mod.d.ts +3 -3
- package/dist/mod.d.ts.map +1 -1
- package/dist/mod.js +3 -2
- package/dist/mod.js.map +1 -1
- package/dist/reactive.d.ts +10 -10
- package/dist/reactive.d.ts.map +1 -1
- package/dist/reactive.js +29 -27
- 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 +215 -0
- package/dist/store/StoreRegistry.d.ts.map +1 -0
- package/dist/store/StoreRegistry.js +267 -0
- package/dist/store/StoreRegistry.js.map +1 -0
- package/dist/store/StoreRegistry.test.d.ts +2 -0
- package/dist/store/StoreRegistry.test.d.ts.map +1 -0
- package/dist/store/StoreRegistry.test.js +384 -0
- package/dist/store/StoreRegistry.test.js.map +1 -0
- package/dist/store/create-store.d.ts +99 -22
- package/dist/store/create-store.d.ts.map +1 -1
- package/dist/store/create-store.js +85 -37
- package/dist/store/create-store.js.map +1 -1
- package/dist/store/devtools.d.ts +6 -18
- package/dist/store/devtools.d.ts.map +1 -1
- package/dist/store/devtools.js +61 -19
- package/dist/store/devtools.js.map +1 -1
- package/dist/store/store-eventstream.test.d.ts +2 -0
- package/dist/store/store-eventstream.test.d.ts.map +1 -0
- package/dist/store/store-eventstream.test.js +65 -0
- package/dist/store/store-eventstream.test.js.map +1 -0
- package/dist/store/store-types.d.ts +271 -31
- package/dist/store/store-types.d.ts.map +1 -1
- package/dist/store/store-types.js +41 -1
- package/dist/store/store-types.js.map +1 -1
- package/dist/store/store-types.test.d.ts +2 -0
- package/dist/store/store-types.test.d.ts.map +1 -0
- package/dist/store/store-types.test.js +39 -0
- package/dist/store/store-types.test.js.map +1 -0
- package/dist/store/store.d.ts +255 -68
- package/dist/store/store.d.ts.map +1 -1
- package/dist/store/store.js +533 -186
- package/dist/store/store.js.map +1 -1
- package/dist/utils/dev.d.ts.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 +3 -2
- package/dist/utils/stack-info.js.map +1 -1
- package/dist/utils/tests/fixture.d.ts +46 -39
- package/dist/utils/tests/fixture.d.ts.map +1 -1
- package/dist/utils/tests/fixture.js +7 -0
- 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 +50 -18
- package/src/QueryCache.ts +1 -1
- package/src/SqliteDbWrapper.test.ts +30 -6
- package/src/SqliteDbWrapper.ts +21 -17
- package/src/ambient.d.ts +0 -7
- package/src/effect/LiveStore.test.ts +61 -0
- package/src/effect/LiveStore.ts +426 -22
- package/src/effect/mod.ts +13 -1
- package/src/live-queries/__snapshots__/db-query.test.ts.snap +814 -178
- package/src/live-queries/base-class.ts +162 -32
- package/src/live-queries/client-document-get-query.ts +6 -4
- package/src/live-queries/computed.ts +65 -8
- package/src/live-queries/db-query.test.ts +168 -30
- package/src/live-queries/db-query.ts +58 -43
- package/src/live-queries/signal.test.ts +5 -4
- package/src/live-queries/signal.ts +52 -3
- package/src/mod.ts +19 -2
- package/src/reactive.test.ts +3 -2
- package/src/reactive.ts +58 -49
- package/src/store/StoreRegistry.test.ts +543 -0
- package/src/store/StoreRegistry.ts +429 -0
- package/src/store/create-store.ts +227 -74
- package/src/store/devtools.ts +305 -261
- package/src/store/store-eventstream.test.ts +123 -0
- package/src/store/store-types.test.ts +52 -0
- package/src/store/store-types.ts +318 -44
- package/src/store/store.ts +695 -283
- package/src/utils/dev.ts +3 -4
- package/src/utils/stack-info.ts +5 -2
- package/src/utils/tests/fixture.ts +9 -1
- package/src/utils/tests/otel.ts +8 -7
package/src/store/store.ts
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
|
+
import * as otel from '@opentelemetry/api'
|
|
2
|
+
|
|
1
3
|
import {
|
|
2
4
|
type Bindable,
|
|
3
5
|
type ClientSession,
|
|
4
|
-
type ClientSessionSyncProcessor,
|
|
5
6
|
Devtools,
|
|
6
7
|
getExecStatementsFromMaterializer,
|
|
7
8
|
getResultSchema,
|
|
@@ -12,18 +13,21 @@ import {
|
|
|
12
13
|
MaterializeError,
|
|
13
14
|
MaterializerHashMismatchError,
|
|
14
15
|
makeClientSessionSyncProcessor,
|
|
15
|
-
type PreparedBindValues,
|
|
16
16
|
prepareBindValues,
|
|
17
|
-
type QueryBuilder,
|
|
18
17
|
QueryBuilderAstSymbol,
|
|
19
|
-
|
|
20
|
-
|
|
18
|
+
resolveSessionIdSymbolInBindValues,
|
|
19
|
+
SqliteDbHelper,
|
|
20
|
+
StateHead,
|
|
21
|
+
type StorageMode,
|
|
22
|
+
type SyncState,
|
|
23
|
+
UnknownError,
|
|
21
24
|
} from '@livestore/common'
|
|
25
|
+
import type { StreamEventsOptions } from '@livestore/common/leader-thread'
|
|
22
26
|
import type { LiveStoreSchema } from '@livestore/common/schema'
|
|
23
|
-
import {
|
|
24
|
-
import { assertNever, isDevEnv,
|
|
25
|
-
import type { Scope } from '@livestore/utils/effect'
|
|
27
|
+
import { EventSequenceNumber, LiveStoreEvent, resolveEventDef, SystemTables } from '@livestore/common/schema'
|
|
28
|
+
import { assertNever, isDevEnv, objectToString, omitUndefineds, shouldNeverHappen } from '@livestore/utils'
|
|
26
29
|
import {
|
|
30
|
+
type Scope,
|
|
27
31
|
Cause,
|
|
28
32
|
Effect,
|
|
29
33
|
Exit,
|
|
@@ -31,20 +35,14 @@ import {
|
|
|
31
35
|
Inspectable,
|
|
32
36
|
Option,
|
|
33
37
|
OtelTracer,
|
|
34
|
-
|
|
38
|
+
Queue,
|
|
39
|
+
Result,
|
|
35
40
|
Schema,
|
|
36
41
|
Stream,
|
|
37
42
|
} from '@livestore/utils/effect'
|
|
38
43
|
import { nanoid } from '@livestore/utils/nanoid'
|
|
39
|
-
import * as otel from '@opentelemetry/api'
|
|
40
44
|
|
|
41
|
-
import type {
|
|
42
|
-
LiveQuery,
|
|
43
|
-
LiveQueryDef,
|
|
44
|
-
ReactivityGraph,
|
|
45
|
-
ReactivityGraphContext,
|
|
46
|
-
SignalDef,
|
|
47
|
-
} from '../live-queries/base-class.ts'
|
|
45
|
+
import type { LiveQuery, ReactivityGraphContext, SignalDef } from '../live-queries/base-class.ts'
|
|
48
46
|
import { makeReactivityGraph } from '../live-queries/base-class.ts'
|
|
49
47
|
import { makeExecBeforeFirstRun } from '../live-queries/client-document-get-query.ts'
|
|
50
48
|
import { queryDb } from '../live-queries/db-query.ts'
|
|
@@ -52,52 +50,143 @@ import type { Ref } from '../reactive.ts'
|
|
|
52
50
|
import { SqliteDbWrapper } from '../SqliteDbWrapper.ts'
|
|
53
51
|
import { ReferenceCountedSet } from '../utils/data-structures.ts'
|
|
54
52
|
import { downloadBlob, exposeDebugUtils } from '../utils/dev.ts'
|
|
55
|
-
import
|
|
56
|
-
|
|
57
|
-
RefreshReason,
|
|
58
|
-
StoreCommitOptions,
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
53
|
+
import {
|
|
54
|
+
type Queryable,
|
|
55
|
+
type RefreshReason,
|
|
56
|
+
type StoreCommitOptions,
|
|
57
|
+
type StoreConstructorParams,
|
|
58
|
+
type StoreEventsOptions,
|
|
59
|
+
type StoreInternals,
|
|
60
|
+
StoreInternalsSymbol,
|
|
61
|
+
type StoreOtel,
|
|
62
|
+
type SubscribeOptions,
|
|
63
|
+
type SyncStatus,
|
|
64
|
+
type Unsubscribe,
|
|
63
65
|
} from './store-types.ts'
|
|
64
66
|
|
|
65
|
-
|
|
67
|
+
export type SubscribeFn = {
|
|
68
|
+
<TResult>(
|
|
69
|
+
query: Queryable<TResult>,
|
|
70
|
+
onUpdate: (value: TResult) => void,
|
|
71
|
+
options?: SubscribeOptions<TResult>,
|
|
72
|
+
): Unsubscribe
|
|
73
|
+
<TResult>(query: Queryable<TResult>, options?: SubscribeOptions<TResult>): AsyncIterable<TResult>
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
if (isDevEnv() === true) {
|
|
66
77
|
exposeDebugUtils()
|
|
67
78
|
}
|
|
68
79
|
|
|
80
|
+
/**
|
|
81
|
+
* Default parameters for the Store. Also used in `create-store.ts`
|
|
82
|
+
*/
|
|
83
|
+
export const STORE_DEFAULT_PARAMS = {
|
|
84
|
+
leaderPushBatchSize: 100,
|
|
85
|
+
eventQueryBatchSize: 100,
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
//
|
|
89
|
+
/**
|
|
90
|
+
* Central interface to a LiveStore database providing reactive queries, event commits, and sync.
|
|
91
|
+
*
|
|
92
|
+
* A `Store` instance wraps a local SQLite database that is kept in sync with other clients via
|
|
93
|
+
* an event log. Instead of mutating state directly, you commit events that get materialized
|
|
94
|
+
* into database rows. Queries automatically re-run when their underlying tables change.
|
|
95
|
+
*
|
|
96
|
+
* ## Creating a Store
|
|
97
|
+
*
|
|
98
|
+
* Use `createStore` (Effect-based) or `createStorePromise` to obtain a Store instance.
|
|
99
|
+
* In React applications, use `StoreRegistry` with `<StoreRegistryProvider>` and the `useStore()` hook
|
|
100
|
+
* which manages the Store lifecycle.
|
|
101
|
+
*
|
|
102
|
+
* ## Querying Data
|
|
103
|
+
*
|
|
104
|
+
* Use {@link Store.query} for one-shot reads or {@link Store.subscribe} for reactive subscriptions.
|
|
105
|
+
* Both accept query builders (e.g. `tables.todo.where({ complete: true })`) or custom `LiveQueryDef`s.
|
|
106
|
+
*
|
|
107
|
+
* ## Committing Events
|
|
108
|
+
*
|
|
109
|
+
* Use {@link Store.commit} to persist events. Events are immediately materialized locally and
|
|
110
|
+
* asynchronously synced to other clients. Multiple events can be committed atomically.
|
|
111
|
+
*
|
|
112
|
+
* ## Lifecycle
|
|
113
|
+
*
|
|
114
|
+
* The Store must be shut down when no longer needed via {@link Store.shutdown} or
|
|
115
|
+
* {@link Store.shutdownPromise}. Framework integrations (React, Effect) handle this automatically.
|
|
116
|
+
*
|
|
117
|
+
* @typeParam TSchema - The LiveStore schema defining tables and events
|
|
118
|
+
* @typeParam TContext - Optional user-defined context attached to the Store (e.g. for dependency injection)
|
|
119
|
+
*
|
|
120
|
+
* @example
|
|
121
|
+
* ```ts
|
|
122
|
+
* // Query data
|
|
123
|
+
* const todos = store.query(tables.todo.where({ complete: false }))
|
|
124
|
+
*
|
|
125
|
+
* // Subscribe to changes
|
|
126
|
+
* const unsubscribe = store.subscribe(tables.todo.all(), (todos) => {
|
|
127
|
+
* console.log('Todos updated:', todos)
|
|
128
|
+
* })
|
|
129
|
+
*
|
|
130
|
+
* // Commit an event
|
|
131
|
+
* store.commit(events.todoCreated({ id: nanoid(), text: 'Buy milk' }))
|
|
132
|
+
* ```
|
|
133
|
+
*/
|
|
69
134
|
export class Store<TSchema extends LiveStoreSchema = LiveStoreSchema.Any, TContext = {}> extends Inspectable.Class {
|
|
135
|
+
/** Unique identifier for this Store instance, stable for its lifetime. */
|
|
70
136
|
readonly storeId: string
|
|
71
|
-
reactivityGraph: ReactivityGraph
|
|
72
|
-
sqliteDbWrapper: SqliteDbWrapper
|
|
73
|
-
clientSession: ClientSession
|
|
74
|
-
schema: LiveStoreSchema
|
|
75
|
-
context: TContext
|
|
76
|
-
otel: StoreOtel
|
|
77
|
-
/**
|
|
78
|
-
* Note we're using `Ref<null>` here as we don't care about the value but only about *that* something has changed.
|
|
79
|
-
* This only works in combination with `equal: () => false` which will always trigger a refresh.
|
|
80
|
-
*/
|
|
81
|
-
tableRefs: { [key: string]: Ref<null, ReactivityGraphContext, RefreshReason> }
|
|
82
137
|
|
|
83
|
-
/**
|
|
84
|
-
|
|
138
|
+
/** The LiveStore schema defining tables, events, and materializers. */
|
|
139
|
+
readonly schema: LiveStoreSchema
|
|
85
140
|
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
lifetimeScope: Scope.Scope
|
|
89
|
-
}
|
|
141
|
+
/** User-defined context attached to this Store (e.g. for dependency injection). */
|
|
142
|
+
readonly context: TContext
|
|
90
143
|
|
|
91
|
-
/**
|
|
92
|
-
|
|
144
|
+
/** Options provided to the Store constructor. */
|
|
145
|
+
readonly params: StoreConstructorParams<TSchema, TContext>['params']
|
|
93
146
|
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
147
|
+
/**
|
|
148
|
+
* Reactive connectivity updates emitted by the backing sync backend.
|
|
149
|
+
*
|
|
150
|
+
* @example
|
|
151
|
+
* ```ts
|
|
152
|
+
* import { Effect, Stream } from 'effect'
|
|
153
|
+
*
|
|
154
|
+
* const status = await store.networkStatus.pipe(Effect.runPromise)
|
|
155
|
+
*
|
|
156
|
+
* await store.networkStatus.changes.pipe(
|
|
157
|
+
* Stream.tap((next) => console.log('network status update', next)),
|
|
158
|
+
* Stream.runDrain,
|
|
159
|
+
* Effect.scoped,
|
|
160
|
+
* Effect.runPromise,
|
|
161
|
+
* )
|
|
162
|
+
* ```
|
|
163
|
+
*/
|
|
164
|
+
readonly networkStatus: ClientSession['leaderThread']['networkStatus']
|
|
97
165
|
|
|
98
|
-
|
|
166
|
+
/**
|
|
167
|
+
* Indicates how data is being stored.
|
|
168
|
+
*
|
|
169
|
+
* - `persisted`: Data is persisted to disk (e.g., via OPFS on web, SQLite file on native)
|
|
170
|
+
* - `in-memory`: Data is only stored in memory and will be lost on page refresh
|
|
171
|
+
*
|
|
172
|
+
* The store operates in `in-memory` mode when persistent storage is unavailable,
|
|
173
|
+
* such as in Safari/Firefox private browsing mode where OPFS is restricted.
|
|
174
|
+
*
|
|
175
|
+
* @example
|
|
176
|
+
* ```tsx
|
|
177
|
+
* if (store.storageMode === 'in-memory') {
|
|
178
|
+
* showWarning('Data will not be persisted in private browsing mode')
|
|
179
|
+
* }
|
|
180
|
+
* ```
|
|
181
|
+
*/
|
|
182
|
+
readonly storageMode: StorageMode
|
|
99
183
|
|
|
100
|
-
|
|
184
|
+
/**
|
|
185
|
+
* Store internals. Not part of the public API — shapes and semantics may change without notice.
|
|
186
|
+
*/
|
|
187
|
+
readonly [StoreInternalsSymbol]: StoreInternals
|
|
188
|
+
|
|
189
|
+
//#region constructor
|
|
101
190
|
constructor({
|
|
102
191
|
clientSession,
|
|
103
192
|
schema,
|
|
@@ -109,40 +198,53 @@ export class Store<TSchema extends LiveStoreSchema = LiveStoreSchema.Any, TConte
|
|
|
109
198
|
params,
|
|
110
199
|
confirmUnsavedChanges,
|
|
111
200
|
__runningInDevtools,
|
|
112
|
-
}:
|
|
201
|
+
}: StoreConstructorParams<TSchema, TContext>) {
|
|
113
202
|
super()
|
|
114
203
|
|
|
115
204
|
this.storeId = storeId
|
|
116
|
-
|
|
117
|
-
this.sqliteDbWrapper = new SqliteDbWrapper({ otel: otelOptions, db: clientSession.sqliteDb })
|
|
118
|
-
this.clientSession = clientSession
|
|
119
205
|
this.schema = schema
|
|
120
206
|
this.context = context
|
|
121
|
-
|
|
122
|
-
this.
|
|
207
|
+
this.params = params
|
|
208
|
+
this.networkStatus = clientSession.leaderThread.networkStatus
|
|
209
|
+
this.storageMode = clientSession.leaderThread.initialState.storageMode
|
|
123
210
|
|
|
124
211
|
const reactivityGraph = makeReactivityGraph()
|
|
212
|
+
const stateHead = StateHead.make({ dbState: clientSession.sqliteDb })
|
|
125
213
|
|
|
126
|
-
const
|
|
127
|
-
|
|
128
|
-
this.syncProcessor = makeClientSessionSyncProcessor({
|
|
214
|
+
const syncProcessor = makeClientSessionSyncProcessor({
|
|
129
215
|
schema,
|
|
130
216
|
clientSession,
|
|
131
|
-
runtime: effectContext.runtime,
|
|
132
217
|
materializeEvent: Effect.fn('client-session-sync-processor:materialize-event')(
|
|
133
|
-
(
|
|
218
|
+
(eventEncoded, { materializerHashLeader }) =>
|
|
134
219
|
// We need to use `Effect.gen` (even though we're using `Effect.fn`) so that we can pass `this` to the function
|
|
135
|
-
Effect.gen(this, function* () {
|
|
136
|
-
const
|
|
220
|
+
Effect.gen({ self: this }, function* () {
|
|
221
|
+
const resolution = yield* resolveEventDef(schema, {
|
|
222
|
+
operation: '@livestore/livestore:store:materializeEvent',
|
|
223
|
+
event: eventEncoded,
|
|
224
|
+
})
|
|
225
|
+
|
|
226
|
+
if (resolution._tag === 'unknown') {
|
|
227
|
+
// Runtime schema doesn't know this event yet; skip materialization but
|
|
228
|
+
// keep the log entry so upgraded clients can replay it later.
|
|
229
|
+
yield* stateHead.set(eventEncoded.seqNum)
|
|
230
|
+
return {
|
|
231
|
+
writeTables: new Set<string>(),
|
|
232
|
+
sessionChangeset: { _tag: 'no-op' as const },
|
|
233
|
+
materializerHash: Option.none(),
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
const { eventDef, materializer } = resolution
|
|
137
238
|
|
|
138
239
|
const execArgsArr = getExecStatementsFromMaterializer({
|
|
139
240
|
eventDef,
|
|
140
241
|
materializer,
|
|
141
|
-
dbState: this.sqliteDbWrapper,
|
|
142
|
-
event: { decoded:
|
|
242
|
+
dbState: this[StoreInternalsSymbol].sqliteDbWrapper,
|
|
243
|
+
event: { decoded: undefined, encoded: eventEncoded },
|
|
143
244
|
})
|
|
144
245
|
|
|
145
|
-
const materializerHash =
|
|
246
|
+
const materializerHash =
|
|
247
|
+
isDevEnv() === true ? Option.some(hashMaterializerResults(execArgsArr)) : Option.none()
|
|
146
248
|
|
|
147
249
|
// Hash mismatch detection only occurs during the pull path (when receiving events from the leader).
|
|
148
250
|
// During push path (local commits), materializerHashLeader is always Option.none(), so this condition
|
|
@@ -153,7 +255,7 @@ export class Store<TSchema extends LiveStoreSchema = LiveStoreSchema.Any, TConte
|
|
|
153
255
|
materializerHash._tag === 'Some' &&
|
|
154
256
|
materializerHashLeader.value !== materializerHash.value
|
|
155
257
|
) {
|
|
156
|
-
return yield* MaterializerHashMismatchError.make({ eventName:
|
|
258
|
+
return yield* MaterializerHashMismatchError.make({ eventName: eventEncoded.name })
|
|
157
259
|
}
|
|
158
260
|
|
|
159
261
|
const span = yield* OtelTracer.currentOtelSpan.pipe(Effect.orDie)
|
|
@@ -165,15 +267,18 @@ export class Store<TSchema extends LiveStoreSchema = LiveStoreSchema.Any, TConte
|
|
|
165
267
|
for (const {
|
|
166
268
|
statementSql,
|
|
167
269
|
bindValues,
|
|
168
|
-
writeTables = this.sqliteDbWrapper.getTablesUsed(statementSql),
|
|
270
|
+
writeTables = this[StoreInternalsSymbol].sqliteDbWrapper.getTablesUsed(statementSql),
|
|
169
271
|
} of execArgsArr) {
|
|
170
272
|
try {
|
|
171
|
-
this.sqliteDbWrapper.cachedExecute(statementSql, bindValues, {
|
|
273
|
+
this[StoreInternalsSymbol].sqliteDbWrapper.cachedExecute(statementSql, bindValues, {
|
|
274
|
+
otelContext,
|
|
275
|
+
writeTables,
|
|
276
|
+
})
|
|
172
277
|
} catch (cause) {
|
|
173
278
|
// TOOD refactor with `SqliteError`
|
|
174
|
-
throw
|
|
279
|
+
throw UnknownError.make({
|
|
175
280
|
cause,
|
|
176
|
-
note: `Error executing materializer for event "${
|
|
281
|
+
note: `Error executing materializer for event "${eventEncoded.name}".\nStatement: ${statementSql}\nBind values: ${JSON.stringify(bindValues)}`,
|
|
177
282
|
})
|
|
178
283
|
}
|
|
179
284
|
|
|
@@ -182,52 +287,42 @@ export class Store<TSchema extends LiveStoreSchema = LiveStoreSchema.Any, TConte
|
|
|
182
287
|
writeTablesForEvent.add(table)
|
|
183
288
|
}
|
|
184
289
|
|
|
185
|
-
this.sqliteDbWrapper.debug.head =
|
|
290
|
+
this[StoreInternalsSymbol].sqliteDbWrapper.debug.head = eventEncoded.seqNum
|
|
186
291
|
}
|
|
187
292
|
}
|
|
188
293
|
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
| { _tag: 'no-op' }
|
|
192
|
-
| { _tag: 'unset' } = { _tag: 'unset' }
|
|
193
|
-
if (withChangeset === true) {
|
|
194
|
-
sessionChangeset = this.sqliteDbWrapper.withChangeset(exec).changeset
|
|
195
|
-
} else {
|
|
196
|
-
exec()
|
|
197
|
-
}
|
|
294
|
+
const sessionChangeset = this[StoreInternalsSymbol].sqliteDbWrapper.withChangeset(exec).changeset
|
|
295
|
+
yield* stateHead.set(eventEncoded.seqNum)
|
|
198
296
|
|
|
199
297
|
return { writeTables: writeTablesForEvent, sessionChangeset, materializerHash }
|
|
200
|
-
}).pipe(
|
|
298
|
+
}).pipe(
|
|
299
|
+
SqliteDbHelper.withSavepoint(clientSession.sqliteDb),
|
|
300
|
+
Effect.mapError((cause) => MaterializeError.make({ cause })),
|
|
301
|
+
),
|
|
201
302
|
),
|
|
202
303
|
rollback: (changeset) => {
|
|
203
|
-
this.sqliteDbWrapper.rollback(changeset)
|
|
304
|
+
this[StoreInternalsSymbol].sqliteDbWrapper.rollback(changeset)
|
|
204
305
|
},
|
|
205
306
|
refreshTables: (tables) => {
|
|
206
307
|
const tablesToUpdate = [] as [Ref<null, ReactivityGraphContext, RefreshReason>, null][]
|
|
207
308
|
for (const tableName of tables) {
|
|
208
|
-
const tableRef = this.tableRefs[tableName]
|
|
309
|
+
const tableRef = this[StoreInternalsSymbol].tableRefs[tableName]
|
|
209
310
|
assertNever(tableRef !== undefined, `No table ref found for ${tableName}`)
|
|
210
311
|
tablesToUpdate.push([tableRef!, null])
|
|
211
312
|
}
|
|
212
313
|
reactivityGraph.setRefs(tablesToUpdate)
|
|
213
314
|
},
|
|
214
|
-
span: syncSpan,
|
|
215
315
|
params: {
|
|
216
316
|
...omitUndefineds({
|
|
217
317
|
leaderPushBatchSize: params.leaderPushBatchSize,
|
|
218
318
|
}),
|
|
219
|
-
...(params.simulation?.clientSessionSyncProcessor !== undefined
|
|
220
|
-
? { simulation: params.simulation.clientSessionSyncProcessor }
|
|
221
|
-
: {}),
|
|
222
319
|
},
|
|
223
320
|
confirmUnsavedChanges,
|
|
224
|
-
})
|
|
225
|
-
|
|
226
|
-
this.__eventSchema = LiveStoreEvent.makeEventDefSchemaMemo(schema)
|
|
321
|
+
}).pipe(Effect.provideService(StateHead.StateHead, stateHead), Effect.runSyncWith(effectContext.services))
|
|
227
322
|
|
|
228
323
|
// TODO generalize the `tableRefs` concept to allow finer-grained refs
|
|
229
|
-
|
|
230
|
-
|
|
324
|
+
const tableRefs: { [key: string]: Ref<null, ReactivityGraphContext, RefreshReason> } = {}
|
|
325
|
+
const activeQueries = new ReferenceCountedSet<LiveQuery<any>>()
|
|
231
326
|
|
|
232
327
|
const commitsSpan = otelOptions.tracer.startSpan('LiveStore:commits', {}, otelOptions.rootSpanContext)
|
|
233
328
|
const otelMuationsSpanContext = otel.trace.setSpan(otel.context.active(), commitsSpan)
|
|
@@ -235,8 +330,7 @@ export class Store<TSchema extends LiveStoreSchema = LiveStoreSchema.Any, TConte
|
|
|
235
330
|
const queriesSpan = otelOptions.tracer.startSpan('LiveStore:queries', {}, otelOptions.rootSpanContext)
|
|
236
331
|
const otelQueriesSpanContext = otel.trace.setSpan(otel.context.active(), queriesSpan)
|
|
237
332
|
|
|
238
|
-
|
|
239
|
-
this.reactivityGraph.context = {
|
|
333
|
+
reactivityGraph.context = {
|
|
240
334
|
store: this as unknown as Store<LiveStoreSchema>,
|
|
241
335
|
defRcMap: new Map(),
|
|
242
336
|
reactivityGraph: new WeakRef(reactivityGraph),
|
|
@@ -244,8 +338,7 @@ export class Store<TSchema extends LiveStoreSchema = LiveStoreSchema.Any, TConte
|
|
|
244
338
|
rootOtelContext: otelQueriesSpanContext,
|
|
245
339
|
effectsWrapper: batchUpdates,
|
|
246
340
|
}
|
|
247
|
-
|
|
248
|
-
this.otel = {
|
|
341
|
+
const otelObj: StoreOtel = {
|
|
249
342
|
tracer: otelOptions.tracer,
|
|
250
343
|
rootSpanContext: otelOptions.rootSpanContext,
|
|
251
344
|
commitsSpanContext: otelMuationsSpanContext,
|
|
@@ -256,58 +349,93 @@ export class Store<TSchema extends LiveStoreSchema = LiveStoreSchema.Any, TConte
|
|
|
256
349
|
const allTableNames = new Set(
|
|
257
350
|
// NOTE we're excluding the LiveStore schema and events tables as they are not user-facing
|
|
258
351
|
// unless LiveStore is running in the devtools
|
|
259
|
-
__runningInDevtools
|
|
352
|
+
__runningInDevtools === true
|
|
260
353
|
? this.schema.state.sqlite.tables.keys()
|
|
261
354
|
: Array.from(this.schema.state.sqlite.tables.keys()).filter((_) => !SystemTables.isStateSystemTable(_)),
|
|
262
355
|
)
|
|
263
356
|
const existingTableRefs = new Map(
|
|
264
|
-
Array.from(
|
|
357
|
+
Array.from(reactivityGraph.atoms.values())
|
|
265
358
|
.filter((_): _ is Ref<any, any, any> => _._tag === 'ref' && _.label?.startsWith('tableRef:') === true)
|
|
266
359
|
.map((_) => [_.label!.slice('tableRef:'.length), _] as const),
|
|
267
360
|
)
|
|
268
361
|
for (const tableName of allTableNames) {
|
|
269
|
-
|
|
362
|
+
tableRefs[tableName] =
|
|
270
363
|
existingTableRefs.get(tableName) ??
|
|
271
|
-
|
|
364
|
+
reactivityGraph.makeRef(null, {
|
|
272
365
|
equal: () => false,
|
|
273
|
-
label: `tableRef:${tableName}`,
|
|
366
|
+
label: `tableRef:${String(tableName)}`,
|
|
274
367
|
meta: { liveStoreRefType: 'table' },
|
|
275
368
|
})
|
|
276
369
|
}
|
|
277
370
|
|
|
278
|
-
|
|
371
|
+
const boot = Effect.gen({ self: this }, function* () {
|
|
279
372
|
yield* Effect.addFinalizer(() =>
|
|
280
373
|
Effect.sync(() => {
|
|
281
374
|
// Remove all table refs from the reactivity graph
|
|
282
|
-
for (const tableRef of Object.values(
|
|
375
|
+
for (const tableRef of Object.values(tableRefs)) {
|
|
283
376
|
for (const superComp of tableRef.super) {
|
|
284
|
-
this.reactivityGraph.removeEdge(superComp, tableRef)
|
|
377
|
+
this[StoreInternalsSymbol].reactivityGraph.removeEdge(superComp, tableRef)
|
|
285
378
|
}
|
|
286
379
|
}
|
|
287
380
|
|
|
288
381
|
// End the otel spans
|
|
289
|
-
syncSpan.end()
|
|
290
382
|
commitsSpan.end()
|
|
291
383
|
queriesSpan.end()
|
|
292
384
|
}),
|
|
293
385
|
)
|
|
294
386
|
|
|
295
|
-
yield*
|
|
387
|
+
yield* syncProcessor.boot
|
|
296
388
|
})
|
|
389
|
+
|
|
390
|
+
// Build Sqlite wrapper last to avoid using getters before internals are set
|
|
391
|
+
const sqliteDbWrapper = new SqliteDbWrapper({ otel: otelOptions, db: clientSession.sqliteDb })
|
|
392
|
+
|
|
393
|
+
// Initialize internals bag
|
|
394
|
+
this[StoreInternalsSymbol] = {
|
|
395
|
+
eventSchema: LiveStoreEvent.Client.makeSchemaMemo(schema) as Schema.Codec<
|
|
396
|
+
LiveStoreEvent.Client.Decoded,
|
|
397
|
+
LiveStoreEvent.Client.Encoded
|
|
398
|
+
>,
|
|
399
|
+
clientSession,
|
|
400
|
+
sqliteDbWrapper,
|
|
401
|
+
effectContext,
|
|
402
|
+
otel: otelObj,
|
|
403
|
+
reactivityGraph,
|
|
404
|
+
tableRefs,
|
|
405
|
+
activeQueries,
|
|
406
|
+
syncProcessor,
|
|
407
|
+
boot,
|
|
408
|
+
isShutdown: false,
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
// Initialize stable network status property from client session
|
|
412
|
+
this.networkStatus = clientSession.leaderThread.networkStatus
|
|
297
413
|
}
|
|
298
|
-
|
|
414
|
+
//#endregion constructor
|
|
299
415
|
|
|
416
|
+
/**
|
|
417
|
+
* Current session identifier for this Store instance.
|
|
418
|
+
*
|
|
419
|
+
* - Stable for the lifetime of the Store
|
|
420
|
+
* - Useful for correlating events or scoping per-session data
|
|
421
|
+
*/
|
|
300
422
|
get sessionId(): string {
|
|
301
|
-
return this.clientSession.sessionId
|
|
423
|
+
return this[StoreInternalsSymbol].clientSession.sessionId
|
|
302
424
|
}
|
|
303
425
|
|
|
426
|
+
/**
|
|
427
|
+
* Stable client identifier for the process/device using this Store.
|
|
428
|
+
*
|
|
429
|
+
* - Shared across Store instances created by the same client
|
|
430
|
+
* - Useful for diagnostics and multi-client correlation
|
|
431
|
+
*/
|
|
304
432
|
get clientId(): string {
|
|
305
|
-
return this.clientSession.clientId
|
|
433
|
+
return this[StoreInternalsSymbol].clientSession.clientId
|
|
306
434
|
}
|
|
307
435
|
|
|
308
436
|
private checkShutdown = (operation: string): void => {
|
|
309
|
-
if (this.isShutdown) {
|
|
310
|
-
throw new
|
|
437
|
+
if (this[StoreInternalsSymbol].isShutdown === true) {
|
|
438
|
+
throw new UnknownError({
|
|
311
439
|
cause: `Store has been shut down (while performing "${operation}").`,
|
|
312
440
|
note: `You cannot perform this operation after the store has been shut down.`,
|
|
313
441
|
})
|
|
@@ -315,80 +443,109 @@ export class Store<TSchema extends LiveStoreSchema = LiveStoreSchema.Any, TConte
|
|
|
315
443
|
}
|
|
316
444
|
|
|
317
445
|
/**
|
|
318
|
-
* Subscribe to the results of a query
|
|
319
|
-
*
|
|
446
|
+
* Subscribe to the results of a query.
|
|
447
|
+
*
|
|
448
|
+
* - When providing an `onUpdate` callback it returns an {@link Unsubscribe} function.
|
|
449
|
+
* - Without a callback it returns an {@link AsyncIterable} that yields query results.
|
|
320
450
|
*
|
|
321
451
|
* @example
|
|
322
452
|
* ```ts
|
|
323
|
-
* const unsubscribe = store.subscribe(query$,
|
|
453
|
+
* const unsubscribe = store.subscribe(query$, (result) => console.log(result))
|
|
454
|
+
* ```
|
|
455
|
+
*
|
|
456
|
+
* @example
|
|
457
|
+
* ```ts
|
|
458
|
+
* for await (const result of store.subscribe(query$)) {
|
|
459
|
+
* console.log(result)
|
|
460
|
+
* }
|
|
324
461
|
* ```
|
|
325
462
|
*/
|
|
326
|
-
subscribe = <TResult>(
|
|
327
|
-
query:
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
stackInfo?: StackInfo
|
|
343
|
-
},
|
|
463
|
+
subscribe = (<TResult>(
|
|
464
|
+
query: Queryable<TResult>,
|
|
465
|
+
onUpdateOrOptions?: ((value: TResult) => void) | SubscribeOptions<TResult>,
|
|
466
|
+
maybeOptions?: SubscribeOptions<TResult>,
|
|
467
|
+
): Unsubscribe | AsyncIterable<TResult> => {
|
|
468
|
+
if (typeof onUpdateOrOptions === 'function') {
|
|
469
|
+
return this.subscribeWithCallback(query, onUpdateOrOptions, maybeOptions)
|
|
470
|
+
}
|
|
471
|
+
|
|
472
|
+
return this.subscribeAsAsyncIterable(query, onUpdateOrOptions)
|
|
473
|
+
}) as SubscribeFn
|
|
474
|
+
|
|
475
|
+
private subscribeWithCallback = <TResult>(
|
|
476
|
+
query: Queryable<TResult>,
|
|
477
|
+
onUpdate: (value: TResult) => void,
|
|
478
|
+
options?: SubscribeOptions<TResult>,
|
|
344
479
|
): Unsubscribe => {
|
|
345
480
|
this.checkShutdown('subscribe')
|
|
346
481
|
|
|
347
|
-
return this.otel.tracer.startActiveSpan(
|
|
482
|
+
return this[StoreInternalsSymbol].otel.tracer.startActiveSpan(
|
|
348
483
|
`LiveStore.subscribe`,
|
|
349
|
-
{
|
|
350
|
-
|
|
484
|
+
{
|
|
485
|
+
attributes: {
|
|
486
|
+
label: options?.label,
|
|
487
|
+
queryLabel: isQueryBuilder(query) === true ? query.toString() : query.label,
|
|
488
|
+
},
|
|
489
|
+
},
|
|
490
|
+
options?.otelContext ?? this[StoreInternalsSymbol].otel.queriesSpanContext,
|
|
351
491
|
(span) => {
|
|
352
|
-
// console.debug('store sub', query$.id, query$.label)
|
|
353
492
|
const otelContext = otel.trace.setSpan(otel.context.active(), span)
|
|
354
493
|
|
|
355
|
-
const queryRcRef =
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
494
|
+
const queryRcRef =
|
|
495
|
+
isQueryBuilder(query) === true
|
|
496
|
+
? queryDb(query).make(this[StoreInternalsSymbol].reactivityGraph.context!)
|
|
497
|
+
: query._tag === 'def' || query._tag === 'signal-def'
|
|
498
|
+
? query.make(this[StoreInternalsSymbol].reactivityGraph.context!)
|
|
499
|
+
: {
|
|
500
|
+
value: query,
|
|
501
|
+
deref: () => {},
|
|
502
|
+
}
|
|
363
503
|
const query$ = queryRcRef.value
|
|
364
504
|
|
|
365
505
|
const label = `subscribe:${options?.label}`
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
506
|
+
let suppressCallback = options?.skipInitialRun === true
|
|
507
|
+
const effect = this[StoreInternalsSymbol].reactivityGraph.makeEffect(
|
|
508
|
+
(get, _otelContext, debugRefreshReason) => {
|
|
509
|
+
const result = get(query$.results$, otelContext, debugRefreshReason)
|
|
510
|
+
if (suppressCallback === true) {
|
|
511
|
+
return
|
|
512
|
+
}
|
|
513
|
+
onUpdate(result)
|
|
514
|
+
},
|
|
369
515
|
{ label },
|
|
370
516
|
)
|
|
517
|
+
const runInitialEffect = () => {
|
|
518
|
+
effect.doEffect(otelContext, {
|
|
519
|
+
_tag: 'subscribe.initial',
|
|
520
|
+
label: `subscribe-initial-run:${options?.label}`,
|
|
521
|
+
})
|
|
522
|
+
}
|
|
371
523
|
|
|
372
|
-
if (options?.stackInfo) {
|
|
524
|
+
if (options?.stackInfo !== undefined) {
|
|
373
525
|
query$.activeSubscriptions.add(options.stackInfo)
|
|
374
526
|
}
|
|
375
527
|
|
|
376
528
|
options?.onSubscribe?.(query$)
|
|
377
529
|
|
|
378
|
-
this.activeQueries.add(query$ as LiveQuery<TResult>)
|
|
530
|
+
this[StoreInternalsSymbol].activeQueries.add(query$ as LiveQuery<TResult>)
|
|
379
531
|
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
532
|
+
if (query$.isDestroyed === false) {
|
|
533
|
+
if (suppressCallback === true) {
|
|
534
|
+
// We still run once to register dependencies in the reactive graph, but suppress the initial callback so the
|
|
535
|
+
// caller truly skips the first emission; subsequent runs (after commits) will call the callback.
|
|
536
|
+
runInitialEffect()
|
|
537
|
+
suppressCallback = false
|
|
538
|
+
} else {
|
|
539
|
+
runInitialEffect()
|
|
540
|
+
}
|
|
383
541
|
}
|
|
384
542
|
|
|
385
543
|
const unsubscribe = () => {
|
|
386
|
-
// console.debug('store unsub', query$.id, query$.label)
|
|
387
544
|
try {
|
|
388
|
-
this.reactivityGraph.destroyNode(effect)
|
|
389
|
-
this.activeQueries.remove(query$ as LiveQuery<TResult>)
|
|
545
|
+
this[StoreInternalsSymbol].reactivityGraph.destroyNode(effect)
|
|
546
|
+
this[StoreInternalsSymbol].activeQueries.remove(query$ as LiveQuery<TResult>)
|
|
390
547
|
|
|
391
|
-
if (options?.stackInfo) {
|
|
548
|
+
if (options?.stackInfo !== undefined) {
|
|
392
549
|
query$.activeSubscriptions.delete(options.stackInfo)
|
|
393
550
|
}
|
|
394
551
|
|
|
@@ -405,23 +562,117 @@ export class Store<TSchema extends LiveStoreSchema = LiveStoreSchema.Any, TConte
|
|
|
405
562
|
)
|
|
406
563
|
}
|
|
407
564
|
|
|
408
|
-
|
|
409
|
-
query
|
|
410
|
-
options?:
|
|
411
|
-
):
|
|
412
|
-
|
|
413
|
-
|
|
565
|
+
private subscribeAsAsyncIterable = <TResult>(
|
|
566
|
+
query: Queryable<TResult>,
|
|
567
|
+
options?: SubscribeOptions<TResult>,
|
|
568
|
+
): AsyncIterable<TResult> => {
|
|
569
|
+
this.checkShutdown('subscribe')
|
|
570
|
+
|
|
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
|
+
}
|
|
653
|
+
}
|
|
654
|
+
|
|
655
|
+
subscribeStream = <TResult>(query: Queryable<TResult>, options?: SubscribeOptions<TResult>): Stream.Stream<TResult> =>
|
|
656
|
+
Stream.callback<TResult>((emit) =>
|
|
657
|
+
Effect.gen({ self: this }, function* () {
|
|
414
658
|
const otelSpan = yield* OtelTracer.currentOtelSpan.pipe(
|
|
415
|
-
Effect.catchTag('
|
|
659
|
+
Effect.catchTag('NoSuchElementError', () => Effect.void),
|
|
416
660
|
)
|
|
417
|
-
const otelContext =
|
|
661
|
+
const otelContext =
|
|
662
|
+
otelSpan !== undefined ? otel.trace.setSpan(otel.context.active(), otelSpan) : otel.context.active()
|
|
418
663
|
|
|
419
664
|
yield* Effect.acquireRelease(
|
|
420
665
|
Effect.sync(() =>
|
|
421
|
-
this.subscribe(
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
666
|
+
this.subscribe(
|
|
667
|
+
query,
|
|
668
|
+
(result) => {
|
|
669
|
+
Queue.offerUnsafe(emit, result)
|
|
670
|
+
},
|
|
671
|
+
{
|
|
672
|
+
...options,
|
|
673
|
+
otelContext,
|
|
674
|
+
},
|
|
675
|
+
),
|
|
425
676
|
),
|
|
426
677
|
(unsub) => Effect.sync(() => unsub()),
|
|
427
678
|
)
|
|
@@ -443,25 +694,24 @@ export class Store<TSchema extends LiveStoreSchema = LiveStoreSchema.Any, TConte
|
|
|
443
694
|
* ```
|
|
444
695
|
*/
|
|
445
696
|
query = <TResult>(
|
|
446
|
-
query:
|
|
447
|
-
| QueryBuilder<TResult, any, any>
|
|
448
|
-
| LiveQuery<TResult>
|
|
449
|
-
| LiveQueryDef<TResult>
|
|
450
|
-
| SignalDef<TResult>
|
|
451
|
-
| { query: string; bindValues: Bindable; schema?: Schema.Schema<TResult> },
|
|
697
|
+
query: Queryable<TResult> | { query: string; bindValues: Bindable; schema?: Schema.Decoder<TResult> },
|
|
452
698
|
options?: { otelContext?: otel.Context; debugRefreshReason?: RefreshReason },
|
|
453
699
|
): TResult => {
|
|
454
700
|
this.checkShutdown('query')
|
|
455
701
|
|
|
456
702
|
if (typeof query === 'object' && 'query' in query && 'bindValues' in query) {
|
|
457
|
-
const res = this.sqliteDbWrapper.cachedSelect(
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
703
|
+
const res = this[StoreInternalsSymbol].sqliteDbWrapper.cachedSelect(
|
|
704
|
+
query.query,
|
|
705
|
+
prepareBindValues(query.bindValues, query.query),
|
|
706
|
+
{
|
|
707
|
+
...omitUndefineds({ otelContext: options?.otelContext }),
|
|
708
|
+
},
|
|
709
|
+
) as any
|
|
710
|
+
if (query.schema !== undefined) {
|
|
711
|
+
return Schema.decodeUnknownSync(query.schema)(res)
|
|
462
712
|
}
|
|
463
713
|
return res
|
|
464
|
-
} else if (isQueryBuilder(query)) {
|
|
714
|
+
} else if (isQueryBuilder(query) === true) {
|
|
465
715
|
const ast = query[QueryBuilderAstSymbol]
|
|
466
716
|
if (ast._tag === 'RowQuery') {
|
|
467
717
|
makeExecBeforeFirstRun({
|
|
@@ -469,42 +719,48 @@ export class Store<TSchema extends LiveStoreSchema = LiveStoreSchema.Any, TConte
|
|
|
469
719
|
id: ast.id,
|
|
470
720
|
explicitDefaultValues: ast.explicitDefaultValues,
|
|
471
721
|
otelContext: options?.otelContext,
|
|
472
|
-
})(this.reactivityGraph.context!)
|
|
722
|
+
})(this[StoreInternalsSymbol].reactivityGraph.context!)
|
|
473
723
|
}
|
|
474
724
|
|
|
475
725
|
const sqlRes = query.asSql()
|
|
476
726
|
const schema = getResultSchema(query)
|
|
477
727
|
|
|
478
|
-
//
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
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)
|
|
734
|
+
|
|
735
|
+
const rawRes = this[StoreInternalsSymbol].sqliteDbWrapper.cachedSelect(
|
|
736
|
+
sqlRes.query,
|
|
737
|
+
resolvedBindValues === undefined ? undefined : prepareBindValues(resolvedBindValues, sqlRes.query),
|
|
738
|
+
{
|
|
739
|
+
...omitUndefineds({ otelContext: options?.otelContext }),
|
|
740
|
+
queriedTables: new Set([query[QueryBuilderAstSymbol].tableDef.sqliteDef.name]),
|
|
741
|
+
},
|
|
742
|
+
)
|
|
487
743
|
|
|
488
|
-
const decodeResult = Schema.
|
|
489
|
-
if (decodeResult
|
|
490
|
-
return decodeResult.
|
|
744
|
+
const decodeResult = Schema.decodeResult(schema)(rawRes)
|
|
745
|
+
if (Result.isSuccess(decodeResult) === true) {
|
|
746
|
+
return decodeResult.success
|
|
491
747
|
} else {
|
|
492
748
|
return shouldNeverHappen(
|
|
493
|
-
|
|
494
|
-
schema
|
|
749
|
+
'Failed to decode query result with for schema:',
|
|
750
|
+
objectToString(schema),
|
|
495
751
|
'raw result:',
|
|
496
752
|
rawRes,
|
|
497
753
|
'decode error:',
|
|
498
|
-
decodeResult.
|
|
754
|
+
decodeResult.failure,
|
|
499
755
|
)
|
|
500
756
|
}
|
|
501
757
|
} else if (query._tag === 'def') {
|
|
502
|
-
const query$ = query.make(this.reactivityGraph.context!)
|
|
758
|
+
const query$ = query.make(this[StoreInternalsSymbol].reactivityGraph.context!)
|
|
503
759
|
const result = this.query(query$.value, options)
|
|
504
760
|
query$.deref()
|
|
505
761
|
return result
|
|
506
762
|
} else if (query._tag === 'signal-def') {
|
|
507
|
-
const signal$ = query.make(this.reactivityGraph.context!)
|
|
763
|
+
const signal$ = query.make(this[StoreInternalsSymbol].reactivityGraph.context!)
|
|
508
764
|
return signal$.value.get()
|
|
509
765
|
} else {
|
|
510
766
|
return query.run({
|
|
@@ -531,7 +787,7 @@ export class Store<TSchema extends LiveStoreSchema = LiveStoreSchema.Any, TConte
|
|
|
531
787
|
setSignal = <T>(signalDef: SignalDef<T>, value: T | ((prev: T) => T)): void => {
|
|
532
788
|
this.checkShutdown('setSignal')
|
|
533
789
|
|
|
534
|
-
const signalRef = signalDef.make(this.reactivityGraph.context!)
|
|
790
|
+
const signalRef = signalDef.make(this[StoreInternalsSymbol].reactivityGraph.context!)
|
|
535
791
|
const newValue: T = typeof value === 'function' ? (value as any)(signalRef.value.get()) : value
|
|
536
792
|
signalRef.value.set(newValue)
|
|
537
793
|
|
|
@@ -545,7 +801,7 @@ export class Store<TSchema extends LiveStoreSchema = LiveStoreSchema.Any, TConte
|
|
|
545
801
|
}
|
|
546
802
|
}
|
|
547
803
|
|
|
548
|
-
|
|
804
|
+
//#region commit
|
|
549
805
|
/**
|
|
550
806
|
* Commit a list of events to the store which will immediately update the local database
|
|
551
807
|
* and sync the events across other clients (similar to a `git commit`).
|
|
@@ -598,19 +854,19 @@ export class Store<TSchema extends LiveStoreSchema = LiveStoreSchema.Any, TConte
|
|
|
598
854
|
* ```
|
|
599
855
|
*/
|
|
600
856
|
commit: {
|
|
601
|
-
<const TCommitArg extends ReadonlyArray<LiveStoreEvent.
|
|
857
|
+
<const TCommitArg extends ReadonlyArray<LiveStoreEvent.Input.ForSchema<TSchema>>>(...list: TCommitArg): void
|
|
602
858
|
(
|
|
603
|
-
txn: <const TCommitArg extends ReadonlyArray<LiveStoreEvent.
|
|
859
|
+
txn: <const TCommitArg extends ReadonlyArray<LiveStoreEvent.Input.ForSchema<TSchema>>>(
|
|
604
860
|
...list: TCommitArg
|
|
605
861
|
) => void,
|
|
606
862
|
): void
|
|
607
|
-
<const TCommitArg extends ReadonlyArray<LiveStoreEvent.
|
|
863
|
+
<const TCommitArg extends ReadonlyArray<LiveStoreEvent.Input.ForSchema<TSchema>>>(
|
|
608
864
|
options: StoreCommitOptions,
|
|
609
865
|
...list: TCommitArg
|
|
610
866
|
): void
|
|
611
867
|
(
|
|
612
868
|
options: StoreCommitOptions,
|
|
613
|
-
txn: <const TCommitArg extends ReadonlyArray<LiveStoreEvent.
|
|
869
|
+
txn: <const TCommitArg extends ReadonlyArray<LiveStoreEvent.Input.ForSchema<TSchema>>>(
|
|
614
870
|
...list: TCommitArg
|
|
615
871
|
) => void,
|
|
616
872
|
): void
|
|
@@ -619,41 +875,34 @@ export class Store<TSchema extends LiveStoreSchema = LiveStoreSchema.Any, TConte
|
|
|
619
875
|
|
|
620
876
|
const { events, options } = this.getCommitArgs(firstEventOrTxnFnOrOptions, restEvents)
|
|
621
877
|
|
|
622
|
-
Effect.gen(this, function* () {
|
|
623
|
-
const commitsSpan = otel.trace.getSpan(this.otel.commitsSpanContext)
|
|
878
|
+
Effect.gen({ self: this }, function* () {
|
|
879
|
+
const commitsSpan = otel.trace.getSpan(this[StoreInternalsSymbol].otel.commitsSpanContext)
|
|
624
880
|
commitsSpan?.addEvent('commit')
|
|
625
881
|
const currentSpan = yield* OtelTracer.currentOtelSpan.pipe(Effect.orDie)
|
|
626
882
|
commitsSpan?.addLink({ context: currentSpan.spanContext() })
|
|
627
883
|
|
|
628
|
-
for (const event of events) {
|
|
629
|
-
replaceSessionIdSymbol(event.args, this.clientSession.sessionId)
|
|
630
|
-
}
|
|
631
|
-
|
|
632
884
|
if (events.length === 0) return
|
|
633
885
|
|
|
634
|
-
const
|
|
886
|
+
const localServices = yield* Effect.context()
|
|
635
887
|
|
|
636
|
-
const
|
|
637
|
-
try: () => {
|
|
638
|
-
const runMaterializeEvents = () => {
|
|
639
|
-
return this.syncProcessor.push(events).pipe(Runtime.runSync(localRuntime))
|
|
640
|
-
}
|
|
888
|
+
const encodedEvents = yield* this[StoreInternalsSymbol].syncProcessor.encodeEvents(events)
|
|
641
889
|
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
890
|
+
const { writeTables } = yield* Effect.try({
|
|
891
|
+
try: () => {
|
|
892
|
+
const materialize = () =>
|
|
893
|
+
this[StoreInternalsSymbol].syncProcessor
|
|
894
|
+
.materializeEvents(encodedEvents)
|
|
895
|
+
.pipe(Effect.runSyncWith(localServices))
|
|
896
|
+
return events.length > 1 ? this[StoreInternalsSymbol].sqliteDbWrapper.txn(materialize) : materialize()
|
|
647
897
|
},
|
|
648
|
-
catch: (cause) =>
|
|
898
|
+
catch: (cause) => UnknownError.make({ cause }),
|
|
649
899
|
})
|
|
650
900
|
|
|
651
|
-
|
|
652
|
-
const { writeTables } = yield* materializeEventsTx
|
|
901
|
+
yield* this[StoreInternalsSymbol].syncProcessor.push(encodedEvents)
|
|
653
902
|
|
|
654
903
|
const tablesToUpdate: [Ref<null, ReactivityGraphContext, RefreshReason>, null][] = []
|
|
655
904
|
for (const tableName of writeTables) {
|
|
656
|
-
const tableRef = this.tableRefs[tableName]
|
|
905
|
+
const tableRef = this[StoreInternalsSymbol].tableRefs[tableName]
|
|
657
906
|
assertNever(tableRef !== undefined, `No table ref found for ${tableName}`)
|
|
658
907
|
tablesToUpdate.push([tableRef!, null])
|
|
659
908
|
}
|
|
@@ -666,7 +915,7 @@ export class Store<TSchema extends LiveStoreSchema = LiveStoreSchema.Any, TConte
|
|
|
666
915
|
const skipRefresh = options?.skipRefresh ?? false
|
|
667
916
|
|
|
668
917
|
// Update all table refs together in a batch, to only trigger one reactive update
|
|
669
|
-
this.reactivityGraph.setRefs(tablesToUpdate, {
|
|
918
|
+
this[StoreInternalsSymbol].reactivityGraph.setRefs(tablesToUpdate, {
|
|
670
919
|
debugRefreshReason,
|
|
671
920
|
skipRefresh,
|
|
672
921
|
otelContext: otel.trace.setSpan(otel.context.active(), currentSpan),
|
|
@@ -681,46 +930,204 @@ export class Store<TSchema extends LiveStoreSchema = LiveStoreSchema.Any, TConte
|
|
|
681
930
|
},
|
|
682
931
|
links: [
|
|
683
932
|
// Span link to LiveStore:commits
|
|
684
|
-
OtelTracer.makeSpanLink({
|
|
933
|
+
OtelTracer.makeSpanLink({
|
|
934
|
+
context: otel.trace.getSpanContext(this[StoreInternalsSymbol].otel.commitsSpanContext)!,
|
|
935
|
+
}),
|
|
685
936
|
// User-provided span links
|
|
686
937
|
...(options?.spanLinks?.map(OtelTracer.makeSpanLink) ?? []),
|
|
687
938
|
],
|
|
688
939
|
}),
|
|
689
|
-
Effect.
|
|
690
|
-
Effect.
|
|
691
|
-
|
|
940
|
+
Effect.tapCause(Effect.logError),
|
|
941
|
+
Effect.catchCause((cause) => Effect.forkChild(this.shutdown(cause))),
|
|
942
|
+
Effect.runSyncWith(this[StoreInternalsSymbol].effectContext.services),
|
|
692
943
|
)
|
|
693
944
|
}
|
|
694
|
-
|
|
945
|
+
//#endregion commit
|
|
695
946
|
|
|
696
947
|
/**
|
|
697
|
-
* Returns an async iterable of events.
|
|
948
|
+
* Returns an async iterable of events from the eventlog.
|
|
949
|
+
* Currently only events confirmed by the sync backend is supported.
|
|
950
|
+
*
|
|
951
|
+
* Defaults to tracking upstreamHead as it advances. If an `until` event is
|
|
952
|
+
* supplied the stream finalizes upon reaching it.
|
|
953
|
+
*
|
|
954
|
+
* To start streaming from a specific point in the eventlog
|
|
955
|
+
* you can provide a `since` event.
|
|
956
|
+
*
|
|
957
|
+
* Allows filtering by:
|
|
958
|
+
* - `filter`: event types
|
|
959
|
+
* - `clientIds`: client identifiers
|
|
960
|
+
* - `sessionIds`: session identifiers
|
|
961
|
+
*
|
|
962
|
+
* The batchSize option controls the maximum amount of events that are fetched
|
|
963
|
+
* from the eventlog in each query. Defaults to 100 and has a max allowed
|
|
964
|
+
* value of 1000.
|
|
965
|
+
*
|
|
966
|
+
* TODO:
|
|
967
|
+
* - Support streaming unconfirmed events
|
|
968
|
+
* - Leader level
|
|
969
|
+
* - Session level
|
|
970
|
+
* - Support streaming client-only events
|
|
698
971
|
*
|
|
699
972
|
* @example
|
|
700
973
|
* ```ts
|
|
701
|
-
*
|
|
974
|
+
* // Stream todoCompleted events from the start
|
|
975
|
+
* for await (const event of store.events(filter: ['todoCompleted'])) {
|
|
702
976
|
* console.log(event)
|
|
703
977
|
* }
|
|
704
978
|
* ```
|
|
705
979
|
*
|
|
706
980
|
* @example
|
|
707
981
|
* ```ts
|
|
708
|
-
* //
|
|
709
|
-
* for await (const event of store.events({
|
|
982
|
+
* // Start streaming from a specific event
|
|
983
|
+
* for await (const event of store.events({ since: EventSequenceNumber.Client.fromString('e3') })) {
|
|
710
984
|
* console.log(event)
|
|
711
985
|
* }
|
|
712
986
|
* ```
|
|
713
987
|
*/
|
|
714
|
-
events = (
|
|
715
|
-
this.
|
|
988
|
+
events = (options?: StoreEventsOptions<TSchema>): AsyncIterable<LiveStoreEvent.Client.ForSchema<TSchema>> => {
|
|
989
|
+
const stream = this.eventsStream(options)
|
|
990
|
+
return {
|
|
991
|
+
async *[Symbol.asyncIterator]() {
|
|
992
|
+
const iterator = Stream.toAsyncIterable(stream)
|
|
993
|
+
for await (const event of iterator) {
|
|
994
|
+
yield event
|
|
995
|
+
}
|
|
996
|
+
},
|
|
997
|
+
}
|
|
998
|
+
}
|
|
999
|
+
|
|
1000
|
+
/**
|
|
1001
|
+
* Returns an Effect Stream of events from the eventlog.
|
|
1002
|
+
* See `store.events` for details on options and behaviour.
|
|
1003
|
+
*/
|
|
1004
|
+
eventsStream = (
|
|
1005
|
+
options?: StoreEventsOptions<TSchema>,
|
|
1006
|
+
): Stream.Stream<LiveStoreEvent.Client.ForSchema<TSchema>, UnknownError> => {
|
|
1007
|
+
const { clientSession } = this[StoreInternalsSymbol]
|
|
1008
|
+
const eventSchema = LiveStoreEvent.Client.makeSchema(this.schema)
|
|
1009
|
+
|
|
1010
|
+
const preferredBatchSize =
|
|
1011
|
+
options?.batchSize ?? this.params.eventQueryBatchSize ?? STORE_DEFAULT_PARAMS.eventQueryBatchSize
|
|
1012
|
+
|
|
1013
|
+
const baseOptions: StreamEventsOptions = {
|
|
1014
|
+
...options,
|
|
1015
|
+
filter: options?.filter as readonly string[],
|
|
1016
|
+
batchSize: preferredBatchSize,
|
|
1017
|
+
}
|
|
716
1018
|
|
|
717
|
-
return
|
|
1019
|
+
return clientSession.leaderThread.events.stream(baseOptions).pipe(
|
|
1020
|
+
Stream.mapEffect((event) => Schema.decodeEffect(eventSchema)(event)),
|
|
1021
|
+
Stream.catchTag('SchemaError', (cause) => Stream.fail(UnknownError.make({ cause }))),
|
|
1022
|
+
Stream.tapError((error) => Effect.logError('Error in eventsStream', error)),
|
|
1023
|
+
)
|
|
718
1024
|
}
|
|
719
1025
|
|
|
720
|
-
|
|
721
|
-
|
|
1026
|
+
/**
|
|
1027
|
+
* Returns the current synchronization status of the store.
|
|
1028
|
+
*
|
|
1029
|
+
* This is a synchronous operation that returns the sync state between the
|
|
1030
|
+
* client session and the leader thread. Use this to display sync indicators
|
|
1031
|
+
* or check if local changes have been pushed to the leader.
|
|
1032
|
+
*
|
|
1033
|
+
* @example
|
|
1034
|
+
* ```ts
|
|
1035
|
+
* const status = store.syncStatus()
|
|
1036
|
+
* console.log(status.isSynced ? 'Synced' : `${status.pendingCount} pending`)
|
|
1037
|
+
* ```
|
|
1038
|
+
*
|
|
1039
|
+
* @example
|
|
1040
|
+
* ```ts
|
|
1041
|
+
* // Health check for backend connectivity
|
|
1042
|
+
* const status = store.syncStatus()
|
|
1043
|
+
* if (!status.isSynced && status.pendingCount > 100) {
|
|
1044
|
+
* console.warn('Large backlog of unsynced events')
|
|
1045
|
+
* }
|
|
1046
|
+
* ```
|
|
1047
|
+
*/
|
|
1048
|
+
syncStatus = (): SyncStatus => {
|
|
1049
|
+
this.checkShutdown('syncStatus')
|
|
1050
|
+
|
|
1051
|
+
const syncState = this[StoreInternalsSymbol].syncProcessor.syncState.pipe(Effect.runSync)
|
|
1052
|
+
const pendingCount = syncState.pending.length
|
|
1053
|
+
|
|
1054
|
+
return {
|
|
1055
|
+
localHead: EventSequenceNumber.Client.toString(syncState.localHead),
|
|
1056
|
+
upstreamHead: EventSequenceNumber.Client.toString(syncState.upstreamHead),
|
|
1057
|
+
pendingCount,
|
|
1058
|
+
isSynced: pendingCount === 0,
|
|
1059
|
+
}
|
|
1060
|
+
}
|
|
1061
|
+
|
|
1062
|
+
/**
|
|
1063
|
+
* Returns an Effect Stream of sync status updates.
|
|
1064
|
+
*
|
|
1065
|
+
* Emits the current status immediately and then whenever the sync state changes.
|
|
1066
|
+
* Use this for Effect-based workflows or when you need more control over the stream.
|
|
1067
|
+
*
|
|
1068
|
+
* @example
|
|
1069
|
+
* ```ts
|
|
1070
|
+
* store.syncStatusStream().pipe(
|
|
1071
|
+
* Stream.tap((status) => Effect.log(`Sync status: ${status.isSynced}`)),
|
|
1072
|
+
* Stream.runDrain,
|
|
1073
|
+
* )
|
|
1074
|
+
* ```
|
|
1075
|
+
*/
|
|
1076
|
+
syncStatusStream = (): Stream.Stream<SyncStatus> => {
|
|
1077
|
+
const syncStateSubscribable = this[StoreInternalsSymbol].syncProcessor.syncState
|
|
1078
|
+
|
|
1079
|
+
return Stream.concat(
|
|
1080
|
+
Stream.fromEffect(syncStateSubscribable.pipe(Effect.map(this.makeSyncStatus))),
|
|
1081
|
+
syncStateSubscribable.changes.pipe(Stream.map(this.makeSyncStatus)),
|
|
1082
|
+
)
|
|
1083
|
+
}
|
|
1084
|
+
|
|
1085
|
+
/**
|
|
1086
|
+
* Subscribes to sync status changes.
|
|
1087
|
+
*
|
|
1088
|
+
* The callback is invoked immediately with the current status and then
|
|
1089
|
+
* whenever the sync state changes (e.g., when events are pushed or confirmed).
|
|
1090
|
+
*
|
|
1091
|
+
* @param onUpdate - Callback invoked with the current sync status
|
|
1092
|
+
* @returns Unsubscribe function to stop receiving updates
|
|
1093
|
+
*
|
|
1094
|
+
* @example
|
|
1095
|
+
* ```ts
|
|
1096
|
+
* const unsubscribe = store.subscribeSyncStatus((status) => {
|
|
1097
|
+
* updateUI(status.isSynced ? 'Synced' : 'Syncing...')
|
|
1098
|
+
* })
|
|
1099
|
+
*
|
|
1100
|
+
* // Later, stop listening
|
|
1101
|
+
* unsubscribe()
|
|
1102
|
+
* ```
|
|
1103
|
+
*/
|
|
1104
|
+
subscribeSyncStatus = (onUpdate: (status: SyncStatus) => void): Unsubscribe => {
|
|
1105
|
+
this.checkShutdown('subscribeSyncStatus')
|
|
1106
|
+
|
|
1107
|
+
const fiber = this.syncStatusStream().pipe(
|
|
1108
|
+
Stream.tap((status) => Effect.sync(() => onUpdate(status))),
|
|
1109
|
+
Stream.runDrain,
|
|
1110
|
+
this.runEffectFork,
|
|
1111
|
+
)
|
|
722
1112
|
|
|
723
|
-
return
|
|
1113
|
+
return () => {
|
|
1114
|
+
Fiber.interrupt(fiber).pipe(Effect.runForkWith(this[StoreInternalsSymbol].effectContext.services))
|
|
1115
|
+
}
|
|
1116
|
+
}
|
|
1117
|
+
|
|
1118
|
+
private makeSyncStatus = (syncState: {
|
|
1119
|
+
localHead: EventSequenceNumber.Client.Composite
|
|
1120
|
+
upstreamHead: EventSequenceNumber.Client.Composite
|
|
1121
|
+
pending: readonly any[]
|
|
1122
|
+
}): SyncStatus => {
|
|
1123
|
+
const pendingCount = syncState.pending.length
|
|
1124
|
+
|
|
1125
|
+
return {
|
|
1126
|
+
localHead: EventSequenceNumber.Client.toString(syncState.localHead),
|
|
1127
|
+
upstreamHead: EventSequenceNumber.Client.toString(syncState.upstreamHead),
|
|
1128
|
+
pendingCount,
|
|
1129
|
+
isSynced: pendingCount === 0,
|
|
1130
|
+
}
|
|
724
1131
|
}
|
|
725
1132
|
|
|
726
1133
|
/**
|
|
@@ -731,13 +1138,13 @@ export class Store<TSchema extends LiveStoreSchema = LiveStoreSchema.Any, TConte
|
|
|
731
1138
|
this.checkShutdown('manualRefresh')
|
|
732
1139
|
|
|
733
1140
|
const { label } = options ?? {}
|
|
734
|
-
this.otel.tracer.startActiveSpan(
|
|
1141
|
+
this[StoreInternalsSymbol].otel.tracer.startActiveSpan(
|
|
735
1142
|
'LiveStore:manualRefresh',
|
|
736
1143
|
{ attributes: { 'livestore.manualRefreshLabel': label } },
|
|
737
|
-
this.otel.commitsSpanContext,
|
|
1144
|
+
this[StoreInternalsSymbol].otel.commitsSpanContext,
|
|
738
1145
|
(span) => {
|
|
739
1146
|
const otelContext = otel.trace.setSpan(otel.context.active(), span)
|
|
740
|
-
this.reactivityGraph.runDeferredEffects({ otelContext })
|
|
1147
|
+
this[StoreInternalsSymbol].reactivityGraph.runDeferredEffects({ otelContext })
|
|
741
1148
|
span.end()
|
|
742
1149
|
},
|
|
743
1150
|
)
|
|
@@ -748,11 +1155,15 @@ export class Store<TSchema extends LiveStoreSchema = LiveStoreSchema.Any, TConte
|
|
|
748
1155
|
*
|
|
749
1156
|
* This is called automatically when the store was created using the React or Effect API.
|
|
750
1157
|
*/
|
|
751
|
-
shutdownPromise = async (cause?:
|
|
1158
|
+
shutdownPromise = async (cause?: UnknownError) => {
|
|
752
1159
|
this.checkShutdown('shutdownPromise')
|
|
753
1160
|
|
|
754
|
-
this.isShutdown = true
|
|
755
|
-
await this.shutdown(cause ? Cause.fail(cause) : undefined).pipe(
|
|
1161
|
+
this[StoreInternalsSymbol].isShutdown = true
|
|
1162
|
+
await this.shutdown(cause !== undefined ? Cause.fail(cause) : undefined).pipe(
|
|
1163
|
+
this.runEffectFork,
|
|
1164
|
+
Fiber.join,
|
|
1165
|
+
Effect.runPromise,
|
|
1166
|
+
)
|
|
756
1167
|
}
|
|
757
1168
|
|
|
758
1169
|
/**
|
|
@@ -760,10 +1171,10 @@ export class Store<TSchema extends LiveStoreSchema = LiveStoreSchema.Any, TConte
|
|
|
760
1171
|
*
|
|
761
1172
|
* This is called automatically when the store was created using the React or Effect API.
|
|
762
1173
|
*/
|
|
763
|
-
shutdown = (cause?: Cause.Cause<
|
|
764
|
-
this.isShutdown = true
|
|
765
|
-
return this.clientSession.shutdown(
|
|
766
|
-
cause ? Exit.failCause(cause) : Exit.succeed(IntentionalShutdownCause.make({ reason: 'manual' })),
|
|
1174
|
+
shutdown = (cause?: Cause.Cause<UnknownError | MaterializeError>): Effect.Effect<void> => {
|
|
1175
|
+
this[StoreInternalsSymbol].isShutdown = true
|
|
1176
|
+
return this[StoreInternalsSymbol].clientSession.shutdown(
|
|
1177
|
+
cause !== undefined ? Exit.failCause(cause) : Exit.succeed(IntentionalShutdownCause.make({ reason: 'manual' })),
|
|
767
1178
|
)
|
|
768
1179
|
}
|
|
769
1180
|
|
|
@@ -774,31 +1185,34 @@ export class Store<TSchema extends LiveStoreSchema = LiveStoreSchema.Any, TConte
|
|
|
774
1185
|
*/
|
|
775
1186
|
_dev = {
|
|
776
1187
|
downloadDb: (source: 'local' | 'leader' = 'local') => {
|
|
777
|
-
Effect.gen(this, function* () {
|
|
778
|
-
const data =
|
|
1188
|
+
Effect.gen({ self: this }, function* () {
|
|
1189
|
+
const data =
|
|
1190
|
+
source === 'local'
|
|
1191
|
+
? this[StoreInternalsSymbol].sqliteDbWrapper.export()
|
|
1192
|
+
: yield* this[StoreInternalsSymbol].clientSession.leaderThread.export
|
|
779
1193
|
downloadBlob(data, `livestore-${Date.now()}.db`)
|
|
780
1194
|
}).pipe(this.runEffectFork)
|
|
781
1195
|
},
|
|
782
1196
|
|
|
783
1197
|
downloadEventlogDb: () => {
|
|
784
|
-
Effect.gen(this, function* () {
|
|
785
|
-
const data = yield* this.clientSession.leaderThread.getEventlogData
|
|
1198
|
+
Effect.gen({ self: this }, function* () {
|
|
1199
|
+
const data = yield* this[StoreInternalsSymbol].clientSession.leaderThread.getEventlogData
|
|
786
1200
|
downloadBlob(data, `livestore-eventlog-${Date.now()}.db`)
|
|
787
1201
|
}).pipe(this.runEffectFork)
|
|
788
1202
|
},
|
|
789
1203
|
|
|
790
1204
|
hardReset: (mode: 'all-data' | 'only-app-db' = 'all-data') => {
|
|
791
|
-
Effect.gen(this, function* () {
|
|
792
|
-
const clientId = this.clientSession.clientId
|
|
793
|
-
yield* this.clientSession.leaderThread.sendDevtoolsMessage(
|
|
1205
|
+
Effect.gen({ self: this }, function* () {
|
|
1206
|
+
const clientId = this[StoreInternalsSymbol].clientSession.clientId
|
|
1207
|
+
yield* this[StoreInternalsSymbol].clientSession.leaderThread.sendDevtoolsMessage(
|
|
794
1208
|
Devtools.Leader.ResetAllData.Request.make({ liveStoreVersion, mode, requestId: nanoid(), clientId }),
|
|
795
1209
|
)
|
|
796
1210
|
}).pipe(this.runEffectFork)
|
|
797
1211
|
},
|
|
798
1212
|
|
|
799
1213
|
overrideNetworkStatus: (status: 'online' | 'offline') => {
|
|
800
|
-
const clientId = this.clientSession.clientId
|
|
801
|
-
this.clientSession.leaderThread
|
|
1214
|
+
const clientId = this[StoreInternalsSymbol].clientSession.clientId
|
|
1215
|
+
this[StoreInternalsSymbol].clientSession.leaderThread
|
|
802
1216
|
.sendDevtoolsMessage(
|
|
803
1217
|
Devtools.Leader.SetSyncLatch.Request.make({
|
|
804
1218
|
clientId,
|
|
@@ -810,56 +1224,60 @@ export class Store<TSchema extends LiveStoreSchema = LiveStoreSchema.Any, TConte
|
|
|
810
1224
|
.pipe(this.runEffectFork)
|
|
811
1225
|
},
|
|
812
1226
|
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
const
|
|
1227
|
+
// NOTE: Explicit return type needed to avoid TS2742 (inferred type references internal path)
|
|
1228
|
+
syncStates: (): Promise<{ session: SyncState.SyncState; leader: SyncState.SyncState }> =>
|
|
1229
|
+
Effect.gen({ self: this }, function* () {
|
|
1230
|
+
const session = yield* this[StoreInternalsSymbol].syncProcessor.syncState
|
|
1231
|
+
const leader = yield* this[StoreInternalsSymbol].clientSession.leaderThread.syncState
|
|
817
1232
|
return { session, leader }
|
|
818
1233
|
}).pipe(this.runEffectPromise),
|
|
819
1234
|
|
|
820
1235
|
printSyncStates: () => {
|
|
821
|
-
Effect.gen(this, function* () {
|
|
822
|
-
const session = yield* this.syncProcessor.syncState
|
|
1236
|
+
Effect.gen({ self: this }, function* () {
|
|
1237
|
+
const session = yield* this[StoreInternalsSymbol].syncProcessor.syncState
|
|
823
1238
|
yield* Effect.log(
|
|
824
|
-
`Session sync state: ${session.localHead} (upstream: ${session.upstreamHead})`,
|
|
1239
|
+
`Session sync state: ${objectToString(session.localHead)} (upstream: ${objectToString(session.upstreamHead)})`,
|
|
825
1240
|
session.toJSON(),
|
|
826
1241
|
)
|
|
827
|
-
const leader = yield* this.clientSession.leaderThread.
|
|
828
|
-
yield* Effect.log(
|
|
1242
|
+
const leader = yield* this[StoreInternalsSymbol].clientSession.leaderThread.syncState
|
|
1243
|
+
yield* Effect.log(
|
|
1244
|
+
`Leader sync state: ${objectToString(leader.localHead)} (upstream: ${objectToString(leader.upstreamHead)})`,
|
|
1245
|
+
leader.toJSON(),
|
|
1246
|
+
)
|
|
829
1247
|
}).pipe(this.runEffectFork)
|
|
830
1248
|
},
|
|
831
1249
|
|
|
832
1250
|
version: liveStoreVersion,
|
|
833
1251
|
|
|
834
1252
|
otel: {
|
|
835
|
-
rootSpanContext: () => otel.trace.getSpan(this.otel.rootSpanContext)?.spanContext(),
|
|
1253
|
+
rootSpanContext: () => otel.trace.getSpan(this[StoreInternalsSymbol].otel.rootSpanContext)?.spanContext(),
|
|
836
1254
|
},
|
|
837
1255
|
}
|
|
838
1256
|
|
|
839
1257
|
// NOTE This is needed because when booting a Store via Effect it seems to call `toJSON` in the error path
|
|
840
1258
|
toJSON = () => ({
|
|
841
1259
|
_tag: 'livestore.Store',
|
|
842
|
-
reactivityGraph: this.reactivityGraph.getSnapshot({ includeResults: true }),
|
|
1260
|
+
reactivityGraph: this[StoreInternalsSymbol].reactivityGraph.getSnapshot({ includeResults: true }),
|
|
843
1261
|
})
|
|
844
1262
|
|
|
845
1263
|
private runEffectFork = <A, E>(effect: Effect.Effect<A, E, Scope.Scope>) =>
|
|
846
1264
|
effect.pipe(
|
|
847
|
-
Effect.forkIn(this.effectContext.lifetimeScope),
|
|
1265
|
+
Effect.forkIn(this[StoreInternalsSymbol].effectContext.lifetimeScope),
|
|
848
1266
|
Effect.tapCauseLogPretty,
|
|
849
|
-
|
|
1267
|
+
Effect.runForkWith(this[StoreInternalsSymbol].effectContext.services),
|
|
850
1268
|
)
|
|
851
1269
|
|
|
852
1270
|
private runEffectPromise = <A, E>(effect: Effect.Effect<A, E, Scope.Scope>) =>
|
|
853
|
-
effect.pipe(Effect.tapCauseLogPretty,
|
|
1271
|
+
effect.pipe(Effect.tapCauseLogPretty, Effect.runPromiseWith(this[StoreInternalsSymbol].effectContext.services))
|
|
854
1272
|
|
|
855
1273
|
private getCommitArgs = (
|
|
856
1274
|
firstEventOrTxnFnOrOptions: any,
|
|
857
1275
|
restEvents: any[],
|
|
858
1276
|
): {
|
|
859
|
-
events: LiveStoreEvent.
|
|
1277
|
+
events: LiveStoreEvent.Input.ForSchema<TSchema>[]
|
|
860
1278
|
options: StoreCommitOptions | undefined
|
|
861
1279
|
} => {
|
|
862
|
-
let events: LiveStoreEvent.
|
|
1280
|
+
let events: LiveStoreEvent.Input.ForSchema<TSchema>[]
|
|
863
1281
|
let options: StoreCommitOptions | undefined
|
|
864
1282
|
|
|
865
1283
|
if (typeof firstEventOrTxnFnOrOptions === 'function') {
|
|
@@ -880,12 +1298,6 @@ export class Store<TSchema extends LiveStoreSchema = LiveStoreSchema.Any, TConte
|
|
|
880
1298
|
events = [firstEventOrTxnFnOrOptions, ...restEvents]
|
|
881
1299
|
}
|
|
882
1300
|
|
|
883
|
-
// for (const event of events) {
|
|
884
|
-
// if (event.args.id === SessionIdSymbol) {
|
|
885
|
-
// event.args.id = this.clientSession.sessionId
|
|
886
|
-
// }
|
|
887
|
-
// }
|
|
888
|
-
|
|
889
1301
|
return { events, options }
|
|
890
1302
|
}
|
|
891
1303
|
}
|