@livestore/livestore 0.3.0-dev.10 → 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 (114) 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 +14 -5
  7. package/dist/SynchronousDatabaseWrapper.d.ts.map +1 -1
  8. package/dist/SynchronousDatabaseWrapper.js +24 -4
  9. package/dist/SynchronousDatabaseWrapper.js.map +1 -1
  10. package/dist/effect/LiveStore.d.ts +12 -8
  11. package/dist/effect/LiveStore.d.ts.map +1 -1
  12. package/dist/effect/LiveStore.js +9 -2
  13. package/dist/effect/LiveStore.js.map +1 -1
  14. package/dist/index.d.ts +6 -7
  15. package/dist/index.d.ts.map +1 -1
  16. package/dist/index.js +4 -4
  17. package/dist/index.js.map +1 -1
  18. package/dist/live-queries/base-class.d.ts +64 -21
  19. package/dist/live-queries/base-class.d.ts.map +1 -1
  20. package/dist/live-queries/base-class.js +56 -13
  21. package/dist/live-queries/base-class.js.map +1 -1
  22. package/dist/live-queries/computed.d.ts +7 -7
  23. package/dist/live-queries/computed.d.ts.map +1 -1
  24. package/dist/live-queries/computed.js +35 -11
  25. package/dist/live-queries/computed.js.map +1 -1
  26. package/dist/live-queries/db-query.d.ts +67 -0
  27. package/dist/live-queries/db-query.d.ts.map +1 -0
  28. package/dist/live-queries/db-query.js +244 -0
  29. package/dist/live-queries/db-query.js.map +1 -0
  30. package/dist/live-queries/db-query.test.d.ts +2 -0
  31. package/dist/live-queries/db-query.test.d.ts.map +1 -0
  32. package/dist/live-queries/db-query.test.js +123 -0
  33. package/dist/live-queries/db-query.test.js.map +1 -0
  34. package/dist/live-queries/db.d.ts +12 -15
  35. package/dist/live-queries/db.d.ts.map +1 -1
  36. package/dist/live-queries/db.js +44 -25
  37. package/dist/live-queries/db.js.map +1 -1
  38. package/dist/live-queries/db.test.js +16 -14
  39. package/dist/live-queries/db.test.js.map +1 -1
  40. package/dist/live-queries/graphql.d.ts +8 -8
  41. package/dist/live-queries/graphql.d.ts.map +1 -1
  42. package/dist/live-queries/graphql.js +35 -9
  43. package/dist/live-queries/graphql.js.map +1 -1
  44. package/dist/live-queries/make-ref.d.ts +20 -0
  45. package/dist/live-queries/make-ref.d.ts.map +1 -0
  46. package/dist/live-queries/make-ref.js +33 -0
  47. package/dist/live-queries/make-ref.js.map +1 -0
  48. package/dist/reactive.d.ts +15 -13
  49. package/dist/reactive.d.ts.map +1 -1
  50. package/dist/reactive.js +15 -9
  51. package/dist/reactive.js.map +1 -1
  52. package/dist/row-query-utils.d.ts +4 -4
  53. package/dist/row-query-utils.d.ts.map +1 -1
  54. package/dist/row-query-utils.js +14 -10
  55. package/dist/row-query-utils.js.map +1 -1
  56. package/dist/store/create-store.d.ts +3 -4
  57. package/dist/store/create-store.d.ts.map +1 -1
  58. package/dist/store/create-store.js +7 -7
  59. package/dist/store/create-store.js.map +1 -1
  60. package/dist/store/devtools.d.ts +2 -2
  61. package/dist/store/devtools.d.ts.map +1 -1
  62. package/dist/store/devtools.js +15 -15
  63. package/dist/store/devtools.js.map +1 -1
  64. package/dist/store/store-types.d.ts +9 -4
  65. package/dist/store/store-types.d.ts.map +1 -1
  66. package/dist/store/store-types.js.map +1 -1
  67. package/dist/store/store.d.ts +34 -16
  68. package/dist/store/store.d.ts.map +1 -1
  69. package/dist/store/store.js +125 -75
  70. package/dist/store/store.js.map +1 -1
  71. package/dist/utils/expo.d.ts +2 -0
  72. package/dist/utils/expo.d.ts.map +1 -0
  73. package/dist/utils/expo.js +8 -0
  74. package/dist/utils/expo.js.map +1 -0
  75. package/dist/utils/function-string.d.ts +7 -0
  76. package/dist/utils/function-string.d.ts.map +1 -0
  77. package/dist/utils/function-string.js +9 -0
  78. package/dist/utils/function-string.js.map +1 -0
  79. package/dist/utils/stack-info.d.ts.map +1 -1
  80. package/dist/utils/stack-info.js +6 -1
  81. package/dist/utils/stack-info.js.map +1 -1
  82. package/dist/utils/stack-info.test.js +54 -1
  83. package/dist/utils/stack-info.test.js.map +1 -1
  84. package/dist/utils/tests/fixture.d.ts +2 -6
  85. package/dist/utils/tests/fixture.d.ts.map +1 -1
  86. package/dist/utils/tests/fixture.js +3 -5
  87. package/dist/utils/tests/fixture.js.map +1 -1
  88. package/dist/utils/tests/mod.d.ts +1 -0
  89. package/dist/utils/tests/mod.d.ts.map +1 -1
  90. package/dist/utils/tests/mod.js +1 -0
  91. package/dist/utils/tests/mod.js.map +1 -1
  92. package/package.json +5 -5
  93. package/src/{SynchronousDatabaseWrapper.ts → SqliteDbWrapper.ts} +41 -11
  94. package/src/effect/LiveStore.ts +22 -14
  95. package/src/index.ts +14 -7
  96. package/src/live-queries/__snapshots__/{db.test.ts.snap → db-query.test.ts.snap} +196 -42
  97. package/src/live-queries/base-class.ts +160 -40
  98. package/src/live-queries/computed.ts +45 -19
  99. package/src/live-queries/{db.test.ts → db-query.test.ts} +21 -11
  100. package/src/live-queries/{db.ts → db-query.ts} +97 -39
  101. package/src/live-queries/graphql.ts +47 -21
  102. package/src/live-queries/make-ref.ts +47 -0
  103. package/src/reactive.ts +52 -27
  104. package/src/row-query-utils.ts +29 -18
  105. package/src/store/create-store.ts +20 -23
  106. package/src/store/devtools.ts +17 -17
  107. package/src/store/store-types.ts +6 -4
  108. package/src/store/store.ts +227 -120
  109. package/src/utils/function-string.ts +12 -0
  110. package/src/utils/stack-info.test.ts +58 -1
  111. package/src/utils/stack-info.ts +6 -1
  112. package/src/utils/tests/fixture.ts +2 -7
  113. package/src/utils/tests/mod.ts +1 -0
  114. package/src/global-state.ts +0 -20
@@ -27,19 +27,44 @@ import {
27
27
  } from '@livestore/common/schema'
28
28
  import { assertNever, isDevEnv } from '@livestore/utils'
29
29
  import type { Scope } from '@livestore/utils/effect'
30
- import { Cause, Data, Effect, Inspectable, MutableHashMap, Runtime, Schema } 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'
31
41
  import { nanoid } from '@livestore/utils/nanoid'
32
42
  import * as otel from '@opentelemetry/api'
33
43
  import { type GraphQLSchema } from 'graphql'
34
44
 
35
- import type { LiveQuery, QueryContext, ReactivityGraph } from '../live-queries/base-class.js'
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'
36
53
  import type { Ref } from '../reactive.js'
37
54
  import { makeExecBeforeFirstRun } from '../row-query-utils.js'
38
- import { SynchronousDatabaseWrapper } from '../SynchronousDatabaseWrapper.js'
55
+ import { SqliteDbWrapper } from '../SqliteDbWrapper.js'
39
56
  import { ReferenceCountedSet } from '../utils/data-structures.js'
40
57
  import { downloadBlob, exposeDebugUtils } from '../utils/dev.js'
41
58
  import { getDurationMsFromSpan } from '../utils/otel.js'
42
- import type { BaseGraphQLContext, RefreshReason, StoreMutateOptions, StoreOptions, StoreOtel } from './store-types.js'
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'
43
68
 
44
69
  if (isDevEnv()) {
45
70
  exposeDebugUtils()
@@ -51,7 +76,7 @@ export class Store<
51
76
  > extends Inspectable.Class {
52
77
  readonly storeId: string
53
78
  reactivityGraph: ReactivityGraph
54
- syncDbWrapper: SynchronousDatabaseWrapper
79
+ sqliteDbWrapper: SqliteDbWrapper
55
80
  clientSession: ClientSession
56
81
  schema: LiveStoreSchema
57
82
  graphQLSchema?: GraphQLSchema
@@ -61,7 +86,7 @@ export class Store<
61
86
  * Note we're using `Ref<null>` here as we don't care about the value but only about *that* something has changed.
62
87
  * This only works in combination with `equal: () => false` which will always trigger a refresh.
63
88
  */
64
- tableRefs: { [key: string]: Ref<null, QueryContext, RefreshReason> }
89
+ tableRefs: { [key: string]: Ref<null, ReactivityGraphContext, RefreshReason> }
65
90
 
66
91
  private runtime: Runtime.Runtime<Scope.Scope>
67
92
 
@@ -74,12 +99,13 @@ export class Store<
74
99
  private syncProcessor: ClientSessionSyncProcessor
75
100
  readonly lifetimeScope: Scope.Scope
76
101
 
102
+ readonly boot: Effect.Effect<void, UnexpectedError, Scope.Scope>
103
+
77
104
  // #region constructor
78
- private constructor({
105
+ constructor({
79
106
  clientSession,
80
107
  schema,
81
108
  graphQLOptions,
82
- reactivityGraph,
83
109
  otelOptions,
84
110
  disableDevtools,
85
111
  batchUpdates,
@@ -93,25 +119,21 @@ export class Store<
93
119
  this.storeId = storeId
94
120
  this.unsyncedMutationEvents = unsyncedMutationEvents
95
121
 
96
- this.syncDbWrapper = new SynchronousDatabaseWrapper({ otel: otelOptions, db: clientSession.syncDb })
122
+ this.sqliteDbWrapper = new SqliteDbWrapper({ otel: otelOptions, db: clientSession.sqliteDb })
97
123
  this.clientSession = clientSession
98
124
  this.schema = schema
99
125
 
100
126
  this.lifetimeScope = lifetimeScope
101
127
  this.runtime = runtime
102
128
 
129
+ const reactivityGraph = makeReactivityGraph()
130
+
103
131
  const syncSpan = otelOptions.tracer.startSpan('LiveStore:sync', {}, otelOptions.rootSpanContext)
104
132
 
105
133
  this.syncProcessor = makeClientSessionSyncProcessor({
106
134
  schema,
107
- initialLeaderHead: clientSession.leaderThread.mutations.initialMutationEventId,
108
- pushToLeader: (batch) =>
109
- clientSession.leaderThread.mutations.push(batch).pipe(
110
- // NOTE we don't want to shutdown in case of an invalid push error, since it will be retried
111
- Effect.catchTag('InvalidPushError', Effect.ignoreLogged),
112
- this.runEffectFork,
113
- ),
114
- pullFromLeader: clientSession.leaderThread.mutations.pull,
135
+ clientSession,
136
+ runtime,
115
137
  applyMutation: (mutationEventDecoded, { otelContext, withChangeset }) => {
116
138
  const mutationDef = schema.mutations.get(mutationEventDecoded.mutation)!
117
139
  const execArgsArr = getExecArgsFromMutation({
@@ -125,9 +147,9 @@ export class Store<
125
147
  for (const {
126
148
  statementSql,
127
149
  bindValues,
128
- writeTables = this.syncDbWrapper.getTablesUsed(statementSql),
150
+ writeTables = this.sqliteDbWrapper.getTablesUsed(statementSql),
129
151
  } of execArgsArr) {
130
- this.syncDbWrapper.execute(statementSql, bindValues, writeTables, { otelContext })
152
+ this.sqliteDbWrapper.execute(statementSql, bindValues, { otelContext, writeTables })
131
153
 
132
154
  // durationMsTotal += durationMs
133
155
  writeTables.forEach((table) => writeTablesForEvent.add(table))
@@ -136,7 +158,7 @@ export class Store<
136
158
 
137
159
  let sessionChangeset: Uint8Array | undefined
138
160
  if (withChangeset === true) {
139
- sessionChangeset = this.syncDbWrapper.withChangeset(exec).changeset
161
+ sessionChangeset = this.sqliteDbWrapper.withChangeset(exec).changeset
140
162
  } else {
141
163
  exec()
142
164
  }
@@ -144,16 +166,16 @@ export class Store<
144
166
  return { writeTables: writeTablesForEvent, sessionChangeset }
145
167
  },
146
168
  rollback: (changeset) => {
147
- this.syncDbWrapper.rollback(changeset)
169
+ this.sqliteDbWrapper.rollback(changeset)
148
170
  },
149
171
  refreshTables: (tables) => {
150
- const tablesToUpdate = [] as [Ref<null, QueryContext, RefreshReason>, null][]
172
+ const tablesToUpdate = [] as [Ref<null, ReactivityGraphContext, RefreshReason>, null][]
151
173
  for (const tableName of tables) {
152
174
  const tableRef = this.tableRefs[tableName]
153
175
  assertNever(tableRef !== undefined, `No table ref found for ${tableName}`)
154
176
  tablesToUpdate.push([tableRef!, null])
155
177
  }
156
- this.reactivityGraph.setRefs(tablesToUpdate)
178
+ reactivityGraph.setRefs(tablesToUpdate)
157
179
  },
158
180
  span: syncSpan,
159
181
  })
@@ -173,6 +195,8 @@ export class Store<
173
195
  this.reactivityGraph = reactivityGraph
174
196
  this.reactivityGraph.context = {
175
197
  store: this as unknown as Store<BaseGraphQLContext, LiveStoreSchema>,
198
+ liveQueryRCMap: new Map(),
199
+ reactivityGraph: new WeakRef(reactivityGraph),
176
200
  otelTracer: otelOptions.tracer,
177
201
  rootOtelContext: otelQueriesSpanContext,
178
202
  effectsWrapper: batchUpdates,
@@ -209,10 +233,10 @@ export class Store<
209
233
 
210
234
  if (graphQLOptions) {
211
235
  this.graphQLSchema = graphQLOptions.schema
212
- this.graphQLContext = graphQLOptions.makeContext(this.syncDbWrapper, this.otel.tracer, clientSession.sessionId)
236
+ this.graphQLContext = graphQLOptions.makeContext(this.sqliteDbWrapper, this.otel.tracer, clientSession.sessionId)
213
237
  }
214
238
 
215
- Effect.gen(this, function* () {
239
+ this.boot = Effect.gen(this, function* () {
216
240
  yield* Effect.addFinalizer(() =>
217
241
  Effect.sync(() => {
218
242
  // Remove all table refs from the reactivity graph
@@ -230,23 +254,9 @@ export class Store<
230
254
  )
231
255
 
232
256
  yield* this.syncProcessor.boot
233
- }).pipe(this.runEffectFork)
234
- }
235
- // #endregion constructor
236
-
237
- static createStore = <TGraphQLContext extends BaseGraphQLContext, TSchema extends LiveStoreSchema = LiveStoreSchema>(
238
- storeOptions: StoreOptions<TGraphQLContext, TSchema>,
239
- parentSpan: otel.Span,
240
- ): Store<TGraphQLContext, TSchema> => {
241
- const ctx = otel.trace.setSpan(otel.context.active(), parentSpan)
242
- return storeOptions.otelOptions.tracer.startActiveSpan('LiveStore:createStore', {}, ctx, (span) => {
243
- try {
244
- return new Store(storeOptions)
245
- } finally {
246
- span.end()
247
- }
248
257
  })
249
258
  }
259
+ // #endregion constructor
250
260
 
251
261
  get sessionId(): string {
252
262
  return this.clientSession.sessionId
@@ -255,29 +265,66 @@ export class Store<
255
265
  /**
256
266
  * Subscribe to the results of a query
257
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
+ * ```
258
273
  */
259
274
  subscribe = <TResult>(
260
- query$: LiveQuery<TResult, any>,
261
- onNewValue: (value: TResult) => void,
262
- onUnsubsubscribe?: () => void,
263
- options?: { label?: string; otelContext?: otel.Context; skipInitialRun?: boolean } | undefined,
264
- ): (() => 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 =>
265
293
  this.otel.tracer.startActiveSpan(
266
294
  `LiveStore.subscribe`,
267
- { attributes: { label: options?.label, queryLabel: query$.label } },
295
+ { attributes: { label: options?.label, queryLabel: query.label } },
268
296
  options?.otelContext ?? this.otel.queriesSpanContext,
269
297
  (span) => {
270
298
  // console.debug('store sub', query$.id, query$.label)
271
299
  const otelContext = otel.trace.setSpan(otel.context.active(), span)
272
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
+
273
310
  const label = `subscribe:${options?.label}`
274
- 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$)
275
322
 
276
323
  this.activeQueries.add(query$ as LiveQuery<TResult>)
277
324
 
278
325
  // Running effect right away to get initial value (unless `skipInitialRun` is set)
279
- if (options?.skipInitialRun !== true) {
280
- effect.doEffect(otelContext)
326
+ if (options?.skipInitialRun !== true && !query$.isDestroyed) {
327
+ effect.doEffect(otelContext, { _tag: 'subscribe.initial', label: `subscribe-initial-run:${options?.label}` })
281
328
  }
282
329
 
283
330
  const unsubscribe = () => {
@@ -285,7 +332,14 @@ export class Store<
285
332
  try {
286
333
  this.reactivityGraph.destroyNode(effect)
287
334
  this.activeQueries.remove(query$ as LiveQuery<TResult>)
288
- onUnsubsubscribe?.()
335
+
336
+ if (options?.stackInfo) {
337
+ query$.activeSubscriptions.delete(options.stackInfo)
338
+ }
339
+
340
+ queryRcRef.deref()
341
+
342
+ options?.onUnsubsubscribe?.()
289
343
  } finally {
290
344
  span.end()
291
345
  }
@@ -295,6 +349,30 @@ export class Store<
295
349
  },
296
350
  )
297
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
+
298
376
  /**
299
377
  * Synchronously queries the database without creating a LiveQuery.
300
378
  * This is useful for queries that don't need to be reactive.
@@ -310,12 +388,15 @@ export class Store<
310
388
  * ```
311
389
  */
312
390
  query = <TResult>(
313
- query: QueryBuilder<TResult, any, any> | LiveQuery<TResult, any> | { query: string; bindValues: ParamsObject },
314
- 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 },
315
397
  ): TResult => {
316
398
  if (typeof query === 'object' && 'query' in query && 'bindValues' in query) {
317
- return this.syncDbWrapper.select(query.query, {
318
- bindValues: prepareBindValues(query.bindValues, query.query),
399
+ return this.sqliteDbWrapper.select(query.query, prepareBindValues(query.bindValues, query.query), {
319
400
  otelContext: options?.otelContext,
320
401
  }) as any
321
402
  } else if (isQueryBuilder(query)) {
@@ -331,17 +412,38 @@ export class Store<
331
412
 
332
413
  const sqlRes = query.asSql()
333
414
  const schema = getResultSchema(query)
334
- const rawRes = this.syncDbWrapper.select(sqlRes.query, {
335
- bindValues: sqlRes.bindValues as any as PreparedBindValues,
415
+ const rawRes = this.sqliteDbWrapper.select(sqlRes.query, sqlRes.bindValues as any as PreparedBindValues, {
336
416
  otelContext: options?.otelContext,
337
417
  queriedTables: new Set([query[QueryBuilderAstSymbol].tableDef.sqliteDef.name]),
338
418
  })
339
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
340
425
  } else {
341
- return query.run(options?.otelContext)
426
+ return query.run({ otelContext: options?.otelContext, debugRefreshReason: options?.debugRefreshReason })
342
427
  }
343
428
  }
344
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
+
345
447
  // #region mutate
346
448
  mutate: {
347
449
  <const TMutationArg extends ReadonlyArray<MutationEvent.PartialForSchema<TSchema>>>(...list: TMutationArg): void
@@ -369,7 +471,6 @@ export class Store<
369
471
 
370
472
  if (mutationsEvents.length === 0) return
371
473
 
372
- const label = options?.label ?? 'mutate'
373
474
  const skipRefresh = options?.skipRefresh ?? false
374
475
 
375
476
  const mutationsSpan = otel.trace.getSpan(this.otel.mutationsSpanContext)!
@@ -383,40 +484,39 @@ export class Store<
383
484
 
384
485
  return this.otel.tracer.startActiveSpan(
385
486
  'LiveStore:mutate',
386
- { attributes: { 'livestore.mutateLabel': label }, links: options?.spanLinks },
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
+ },
387
495
  options?.otelContext ?? this.otel.mutationsSpanContext,
388
496
  (span) => {
389
497
  const otelContext = otel.trace.setSpan(otel.context.active(), span)
390
498
 
391
499
  try {
392
- const { writeTables } = this.otel.tracer.startActiveSpan(
393
- 'LiveStore:mutate:applyMutations',
394
- { attributes: { 'livestore.mutateLabel': label } },
395
- otel.trace.setSpan(otel.context.active(), span),
396
- (span) => {
397
- try {
398
- const otelContext = otel.trace.setSpan(otel.context.active(), span)
399
- // 5
400
-
401
- const applyMutations = () => this.syncProcessor.push(mutationsEvents, { otelContext })
402
-
403
- if (mutationsEvents.length > 1) {
404
- // TODO: what to do about leader transaction here?
405
- return this.syncDbWrapper.txn(applyMutations)
406
- } else {
407
- return applyMutations()
408
- }
409
- } catch (e: any) {
410
- console.error(e)
411
- span.setStatus({ code: otel.SpanStatusCode.ERROR, message: e.toString() })
412
- throw e
413
- } finally {
414
- 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()
415
509
  }
416
- },
417
- )
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
+ })()
418
518
 
419
- const tablesToUpdate = [] as [Ref<null, QueryContext, RefreshReason>, null][]
519
+ const tablesToUpdate = [] as [Ref<null, ReactivityGraphContext, RefreshReason>, null][]
420
520
  for (const tableName of writeTables) {
421
521
  const tableRef = this.tableRefs[tableName]
422
522
  assertNever(tableRef !== undefined, `No table ref found for ${tableName}`)
@@ -472,41 +572,48 @@ export class Store<
472
572
  meta: { liveStoreRefType: 'table' },
473
573
  })
474
574
 
475
- __devDownloadDb = (source: 'local' | 'leader' = 'local') => {
476
- Effect.gen(this, function* () {
477
- const data = source === 'local' ? this.syncDbWrapper.export() : yield* this.clientSession.leaderThread.export
478
- downloadBlob(data, `livestore-${Date.now()}.db`)
479
- }).pipe(this.runEffectFork)
480
- }
481
-
482
- __devDownloadMutationLogDb = () => {
483
- Effect.gen(this, function* () {
484
- const data = yield* this.clientSession.leaderThread.getMutationLogData
485
- downloadBlob(data, `livestore-mutationlog-${Date.now()}.db`)
486
- }).pipe(this.runEffectFork)
487
- }
488
-
489
- __devHardReset = (mode: 'all-data' | 'only-app-db' = 'all-data') => {
490
- Effect.gen(this, function* () {
491
- yield* this.clientSession.leaderThread.sendDevtoolsMessage(
492
- Devtools.ResetAllDataReq.make({ liveStoreVersion, mode, requestId: nanoid() }),
493
- )
494
- }).pipe(this.runEffectFork)
495
- }
496
-
497
- __devSyncStates = () => {
498
- Effect.gen(this, function* () {
499
- const session = this.syncProcessor.syncStateRef.current
500
- console.log('Session sync state:', session.toJSON())
501
- const leader = yield* this.clientSession.leaderThread.getSyncState
502
- console.log('Leader sync state:', leader.toJSON())
503
- }).pipe(this.runEffectFork)
504
- }
505
-
506
- __devShutdown = (cause?: Cause.Cause<UnexpectedError>) => {
507
- this.clientSession
508
- .shutdown(cause ?? Cause.fail(IntentionalShutdownCause.make({ reason: 'manual' })))
509
- .pipe(Effect.tapCauseLogPretty, Effect.provide(this.runtime), Effect.runFork)
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
+ },
587
+
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
+ },
594
+
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
+ },
510
617
  }
511
618
 
512
619
  // NOTE This is needed because when booting a Store via Effect it seems to call `toJSON` in the error path
@@ -0,0 +1,12 @@
1
+ // Related https://github.com/facebook/hermes/issues/612#issuecomment-2549404649
2
+ const REACT_NATIVE_BAD_FUNCTION_STRING = 'function() { [bytecode] }'
3
+
4
+ export const isValidFunctionString = (
5
+ fnStr: string,
6
+ ): { _tag: 'valid' } | { _tag: 'invalid'; reason: 'react-native' } => {
7
+ if (fnStr === REACT_NATIVE_BAD_FUNCTION_STRING) {
8
+ return { _tag: 'invalid', reason: 'react-native' }
9
+ }
10
+
11
+ return { _tag: 'valid' }
12
+ }
@@ -46,7 +46,7 @@ Error
46
46
 
47
47
  it('Tracklist_ stacktrace', async () => {
48
48
  const stackTrace = `\
49
- stack Error
49
+ Error
50
50
  at https://localhost:8081/@fs/Users/schickling/Code/overtone/submodules/livestore/packages/@livestore/livestore/dist/react/useQuery.js?t=1701368568351:19:23
51
51
  at mountMemo (https://localhost:8081/node_modules/.vite-web/deps/chunk-YKTDXTVC.js?v=86daed82:12817:27)
52
52
  at Object.useMemo (https://localhost:8081/node_modules/.vite-web/deps/chunk-YKTDXTVC.js?v=86daed82:13141:24)
@@ -77,3 +77,60 @@ stack Error
77
77
  }
78
78
  `)
79
79
  })
80
+
81
+ it('React 19', async () => {
82
+ const stackTrace = `\
83
+ Error:
84
+ at /Users/schickling/Code/overtone/submodules/livestore/packages/@livestore/react/src/useQuery.ts:57:19
85
+ at mountMemo (/Users/schickling/Code/overtone/node_modules/.pnpm/react-dom@19.0.0_react@19.0.0/node_modules/react-dom/cjs/react-dom-client.development.js:6816:23)
86
+ at Object.useMemo (/Users/schickling/Code/overtone/node_modules/.pnpm/react-dom@19.0.0_react@19.0.0/node_modules/react-dom/cjs/react-dom-client.development.js:22757:18)
87
+ at Object.process.env.NODE_ENV.exports.useMemo (/Users/schickling/Code/overtone/node_modules/.pnpm/react@19.0.0/node_modules/react/cjs/react.development.js:1488:34)
88
+ at Module.useQueryRef (/Users/schickling/Code/overtone/submodules/livestore/packages/@livestore/react/src/useQuery.ts:54:27)
89
+ at Module.useRow (/Users/schickling/Code/overtone/submodules/livestore/packages/@livestore/react/src/useRow.ts:111:20)
90
+ at TestComponent (/Users/schickling/Code/overtone/node_modules/.pnpm/@testing-library+react@16.1.0_@testing-library+dom@10.4.0_@types+react-dom@19.0.3_@types+reac_2jaiibiag2sxou3wtzbuqx3r5a/node_modules/@testing-library/react/dist/pure.js:309:27)
91
+ at Object.react-stack-bottom-frame (/Users/schickling/Code/overtone/node_modules/.pnpm/react-dom@19.0.0_react@19.0.0/node_modules/react-dom/cjs/react-dom-client.development.js:22428:20)
92
+ at renderWithHooks (/Users/schickling/Code/overtone/node_modules/.pnpm/react-dom@19.0.0_react@19.0.0/node_modules/react-dom/cjs/react-dom-client.development.js:5757:22)
93
+ `
94
+
95
+ const stackInfo = extractStackInfoFromStackTrace(stackTrace)
96
+ expect(stackInfo).toMatchInlineSnapshot(`
97
+ {
98
+ "frames": [
99
+ {
100
+ "filePath": "/Users/schickling/Code/overtone/node_modules/.pnpm/@testing-library+react@16.1.0_@testing-library+dom@10.4.0_@types+react-dom@19.0.3_@types+reac_2jaiibiag2sxou3wtzbuqx3r5a/node_modules/@testing-library/react/dist/pure.js:309:27",
101
+ "name": "TestComponent",
102
+ },
103
+ {
104
+ "filePath": "/Users/schickling/Code/overtone/submodules/livestore/packages/@livestore/react/src/useRow.ts:111:20",
105
+ "name": "useRow",
106
+ },
107
+ ],
108
+ }
109
+ `)
110
+ })
111
+
112
+ it('React 19 - skip react-stack-bottom-frame', async () => {
113
+ const stackTrace = `\
114
+ Error:
115
+ at /Users/schickling/Code/overtone/submodules/livestore/packages/@livestore/react/src/useQuery.ts:57:19
116
+ at mountMemo (/Users/schickling/Code/overtone/node_modules/.pnpm/react-dom@19.0.0_react@19.0.0/node_modules/react-dom/cjs/react-dom-client.development.js:6816:23)
117
+ at Object.useMemo (/Users/schickling/Code/overtone/node_modules/.pnpm/react-dom@19.0.0_react@19.0.0/node_modules/react-dom/cjs/react-dom-client.development.js:22757:18)
118
+ at Object.process.env.NODE_ENV.exports.useMemo (/Users/schickling/Code/overtone/node_modules/.pnpm/react@19.0.0/node_modules/react/cjs/react.development.js:1488:34)
119
+ at Module.useQueryRef (/Users/schickling/Code/overtone/submodules/livestore/packages/@livestore/react/src/useQuery.ts:54:27)
120
+ at Module.useRow (/Users/schickling/Code/overtone/submodules/livestore/packages/@livestore/react/src/useRow.ts:111:20)
121
+ at Object.react-stack-bottom-frame (/Users/schickling/Code/overtone/node_modules/.pnpm/react-dom@19.0.0_react@19.0.0/node_modules/react-dom/cjs/react-dom-client.development.js:22428:20)
122
+ at renderWithHooks (/Users/schickling/Code/overtone/node_modules/.pnpm/react-dom@19.0.0_react@19.0.0/node_modules/react-dom/cjs/react-dom-client.development.js:5757:22)
123
+ `
124
+
125
+ const stackInfo = extractStackInfoFromStackTrace(stackTrace)
126
+ expect(stackInfo).toMatchInlineSnapshot(`
127
+ {
128
+ "frames": [
129
+ {
130
+ "filePath": "/Users/schickling/Code/overtone/submodules/livestore/packages/@livestore/react/src/useRow.ts:111:20",
131
+ "name": "useRow",
132
+ },
133
+ ],
134
+ }
135
+ `)
136
+ })
@@ -34,15 +34,20 @@ export const extractStackInfoFromStackTrace = (stackTrace: string): StackInfo =>
34
34
 
35
35
  while ((match = namePattern.exec(stackTrace)) !== null) {
36
36
  const [, name, filePath] = match as any as [string, string, string]
37
+ // console.debug(name, filePath)
37
38
 
38
39
  // NOTE No idea where this `Module.` comes from - possibly a Vite thing?
39
40
  if ((name.startsWith('use') || name.startsWith('Module.use')) && name.endsWith('QueryRef') === false) {
40
41
  hasReachedStart = true
42
+ // console.debug('hasReachedStart. adding one more frame.')
41
43
 
42
44
  frames.unshift({ name: name.replace(/^Module\./, ''), filePath })
43
45
  } else if (hasReachedStart) {
44
46
  // We've reached the end of the `use*` functions, so we're adding the component name and stop
45
- frames.unshift({ name, filePath })
47
+ // Unless it's `react-stack-bottom-frame`, which we skip
48
+ if (name !== 'Object.react-stack-bottom-frame') {
49
+ frames.unshift({ name, filePath })
50
+ }
46
51
  break
47
52
  }
48
53
  }