@livestore/livestore 0.2.0-dev.2 → 0.3.0-dev.10

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 (100) hide show
  1. package/dist/.tsbuildinfo +1 -1
  2. package/dist/SynchronousDatabaseWrapper.d.ts +6 -1
  3. package/dist/SynchronousDatabaseWrapper.d.ts.map +1 -1
  4. package/dist/SynchronousDatabaseWrapper.js +14 -2
  5. package/dist/SynchronousDatabaseWrapper.js.map +1 -1
  6. package/dist/__tests__/fixture.d.ts +252 -0
  7. package/dist/__tests__/fixture.d.ts.map +1 -0
  8. package/dist/__tests__/fixture.js +18 -0
  9. package/dist/__tests__/fixture.js.map +1 -0
  10. package/dist/effect/LiveStore.d.ts +6 -6
  11. package/dist/effect/LiveStore.d.ts.map +1 -1
  12. package/dist/effect/LiveStore.js +5 -12
  13. package/dist/effect/LiveStore.js.map +1 -1
  14. package/dist/index.d.ts +1 -1
  15. package/dist/index.d.ts.map +1 -1
  16. package/dist/index.js.map +1 -1
  17. package/dist/live-queries/db.d.ts.map +1 -1
  18. package/dist/live-queries/db.js +28 -23
  19. package/dist/live-queries/db.js.map +1 -1
  20. package/dist/live-queries/db.test.js +2 -1
  21. package/dist/live-queries/db.test.js.map +1 -1
  22. package/dist/row-query-utils.js +1 -1
  23. package/dist/row-query-utils.js.map +1 -1
  24. package/dist/store/create-store.d.ts +12 -10
  25. package/dist/store/create-store.d.ts.map +1 -1
  26. package/dist/store/create-store.js +22 -28
  27. package/dist/store/create-store.js.map +1 -1
  28. package/dist/store/devtools.d.ts +1 -1
  29. package/dist/store/devtools.d.ts.map +1 -1
  30. package/dist/store/devtools.js +41 -19
  31. package/dist/store/devtools.js.map +1 -1
  32. package/dist/store/store-types.d.ts +9 -14
  33. package/dist/store/store-types.d.ts.map +1 -1
  34. package/dist/store/store.d.ts +29 -28
  35. package/dist/store/store.d.ts.map +1 -1
  36. package/dist/store/store.js +147 -160
  37. package/dist/store/store.js.map +1 -1
  38. package/dist/store/store.test.d.ts +2 -0
  39. package/dist/store/store.test.d.ts.map +1 -0
  40. package/dist/store/store.test.js +27 -0
  41. package/dist/store/store.test.js.map +1 -0
  42. package/dist/utils/dev.d.ts.map +1 -1
  43. package/dist/utils/dev.js +3 -2
  44. package/dist/utils/dev.js.map +1 -1
  45. package/dist/utils/tests/fixture.d.ts +1 -1
  46. package/dist/utils/tests/fixture.d.ts.map +1 -1
  47. package/dist/utils/tests/fixture.js +4 -8
  48. package/dist/utils/tests/fixture.js.map +1 -1
  49. package/dist/utils/tests/otel.d.ts +60 -1
  50. package/dist/utils/tests/otel.d.ts.map +1 -1
  51. package/dist/utils/tests/otel.js +65 -4
  52. package/dist/utils/tests/otel.js.map +1 -1
  53. package/package.json +12 -12
  54. package/src/SynchronousDatabaseWrapper.ts +18 -2
  55. package/src/ambient.d.ts +1 -1
  56. package/src/effect/LiveStore.ts +11 -20
  57. package/src/index.ts +1 -1
  58. package/src/live-queries/__snapshots__/db.test.ts.snap +42 -45
  59. package/src/live-queries/db.test.ts +2 -1
  60. package/src/live-queries/db.ts +28 -23
  61. package/src/row-query-utils.ts +1 -1
  62. package/src/store/create-store.ts +115 -119
  63. package/src/store/devtools.ts +48 -22
  64. package/src/store/store-types.ts +14 -14
  65. package/src/store/store.ts +188 -224
  66. package/src/utils/dev.ts +4 -2
  67. package/src/utils/tests/fixture.ts +4 -9
  68. package/src/utils/tests/otel.ts +71 -5
  69. package/dist/live-queries/sql.d.ts +0 -62
  70. package/dist/live-queries/sql.d.ts.map +0 -1
  71. package/dist/live-queries/sql.js +0 -175
  72. package/dist/live-queries/sql.js.map +0 -1
  73. package/dist/live-queries/sql.test.d.ts +0 -2
  74. package/dist/live-queries/sql.test.d.ts.map +0 -1
  75. package/dist/live-queries/sql.test.js +0 -285
  76. package/dist/live-queries/sql.test.js.map +0 -1
  77. package/dist/reactiveQueries/base-class.d.ts +0 -64
  78. package/dist/reactiveQueries/base-class.d.ts.map +0 -1
  79. package/dist/reactiveQueries/base-class.js +0 -31
  80. package/dist/reactiveQueries/base-class.js.map +0 -1
  81. package/dist/reactiveQueries/computed.d.ts +0 -26
  82. package/dist/reactiveQueries/computed.d.ts.map +0 -1
  83. package/dist/reactiveQueries/computed.js +0 -38
  84. package/dist/reactiveQueries/computed.js.map +0 -1
  85. package/dist/reactiveQueries/graphql.d.ts +0 -49
  86. package/dist/reactiveQueries/graphql.d.ts.map +0 -1
  87. package/dist/reactiveQueries/graphql.js +0 -122
  88. package/dist/reactiveQueries/graphql.js.map +0 -1
  89. package/dist/reactiveQueries/sql.d.ts +0 -62
  90. package/dist/reactiveQueries/sql.d.ts.map +0 -1
  91. package/dist/reactiveQueries/sql.js +0 -175
  92. package/dist/reactiveQueries/sql.js.map +0 -1
  93. package/dist/reactiveQueries/sql.test.d.ts +0 -2
  94. package/dist/reactiveQueries/sql.test.d.ts.map +0 -1
  95. package/dist/reactiveQueries/sql.test.js +0 -285
  96. package/dist/reactiveQueries/sql.test.js.map +0 -1
  97. package/dist/row-query.d.ts +0 -16
  98. package/dist/row-query.d.ts.map +0 -1
  99. package/dist/row-query.js +0 -30
  100. package/dist/row-query.js.map +0 -1
@@ -1,25 +1,36 @@
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 { Cause, Data, Effect, Inspectable, MutableHashMap, Runtime, Schema } from '@livestore/utils/effect'
31
+ import { nanoid } from '@livestore/utils/nanoid'
21
32
  import * as otel from '@opentelemetry/api'
22
- import type { GraphQLSchema } from 'graphql'
33
+ import { type GraphQLSchema } from 'graphql'
23
34
 
24
35
  import type { LiveQuery, QueryContext, ReactivityGraph } from '../live-queries/base-class.js'
25
36
  import type { Ref } from '../reactive.js'
@@ -30,7 +41,7 @@ import { downloadBlob, exposeDebugUtils } from '../utils/dev.js'
30
41
  import { getDurationMsFromSpan } from '../utils/otel.js'
31
42
  import type { BaseGraphQLContext, RefreshReason, StoreMutateOptions, StoreOptions, StoreOtel } from './store-types.js'
32
43
 
33
- if (import.meta.env.DEV) {
44
+ if (isDevEnv()) {
34
45
  exposeDebugUtils()
35
46
  }
36
47
 
@@ -52,7 +63,6 @@ export class Store<
52
63
  */
53
64
  tableRefs: { [key: string]: Ref<null, QueryContext, RefreshReason> }
54
65
 
55
- private fiberSet: FiberSet.FiberSet
56
66
  private runtime: Runtime.Runtime<Scope.Scope>
57
67
 
58
68
  /** RC-based set to see which queries are currently subscribed to */
@@ -61,6 +71,8 @@ export class Store<
61
71
  // NOTE this is currently exposed for the Devtools databrowser to emit mutation events
62
72
  readonly __mutationEventSchema
63
73
  private unsyncedMutationEvents
74
+ private syncProcessor: ClientSessionSyncProcessor
75
+ readonly lifetimeScope: Scope.Scope
64
76
 
65
77
  // #region constructor
66
78
  private constructor({
@@ -73,24 +85,80 @@ export class Store<
73
85
  batchUpdates,
74
86
  unsyncedMutationEvents,
75
87
  storeId,
76
- fiberSet,
88
+ lifetimeScope,
77
89
  runtime,
78
90
  }: StoreOptions<TGraphQLContext, TSchema>) {
79
91
  super()
80
92
 
81
93
  this.storeId = storeId
82
-
83
94
  this.unsyncedMutationEvents = unsyncedMutationEvents
84
95
 
85
96
  this.syncDbWrapper = new SynchronousDatabaseWrapper({ otel: otelOptions, db: clientSession.syncDb })
86
97
  this.clientSession = clientSession
87
98
  this.schema = schema
88
99
 
89
- this.fiberSet = fiberSet
100
+ this.lifetimeScope = lifetimeScope
90
101
  this.runtime = runtime
91
102
 
92
- // TODO refactor
93
- this.__mutationEventSchema = makeMutationEventSchemaMemo(schema)
103
+ const syncSpan = otelOptions.tracer.startSpan('LiveStore:sync', {}, otelOptions.rootSpanContext)
104
+
105
+ this.syncProcessor = makeClientSessionSyncProcessor({
106
+ 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,
115
+ applyMutation: (mutationEventDecoded, { otelContext, withChangeset }) => {
116
+ const mutationDef = schema.mutations.get(mutationEventDecoded.mutation)!
117
+ const execArgsArr = getExecArgsFromMutation({
118
+ mutationDef,
119
+ mutationEvent: { decoded: mutationEventDecoded, encoded: undefined },
120
+ })
121
+
122
+ const writeTablesForEvent = new Set<string>()
123
+
124
+ const exec = () => {
125
+ for (const {
126
+ statementSql,
127
+ bindValues,
128
+ writeTables = this.syncDbWrapper.getTablesUsed(statementSql),
129
+ } of execArgsArr) {
130
+ this.syncDbWrapper.execute(statementSql, bindValues, writeTables, { otelContext })
131
+
132
+ // durationMsTotal += durationMs
133
+ writeTables.forEach((table) => writeTablesForEvent.add(table))
134
+ }
135
+ }
136
+
137
+ let sessionChangeset: Uint8Array | undefined
138
+ if (withChangeset === true) {
139
+ sessionChangeset = this.syncDbWrapper.withChangeset(exec).changeset
140
+ } else {
141
+ exec()
142
+ }
143
+
144
+ return { writeTables: writeTablesForEvent, sessionChangeset }
145
+ },
146
+ rollback: (changeset) => {
147
+ this.syncDbWrapper.rollback(changeset)
148
+ },
149
+ refreshTables: (tables) => {
150
+ const tablesToUpdate = [] as [Ref<null, QueryContext, RefreshReason>, null][]
151
+ for (const tableName of tables) {
152
+ const tableRef = this.tableRefs[tableName]
153
+ assertNever(tableRef !== undefined, `No table ref found for ${tableName}`)
154
+ tablesToUpdate.push([tableRef!, null])
155
+ }
156
+ this.reactivityGraph.setRefs(tablesToUpdate)
157
+ },
158
+ span: syncSpan,
159
+ })
160
+
161
+ this.__mutationEventSchema = MutationEvent.makeMutationEventSchemaMemo(schema)
94
162
 
95
163
  // TODO generalize the `tableRefs` concept to allow finer-grained refs
96
164
  this.tableRefs = {}
@@ -141,26 +209,10 @@ export class Store<
141
209
 
142
210
  if (graphQLOptions) {
143
211
  this.graphQLSchema = graphQLOptions.schema
144
- this.graphQLContext = graphQLOptions.makeContext(
145
- this.syncDbWrapper,
146
- this.otel.tracer,
147
- clientSession.coordinator.sessionId,
148
- )
212
+ this.graphQLContext = graphQLOptions.makeContext(this.syncDbWrapper, this.otel.tracer, clientSession.sessionId)
149
213
  }
150
214
 
151
215
  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
-
164
216
  yield* Effect.addFinalizer(() =>
165
217
  Effect.sync(() => {
166
218
  // Remove all table refs from the reactivity graph
@@ -171,13 +223,14 @@ export class Store<
171
223
  }
172
224
 
173
225
  // End the otel spans
174
- otel.trace.getSpan(this.otel.mutationsSpanContext)!.end()
175
- otel.trace.getSpan(this.otel.queriesSpanContext)!.end()
226
+ syncSpan.end()
227
+ mutationsSpan.end()
228
+ queriesSpan.end()
176
229
  }),
177
230
  )
178
231
 
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)
232
+ yield* this.syncProcessor.boot
233
+ }).pipe(this.runEffectFork)
181
234
  }
182
235
  // #endregion constructor
183
236
 
@@ -196,7 +249,7 @@ export class Store<
196
249
  }
197
250
 
198
251
  get sessionId(): string {
199
- return this.clientSession.coordinator.sessionId
252
+ return this.clientSession.sessionId
200
253
  }
201
254
 
202
255
  /**
@@ -242,6 +295,20 @@ export class Store<
242
295
  },
243
296
  )
244
297
 
298
+ /**
299
+ * Synchronously queries the database without creating a LiveQuery.
300
+ * This is useful for queries that don't need to be reactive.
301
+ *
302
+ * Example: Query builder
303
+ * ```ts
304
+ * const completedTodos = store.query(tables.todo.where({ complete: true }))
305
+ * ```
306
+ *
307
+ * Example: Raw SQL query
308
+ * ```ts
309
+ * const completedTodos = store.query({ query: 'SELECT * FROM todo WHERE complete = 1', bindValues: {} })
310
+ * ```
311
+ */
245
312
  query = <TResult>(
246
313
  query: QueryBuilder<TResult, any, any> | LiveQuery<TResult, any> | { query: string; bindValues: ParamsObject },
247
314
  options?: { otelContext?: otel.Context },
@@ -294,39 +361,16 @@ export class Store<
294
361
  ) => void,
295
362
  ): void
296
363
  } = (firstMutationOrTxnFnOrOptions: any, ...restMutations: any[]) => {
297
- let mutationsEvents: MutationEvent.ForSchema<TSchema>[]
298
- let options: StoreMutateOptions | undefined
364
+ const { mutationsEvents, options } = this.getMutateArgs(firstMutationOrTxnFnOrOptions, restMutations)
299
365
 
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]
366
+ for (const mutationEvent of mutationsEvents) {
367
+ replaceSessionIdSymbol(mutationEvent.args, this.clientSession.sessionId)
316
368
  }
317
369
 
318
- mutationsEvents = mutationsEvents.filter(
319
- (_) => _.id === undefined || !MutableHashMap.has(this.unsyncedMutationEvents, Data.struct(_.id)),
320
- )
321
-
322
- if (mutationsEvents.length === 0) {
323
- return
324
- }
370
+ if (mutationsEvents.length === 0) return
325
371
 
326
372
  const label = options?.label ?? 'mutate'
327
373
  const skipRefresh = options?.skipRefresh ?? false
328
- const wasSyncMessage = options?.wasSyncMessage ?? false
329
- const persisted = options?.persisted ?? true
330
374
 
331
375
  const mutationsSpan = otel.trace.getSpan(this.otel.mutationsSpanContext)!
332
376
  mutationsSpan.addEvent('mutate')
@@ -337,47 +381,30 @@ export class Store<
337
381
 
338
382
  let durationMs: number
339
383
 
340
- const res = this.otel.tracer.startActiveSpan(
384
+ return this.otel.tracer.startActiveSpan(
341
385
  'LiveStore:mutate',
342
- { attributes: { 'livestore.mutateLabel': label } },
343
- this.otel.mutationsSpanContext,
386
+ { attributes: { 'livestore.mutateLabel': label }, links: options?.spanLinks },
387
+ options?.otelContext ?? this.otel.mutationsSpanContext,
344
388
  (span) => {
345
389
  const otelContext = otel.trace.setSpan(otel.context.active(), span)
346
390
 
347
391
  try {
348
- const writeTables: Set<string> = new Set()
349
-
350
- this.otel.tracer.startActiveSpan(
351
- 'LiveStore:processWrites',
392
+ const { writeTables } = this.otel.tracer.startActiveSpan(
393
+ 'LiveStore:mutate:applyMutations',
352
394
  { attributes: { 'livestore.mutateLabel': label } },
353
395
  otel.trace.setSpan(otel.context.active(), span),
354
396
  (span) => {
355
397
  try {
356
398
  const otelContext = otel.trace.setSpan(otel.context.active(), span)
399
+ // 5
357
400
 
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
- }
401
+ const applyMutations = () => this.syncProcessor.push(mutationsEvents, { otelContext })
375
402
 
376
403
  if (mutationsEvents.length > 1) {
377
- // TODO: what to do about coordinator transaction here?
378
- this.syncDbWrapper.txn(applyMutations)
404
+ // TODO: what to do about leader transaction here?
405
+ return this.syncDbWrapper.txn(applyMutations)
379
406
  } else {
380
- applyMutations()
407
+ return applyMutations()
381
408
  }
382
409
  } catch (e: any) {
383
410
  console.error(e)
@@ -417,16 +444,6 @@ export class Store<
417
444
  return { durationMs }
418
445
  },
419
446
  )
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
447
  }
431
448
  // #endregion mutate
432
449
 
@@ -448,117 +465,6 @@ export class Store<
448
465
  )
449
466
  }
450
467
 
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
468
  private makeTableRef = (tableName: string) =>
563
469
  this.reactivityGraph.makeRef(null, {
564
470
  equal: () => false,
@@ -566,27 +472,85 @@ export class Store<
566
472
  meta: { liveStoreRefType: 'table' },
567
473
  })
568
474
 
569
- __devDownloadDb = () => {
570
- const data = this.syncDbWrapper.export()
571
- downloadBlob(data, `livestore-${Date.now()}.db`)
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)
572
480
  }
573
481
 
574
- __devDownloadMutationLogDb = () =>
482
+ __devDownloadMutationLogDb = () => {
575
483
  Effect.gen(this, function* () {
576
- const data = yield* this.clientSession.coordinator.getMutationLogData
484
+ const data = yield* this.clientSession.leaderThread.getMutationLogData
577
485
  downloadBlob(data, `livestore-mutationlog-${Date.now()}.db`)
578
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
+ }
579
505
 
580
- __devCurrentMutationEventId = () => this.clientSession.coordinator.getCurrentMutationEventId.pipe(Effect.runSync)
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)
510
+ }
581
511
 
582
512
  // 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 }),
513
+ toJSON = () => ({
514
+ _tag: 'livestore.Store',
515
+ reactivityGraph: this.reactivityGraph.getSnapshot({ includeResults: true }),
516
+ })
517
+
518
+ private runEffectFork = <A, E>(effect: Effect.Effect<A, E, Scope.Scope>) =>
519
+ effect.pipe(Effect.forkIn(this.lifetimeScope), Effect.tapCauseLogPretty, Runtime.runFork(this.runtime))
520
+
521
+ private getMutateArgs = (
522
+ firstMutationOrTxnFnOrOptions: any,
523
+ restMutations: any[],
524
+ ): {
525
+ mutationsEvents: (MutationEvent.ForSchema<TSchema> | MutationEvent.PartialForSchema<TSchema>)[]
526
+ options: StoreMutateOptions | undefined
527
+ } => {
528
+ let mutationsEvents: (MutationEvent.ForSchema<TSchema> | MutationEvent.PartialForSchema<TSchema>)[]
529
+ let options: StoreMutateOptions | undefined
530
+
531
+ if (typeof firstMutationOrTxnFnOrOptions === 'function') {
532
+ // TODO ensure that function is synchronous and isn't called in a async way (also write tests for this)
533
+ mutationsEvents = firstMutationOrTxnFnOrOptions((arg: any) => mutationsEvents.push(arg))
534
+ } else if (
535
+ firstMutationOrTxnFnOrOptions?.label !== undefined ||
536
+ firstMutationOrTxnFnOrOptions?.skipRefresh !== undefined ||
537
+ firstMutationOrTxnFnOrOptions?.otelContext !== undefined ||
538
+ firstMutationOrTxnFnOrOptions?.spanLinks !== undefined
539
+ ) {
540
+ options = firstMutationOrTxnFnOrOptions
541
+ mutationsEvents = restMutations
542
+ } else if (firstMutationOrTxnFnOrOptions === undefined) {
543
+ // When `mutate` is called with no arguments (which sometimes happens when dynamically filtering mutations)
544
+ mutationsEvents = []
545
+ } else {
546
+ mutationsEvents = [firstMutationOrTxnFnOrOptions, ...restMutations]
587
547
  }
588
- }
589
548
 
590
- private runEffectFork = <A, E>(effect: Effect.Effect<A, E, never>) =>
591
- effect.pipe(Effect.tapCauseLogPretty, FiberSet.run(this.fiberSet), Runtime.runFork(this.runtime))
549
+ mutationsEvents = mutationsEvents.filter(
550
+ // @ts-expect-error TODO
551
+ (_) => _.id === undefined || !MutableHashMap.has(this.unsyncedMutationEvents, Data.struct(_.id)),
552
+ )
553
+
554
+ return { mutationsEvents, options }
555
+ }
592
556
  }
package/src/utils/dev.ts CHANGED
@@ -1,3 +1,5 @@
1
+ import { isDevEnv } from '@livestore/utils'
2
+
1
3
  /* eslint-disable unicorn/prefer-global-this */
2
4
  export const downloadBlob = (
3
5
  data: Uint8Array | Blob | string,
@@ -24,7 +26,7 @@ export const downloadURL = (data: string, fileName: string) => {
24
26
  }
25
27
 
26
28
  export const exposeDebugUtils = () => {
27
- if (import.meta.env.DEV) {
28
- globalThis.__debugDownloadBlob = downloadBlob
29
+ if (isDevEnv()) {
30
+ globalThis.__debugLiveStoreUtils = { downloadBlob }
29
31
  }
30
32
  }
@@ -1,7 +1,8 @@
1
+ import { provideOtel } from '@livestore/common'
1
2
  import type { FromInputSchema } from '@livestore/common/schema'
2
3
  import type { Store } from '@livestore/livestore'
3
4
  import { createStore, DbSchema, globalReactivityGraph, makeReactivityGraph, makeSchema } from '@livestore/livestore'
4
- import { Effect, FiberSet } from '@livestore/utils/effect'
5
+ import { Effect } from '@livestore/utils/effect'
5
6
  import { makeInMemoryAdapter } from '@livestore/web'
6
7
  import type * as otel from '@opentelemetry/api'
7
8
 
@@ -55,19 +56,13 @@ export const makeTodoMvc = ({
55
56
  Effect.gen(function* () {
56
57
  const reactivityGraph = useGlobalReactivityGraph ? globalReactivityGraph : makeReactivityGraph()
57
58
 
58
- const fiberSet = yield* FiberSet.make()
59
-
60
59
  const store: Store<any, FixtureSchema> = yield* createStore({
61
60
  schema,
62
61
  storeId: 'default',
63
62
  adapter: makeInMemoryAdapter(),
64
63
  reactivityGraph,
65
- otelOptions: {
66
- tracer: otelTracer,
67
- rootSpanContext: otelContext,
68
- },
69
- fiberSet,
64
+ debug: { instanceId: 'test' },
70
65
  })
71
66
 
72
67
  return { store, reactivityGraph }
73
- })
68
+ }).pipe(provideOtel({ parentSpanContext: otelContext, otelTracer: otelTracer }))