@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.
Files changed (132) hide show
  1. package/README.md +0 -1
  2. package/dist/.tsbuildinfo +1 -1
  3. package/dist/QueryCache.js +1 -1
  4. package/dist/QueryCache.js.map +1 -1
  5. package/dist/SqliteDbWrapper.d.ts +5 -5
  6. package/dist/SqliteDbWrapper.d.ts.map +1 -1
  7. package/dist/SqliteDbWrapper.js +15 -13
  8. package/dist/SqliteDbWrapper.js.map +1 -1
  9. package/dist/SqliteDbWrapper.test.js +24 -6
  10. package/dist/SqliteDbWrapper.test.js.map +1 -1
  11. package/dist/effect/LiveStore.d.ts +134 -6
  12. package/dist/effect/LiveStore.d.ts.map +1 -1
  13. package/dist/effect/LiveStore.js +190 -11
  14. package/dist/effect/LiveStore.js.map +1 -1
  15. package/dist/effect/LiveStore.test.d.ts +2 -0
  16. package/dist/effect/LiveStore.test.d.ts.map +1 -0
  17. package/dist/effect/LiveStore.test.js +41 -0
  18. package/dist/effect/LiveStore.test.js.map +1 -0
  19. package/dist/effect/mod.d.ts +1 -1
  20. package/dist/effect/mod.d.ts.map +1 -1
  21. package/dist/effect/mod.js +3 -1
  22. package/dist/effect/mod.js.map +1 -1
  23. package/dist/live-queries/base-class.d.ts +129 -9
  24. package/dist/live-queries/base-class.d.ts.map +1 -1
  25. package/dist/live-queries/base-class.js +30 -4
  26. package/dist/live-queries/base-class.js.map +1 -1
  27. package/dist/live-queries/client-document-get-query.d.ts +1 -1
  28. package/dist/live-queries/client-document-get-query.d.ts.map +1 -1
  29. package/dist/live-queries/client-document-get-query.js +4 -3
  30. package/dist/live-queries/client-document-get-query.js.map +1 -1
  31. package/dist/live-queries/computed.d.ts +56 -0
  32. package/dist/live-queries/computed.d.ts.map +1 -1
  33. package/dist/live-queries/computed.js +62 -6
  34. package/dist/live-queries/computed.js.map +1 -1
  35. package/dist/live-queries/db-query.d.ts +2 -2
  36. package/dist/live-queries/db-query.d.ts.map +1 -1
  37. package/dist/live-queries/db-query.js +41 -30
  38. package/dist/live-queries/db-query.js.map +1 -1
  39. package/dist/live-queries/db-query.test.js +112 -29
  40. package/dist/live-queries/db-query.test.js.map +1 -1
  41. package/dist/live-queries/signal.d.ts +49 -0
  42. package/dist/live-queries/signal.d.ts.map +1 -1
  43. package/dist/live-queries/signal.js +51 -2
  44. package/dist/live-queries/signal.js.map +1 -1
  45. package/dist/live-queries/signal.test.js +4 -4
  46. package/dist/live-queries/signal.test.js.map +1 -1
  47. package/dist/mod.d.ts +3 -3
  48. package/dist/mod.d.ts.map +1 -1
  49. package/dist/mod.js +3 -2
  50. package/dist/mod.js.map +1 -1
  51. package/dist/reactive.d.ts +10 -10
  52. package/dist/reactive.d.ts.map +1 -1
  53. package/dist/reactive.js +29 -27
  54. package/dist/reactive.js.map +1 -1
  55. package/dist/reactive.test.js +2 -2
  56. package/dist/reactive.test.js.map +1 -1
  57. package/dist/store/StoreRegistry.d.ts +215 -0
  58. package/dist/store/StoreRegistry.d.ts.map +1 -0
  59. package/dist/store/StoreRegistry.js +267 -0
  60. package/dist/store/StoreRegistry.js.map +1 -0
  61. package/dist/store/StoreRegistry.test.d.ts +2 -0
  62. package/dist/store/StoreRegistry.test.d.ts.map +1 -0
  63. package/dist/store/StoreRegistry.test.js +384 -0
  64. package/dist/store/StoreRegistry.test.js.map +1 -0
  65. package/dist/store/create-store.d.ts +99 -22
  66. package/dist/store/create-store.d.ts.map +1 -1
  67. package/dist/store/create-store.js +85 -37
  68. package/dist/store/create-store.js.map +1 -1
  69. package/dist/store/devtools.d.ts +6 -18
  70. package/dist/store/devtools.d.ts.map +1 -1
  71. package/dist/store/devtools.js +61 -19
  72. package/dist/store/devtools.js.map +1 -1
  73. package/dist/store/store-eventstream.test.d.ts +2 -0
  74. package/dist/store/store-eventstream.test.d.ts.map +1 -0
  75. package/dist/store/store-eventstream.test.js +65 -0
  76. package/dist/store/store-eventstream.test.js.map +1 -0
  77. package/dist/store/store-types.d.ts +271 -31
  78. package/dist/store/store-types.d.ts.map +1 -1
  79. package/dist/store/store-types.js +41 -1
  80. package/dist/store/store-types.js.map +1 -1
  81. package/dist/store/store-types.test.d.ts +2 -0
  82. package/dist/store/store-types.test.d.ts.map +1 -0
  83. package/dist/store/store-types.test.js +39 -0
  84. package/dist/store/store-types.test.js.map +1 -0
  85. package/dist/store/store.d.ts +255 -68
  86. package/dist/store/store.d.ts.map +1 -1
  87. package/dist/store/store.js +533 -186
  88. package/dist/store/store.js.map +1 -1
  89. package/dist/utils/dev.d.ts.map +1 -1
  90. package/dist/utils/dev.js +1 -1
  91. package/dist/utils/dev.js.map +1 -1
  92. package/dist/utils/stack-info.d.ts.map +1 -1
  93. package/dist/utils/stack-info.js +3 -2
  94. package/dist/utils/stack-info.js.map +1 -1
  95. package/dist/utils/tests/fixture.d.ts +46 -39
  96. package/dist/utils/tests/fixture.d.ts.map +1 -1
  97. package/dist/utils/tests/fixture.js +7 -0
  98. package/dist/utils/tests/fixture.js.map +1 -1
  99. package/dist/utils/tests/otel.d.ts.map +1 -1
  100. package/dist/utils/tests/otel.js +5 -5
  101. package/dist/utils/tests/otel.js.map +1 -1
  102. package/package.json +50 -18
  103. package/src/QueryCache.ts +1 -1
  104. package/src/SqliteDbWrapper.test.ts +30 -6
  105. package/src/SqliteDbWrapper.ts +21 -17
  106. package/src/ambient.d.ts +0 -7
  107. package/src/effect/LiveStore.test.ts +61 -0
  108. package/src/effect/LiveStore.ts +426 -22
  109. package/src/effect/mod.ts +13 -1
  110. package/src/live-queries/__snapshots__/db-query.test.ts.snap +814 -178
  111. package/src/live-queries/base-class.ts +162 -32
  112. package/src/live-queries/client-document-get-query.ts +6 -4
  113. package/src/live-queries/computed.ts +65 -8
  114. package/src/live-queries/db-query.test.ts +168 -30
  115. package/src/live-queries/db-query.ts +58 -43
  116. package/src/live-queries/signal.test.ts +5 -4
  117. package/src/live-queries/signal.ts +52 -3
  118. package/src/mod.ts +19 -2
  119. package/src/reactive.test.ts +3 -2
  120. package/src/reactive.ts +58 -49
  121. package/src/store/StoreRegistry.test.ts +543 -0
  122. package/src/store/StoreRegistry.ts +429 -0
  123. package/src/store/create-store.ts +227 -74
  124. package/src/store/devtools.ts +305 -261
  125. package/src/store/store-eventstream.test.ts +123 -0
  126. package/src/store/store-types.test.ts +52 -0
  127. package/src/store/store-types.ts +318 -44
  128. package/src/store/store.ts +695 -283
  129. package/src/utils/dev.ts +3 -4
  130. package/src/utils/stack-info.ts +5 -2
  131. package/src/utils/tests/fixture.ts +9 -1
  132. package/src/utils/tests/otel.ts +8 -7
@@ -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
- replaceSessionIdSymbol,
20
- UnexpectedError,
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 { getEventDef, LiveStoreEvent, SystemTables } from '@livestore/common/schema'
24
- import { assertNever, isDevEnv, notYetImplemented, omitUndefineds, shouldNeverHappen } from '@livestore/utils'
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
- Runtime,
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 type { StackInfo } from '../utils/stack-info.ts'
56
- import type {
57
- RefreshReason,
58
- StoreCommitOptions,
59
- StoreEventsOptions,
60
- StoreOptions,
61
- StoreOtel,
62
- Unsubscribe,
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
- if (isDevEnv()) {
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
- /** Tracks whether the store has been shut down */
84
- private isShutdown = false
138
+ /** The LiveStore schema defining tables, events, and materializers. */
139
+ readonly schema: LiveStoreSchema
85
140
 
86
- private effectContext: {
87
- runtime: Runtime.Runtime<Scope.Scope>
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
- /** RC-based set to see which queries are currently subscribed to */
92
- activeQueries: ReferenceCountedSet<LiveQuery<any>>
144
+ /** Options provided to the Store constructor. */
145
+ readonly params: StoreConstructorParams<TSchema, TContext>['params']
93
146
 
94
- // NOTE this is currently exposed for the Devtools databrowser to commit events
95
- readonly __eventSchema
96
- readonly syncProcessor: ClientSessionSyncProcessor
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
- readonly boot: Effect.Effect<void, UnexpectedError, Scope.Scope>
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
- // #region constructor
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
- }: StoreOptions<TSchema, TContext>) {
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.effectContext = effectContext
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 syncSpan = otelOptions.tracer.startSpan('LiveStore:sync', {}, otelOptions.rootSpanContext)
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
- (eventDecoded, { withChangeset, materializerHashLeader }) =>
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 { eventDef, materializer } = getEventDef(schema, eventDecoded.name)
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: eventDecoded, encoded: undefined },
242
+ dbState: this[StoreInternalsSymbol].sqliteDbWrapper,
243
+ event: { decoded: undefined, encoded: eventEncoded },
143
244
  })
144
245
 
145
- const materializerHash = isDevEnv() ? Option.some(hashMaterializerResults(execArgsArr)) : Option.none()
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: eventDecoded.name })
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, { otelContext, writeTables })
273
+ this[StoreInternalsSymbol].sqliteDbWrapper.cachedExecute(statementSql, bindValues, {
274
+ otelContext,
275
+ writeTables,
276
+ })
172
277
  } catch (cause) {
173
278
  // TOOD refactor with `SqliteError`
174
- throw UnexpectedError.make({
279
+ throw UnknownError.make({
175
280
  cause,
176
- note: `Error executing materializer for event "${eventDecoded.name}".\nStatement: ${statementSql}\nBind values: ${JSON.stringify(bindValues)}`,
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 = eventDecoded.seqNum
290
+ this[StoreInternalsSymbol].sqliteDbWrapper.debug.head = eventEncoded.seqNum
186
291
  }
187
292
  }
188
293
 
189
- let sessionChangeset:
190
- | { _tag: 'sessionChangeset'; data: Uint8Array<ArrayBuffer>; debug: any }
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(Effect.mapError((cause) => MaterializeError.make({ cause }))),
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
- this.tableRefs = {}
230
- this.activeQueries = new ReferenceCountedSet()
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
- this.reactivityGraph = reactivityGraph
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(this.reactivityGraph.atoms.values())
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
- this.tableRefs[tableName] =
362
+ tableRefs[tableName] =
270
363
  existingTableRefs.get(tableName) ??
271
- this.reactivityGraph.makeRef(null, {
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
- this.boot = Effect.gen(this, function* () {
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(this.tableRefs)) {
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* this.syncProcessor.boot
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
- // #endregion constructor
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 UnexpectedError({
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
- * Returns a function to cancel the subscription.
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$, { onUpdate: (result) => console.log(result) })
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: LiveQueryDef<TResult, 'def' | 'signal-def'> | LiveQuery<TResult> | QueryBuilder<TResult, any, any>,
328
- options: {
329
- /** Called when the query result has changed */
330
- onUpdate: (value: TResult) => void
331
- onSubscribe?: (query$: LiveQuery<TResult>) => void
332
- /** Gets called after the query subscription has been removed */
333
- onUnsubsubscribe?: () => void
334
- label?: string
335
- /**
336
- * Skips the initial `onUpdate` callback
337
- * @default false
338
- */
339
- skipInitialRun?: boolean
340
- otelContext?: otel.Context
341
- /** If provided, the stack info will be added to the `activeSubscriptions` set of the query */
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
- { attributes: { label: options?.label, queryLabel: isQueryBuilder(query) ? query.toString() : query.label } },
350
- options?.otelContext ?? this.otel.queriesSpanContext,
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 = isQueryBuilder(query)
356
- ? queryDb(query).make(this.reactivityGraph.context!)
357
- : query._tag === 'def' || query._tag === 'signal-def'
358
- ? query.make(this.reactivityGraph.context!)
359
- : {
360
- value: query as LiveQuery<TResult>,
361
- deref: () => {},
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
- const effect = this.reactivityGraph.makeEffect(
367
- (get, _otelContext, debugRefreshReason) =>
368
- options.onUpdate(get(query$.results$, otelContext, debugRefreshReason)),
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
- // Running effect right away to get initial value (unless `skipInitialRun` is set)
381
- if (options?.skipInitialRun !== true && !query$.isDestroyed) {
382
- effect.doEffect(otelContext, { _tag: 'subscribe.initial', label: `subscribe-initial-run:${options?.label}` })
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
- subscribeStream = <TResult>(
409
- query$: LiveQueryDef<TResult>,
410
- options?: { label?: string; skipInitialRun?: boolean } | undefined,
411
- ): Stream.Stream<TResult> =>
412
- Stream.asyncPush<TResult>((emit) =>
413
- Effect.gen(this, function* () {
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('NoSuchElementException', () => Effect.succeed(undefined)),
659
+ Effect.catchTag('NoSuchElementError', () => Effect.void),
416
660
  )
417
- const otelContext = otelSpan ? otel.trace.setSpan(otel.context.active(), otelSpan) : otel.context.active()
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(query$, {
422
- onUpdate: (result) => emit.single(result),
423
- ...omitUndefineds({ otelContext, label: options?.label }),
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(query.query, prepareBindValues(query.bindValues, query.query), {
458
- ...omitUndefineds({ otelContext: options?.otelContext }),
459
- }) as any
460
- if (query.schema) {
461
- return Schema.decodeSync(query.schema)(res)
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
- // Replace SessionIdSymbol in bind values before executing the query
479
- if (sqlRes.bindValues) {
480
- replaceSessionIdSymbol(sqlRes.bindValues, this.clientSession.sessionId)
481
- }
482
-
483
- const rawRes = this.sqliteDbWrapper.cachedSelect(sqlRes.query, sqlRes.bindValues as any as PreparedBindValues, {
484
- ...omitUndefineds({ otelContext: options?.otelContext }),
485
- queriedTables: new Set([query[QueryBuilderAstSymbol].tableDef.sqliteDef.name]),
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.decodeEither(schema)(rawRes)
489
- if (decodeResult._tag === 'Right') {
490
- return decodeResult.right
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
- `Failed to decode query result with for schema:`,
494
- schema.toString(),
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.left,
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
- // #region commit
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.PartialForSchema<TSchema>>>(...list: TCommitArg): void
857
+ <const TCommitArg extends ReadonlyArray<LiveStoreEvent.Input.ForSchema<TSchema>>>(...list: TCommitArg): void
602
858
  (
603
- txn: <const TCommitArg extends ReadonlyArray<LiveStoreEvent.PartialForSchema<TSchema>>>(
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.PartialForSchema<TSchema>>>(
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.PartialForSchema<TSchema>>>(
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 localRuntime = yield* Effect.runtime()
886
+ const localServices = yield* Effect.context()
635
887
 
636
- const materializeEventsTx = Effect.try({
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
- if (events.length > 1) {
643
- return this.sqliteDbWrapper.txn(runMaterializeEvents)
644
- } else {
645
- return runMaterializeEvents()
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) => UnexpectedError.make({ cause }),
898
+ catch: (cause) => UnknownError.make({ cause }),
649
899
  })
650
900
 
651
- // Materialize events to state
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({ context: otel.trace.getSpanContext(this.otel.commitsSpanContext)! }),
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.tapErrorCause(Effect.logError),
690
- Effect.catchAllCause((cause) => Effect.fork(this.shutdown(cause))),
691
- Runtime.runSync(this.effectContext.runtime),
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
- // #endregion commit
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
- * for await (const event of store.events()) {
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
- * // Get all events from the beginning of time
709
- * for await (const event of store.events({ cursor: EventSequenceNumber.ROOT })) {
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 = (_options?: StoreEventsOptions<TSchema>): AsyncIterable<LiveStoreEvent.ForSchema<TSchema>> => {
715
- this.checkShutdown('events')
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 notYetImplemented(`store.events() is not yet implemented but planned soon`)
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
- eventsStream = (_options?: StoreEventsOptions<TSchema>): Stream.Stream<LiveStoreEvent.ForSchema<TSchema>> => {
721
- this.checkShutdown('eventsStream')
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 notYetImplemented(`store.eventsStream() is not yet implemented but planned soon`)
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?: UnexpectedError) => {
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(this.runEffectFork, Fiber.join, Effect.runPromise)
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<UnexpectedError | MaterializeError>): Effect.Effect<void> => {
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 = source === 'local' ? this.sqliteDbWrapper.export() : yield* this.clientSession.leaderThread.export
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
- syncStates: () =>
814
- Effect.gen(this, function* () {
815
- const session = yield* this.syncProcessor.syncState
816
- const leader = yield* this.clientSession.leaderThread.getSyncState
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.getSyncState
828
- yield* Effect.log(`Leader sync state: ${leader.localHead} (upstream: ${leader.upstreamHead})`, leader.toJSON())
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
- Runtime.runFork(this.effectContext.runtime),
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, Runtime.runPromise(this.effectContext.runtime))
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.PartialForSchema<TSchema>[]
1277
+ events: LiveStoreEvent.Input.ForSchema<TSchema>[]
860
1278
  options: StoreCommitOptions | undefined
861
1279
  } => {
862
- let events: LiveStoreEvent.PartialForSchema<TSchema>[]
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
  }