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