@livestore/livestore 0.2.0 → 0.3.0-dev.11

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 (163) hide show
  1. package/dist/.tsbuildinfo +1 -1
  2. package/dist/SqliteDbWrapper.d.ts +54 -0
  3. package/dist/SqliteDbWrapper.d.ts.map +1 -0
  4. package/dist/SqliteDbWrapper.js +212 -0
  5. package/dist/SqliteDbWrapper.js.map +1 -0
  6. package/dist/SynchronousDatabaseWrapper.d.ts +20 -6
  7. package/dist/SynchronousDatabaseWrapper.d.ts.map +1 -1
  8. package/dist/SynchronousDatabaseWrapper.js +38 -6
  9. package/dist/SynchronousDatabaseWrapper.js.map +1 -1
  10. package/dist/__tests__/fixture.d.ts +252 -0
  11. package/dist/__tests__/fixture.d.ts.map +1 -0
  12. package/dist/__tests__/fixture.js +18 -0
  13. package/dist/__tests__/fixture.js.map +1 -0
  14. package/dist/effect/LiveStore.d.ts +16 -12
  15. package/dist/effect/LiveStore.d.ts.map +1 -1
  16. package/dist/effect/LiveStore.js +14 -14
  17. package/dist/effect/LiveStore.js.map +1 -1
  18. package/dist/index.d.ts +6 -7
  19. package/dist/index.d.ts.map +1 -1
  20. package/dist/index.js +4 -4
  21. package/dist/index.js.map +1 -1
  22. package/dist/live-queries/base-class.d.ts +64 -21
  23. package/dist/live-queries/base-class.d.ts.map +1 -1
  24. package/dist/live-queries/base-class.js +56 -13
  25. package/dist/live-queries/base-class.js.map +1 -1
  26. package/dist/live-queries/computed.d.ts +7 -7
  27. package/dist/live-queries/computed.d.ts.map +1 -1
  28. package/dist/live-queries/computed.js +35 -11
  29. package/dist/live-queries/computed.js.map +1 -1
  30. package/dist/live-queries/{sql.d.ts → db-query.d.ts} +19 -14
  31. package/dist/live-queries/db-query.d.ts.map +1 -0
  32. package/dist/live-queries/db-query.js +244 -0
  33. package/dist/live-queries/db-query.js.map +1 -0
  34. package/dist/live-queries/db-query.test.d.ts +2 -0
  35. package/dist/live-queries/db-query.test.d.ts.map +1 -0
  36. package/dist/live-queries/db-query.test.js +123 -0
  37. package/dist/live-queries/db-query.test.js.map +1 -0
  38. package/dist/live-queries/db.d.ts +12 -15
  39. package/dist/live-queries/db.d.ts.map +1 -1
  40. package/dist/live-queries/db.js +72 -48
  41. package/dist/live-queries/db.js.map +1 -1
  42. package/dist/live-queries/db.test.js +18 -15
  43. package/dist/live-queries/db.test.js.map +1 -1
  44. package/dist/live-queries/graphql.d.ts +8 -8
  45. package/dist/live-queries/graphql.d.ts.map +1 -1
  46. package/dist/live-queries/graphql.js +35 -9
  47. package/dist/live-queries/graphql.js.map +1 -1
  48. package/dist/live-queries/make-ref.d.ts +20 -0
  49. package/dist/live-queries/make-ref.d.ts.map +1 -0
  50. package/dist/live-queries/make-ref.js +33 -0
  51. package/dist/live-queries/make-ref.js.map +1 -0
  52. package/dist/reactive.d.ts +15 -13
  53. package/dist/reactive.d.ts.map +1 -1
  54. package/dist/reactive.js +15 -9
  55. package/dist/reactive.js.map +1 -1
  56. package/dist/row-query-utils.d.ts +4 -4
  57. package/dist/row-query-utils.d.ts.map +1 -1
  58. package/dist/row-query-utils.js +14 -10
  59. package/dist/row-query-utils.js.map +1 -1
  60. package/dist/store/create-store.d.ts +13 -12
  61. package/dist/store/create-store.d.ts.map +1 -1
  62. package/dist/store/create-store.js +27 -33
  63. package/dist/store/create-store.js.map +1 -1
  64. package/dist/store/devtools.d.ts +3 -3
  65. package/dist/store/devtools.d.ts.map +1 -1
  66. package/dist/store/devtools.js +56 -34
  67. package/dist/store/devtools.js.map +1 -1
  68. package/dist/store/store-types.d.ts +18 -18
  69. package/dist/store/store-types.d.ts.map +1 -1
  70. package/dist/store/store-types.js.map +1 -1
  71. package/dist/store/store.d.ts +57 -38
  72. package/dist/store/store.d.ts.map +1 -1
  73. package/dist/store/store.js +225 -188
  74. package/dist/store/store.js.map +1 -1
  75. package/dist/store/store.test.d.ts +2 -0
  76. package/dist/store/store.test.d.ts.map +1 -0
  77. package/dist/store/store.test.js +27 -0
  78. package/dist/store/store.test.js.map +1 -0
  79. package/dist/utils/dev.d.ts.map +1 -1
  80. package/dist/utils/dev.js +3 -2
  81. package/dist/utils/dev.js.map +1 -1
  82. package/dist/utils/expo.d.ts +2 -0
  83. package/dist/utils/expo.d.ts.map +1 -0
  84. package/dist/utils/expo.js +8 -0
  85. package/dist/utils/expo.js.map +1 -0
  86. package/dist/utils/function-string.d.ts +7 -0
  87. package/dist/utils/function-string.d.ts.map +1 -0
  88. package/dist/utils/function-string.js +9 -0
  89. package/dist/utils/function-string.js.map +1 -0
  90. package/dist/utils/stack-info.d.ts.map +1 -1
  91. package/dist/utils/stack-info.js +6 -1
  92. package/dist/utils/stack-info.js.map +1 -1
  93. package/dist/utils/stack-info.test.js +54 -1
  94. package/dist/utils/stack-info.test.js.map +1 -1
  95. package/dist/utils/tests/fixture.d.ts +2 -6
  96. package/dist/utils/tests/fixture.d.ts.map +1 -1
  97. package/dist/utils/tests/fixture.js +7 -13
  98. package/dist/utils/tests/fixture.js.map +1 -1
  99. package/dist/utils/tests/mod.d.ts +1 -0
  100. package/dist/utils/tests/mod.d.ts.map +1 -1
  101. package/dist/utils/tests/mod.js +1 -0
  102. package/dist/utils/tests/mod.js.map +1 -1
  103. package/dist/utils/tests/otel.d.ts +60 -1
  104. package/dist/utils/tests/otel.d.ts.map +1 -1
  105. package/dist/utils/tests/otel.js +65 -4
  106. package/dist/utils/tests/otel.js.map +1 -1
  107. package/package.json +12 -12
  108. package/src/{SynchronousDatabaseWrapper.ts → SqliteDbWrapper.ts} +59 -13
  109. package/src/ambient.d.ts +1 -1
  110. package/src/effect/LiveStore.ts +32 -33
  111. package/src/index.ts +14 -7
  112. package/src/live-queries/__snapshots__/{db.test.ts.snap → db-query.test.ts.snap} +220 -69
  113. package/src/live-queries/base-class.ts +160 -40
  114. package/src/live-queries/computed.ts +45 -19
  115. package/src/live-queries/{db.test.ts → db-query.test.ts} +23 -12
  116. package/src/live-queries/{db.ts → db-query.ts} +124 -61
  117. package/src/live-queries/graphql.ts +47 -21
  118. package/src/live-queries/make-ref.ts +47 -0
  119. package/src/reactive.ts +52 -27
  120. package/src/row-query-utils.ts +29 -18
  121. package/src/store/create-store.ts +106 -113
  122. package/src/store/devtools.ts +65 -39
  123. package/src/store/store-types.ts +20 -18
  124. package/src/store/store.ts +361 -290
  125. package/src/utils/dev.ts +4 -2
  126. package/src/utils/function-string.ts +12 -0
  127. package/src/utils/stack-info.test.ts +58 -1
  128. package/src/utils/stack-info.ts +6 -1
  129. package/src/utils/tests/fixture.ts +6 -16
  130. package/src/utils/tests/mod.ts +1 -0
  131. package/src/utils/tests/otel.ts +71 -5
  132. package/dist/live-queries/sql.d.ts.map +0 -1
  133. package/dist/live-queries/sql.js +0 -175
  134. package/dist/live-queries/sql.js.map +0 -1
  135. package/dist/live-queries/sql.test.d.ts +0 -2
  136. package/dist/live-queries/sql.test.d.ts.map +0 -1
  137. package/dist/live-queries/sql.test.js +0 -285
  138. package/dist/live-queries/sql.test.js.map +0 -1
  139. package/dist/reactiveQueries/base-class.d.ts +0 -64
  140. package/dist/reactiveQueries/base-class.d.ts.map +0 -1
  141. package/dist/reactiveQueries/base-class.js +0 -31
  142. package/dist/reactiveQueries/base-class.js.map +0 -1
  143. package/dist/reactiveQueries/computed.d.ts +0 -26
  144. package/dist/reactiveQueries/computed.d.ts.map +0 -1
  145. package/dist/reactiveQueries/computed.js +0 -38
  146. package/dist/reactiveQueries/computed.js.map +0 -1
  147. package/dist/reactiveQueries/graphql.d.ts +0 -49
  148. package/dist/reactiveQueries/graphql.d.ts.map +0 -1
  149. package/dist/reactiveQueries/graphql.js +0 -122
  150. package/dist/reactiveQueries/graphql.js.map +0 -1
  151. package/dist/reactiveQueries/sql.d.ts +0 -62
  152. package/dist/reactiveQueries/sql.d.ts.map +0 -1
  153. package/dist/reactiveQueries/sql.js +0 -175
  154. package/dist/reactiveQueries/sql.js.map +0 -1
  155. package/dist/reactiveQueries/sql.test.d.ts +0 -2
  156. package/dist/reactiveQueries/sql.test.d.ts.map +0 -1
  157. package/dist/reactiveQueries/sql.test.js +0 -285
  158. package/dist/reactiveQueries/sql.test.js.map +0 -1
  159. package/dist/row-query.d.ts +0 -16
  160. package/dist/row-query.d.ts.map +0 -1
  161. package/dist/row-query.js +0 -30
  162. package/dist/row-query.js.map +0 -1
  163. package/src/global-state.ts +0 -20
@@ -1,36 +1,72 @@
1
- import type { ClientSession, ParamsObject, PreparedBindValues, QueryBuilder } from '@livestore/common'
1
+ import type {
2
+ ClientSession,
3
+ ClientSessionSyncProcessor,
4
+ ParamsObject,
5
+ PreparedBindValues,
6
+ QueryBuilder,
7
+ UnexpectedError,
8
+ } from '@livestore/common'
2
9
  import {
10
+ Devtools,
3
11
  getExecArgsFromMutation,
4
12
  getResultSchema,
13
+ IntentionalShutdownCause,
5
14
  isQueryBuilder,
15
+ liveStoreVersion,
16
+ makeClientSessionSyncProcessor,
6
17
  prepareBindValues,
7
18
  QueryBuilderAstSymbol,
8
19
  replaceSessionIdSymbol,
9
20
  } from '@livestore/common'
10
- import type { LiveStoreSchema, MutationEvent } from '@livestore/common/schema'
21
+ import type { LiveStoreSchema } from '@livestore/common/schema'
11
22
  import {
12
- isPartialMutationEvent,
13
- makeMutationEventSchemaMemo,
23
+ MutationEvent,
14
24
  SCHEMA_META_TABLE,
15
25
  SCHEMA_MUTATIONS_META_TABLE,
16
26
  SESSION_CHANGESET_META_TABLE,
17
27
  } from '@livestore/common/schema'
18
- import { assertNever, shouldNeverHappen } from '@livestore/utils'
28
+ import { assertNever, isDevEnv } from '@livestore/utils'
19
29
  import type { Scope } from '@livestore/utils/effect'
20
- import { Data, Effect, FiberSet, Inspectable, MutableHashMap, Runtime, Schema, Stream } from '@livestore/utils/effect'
30
+ import {
31
+ Cause,
32
+ Data,
33
+ Effect,
34
+ Inspectable,
35
+ MutableHashMap,
36
+ OtelTracer,
37
+ Runtime,
38
+ Schema,
39
+ Stream,
40
+ } from '@livestore/utils/effect'
41
+ import { nanoid } from '@livestore/utils/nanoid'
21
42
  import * as otel from '@opentelemetry/api'
22
- import type { GraphQLSchema } from 'graphql'
23
-
24
- import type { LiveQuery, QueryContext, ReactivityGraph } from '../live-queries/base-class.js'
43
+ import { type GraphQLSchema } from 'graphql'
44
+
45
+ import type {
46
+ ILiveQueryRefDef,
47
+ LiveQuery,
48
+ LiveQueryDef,
49
+ ReactivityGraph,
50
+ ReactivityGraphContext,
51
+ } from '../live-queries/base-class.js'
52
+ import { makeReactivityGraph } from '../live-queries/base-class.js'
25
53
  import type { Ref } from '../reactive.js'
26
54
  import { makeExecBeforeFirstRun } from '../row-query-utils.js'
27
- import { SynchronousDatabaseWrapper } from '../SynchronousDatabaseWrapper.js'
55
+ import { SqliteDbWrapper } from '../SqliteDbWrapper.js'
28
56
  import { ReferenceCountedSet } from '../utils/data-structures.js'
29
57
  import { downloadBlob, exposeDebugUtils } from '../utils/dev.js'
30
58
  import { getDurationMsFromSpan } from '../utils/otel.js'
31
- import type { BaseGraphQLContext, RefreshReason, StoreMutateOptions, StoreOptions, StoreOtel } from './store-types.js'
32
-
33
- if (import.meta.env.DEV) {
59
+ import type { StackInfo } from '../utils/stack-info.js'
60
+ import type {
61
+ BaseGraphQLContext,
62
+ RefreshReason,
63
+ StoreMutateOptions,
64
+ StoreOptions,
65
+ StoreOtel,
66
+ Unsubscribe,
67
+ } from './store-types.js'
68
+
69
+ if (isDevEnv()) {
34
70
  exposeDebugUtils()
35
71
  }
36
72
 
@@ -40,7 +76,7 @@ export class Store<
40
76
  > extends Inspectable.Class {
41
77
  readonly storeId: string
42
78
  reactivityGraph: ReactivityGraph
43
- syncDbWrapper: SynchronousDatabaseWrapper
79
+ sqliteDbWrapper: SqliteDbWrapper
44
80
  clientSession: ClientSession
45
81
  schema: LiveStoreSchema
46
82
  graphQLSchema?: GraphQLSchema
@@ -50,9 +86,8 @@ export class Store<
50
86
  * Note we're using `Ref<null>` here as we don't care about the value but only about *that* something has changed.
51
87
  * This only works in combination with `equal: () => false` which will always trigger a refresh.
52
88
  */
53
- tableRefs: { [key: string]: Ref<null, QueryContext, RefreshReason> }
89
+ tableRefs: { [key: string]: Ref<null, ReactivityGraphContext, RefreshReason> }
54
90
 
55
- private fiberSet: FiberSet.FiberSet
56
91
  private runtime: Runtime.Runtime<Scope.Scope>
57
92
 
58
93
  /** RC-based set to see which queries are currently subscribed to */
@@ -61,36 +96,91 @@ export class Store<
61
96
  // NOTE this is currently exposed for the Devtools databrowser to emit mutation events
62
97
  readonly __mutationEventSchema
63
98
  private unsyncedMutationEvents
99
+ private syncProcessor: ClientSessionSyncProcessor
100
+ readonly lifetimeScope: Scope.Scope
101
+
102
+ readonly boot: Effect.Effect<void, UnexpectedError, Scope.Scope>
64
103
 
65
104
  // #region constructor
66
- private constructor({
105
+ constructor({
67
106
  clientSession,
68
107
  schema,
69
108
  graphQLOptions,
70
- reactivityGraph,
71
109
  otelOptions,
72
110
  disableDevtools,
73
111
  batchUpdates,
74
112
  unsyncedMutationEvents,
75
113
  storeId,
76
- fiberSet,
114
+ lifetimeScope,
77
115
  runtime,
78
116
  }: StoreOptions<TGraphQLContext, TSchema>) {
79
117
  super()
80
118
 
81
119
  this.storeId = storeId
82
-
83
120
  this.unsyncedMutationEvents = unsyncedMutationEvents
84
121
 
85
- this.syncDbWrapper = new SynchronousDatabaseWrapper({ otel: otelOptions, db: clientSession.syncDb })
122
+ this.sqliteDbWrapper = new SqliteDbWrapper({ otel: otelOptions, db: clientSession.sqliteDb })
86
123
  this.clientSession = clientSession
87
124
  this.schema = schema
88
125
 
89
- this.fiberSet = fiberSet
126
+ this.lifetimeScope = lifetimeScope
90
127
  this.runtime = runtime
91
128
 
92
- // TODO refactor
93
- this.__mutationEventSchema = makeMutationEventSchemaMemo(schema)
129
+ const reactivityGraph = makeReactivityGraph()
130
+
131
+ const syncSpan = otelOptions.tracer.startSpan('LiveStore:sync', {}, otelOptions.rootSpanContext)
132
+
133
+ this.syncProcessor = makeClientSessionSyncProcessor({
134
+ schema,
135
+ clientSession,
136
+ runtime,
137
+ applyMutation: (mutationEventDecoded, { otelContext, withChangeset }) => {
138
+ const mutationDef = schema.mutations.get(mutationEventDecoded.mutation)!
139
+ const execArgsArr = getExecArgsFromMutation({
140
+ mutationDef,
141
+ mutationEvent: { decoded: mutationEventDecoded, encoded: undefined },
142
+ })
143
+
144
+ const writeTablesForEvent = new Set<string>()
145
+
146
+ const exec = () => {
147
+ for (const {
148
+ statementSql,
149
+ bindValues,
150
+ writeTables = this.sqliteDbWrapper.getTablesUsed(statementSql),
151
+ } of execArgsArr) {
152
+ this.sqliteDbWrapper.execute(statementSql, bindValues, { otelContext, writeTables })
153
+
154
+ // durationMsTotal += durationMs
155
+ writeTables.forEach((table) => writeTablesForEvent.add(table))
156
+ }
157
+ }
158
+
159
+ let sessionChangeset: Uint8Array | undefined
160
+ if (withChangeset === true) {
161
+ sessionChangeset = this.sqliteDbWrapper.withChangeset(exec).changeset
162
+ } else {
163
+ exec()
164
+ }
165
+
166
+ return { writeTables: writeTablesForEvent, sessionChangeset }
167
+ },
168
+ rollback: (changeset) => {
169
+ this.sqliteDbWrapper.rollback(changeset)
170
+ },
171
+ refreshTables: (tables) => {
172
+ const tablesToUpdate = [] as [Ref<null, ReactivityGraphContext, RefreshReason>, null][]
173
+ for (const tableName of tables) {
174
+ const tableRef = this.tableRefs[tableName]
175
+ assertNever(tableRef !== undefined, `No table ref found for ${tableName}`)
176
+ tablesToUpdate.push([tableRef!, null])
177
+ }
178
+ reactivityGraph.setRefs(tablesToUpdate)
179
+ },
180
+ span: syncSpan,
181
+ })
182
+
183
+ this.__mutationEventSchema = MutationEvent.makeMutationEventSchemaMemo(schema)
94
184
 
95
185
  // TODO generalize the `tableRefs` concept to allow finer-grained refs
96
186
  this.tableRefs = {}
@@ -105,6 +195,8 @@ export class Store<
105
195
  this.reactivityGraph = reactivityGraph
106
196
  this.reactivityGraph.context = {
107
197
  store: this as unknown as Store<BaseGraphQLContext, LiveStoreSchema>,
198
+ liveQueryRCMap: new Map(),
199
+ reactivityGraph: new WeakRef(reactivityGraph),
108
200
  otelTracer: otelOptions.tracer,
109
201
  rootOtelContext: otelQueriesSpanContext,
110
202
  effectsWrapper: batchUpdates,
@@ -141,26 +233,10 @@ export class Store<
141
233
 
142
234
  if (graphQLOptions) {
143
235
  this.graphQLSchema = graphQLOptions.schema
144
- this.graphQLContext = graphQLOptions.makeContext(
145
- this.syncDbWrapper,
146
- this.otel.tracer,
147
- clientSession.coordinator.sessionId,
148
- )
236
+ this.graphQLContext = graphQLOptions.makeContext(this.sqliteDbWrapper, this.otel.tracer, clientSession.sessionId)
149
237
  }
150
238
 
151
- Effect.gen(this, function* () {
152
- yield* this.clientSession.coordinator.syncMutations.pipe(
153
- Stream.tapChunk((mutationsEventsDecodedChunk) =>
154
- Effect.sync(() => {
155
- this.mutate({ wasSyncMessage: true }, ...mutationsEventsDecodedChunk)
156
- }),
157
- ),
158
- Stream.runDrain,
159
- Effect.interruptible,
160
- Effect.withSpan('LiveStore:syncMutations'),
161
- Effect.forkScoped,
162
- )
163
-
239
+ this.boot = Effect.gen(this, function* () {
164
240
  yield* Effect.addFinalizer(() =>
165
241
  Effect.sync(() => {
166
242
  // Remove all table refs from the reactivity graph
@@ -171,60 +247,84 @@ export class Store<
171
247
  }
172
248
 
173
249
  // End the otel spans
174
- otel.trace.getSpan(this.otel.mutationsSpanContext)!.end()
175
- otel.trace.getSpan(this.otel.queriesSpanContext)!.end()
250
+ syncSpan.end()
251
+ mutationsSpan.end()
252
+ queriesSpan.end()
176
253
  }),
177
254
  )
178
255
 
179
- yield* Effect.never // to keep the scope alive and bind to the parent scope
180
- }).pipe(Effect.scoped, Effect.withSpan('LiveStore:constructor'), this.runEffectFork)
181
- }
182
- // #endregion constructor
183
-
184
- static createStore = <TGraphQLContext extends BaseGraphQLContext, TSchema extends LiveStoreSchema = LiveStoreSchema>(
185
- storeOptions: StoreOptions<TGraphQLContext, TSchema>,
186
- parentSpan: otel.Span,
187
- ): Store<TGraphQLContext, TSchema> => {
188
- const ctx = otel.trace.setSpan(otel.context.active(), parentSpan)
189
- return storeOptions.otelOptions.tracer.startActiveSpan('LiveStore:createStore', {}, ctx, (span) => {
190
- try {
191
- return new Store(storeOptions)
192
- } finally {
193
- span.end()
194
- }
256
+ yield* this.syncProcessor.boot
195
257
  })
196
258
  }
259
+ // #endregion constructor
197
260
 
198
261
  get sessionId(): string {
199
- return this.clientSession.coordinator.sessionId
262
+ return this.clientSession.sessionId
200
263
  }
201
264
 
202
265
  /**
203
266
  * Subscribe to the results of a query
204
267
  * Returns a function to cancel the subscription.
268
+ *
269
+ * @example
270
+ * ```ts
271
+ * const unsubscribe = store.subscribe(query$, { onUpdate: (result) => console.log(result) })
272
+ * ```
205
273
  */
206
274
  subscribe = <TResult>(
207
- query$: LiveQuery<TResult, any>,
208
- onNewValue: (value: TResult) => void,
209
- onUnsubsubscribe?: () => void,
210
- options?: { label?: string; otelContext?: otel.Context; skipInitialRun?: boolean } | undefined,
211
- ): (() => void) =>
275
+ query: LiveQueryDef<TResult, any> | LiveQuery<TResult, any>,
276
+ options: {
277
+ /** Called when the query result has changed */
278
+ onUpdate: (value: TResult) => void
279
+ onSubscribe?: (query$: LiveQuery<TResult, any>) => void
280
+ /** Gets called after the query subscription has been removed */
281
+ onUnsubsubscribe?: () => void
282
+ label?: string
283
+ /**
284
+ * Skips the initial `onUpdate` callback
285
+ * @default false
286
+ */
287
+ skipInitialRun?: boolean
288
+ otelContext?: otel.Context
289
+ /** If provided, the stack info will be added to the `activeSubscriptions` set of the query */
290
+ stackInfo?: StackInfo
291
+ },
292
+ ): Unsubscribe =>
212
293
  this.otel.tracer.startActiveSpan(
213
294
  `LiveStore.subscribe`,
214
- { attributes: { label: options?.label, queryLabel: query$.label } },
295
+ { attributes: { label: options?.label, queryLabel: query.label } },
215
296
  options?.otelContext ?? this.otel.queriesSpanContext,
216
297
  (span) => {
217
298
  // console.debug('store sub', query$.id, query$.label)
218
299
  const otelContext = otel.trace.setSpan(otel.context.active(), span)
219
300
 
301
+ const queryRcRef =
302
+ query._tag === 'def'
303
+ ? query.make(this.reactivityGraph.context!)
304
+ : {
305
+ value: query,
306
+ deref: () => {},
307
+ }
308
+ const query$ = queryRcRef.value
309
+
220
310
  const label = `subscribe:${options?.label}`
221
- const effect = this.reactivityGraph.makeEffect((get) => onNewValue(get(query$.results$)), { label })
311
+ const effect = this.reactivityGraph.makeEffect(
312
+ (get, _otelContext, debugRefreshReason) =>
313
+ options.onUpdate(get(query$.results$, otelContext, debugRefreshReason)),
314
+ { label },
315
+ )
316
+
317
+ if (options?.stackInfo) {
318
+ query$.activeSubscriptions.add(options.stackInfo)
319
+ }
320
+
321
+ options?.onSubscribe?.(query$)
222
322
 
223
323
  this.activeQueries.add(query$ as LiveQuery<TResult>)
224
324
 
225
325
  // Running effect right away to get initial value (unless `skipInitialRun` is set)
226
- if (options?.skipInitialRun !== true) {
227
- effect.doEffect(otelContext)
326
+ if (options?.skipInitialRun !== true && !query$.isDestroyed) {
327
+ effect.doEffect(otelContext, { _tag: 'subscribe.initial', label: `subscribe-initial-run:${options?.label}` })
228
328
  }
229
329
 
230
330
  const unsubscribe = () => {
@@ -232,7 +332,14 @@ export class Store<
232
332
  try {
233
333
  this.reactivityGraph.destroyNode(effect)
234
334
  this.activeQueries.remove(query$ as LiveQuery<TResult>)
235
- onUnsubsubscribe?.()
335
+
336
+ if (options?.stackInfo) {
337
+ query$.activeSubscriptions.delete(options.stackInfo)
338
+ }
339
+
340
+ queryRcRef.deref()
341
+
342
+ options?.onUnsubsubscribe?.()
236
343
  } finally {
237
344
  span.end()
238
345
  }
@@ -242,13 +349,54 @@ export class Store<
242
349
  },
243
350
  )
244
351
 
352
+ subscribeStream = <TResult>(
353
+ query$: LiveQueryDef<TResult, any>,
354
+ options?: { label?: string; skipInitialRun?: boolean } | undefined,
355
+ ): Stream.Stream<TResult> =>
356
+ Stream.asyncPush<TResult>((emit) =>
357
+ Effect.gen(this, function* () {
358
+ const otelSpan = yield* OtelTracer.currentOtelSpan.pipe(
359
+ Effect.catchTag('NoSuchElementException', () => Effect.succeed(undefined)),
360
+ )
361
+ const otelContext = otelSpan ? otel.trace.setSpan(otel.context.active(), otelSpan) : otel.context.active()
362
+
363
+ yield* Effect.acquireRelease(
364
+ Effect.sync(() =>
365
+ this.subscribe(query$, {
366
+ onUpdate: (result) => emit.single(result),
367
+ otelContext,
368
+ label: options?.label,
369
+ }),
370
+ ),
371
+ (unsub) => Effect.sync(() => unsub()),
372
+ )
373
+ }),
374
+ )
375
+
376
+ /**
377
+ * Synchronously queries the database without creating a LiveQuery.
378
+ * This is useful for queries that don't need to be reactive.
379
+ *
380
+ * Example: Query builder
381
+ * ```ts
382
+ * const completedTodos = store.query(tables.todo.where({ complete: true }))
383
+ * ```
384
+ *
385
+ * Example: Raw SQL query
386
+ * ```ts
387
+ * const completedTodos = store.query({ query: 'SELECT * FROM todo WHERE complete = 1', bindValues: {} })
388
+ * ```
389
+ */
245
390
  query = <TResult>(
246
- query: QueryBuilder<TResult, any, any> | LiveQuery<TResult, any> | { query: string; bindValues: ParamsObject },
247
- options?: { otelContext?: otel.Context },
391
+ query:
392
+ | QueryBuilder<TResult, any, any>
393
+ | LiveQuery<TResult, any>
394
+ | LiveQueryDef<TResult, any>
395
+ | { query: string; bindValues: ParamsObject },
396
+ options?: { otelContext?: otel.Context; debugRefreshReason?: RefreshReason },
248
397
  ): TResult => {
249
398
  if (typeof query === 'object' && 'query' in query && 'bindValues' in query) {
250
- return this.syncDbWrapper.select(query.query, {
251
- bindValues: prepareBindValues(query.bindValues, query.query),
399
+ return this.sqliteDbWrapper.select(query.query, prepareBindValues(query.bindValues, query.query), {
252
400
  otelContext: options?.otelContext,
253
401
  }) as any
254
402
  } else if (isQueryBuilder(query)) {
@@ -264,17 +412,38 @@ export class Store<
264
412
 
265
413
  const sqlRes = query.asSql()
266
414
  const schema = getResultSchema(query)
267
- const rawRes = this.syncDbWrapper.select(sqlRes.query, {
268
- bindValues: sqlRes.bindValues as any as PreparedBindValues,
415
+ const rawRes = this.sqliteDbWrapper.select(sqlRes.query, sqlRes.bindValues as any as PreparedBindValues, {
269
416
  otelContext: options?.otelContext,
270
417
  queriedTables: new Set([query[QueryBuilderAstSymbol].tableDef.sqliteDef.name]),
271
418
  })
272
419
  return Schema.decodeSync(schema)(rawRes)
420
+ } else if (query._tag === 'def') {
421
+ const query$ = query.make(this.reactivityGraph.context!)
422
+ const result = this.query(query$.value, options)
423
+ query$.deref()
424
+ return result
273
425
  } else {
274
- return query.run(options?.otelContext)
426
+ return query.run({ otelContext: options?.otelContext, debugRefreshReason: options?.debugRefreshReason })
275
427
  }
276
428
  }
277
429
 
430
+ // makeLive: {
431
+ // <T>(def: LiveQueryDef<T, any>): LiveQuery<T, any>
432
+ // <T>(def: ILiveQueryRefDef<T>): ILiveQueryRef<T>
433
+ // } = (def: any) => {
434
+ // if (def._tag === 'live-ref-def') {
435
+ // return (def as ILiveQueryRefDef<any>).make(this.reactivityGraph.context!)
436
+ // } else {
437
+ // return (def as LiveQueryDef<any, any>).make(this.reactivityGraph.context!) as any
438
+ // }
439
+ // }
440
+
441
+ setRef = <T>(refDef: ILiveQueryRefDef<T>, value: T): void => {
442
+ const ref = refDef.make(this.reactivityGraph.context!)
443
+ ref.value.set(value)
444
+ ref.deref()
445
+ }
446
+
278
447
  // #region mutate
279
448
  mutate: {
280
449
  <const TMutationArg extends ReadonlyArray<MutationEvent.PartialForSchema<TSchema>>>(...list: TMutationArg): void
@@ -294,39 +463,15 @@ export class Store<
294
463
  ) => void,
295
464
  ): void
296
465
  } = (firstMutationOrTxnFnOrOptions: any, ...restMutations: any[]) => {
297
- let mutationsEvents: MutationEvent.ForSchema<TSchema>[]
298
- let options: StoreMutateOptions | undefined
466
+ const { mutationsEvents, options } = this.getMutateArgs(firstMutationOrTxnFnOrOptions, restMutations)
299
467
 
300
- if (typeof firstMutationOrTxnFnOrOptions === 'function') {
301
- // TODO ensure that function is synchronous and isn't called in a async way (also write tests for this)
302
- mutationsEvents = firstMutationOrTxnFnOrOptions((arg: any) => mutationsEvents.push(arg))
303
- } else if (
304
- firstMutationOrTxnFnOrOptions?.label !== undefined ||
305
- firstMutationOrTxnFnOrOptions?.skipRefresh !== undefined ||
306
- firstMutationOrTxnFnOrOptions?.wasSyncMessage !== undefined ||
307
- firstMutationOrTxnFnOrOptions?.persisted !== undefined
308
- ) {
309
- options = firstMutationOrTxnFnOrOptions
310
- mutationsEvents = restMutations
311
- } else if (firstMutationOrTxnFnOrOptions === undefined) {
312
- // When `mutate` is called with no arguments (which sometimes happens when dynamically filtering mutations)
313
- mutationsEvents = []
314
- } else {
315
- mutationsEvents = [firstMutationOrTxnFnOrOptions, ...restMutations]
468
+ for (const mutationEvent of mutationsEvents) {
469
+ replaceSessionIdSymbol(mutationEvent.args, this.clientSession.sessionId)
316
470
  }
317
471
 
318
- mutationsEvents = mutationsEvents.filter(
319
- (_) => _.id === undefined || !MutableHashMap.has(this.unsyncedMutationEvents, Data.struct(_.id)),
320
- )
321
-
322
- if (mutationsEvents.length === 0) {
323
- return
324
- }
472
+ if (mutationsEvents.length === 0) return
325
473
 
326
- const label = options?.label ?? 'mutate'
327
474
  const skipRefresh = options?.skipRefresh ?? false
328
- const wasSyncMessage = options?.wasSyncMessage ?? false
329
- const persisted = options?.persisted ?? true
330
475
 
331
476
  const mutationsSpan = otel.trace.getSpan(this.otel.mutationsSpanContext)!
332
477
  mutationsSpan.addEvent('mutate')
@@ -337,59 +482,41 @@ export class Store<
337
482
 
338
483
  let durationMs: number
339
484
 
340
- const res = this.otel.tracer.startActiveSpan(
485
+ return this.otel.tracer.startActiveSpan(
341
486
  'LiveStore:mutate',
342
- { attributes: { 'livestore.mutateLabel': label } },
343
- this.otel.mutationsSpanContext,
487
+ {
488
+ attributes: {
489
+ 'livestore.mutationEventsCount': mutationsEvents.length,
490
+ 'livestore.mutationEventTags': mutationsEvents.map((_) => _.mutation),
491
+ 'livestore.mutateLabel': options?.label,
492
+ },
493
+ links: options?.spanLinks,
494
+ },
495
+ options?.otelContext ?? this.otel.mutationsSpanContext,
344
496
  (span) => {
345
497
  const otelContext = otel.trace.setSpan(otel.context.active(), span)
346
498
 
347
499
  try {
348
- const writeTables: Set<string> = new Set()
349
-
350
- this.otel.tracer.startActiveSpan(
351
- 'LiveStore:processWrites',
352
- { attributes: { 'livestore.mutateLabel': label } },
353
- otel.trace.setSpan(otel.context.active(), span),
354
- (span) => {
355
- try {
356
- const otelContext = otel.trace.setSpan(otel.context.active(), span)
357
-
358
- const applyMutations = () => {
359
- for (const mutationEvent of mutationsEvents) {
360
- try {
361
- const { writeTables: writeTablesForEvent } = this.mutateWithoutRefresh(mutationEvent, {
362
- otelContext,
363
- // NOTE if it was a sync message, it's already coming from the coordinator, so we can skip the coordinator
364
- coordinatorMode: wasSyncMessage ? 'skip-coordinator' : persisted ? 'default' : 'skip-persist',
365
- })
366
- for (const tableName of writeTablesForEvent) {
367
- writeTables.add(tableName)
368
- }
369
- } catch (e: any) {
370
- console.error(e, mutationEvent)
371
- throw e
372
- }
373
- }
374
- }
375
-
376
- if (mutationsEvents.length > 1) {
377
- // TODO: what to do about coordinator transaction here?
378
- this.syncDbWrapper.txn(applyMutations)
379
- } else {
380
- applyMutations()
381
- }
382
- } catch (e: any) {
383
- console.error(e)
384
- span.setStatus({ code: otel.SpanStatusCode.ERROR, message: e.toString() })
385
- throw e
386
- } finally {
387
- span.end()
500
+ const { writeTables } = (() => {
501
+ try {
502
+ const applyMutations = () => this.syncProcessor.push(mutationsEvents, { otelContext })
503
+
504
+ if (mutationsEvents.length > 1) {
505
+ // TODO: what to do about leader transaction here?
506
+ return this.sqliteDbWrapper.txn(applyMutations)
507
+ } else {
508
+ return applyMutations()
388
509
  }
389
- },
390
- )
510
+ } catch (e: any) {
511
+ console.error(e)
512
+ span.setStatus({ code: otel.SpanStatusCode.ERROR, message: e.toString() })
513
+ throw e
514
+ } finally {
515
+ span.end()
516
+ }
517
+ })()
391
518
 
392
- const tablesToUpdate = [] as [Ref<null, QueryContext, RefreshReason>, null][]
519
+ const tablesToUpdate = [] as [Ref<null, ReactivityGraphContext, RefreshReason>, null][]
393
520
  for (const tableName of writeTables) {
394
521
  const tableRef = this.tableRefs[tableName]
395
522
  assertNever(tableRef !== undefined, `No table ref found for ${tableName}`)
@@ -417,16 +544,6 @@ export class Store<
417
544
  return { durationMs }
418
545
  },
419
546
  )
420
-
421
- // NOTE we need to add the mutation events to the unsynced mutation events map only after running the code above
422
- // so the short-circuiting in `mutateWithoutRefresh` doesn't kick in for those events
423
- for (const mutationEvent of mutationsEvents) {
424
- if (mutationEvent.id !== undefined) {
425
- MutableHashMap.set(this.unsyncedMutationEvents, Data.struct(mutationEvent.id), mutationEvent)
426
- }
427
- }
428
-
429
- return res
430
547
  }
431
548
  // #endregion mutate
432
549
 
@@ -448,117 +565,6 @@ export class Store<
448
565
  )
449
566
  }
450
567
 
451
- // #region mutateWithoutRefresh
452
- /**
453
- * Apply a mutation to the store.
454
- * Returns the tables that were affected by the event.
455
- * This is an internal method that doesn't trigger a refresh;
456
- * the caller must refresh queries after calling this method.
457
- */
458
- mutateWithoutRefresh = (
459
- mutationEventDecoded_: MutationEvent.ForSchema<TSchema> | MutationEvent.PartialForSchema<TSchema>,
460
- options: {
461
- otelContext: otel.Context
462
- // TODO adjust `skip-persist` with new rebase sync strategy
463
- coordinatorMode: 'default' | 'skip-coordinator' | 'skip-persist'
464
- },
465
- ): { writeTables: ReadonlySet<string>; durationMs: number } => {
466
- const mutationDef =
467
- this.schema.mutations.get(mutationEventDecoded_.mutation) ??
468
- shouldNeverHappen(`Unknown mutation type: ${mutationEventDecoded_.mutation}`)
469
-
470
- // Needs to happen only for partial mutation events (thus a function)
471
- const nextMutationEventId = () => {
472
- const { id, parentId } = this.clientSession.coordinator
473
- .nextMutationEventIdPair({ localOnly: mutationDef.options.localOnly })
474
- .pipe(Effect.runSync)
475
-
476
- return { id, parentId }
477
- }
478
-
479
- const mutationEventDecoded: MutationEvent.ForSchema<TSchema> = isPartialMutationEvent(mutationEventDecoded_)
480
- ? { ...mutationEventDecoded_, ...nextMutationEventId() }
481
- : mutationEventDecoded_
482
-
483
- // NOTE we also need this temporary workaround here since some code-paths use `mutateWithoutRefresh` directly
484
- // e.g. the row-query functionality
485
- if (MutableHashMap.has(this.unsyncedMutationEvents, Data.struct(mutationEventDecoded.id))) {
486
- // NOTE this data should never be used
487
- return { writeTables: new Set(), durationMs: 0 }
488
- } else {
489
- MutableHashMap.set(this.unsyncedMutationEvents, Data.struct(mutationEventDecoded.id), mutationEventDecoded)
490
- }
491
-
492
- const { otelContext, coordinatorMode = 'default' } = options
493
-
494
- return this.otel.tracer.startActiveSpan(
495
- 'LiveStore:mutateWithoutRefresh',
496
- {
497
- attributes: {
498
- 'livestore.mutation': mutationEventDecoded.mutation,
499
- 'livestore.args': JSON.stringify(mutationEventDecoded.args, null, 2),
500
- },
501
- },
502
- otelContext,
503
- (span) => {
504
- const otelContext = otel.trace.setSpan(otel.context.active(), span)
505
-
506
- const allWriteTables = new Set<string>()
507
- let durationMsTotal = 0
508
-
509
- replaceSessionIdSymbol(mutationEventDecoded.args, this.clientSession.coordinator.sessionId)
510
-
511
- const execArgsArr = getExecArgsFromMutation({ mutationDef, mutationEventDecoded })
512
-
513
- for (const {
514
- statementSql,
515
- bindValues,
516
- writeTables = this.syncDbWrapper.getTablesUsed(statementSql),
517
- } of execArgsArr) {
518
- // TODO when the store doesn't have the lock, we need wait for the coordinator to confirm the mutation
519
- // before executing the mutation on the main db
520
- const { durationMs } = this.syncDbWrapper.execute(statementSql, bindValues, writeTables, { otelContext })
521
-
522
- durationMsTotal += durationMs
523
- writeTables.forEach((table) => allWriteTables.add(table))
524
- }
525
-
526
- const mutationEventEncoded = Schema.encodeUnknownSync(this.__mutationEventSchema)(mutationEventDecoded)
527
-
528
- if (coordinatorMode !== 'skip-coordinator') {
529
- // Asynchronously apply mutation to a persistent storage (we're not awaiting this promise here)
530
- this.clientSession.coordinator
531
- .mutate(mutationEventEncoded as MutationEvent.AnyEncoded, { persisted: coordinatorMode !== 'skip-persist' })
532
- .pipe(this.runEffectFork)
533
- }
534
-
535
- // Uncomment to print a list of queries currently registered on the store
536
- // console.debug(JSON.parse(JSON.stringify([...this.queries].map((q) => `${labelForKey(q.componentKey)}/${q.label}`))))
537
-
538
- span.end()
539
-
540
- return { writeTables: allWriteTables, durationMs: durationMsTotal }
541
- },
542
- )
543
- }
544
- // #endregion mutateWithoutRefresh
545
-
546
- /**
547
- * Directly execute a SQL query on the Store.
548
- * This should only be used for framework-internal purposes;
549
- * all app writes should go through mutate.
550
- */
551
- __execute = (
552
- query: string,
553
- params: ParamsObject = {},
554
- writeTables?: ReadonlySet<string>,
555
- otelContext?: otel.Context,
556
- ) => {
557
- this.syncDbWrapper.execute(query, prepareBindValues(params, query), writeTables, { otelContext })
558
-
559
- this.clientSession.coordinator.execute(query, prepareBindValues(params, query)).pipe(this.runEffectFork)
560
- }
561
-
562
568
  private makeTableRef = (tableName: string) =>
563
569
  this.reactivityGraph.makeRef(null, {
564
570
  equal: () => false,
@@ -566,27 +572,92 @@ export class Store<
566
572
  meta: { liveStoreRefType: 'table' },
567
573
  })
568
574
 
569
- __devDownloadDb = () => {
570
- const data = this.syncDbWrapper.export()
571
- downloadBlob(data, `livestore-${Date.now()}.db`)
572
- }
575
+ /**
576
+ * Helper methods useful during development
577
+ *
578
+ * @internal
579
+ */
580
+ _dev = {
581
+ downloadDb: (source: 'local' | 'leader' = 'local') => {
582
+ Effect.gen(this, function* () {
583
+ const data = source === 'local' ? this.sqliteDbWrapper.export() : yield* this.clientSession.leaderThread.export
584
+ downloadBlob(data, `livestore-${Date.now()}.db`)
585
+ }).pipe(this.runEffectFork)
586
+ },
573
587
 
574
- __devDownloadMutationLogDb = () =>
575
- Effect.gen(this, function* () {
576
- const data = yield* this.clientSession.coordinator.getMutationLogData
577
- downloadBlob(data, `livestore-mutationlog-${Date.now()}.db`)
578
- }).pipe(this.runEffectFork)
588
+ downloadMutationLogDb: () => {
589
+ Effect.gen(this, function* () {
590
+ const data = yield* this.clientSession.leaderThread.getMutationLogData
591
+ downloadBlob(data, `livestore-mutationlog-${Date.now()}.db`)
592
+ }).pipe(this.runEffectFork)
593
+ },
579
594
 
580
- __devCurrentMutationEventId = () => this.clientSession.coordinator.getCurrentMutationEventId.pipe(Effect.runSync)
595
+ hardReset: (mode: 'all-data' | 'only-app-db' = 'all-data') => {
596
+ Effect.gen(this, function* () {
597
+ yield* this.clientSession.leaderThread.sendDevtoolsMessage(
598
+ Devtools.ResetAllDataReq.make({ liveStoreVersion, mode, requestId: nanoid() }),
599
+ )
600
+ }).pipe(this.runEffectFork)
601
+ },
602
+
603
+ syncStates: () => {
604
+ Effect.gen(this, function* () {
605
+ const session = this.syncProcessor.syncStateRef.current
606
+ console.log('Session sync state:', session.toJSON())
607
+ const leader = yield* this.clientSession.leaderThread.getSyncState
608
+ console.log('Leader sync state:', leader.toJSON())
609
+ }).pipe(this.runEffectFork)
610
+ },
611
+
612
+ shutdown: (cause?: Cause.Cause<UnexpectedError>) => {
613
+ this.clientSession
614
+ .shutdown(cause ?? Cause.fail(IntentionalShutdownCause.make({ reason: 'manual' })))
615
+ .pipe(Effect.tapCauseLogPretty, Effect.provide(this.runtime), Effect.runFork)
616
+ },
617
+ }
581
618
 
582
619
  // NOTE This is needed because when booting a Store via Effect it seems to call `toJSON` in the error path
583
- toJSON = () => {
584
- return {
585
- _tag: 'livestore.Store',
586
- reactivityGraph: this.reactivityGraph.getSnapshot({ includeResults: true }),
620
+ toJSON = () => ({
621
+ _tag: 'livestore.Store',
622
+ reactivityGraph: this.reactivityGraph.getSnapshot({ includeResults: true }),
623
+ })
624
+
625
+ private runEffectFork = <A, E>(effect: Effect.Effect<A, E, Scope.Scope>) =>
626
+ effect.pipe(Effect.forkIn(this.lifetimeScope), Effect.tapCauseLogPretty, Runtime.runFork(this.runtime))
627
+
628
+ private getMutateArgs = (
629
+ firstMutationOrTxnFnOrOptions: any,
630
+ restMutations: any[],
631
+ ): {
632
+ mutationsEvents: (MutationEvent.ForSchema<TSchema> | MutationEvent.PartialForSchema<TSchema>)[]
633
+ options: StoreMutateOptions | undefined
634
+ } => {
635
+ let mutationsEvents: (MutationEvent.ForSchema<TSchema> | MutationEvent.PartialForSchema<TSchema>)[]
636
+ let options: StoreMutateOptions | undefined
637
+
638
+ if (typeof firstMutationOrTxnFnOrOptions === 'function') {
639
+ // TODO ensure that function is synchronous and isn't called in a async way (also write tests for this)
640
+ mutationsEvents = firstMutationOrTxnFnOrOptions((arg: any) => mutationsEvents.push(arg))
641
+ } else if (
642
+ firstMutationOrTxnFnOrOptions?.label !== undefined ||
643
+ firstMutationOrTxnFnOrOptions?.skipRefresh !== undefined ||
644
+ firstMutationOrTxnFnOrOptions?.otelContext !== undefined ||
645
+ firstMutationOrTxnFnOrOptions?.spanLinks !== undefined
646
+ ) {
647
+ options = firstMutationOrTxnFnOrOptions
648
+ mutationsEvents = restMutations
649
+ } else if (firstMutationOrTxnFnOrOptions === undefined) {
650
+ // When `mutate` is called with no arguments (which sometimes happens when dynamically filtering mutations)
651
+ mutationsEvents = []
652
+ } else {
653
+ mutationsEvents = [firstMutationOrTxnFnOrOptions, ...restMutations]
587
654
  }
588
- }
589
655
 
590
- private runEffectFork = <A, E>(effect: Effect.Effect<A, E, never>) =>
591
- effect.pipe(Effect.tapCauseLogPretty, FiberSet.run(this.fiberSet), Runtime.runFork(this.runtime))
656
+ mutationsEvents = mutationsEvents.filter(
657
+ // @ts-expect-error TODO
658
+ (_) => _.id === undefined || !MutableHashMap.has(this.unsyncedMutationEvents, Data.struct(_.id)),
659
+ )
660
+
661
+ return { mutationsEvents, options }
662
+ }
592
663
  }