@livestore/livestore 0.3.0-dev.5 → 0.3.0-dev.50

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 (170) hide show
  1. package/dist/.tsbuildinfo +1 -1
  2. package/dist/QueryCache.d.ts.map +1 -1
  3. package/dist/SqliteDbWrapper.d.ts +60 -0
  4. package/dist/SqliteDbWrapper.d.ts.map +1 -0
  5. package/dist/{SynchronousDatabaseWrapper.js → SqliteDbWrapper.js} +69 -34
  6. package/dist/SqliteDbWrapper.js.map +1 -0
  7. package/dist/effect/LiveStore.d.ts +6 -34
  8. package/dist/effect/LiveStore.d.ts.map +1 -1
  9. package/dist/effect/LiveStore.js +10 -12
  10. package/dist/effect/LiveStore.js.map +1 -1
  11. package/dist/effect/mod.d.ts +3 -0
  12. package/dist/effect/mod.d.ts.map +1 -0
  13. package/dist/effect/mod.js +3 -0
  14. package/dist/effect/mod.js.map +1 -0
  15. package/dist/internal/mod.d.ts +3 -0
  16. package/dist/internal/mod.d.ts.map +1 -0
  17. package/dist/internal/mod.js +3 -0
  18. package/dist/internal/mod.js.map +1 -0
  19. package/dist/live-queries/base-class.d.ts +65 -27
  20. package/dist/live-queries/base-class.d.ts.map +1 -1
  21. package/dist/live-queries/base-class.js +54 -13
  22. package/dist/live-queries/base-class.js.map +1 -1
  23. package/dist/live-queries/client-document-get-query.d.ts +12 -0
  24. package/dist/live-queries/client-document-get-query.d.ts.map +1 -0
  25. package/dist/live-queries/client-document-get-query.js +18 -0
  26. package/dist/live-queries/client-document-get-query.js.map +1 -0
  27. package/dist/live-queries/computed.d.ts +12 -14
  28. package/dist/live-queries/computed.d.ts.map +1 -1
  29. package/dist/live-queries/computed.js +37 -15
  30. package/dist/live-queries/computed.js.map +1 -1
  31. package/dist/live-queries/db-query.d.ts +93 -0
  32. package/dist/live-queries/db-query.d.ts.map +1 -0
  33. package/dist/live-queries/{db.js → db-query.js} +111 -40
  34. package/dist/live-queries/db-query.js.map +1 -0
  35. package/dist/live-queries/db-query.test.d.ts +2 -0
  36. package/dist/live-queries/db-query.test.d.ts.map +1 -0
  37. package/dist/live-queries/db-query.test.js +133 -0
  38. package/dist/live-queries/db-query.test.js.map +1 -0
  39. package/dist/live-queries/mod.d.ts +5 -0
  40. package/dist/live-queries/mod.d.ts.map +1 -0
  41. package/dist/live-queries/mod.js +5 -0
  42. package/dist/live-queries/mod.js.map +1 -0
  43. package/dist/live-queries/signal.d.ts +20 -0
  44. package/dist/live-queries/signal.d.ts.map +1 -0
  45. package/dist/live-queries/signal.js +33 -0
  46. package/dist/live-queries/signal.js.map +1 -0
  47. package/dist/live-queries/signal.test.d.ts +2 -0
  48. package/dist/live-queries/signal.test.d.ts.map +1 -0
  49. package/dist/live-queries/signal.test.js +25 -0
  50. package/dist/live-queries/signal.test.js.map +1 -0
  51. package/dist/mod.d.ts +14 -0
  52. package/dist/mod.d.ts.map +1 -0
  53. package/dist/mod.js +13 -0
  54. package/dist/mod.js.map +1 -0
  55. package/dist/reactive.d.ts +23 -17
  56. package/dist/reactive.d.ts.map +1 -1
  57. package/dist/reactive.js +23 -19
  58. package/dist/reactive.js.map +1 -1
  59. package/dist/reactive.test.js +1 -1
  60. package/dist/reactive.test.js.map +1 -1
  61. package/dist/store/create-store.d.ts +70 -12
  62. package/dist/store/create-store.d.ts.map +1 -1
  63. package/dist/store/create-store.js +68 -19
  64. package/dist/store/create-store.js.map +1 -1
  65. package/dist/store/devtools.d.ts +5 -4
  66. package/dist/store/devtools.d.ts.map +1 -1
  67. package/dist/store/devtools.js +92 -40
  68. package/dist/store/devtools.js.map +1 -1
  69. package/dist/store/store-types.d.ts +54 -42
  70. package/dist/store/store-types.d.ts.map +1 -1
  71. package/dist/store/store-types.js +2 -5
  72. package/dist/store/store-types.js.map +1 -1
  73. package/dist/store/store.d.ts +141 -35
  74. package/dist/store/store.d.ts.map +1 -1
  75. package/dist/store/store.js +319 -153
  76. package/dist/store/store.js.map +1 -1
  77. package/dist/utils/data-structures.d.ts.map +1 -1
  78. package/dist/utils/dev.d.ts.map +1 -1
  79. package/dist/utils/dev.js +6 -1
  80. package/dist/utils/dev.js.map +1 -1
  81. package/dist/utils/function-string.d.ts +7 -0
  82. package/dist/utils/function-string.d.ts.map +1 -0
  83. package/dist/utils/function-string.js +9 -0
  84. package/dist/utils/function-string.js.map +1 -0
  85. package/dist/utils/stack-info.d.ts.map +1 -1
  86. package/dist/utils/stack-info.js +6 -1
  87. package/dist/utils/stack-info.js.map +1 -1
  88. package/dist/utils/stack-info.test.js +54 -1
  89. package/dist/utils/stack-info.test.js.map +1 -1
  90. package/dist/utils/tests/fixture.d.ts +59 -216
  91. package/dist/utils/tests/fixture.d.ts.map +1 -1
  92. package/dist/utils/tests/fixture.js +23 -18
  93. package/dist/utils/tests/fixture.js.map +1 -1
  94. package/dist/utils/tests/mod.d.ts +1 -0
  95. package/dist/utils/tests/mod.d.ts.map +1 -1
  96. package/dist/utils/tests/mod.js +1 -0
  97. package/dist/utils/tests/mod.js.map +1 -1
  98. package/dist/utils/tests/otel.d.ts.map +1 -1
  99. package/dist/utils/tests/otel.js +8 -3
  100. package/dist/utils/tests/otel.js.map +1 -1
  101. package/package.json +29 -26
  102. package/src/{SynchronousDatabaseWrapper.ts → SqliteDbWrapper.ts} +92 -42
  103. package/src/effect/LiveStore.ts +27 -64
  104. package/src/effect/{index.ts → mod.ts} +2 -3
  105. package/src/internal/mod.ts +2 -0
  106. package/src/live-queries/__snapshots__/{db.test.ts.snap → db-query.test.ts.snap} +241 -45
  107. package/src/live-queries/base-class.ts +152 -50
  108. package/src/live-queries/client-document-get-query.ts +52 -0
  109. package/src/live-queries/computed.ts +51 -33
  110. package/src/live-queries/db-query.test.ts +192 -0
  111. package/src/live-queries/{db.ts → db-query.ts} +168 -81
  112. package/src/live-queries/mod.ts +4 -0
  113. package/src/live-queries/signal.test.ts +40 -0
  114. package/src/live-queries/signal.ts +47 -0
  115. package/src/mod.ts +42 -0
  116. package/src/reactive.test.ts +1 -1
  117. package/src/reactive.ts +66 -43
  118. package/src/store/create-store.ts +188 -62
  119. package/src/store/devtools.ts +124 -46
  120. package/src/store/store-types.ts +54 -43
  121. package/src/store/store.ts +454 -236
  122. package/src/utils/dev.ts +6 -1
  123. package/src/utils/function-string.ts +12 -0
  124. package/src/utils/stack-info.test.ts +58 -1
  125. package/src/utils/stack-info.ts +6 -1
  126. package/src/utils/tests/fixture.ts +22 -31
  127. package/src/utils/tests/mod.ts +1 -0
  128. package/src/utils/tests/otel.ts +10 -3
  129. package/dist/SynchronousDatabaseWrapper.d.ts +0 -41
  130. package/dist/SynchronousDatabaseWrapper.d.ts.map +0 -1
  131. package/dist/SynchronousDatabaseWrapper.js.map +0 -1
  132. package/dist/effect/index.d.ts +0 -2
  133. package/dist/effect/index.d.ts.map +0 -1
  134. package/dist/effect/index.js +0 -2
  135. package/dist/effect/index.js.map +0 -1
  136. package/dist/global-state.d.ts +0 -14
  137. package/dist/global-state.d.ts.map +0 -1
  138. package/dist/global-state.js +0 -16
  139. package/dist/global-state.js.map +0 -1
  140. package/dist/index.d.ts +0 -20
  141. package/dist/index.d.ts.map +0 -1
  142. package/dist/index.js +0 -16
  143. package/dist/index.js.map +0 -1
  144. package/dist/live-queries/db.d.ts +0 -66
  145. package/dist/live-queries/db.d.ts.map +0 -1
  146. package/dist/live-queries/db.js.map +0 -1
  147. package/dist/live-queries/db.test.d.ts +0 -2
  148. package/dist/live-queries/db.test.d.ts.map +0 -1
  149. package/dist/live-queries/db.test.js +0 -118
  150. package/dist/live-queries/db.test.js.map +0 -1
  151. package/dist/live-queries/graphql.d.ts +0 -49
  152. package/dist/live-queries/graphql.d.ts.map +0 -1
  153. package/dist/live-queries/graphql.js +0 -122
  154. package/dist/live-queries/graphql.js.map +0 -1
  155. package/dist/row-query-utils.d.ts +0 -17
  156. package/dist/row-query-utils.d.ts.map +0 -1
  157. package/dist/row-query-utils.js +0 -30
  158. package/dist/row-query-utils.js.map +0 -1
  159. package/dist/utils/otel.d.ts +0 -4
  160. package/dist/utils/otel.d.ts.map +0 -1
  161. package/dist/utils/otel.js +0 -6
  162. package/dist/utils/otel.js.map +0 -1
  163. package/src/global-state.ts +0 -20
  164. package/src/index.ts +0 -66
  165. package/src/live-queries/db.test.ts +0 -154
  166. package/src/live-queries/graphql.ts +0 -219
  167. package/src/row-query-utils.ts +0 -65
  168. package/src/utils/otel.ts +0 -9
  169. package/tsconfig.json +0 -18
  170. package/vitest.config.js +0 -9
@@ -8,7 +8,8 @@ import type {
8
8
  } from '@livestore/common'
9
9
  import {
10
10
  Devtools,
11
- getExecArgsFromMutation,
11
+ getDurationMsFromSpan,
12
+ getExecArgsFromEvent,
12
13
  getResultSchema,
13
14
  IntentionalShutdownCause,
14
15
  isQueryBuilder,
@@ -19,103 +20,109 @@ import {
19
20
  replaceSessionIdSymbol,
20
21
  } from '@livestore/common'
21
22
  import type { LiveStoreSchema } from '@livestore/common/schema'
22
- import {
23
- MutationEvent,
24
- SCHEMA_META_TABLE,
25
- SCHEMA_MUTATIONS_META_TABLE,
26
- SESSION_CHANGESET_META_TABLE,
27
- } from '@livestore/common/schema'
28
- import { assertNever, isDevEnv } from '@livestore/utils'
23
+ import { getEventDef, LiveStoreEvent, SystemTables } from '@livestore/common/schema'
24
+ import { assertNever, isDevEnv, notYetImplemented } from '@livestore/utils'
29
25
  import type { Scope } from '@livestore/utils/effect'
30
- import { Cause, Data, Effect, Inspectable, MutableHashMap, Runtime, Schema } from '@livestore/utils/effect'
26
+ import { Cause, Effect, Fiber, Inspectable, OtelTracer, Runtime, Schema, Stream } from '@livestore/utils/effect'
31
27
  import { nanoid } from '@livestore/utils/nanoid'
32
28
  import * as otel from '@opentelemetry/api'
33
- import { type GraphQLSchema } from 'graphql'
34
29
 
35
- import type { LiveQuery, QueryContext, ReactivityGraph } from '../live-queries/base-class.js'
30
+ import type {
31
+ LiveQuery,
32
+ LiveQueryDef,
33
+ ReactivityGraph,
34
+ ReactivityGraphContext,
35
+ SignalDef,
36
+ } from '../live-queries/base-class.js'
37
+ import { makeReactivityGraph } from '../live-queries/base-class.js'
38
+ import { makeExecBeforeFirstRun } from '../live-queries/client-document-get-query.js'
36
39
  import type { Ref } from '../reactive.js'
37
- import { makeExecBeforeFirstRun } from '../row-query-utils.js'
38
- import { SynchronousDatabaseWrapper } from '../SynchronousDatabaseWrapper.js'
40
+ import { SqliteDbWrapper } from '../SqliteDbWrapper.js'
39
41
  import { ReferenceCountedSet } from '../utils/data-structures.js'
40
42
  import { downloadBlob, exposeDebugUtils } from '../utils/dev.js'
41
- import { getDurationMsFromSpan } from '../utils/otel.js'
42
- import type { BaseGraphQLContext, RefreshReason, StoreMutateOptions, StoreOptions, StoreOtel } from './store-types.js'
43
+ import type { StackInfo } from '../utils/stack-info.js'
44
+ import type {
45
+ RefreshReason,
46
+ StoreCommitOptions,
47
+ StoreEventsOptions,
48
+ StoreOptions,
49
+ StoreOtel,
50
+ Unsubscribe,
51
+ } from './store-types.js'
43
52
 
44
53
  if (isDevEnv()) {
45
54
  exposeDebugUtils()
46
55
  }
47
56
 
48
- export class Store<
49
- TGraphQLContext extends BaseGraphQLContext = BaseGraphQLContext,
50
- TSchema extends LiveStoreSchema = LiveStoreSchema,
51
- > extends Inspectable.Class {
57
+ export class Store<TSchema extends LiveStoreSchema = LiveStoreSchema, TContext = {}> extends Inspectable.Class {
52
58
  readonly storeId: string
53
59
  reactivityGraph: ReactivityGraph
54
- syncDbWrapper: SynchronousDatabaseWrapper
60
+ sqliteDbWrapper: SqliteDbWrapper
55
61
  clientSession: ClientSession
56
62
  schema: LiveStoreSchema
57
- graphQLSchema?: GraphQLSchema
58
- graphQLContext?: TGraphQLContext
63
+ context: TContext
59
64
  otel: StoreOtel
60
65
  /**
61
66
  * Note we're using `Ref<null>` here as we don't care about the value but only about *that* something has changed.
62
67
  * This only works in combination with `equal: () => false` which will always trigger a refresh.
63
68
  */
64
- tableRefs: { [key: string]: Ref<null, QueryContext, RefreshReason> }
69
+ tableRefs: { [key: string]: Ref<null, ReactivityGraphContext, RefreshReason> }
65
70
 
66
- private runtime: Runtime.Runtime<Scope.Scope>
71
+ private effectContext: {
72
+ runtime: Runtime.Runtime<Scope.Scope>
73
+ lifetimeScope: Scope.Scope
74
+ }
67
75
 
68
76
  /** RC-based set to see which queries are currently subscribed to */
69
77
  activeQueries: ReferenceCountedSet<LiveQuery<any>>
70
78
 
71
- // NOTE this is currently exposed for the Devtools databrowser to emit mutation events
72
- readonly __mutationEventSchema
73
- private unsyncedMutationEvents
74
- private syncProcessor: ClientSessionSyncProcessor
75
- readonly lifetimeScope: Scope.Scope
79
+ // NOTE this is currently exposed for the Devtools databrowser to commit events
80
+ readonly __eventSchema
81
+ readonly syncProcessor: ClientSessionSyncProcessor
82
+
83
+ readonly boot: Effect.Effect<void, UnexpectedError, Scope.Scope>
76
84
 
77
85
  // #region constructor
78
- private constructor({
86
+ constructor({
79
87
  clientSession,
80
88
  schema,
81
- graphQLOptions,
82
- reactivityGraph,
83
89
  otelOptions,
84
- disableDevtools,
90
+ context,
85
91
  batchUpdates,
86
- unsyncedMutationEvents,
87
92
  storeId,
88
- lifetimeScope,
89
- runtime,
90
- }: StoreOptions<TGraphQLContext, TSchema>) {
93
+ effectContext,
94
+ params,
95
+ confirmUnsavedChanges,
96
+ __runningInDevtools,
97
+ }: StoreOptions<TSchema, TContext>) {
91
98
  super()
92
99
 
93
100
  this.storeId = storeId
94
- this.unsyncedMutationEvents = unsyncedMutationEvents
95
101
 
96
- this.syncDbWrapper = new SynchronousDatabaseWrapper({ otel: otelOptions, db: clientSession.syncDb })
102
+ this.sqliteDbWrapper = new SqliteDbWrapper({ otel: otelOptions, db: clientSession.sqliteDb })
97
103
  this.clientSession = clientSession
98
104
  this.schema = schema
105
+ this.context = context
106
+
107
+ this.effectContext = effectContext
99
108
 
100
- this.lifetimeScope = lifetimeScope
101
- this.runtime = runtime
109
+ const reactivityGraph = makeReactivityGraph()
102
110
 
103
111
  const syncSpan = otelOptions.tracer.startSpan('LiveStore:sync', {}, otelOptions.rootSpanContext)
104
112
 
105
113
  this.syncProcessor = makeClientSessionSyncProcessor({
106
114
  schema,
107
- initialLeaderHead: clientSession.leaderThread.mutations.initialMutationEventId,
108
- // rebaseBehaviour: 'auto-rebase',
109
- pushToLeader: (batch) =>
110
- clientSession.leaderThread.mutations.push(batch).pipe(
111
- // NOTE we don't want to shutdown in case of an invalid push error, since it will be retried
112
- Effect.catchTag('InvalidPushError', Effect.ignoreLogged),
113
- this.runEffectFork,
114
- ),
115
- pullFromLeader: clientSession.leaderThread.mutations.pull,
116
- applyMutation: (mutationEventDecoded, { otelContext, withChangeset }) => {
117
- const mutationDef = schema.mutations.get(mutationEventDecoded.mutation)!
118
- const execArgsArr = getExecArgsFromMutation({ mutationDef, mutationEventDecoded })
115
+ clientSession,
116
+ runtime: effectContext.runtime,
117
+ materializeEvent: (eventDecoded, { otelContext, withChangeset }) => {
118
+ const { eventDef, materializer } = getEventDef(schema, eventDecoded.name)
119
+
120
+ const execArgsArr = getExecArgsFromEvent({
121
+ eventDef,
122
+ materializer,
123
+ db: this.sqliteDbWrapper,
124
+ event: { decoded: eventDecoded, encoded: undefined },
125
+ })
119
126
 
120
127
  const writeTablesForEvent = new Set<string>()
121
128
 
@@ -123,18 +130,21 @@ export class Store<
123
130
  for (const {
124
131
  statementSql,
125
132
  bindValues,
126
- writeTables = this.syncDbWrapper.getTablesUsed(statementSql),
133
+ writeTables = this.sqliteDbWrapper.getTablesUsed(statementSql),
127
134
  } of execArgsArr) {
128
- this.syncDbWrapper.execute(statementSql, bindValues, writeTables, { otelContext })
135
+ this.sqliteDbWrapper.execute(statementSql, bindValues, { otelContext, writeTables })
129
136
 
130
137
  // durationMsTotal += durationMs
131
138
  writeTables.forEach((table) => writeTablesForEvent.add(table))
132
139
  }
133
140
  }
134
141
 
135
- let sessionChangeset: Uint8Array | undefined
142
+ let sessionChangeset:
143
+ | { _tag: 'sessionChangeset'; data: Uint8Array; debug: any }
144
+ | { _tag: 'no-op' }
145
+ | { _tag: 'unset' } = { _tag: 'unset' }
136
146
  if (withChangeset === true) {
137
- sessionChangeset = this.syncDbWrapper.withChangeset(exec).changeset
147
+ sessionChangeset = this.sqliteDbWrapper.withChangeset(exec).changeset
138
148
  } else {
139
149
  exec()
140
150
  }
@@ -142,35 +152,41 @@ export class Store<
142
152
  return { writeTables: writeTablesForEvent, sessionChangeset }
143
153
  },
144
154
  rollback: (changeset) => {
145
- this.syncDbWrapper.rollback(changeset)
155
+ this.sqliteDbWrapper.rollback(changeset)
146
156
  },
147
157
  refreshTables: (tables) => {
148
- const tablesToUpdate = [] as [Ref<null, QueryContext, RefreshReason>, null][]
158
+ const tablesToUpdate = [] as [Ref<null, ReactivityGraphContext, RefreshReason>, null][]
149
159
  for (const tableName of tables) {
150
160
  const tableRef = this.tableRefs[tableName]
151
161
  assertNever(tableRef !== undefined, `No table ref found for ${tableName}`)
152
162
  tablesToUpdate.push([tableRef!, null])
153
163
  }
154
- this.reactivityGraph.setRefs(tablesToUpdate)
164
+ reactivityGraph.setRefs(tablesToUpdate)
155
165
  },
156
166
  span: syncSpan,
167
+ params: {
168
+ leaderPushBatchSize: params.leaderPushBatchSize,
169
+ },
170
+ confirmUnsavedChanges,
157
171
  })
158
172
 
159
- this.__mutationEventSchema = MutationEvent.makeMutationEventSchemaMemo(schema)
173
+ this.__eventSchema = LiveStoreEvent.makeEventDefSchemaMemo(schema)
160
174
 
161
175
  // TODO generalize the `tableRefs` concept to allow finer-grained refs
162
176
  this.tableRefs = {}
163
177
  this.activeQueries = new ReferenceCountedSet()
164
178
 
165
- const mutationsSpan = otelOptions.tracer.startSpan('LiveStore:mutations', {}, otelOptions.rootSpanContext)
166
- const otelMuationsSpanContext = otel.trace.setSpan(otel.context.active(), mutationsSpan)
179
+ const commitsSpan = otelOptions.tracer.startSpan('LiveStore:commits', {}, otelOptions.rootSpanContext)
180
+ const otelMuationsSpanContext = otel.trace.setSpan(otel.context.active(), commitsSpan)
167
181
 
168
182
  const queriesSpan = otelOptions.tracer.startSpan('LiveStore:queries', {}, otelOptions.rootSpanContext)
169
183
  const otelQueriesSpanContext = otel.trace.setSpan(otel.context.active(), queriesSpan)
170
184
 
171
185
  this.reactivityGraph = reactivityGraph
172
186
  this.reactivityGraph.context = {
173
- store: this as unknown as Store<BaseGraphQLContext, LiveStoreSchema>,
187
+ store: this as unknown as Store<LiveStoreSchema>,
188
+ defRcMap: new Map(),
189
+ reactivityGraph: new WeakRef(reactivityGraph),
174
190
  otelTracer: otelOptions.tracer,
175
191
  rootOtelContext: otelQueriesSpanContext,
176
192
  effectsWrapper: batchUpdates,
@@ -178,23 +194,18 @@ export class Store<
178
194
 
179
195
  this.otel = {
180
196
  tracer: otelOptions.tracer,
181
- mutationsSpanContext: otelMuationsSpanContext,
197
+ rootSpanContext: otelOptions.rootSpanContext,
198
+ commitsSpanContext: otelMuationsSpanContext,
182
199
  queriesSpanContext: otelQueriesSpanContext,
183
200
  }
184
201
 
185
- // TODO find a better way to detect if we're running LiveStore in the LiveStore devtools
186
- // But for now this is a good enough approximation with little downsides
187
- const isRunningInDevtools = disableDevtools === true
188
-
189
202
  // Need a set here since `schema.tables` might contain duplicates and some componentStateTables
190
203
  const allTableNames = new Set(
191
- // NOTE we're excluding the LiveStore schema and mutations tables as they are not user-facing
204
+ // NOTE we're excluding the LiveStore schema and events tables as they are not user-facing
192
205
  // unless LiveStore is running in the devtools
193
- isRunningInDevtools
194
- ? this.schema.tables.keys()
195
- : Array.from(this.schema.tables.keys()).filter(
196
- (_) => _ !== SCHEMA_META_TABLE && _ !== SCHEMA_MUTATIONS_META_TABLE && _ !== SESSION_CHANGESET_META_TABLE,
197
- ),
206
+ __runningInDevtools
207
+ ? this.schema.state.sqlite.tables.keys()
208
+ : Array.from(this.schema.state.sqlite.tables.keys()).filter((_) => !SystemTables.isStateSystemTable(_)),
198
209
  )
199
210
  const existingTableRefs = new Map(
200
211
  Array.from(this.reactivityGraph.atoms.values())
@@ -202,15 +213,16 @@ export class Store<
202
213
  .map((_) => [_.label!.slice('tableRef:'.length), _] as const),
203
214
  )
204
215
  for (const tableName of allTableNames) {
205
- this.tableRefs[tableName] = existingTableRefs.get(tableName) ?? this.makeTableRef(tableName)
216
+ this.tableRefs[tableName] =
217
+ existingTableRefs.get(tableName) ??
218
+ this.reactivityGraph.makeRef(null, {
219
+ equal: () => false,
220
+ label: `tableRef:${tableName}`,
221
+ meta: { liveStoreRefType: 'table' },
222
+ })
206
223
  }
207
224
 
208
- if (graphQLOptions) {
209
- this.graphQLSchema = graphQLOptions.schema
210
- this.graphQLContext = graphQLOptions.makeContext(this.syncDbWrapper, this.otel.tracer, clientSession.sessionId)
211
- }
212
-
213
- Effect.gen(this, function* () {
225
+ this.boot = Effect.gen(this, function* () {
214
226
  yield* Effect.addFinalizer(() =>
215
227
  Effect.sync(() => {
216
228
  // Remove all table refs from the reactivity graph
@@ -222,60 +234,87 @@ export class Store<
222
234
 
223
235
  // End the otel spans
224
236
  syncSpan.end()
225
- mutationsSpan.end()
237
+ commitsSpan.end()
226
238
  queriesSpan.end()
227
239
  }),
228
240
  )
229
241
 
230
242
  yield* this.syncProcessor.boot
231
- }).pipe(this.runEffectFork)
232
- }
233
- // #endregion constructor
234
-
235
- static createStore = <TGraphQLContext extends BaseGraphQLContext, TSchema extends LiveStoreSchema = LiveStoreSchema>(
236
- storeOptions: StoreOptions<TGraphQLContext, TSchema>,
237
- parentSpan: otel.Span,
238
- ): Store<TGraphQLContext, TSchema> => {
239
- const ctx = otel.trace.setSpan(otel.context.active(), parentSpan)
240
- return storeOptions.otelOptions.tracer.startActiveSpan('LiveStore:createStore', {}, ctx, (span) => {
241
- try {
242
- return new Store(storeOptions)
243
- } finally {
244
- span.end()
245
- }
246
243
  })
247
244
  }
245
+ // #endregion constructor
248
246
 
249
247
  get sessionId(): string {
250
248
  return this.clientSession.sessionId
251
249
  }
252
250
 
251
+ get clientId(): string {
252
+ return this.clientSession.clientId
253
+ }
254
+
253
255
  /**
254
256
  * Subscribe to the results of a query
255
257
  * Returns a function to cancel the subscription.
258
+ *
259
+ * @example
260
+ * ```ts
261
+ * const unsubscribe = store.subscribe(query$, { onUpdate: (result) => console.log(result) })
262
+ * ```
256
263
  */
257
264
  subscribe = <TResult>(
258
- query$: LiveQuery<TResult, any>,
259
- onNewValue: (value: TResult) => void,
260
- onUnsubsubscribe?: () => void,
261
- options?: { label?: string; otelContext?: otel.Context; skipInitialRun?: boolean } | undefined,
262
- ): (() => void) =>
265
+ query: LiveQueryDef<TResult> | LiveQuery<TResult>,
266
+ options: {
267
+ /** Called when the query result has changed */
268
+ onUpdate: (value: TResult) => void
269
+ onSubscribe?: (query$: LiveQuery<TResult>) => void
270
+ /** Gets called after the query subscription has been removed */
271
+ onUnsubsubscribe?: () => void
272
+ label?: string
273
+ /**
274
+ * Skips the initial `onUpdate` callback
275
+ * @default false
276
+ */
277
+ skipInitialRun?: boolean
278
+ otelContext?: otel.Context
279
+ /** If provided, the stack info will be added to the `activeSubscriptions` set of the query */
280
+ stackInfo?: StackInfo
281
+ },
282
+ ): Unsubscribe =>
263
283
  this.otel.tracer.startActiveSpan(
264
284
  `LiveStore.subscribe`,
265
- { attributes: { label: options?.label, queryLabel: query$.label } },
285
+ { attributes: { label: options?.label, queryLabel: query.label } },
266
286
  options?.otelContext ?? this.otel.queriesSpanContext,
267
287
  (span) => {
268
288
  // console.debug('store sub', query$.id, query$.label)
269
289
  const otelContext = otel.trace.setSpan(otel.context.active(), span)
270
290
 
291
+ const queryRcRef =
292
+ query._tag === 'def'
293
+ ? query.make(this.reactivityGraph.context!)
294
+ : {
295
+ value: query,
296
+ deref: () => {},
297
+ }
298
+ const query$ = queryRcRef.value
299
+
271
300
  const label = `subscribe:${options?.label}`
272
- const effect = this.reactivityGraph.makeEffect((get) => onNewValue(get(query$.results$)), { label })
301
+ const effect = this.reactivityGraph.makeEffect(
302
+ (get, _otelContext, debugRefreshReason) =>
303
+ options.onUpdate(get(query$.results$, otelContext, debugRefreshReason)),
304
+ { label },
305
+ )
306
+
307
+ if (options?.stackInfo) {
308
+ query$.activeSubscriptions.add(options.stackInfo)
309
+ }
310
+
311
+ options?.onSubscribe?.(query$)
273
312
 
274
313
  this.activeQueries.add(query$ as LiveQuery<TResult>)
275
314
 
276
315
  // Running effect right away to get initial value (unless `skipInitialRun` is set)
277
- if (options?.skipInitialRun !== true) {
278
- effect.doEffect(otelContext)
316
+ if (options?.skipInitialRun !== true && !query$.isDestroyed) {
317
+ effect.doEffect(otelContext, { _tag: 'subscribe.initial', label: `subscribe-initial-run:${options?.label}` })
279
318
  }
280
319
 
281
320
  const unsubscribe = () => {
@@ -283,7 +322,14 @@ export class Store<
283
322
  try {
284
323
  this.reactivityGraph.destroyNode(effect)
285
324
  this.activeQueries.remove(query$ as LiveQuery<TResult>)
286
- onUnsubsubscribe?.()
325
+
326
+ if (options?.stackInfo) {
327
+ query$.activeSubscriptions.delete(options.stackInfo)
328
+ }
329
+
330
+ queryRcRef.deref()
331
+
332
+ options?.onUnsubsubscribe?.()
287
333
  } finally {
288
334
  span.end()
289
335
  }
@@ -293,6 +339,30 @@ export class Store<
293
339
  },
294
340
  )
295
341
 
342
+ subscribeStream = <TResult>(
343
+ query$: LiveQueryDef<TResult>,
344
+ options?: { label?: string; skipInitialRun?: boolean } | undefined,
345
+ ): Stream.Stream<TResult> =>
346
+ Stream.asyncPush<TResult>((emit) =>
347
+ Effect.gen(this, function* () {
348
+ const otelSpan = yield* OtelTracer.currentOtelSpan.pipe(
349
+ Effect.catchTag('NoSuchElementException', () => Effect.succeed(undefined)),
350
+ )
351
+ const otelContext = otelSpan ? otel.trace.setSpan(otel.context.active(), otelSpan) : otel.context.active()
352
+
353
+ yield* Effect.acquireRelease(
354
+ Effect.sync(() =>
355
+ this.subscribe(query$, {
356
+ onUpdate: (result) => emit.single(result),
357
+ otelContext,
358
+ label: options?.label,
359
+ }),
360
+ ),
361
+ (unsub) => Effect.sync(() => unsub()),
362
+ )
363
+ }),
364
+ )
365
+
296
366
  /**
297
367
  * Synchronously queries the database without creating a LiveQuery.
298
368
  * This is useful for queries that don't need to be reactive.
@@ -308,12 +378,16 @@ export class Store<
308
378
  * ```
309
379
  */
310
380
  query = <TResult>(
311
- query: QueryBuilder<TResult, any, any> | LiveQuery<TResult, any> | { query: string; bindValues: ParamsObject },
312
- options?: { otelContext?: otel.Context },
381
+ query:
382
+ | QueryBuilder<TResult, any, any>
383
+ | LiveQuery<TResult>
384
+ | LiveQueryDef<TResult>
385
+ | SignalDef<TResult>
386
+ | { query: string; bindValues: ParamsObject },
387
+ options?: { otelContext?: otel.Context; debugRefreshReason?: RefreshReason },
313
388
  ): TResult => {
314
389
  if (typeof query === 'object' && 'query' in query && 'bindValues' in query) {
315
- return this.syncDbWrapper.select(query.query, {
316
- bindValues: prepareBindValues(query.bindValues, query.query),
390
+ return this.sqliteDbWrapper.select(query.query, prepareBindValues(query.bindValues, query.query), {
317
391
  otelContext: options?.otelContext,
318
392
  }) as any
319
393
  } else if (isQueryBuilder(query)) {
@@ -322,99 +396,186 @@ export class Store<
322
396
  makeExecBeforeFirstRun({
323
397
  table: ast.tableDef,
324
398
  id: ast.id,
325
- insertValues: ast.insertValues,
399
+ explicitDefaultValues: ast.explicitDefaultValues,
326
400
  otelContext: options?.otelContext,
327
401
  })(this.reactivityGraph.context!)
328
402
  }
329
403
 
330
404
  const sqlRes = query.asSql()
331
405
  const schema = getResultSchema(query)
332
- const rawRes = this.syncDbWrapper.select(sqlRes.query, {
333
- bindValues: sqlRes.bindValues as any as PreparedBindValues,
406
+ const rawRes = this.sqliteDbWrapper.select(sqlRes.query, sqlRes.bindValues as any as PreparedBindValues, {
334
407
  otelContext: options?.otelContext,
335
408
  queriedTables: new Set([query[QueryBuilderAstSymbol].tableDef.sqliteDef.name]),
336
409
  })
410
+
337
411
  return Schema.decodeSync(schema)(rawRes)
412
+ } else if (query._tag === 'def') {
413
+ const query$ = query.make(this.reactivityGraph.context!)
414
+ const result = this.query(query$.value, options)
415
+ query$.deref()
416
+ return result
417
+ } else if (query._tag === 'signal-def') {
418
+ const signal$ = query.make(this.reactivityGraph.context!)
419
+ return signal$.value.get()
338
420
  } else {
339
- return query.run(options?.otelContext)
421
+ return query.run({ otelContext: options?.otelContext, debugRefreshReason: options?.debugRefreshReason })
422
+ }
423
+ }
424
+
425
+ /**
426
+ * Set the value of a signal
427
+ *
428
+ * @example
429
+ * ```ts
430
+ * const count$ = signal(0, { label: 'count$' })
431
+ * store.setSignal(count$, 2)
432
+ * ```
433
+ *
434
+ * @example
435
+ * ```ts
436
+ * const count$ = signal(0, { label: 'count$' })
437
+ * store.setSignal(count$, (prev) => prev + 1)
438
+ * ```
439
+ */
440
+ setSignal = <T>(signalDef: SignalDef<T>, value: T | ((prev: T) => T)): void => {
441
+ const signalRef = signalDef.make(this.reactivityGraph.context!)
442
+ const newValue: T = typeof value === 'function' ? (value as any)(signalRef.value.get()) : value
443
+ signalRef.value.set(newValue)
444
+
445
+ // The current implementation of signals i.e. the separation into `signal-def` and `signal`
446
+ // can lead to a situation where a reffed signal is immediately de-reffed when calling `store.setSignal`,
447
+ // in case there is nothing else holding a reference to the signal which leads to the set value being "lost".
448
+ // To avoid this, we don't deref the signal here if this set call is the only reference to the signal.
449
+ // Hopefully this won't lead to any issues in the future. 🤞
450
+ if (signalRef.rc > 1) {
451
+ signalRef.deref()
340
452
  }
341
453
  }
342
454
 
343
- // #region mutate
344
- mutate: {
345
- <const TMutationArg extends ReadonlyArray<MutationEvent.PartialForSchema<TSchema>>>(...list: TMutationArg): void
455
+ // #region commit
456
+ /**
457
+ * Commit a list of events to the store which will immediately update the local database
458
+ * and sync the events across other clients (similar to a `git commit`).
459
+ *
460
+ * @example
461
+ * ```ts
462
+ * store.commit(events.todoCreated({ id: nanoid(), text: 'Make coffee' }))
463
+ * ```
464
+ *
465
+ * You can call `commit` with multiple events to apply them in a single database transaction.
466
+ *
467
+ * @example
468
+ * ```ts
469
+ * const todoId = nanoid()
470
+ * store.commit(
471
+ * events.todoCreated({ id: todoId, text: 'Make coffee' }),
472
+ * events.todoCompleted({ id: todoId }))
473
+ * ```
474
+ *
475
+ * For more advanced transaction scenarios, you can pass a synchronous function to `commit` which will receive a callback
476
+ * to which you can pass multiple events to be committed in the same database transaction.
477
+ * Under the hood this will simply collect all events and apply them in a single database transaction.
478
+ *
479
+ * @example
480
+ * ```ts
481
+ * store.commit((commit) => {
482
+ * const todoId = nanoid()
483
+ * if (Math.random() > 0.5) {
484
+ * commit(events.todoCreated({ id: todoId, text: 'Make coffee' }))
485
+ * } else {
486
+ * commit(events.todoCompleted({ id: todoId }))
487
+ * }
488
+ * })
489
+ * ```
490
+ *
491
+ * When committing a large batch of events, you can also skip the database refresh to improve performance
492
+ * and call `store.manualRefresh()` after all events have been committed.
493
+ *
494
+ * @example
495
+ * ```ts
496
+ * const todos = [
497
+ * { id: nanoid(), text: 'Make coffee' },
498
+ * { id: nanoid(), text: 'Buy groceries' },
499
+ * // ... 1000 more todos
500
+ * ]
501
+ * for (const todo of todos) {
502
+ * store.commit({ skipRefresh: true }, events.todoCreated({ id: todo.id, text: todo.text }))
503
+ * }
504
+ * store.manualRefresh()
505
+ * ```
506
+ */
507
+ commit: {
508
+ <const TCommitArg extends ReadonlyArray<LiveStoreEvent.PartialForSchema<TSchema>>>(...list: TCommitArg): void
346
509
  (
347
- txn: <const TMutationArg extends ReadonlyArray<MutationEvent.PartialForSchema<TSchema>>>(
348
- ...list: TMutationArg
510
+ txn: <const TCommitArg extends ReadonlyArray<LiveStoreEvent.PartialForSchema<TSchema>>>(
511
+ ...list: TCommitArg
349
512
  ) => void,
350
513
  ): void
351
- <const TMutationArg extends ReadonlyArray<MutationEvent.PartialForSchema<TSchema>>>(
352
- options: StoreMutateOptions,
353
- ...list: TMutationArg
514
+ <const TCommitArg extends ReadonlyArray<LiveStoreEvent.PartialForSchema<TSchema>>>(
515
+ options: StoreCommitOptions,
516
+ ...list: TCommitArg
354
517
  ): void
355
518
  (
356
- options: StoreMutateOptions,
357
- txn: <const TMutationArg extends ReadonlyArray<MutationEvent.PartialForSchema<TSchema>>>(
358
- ...list: TMutationArg
519
+ options: StoreCommitOptions,
520
+ txn: <const TCommitArg extends ReadonlyArray<LiveStoreEvent.PartialForSchema<TSchema>>>(
521
+ ...list: TCommitArg
359
522
  ) => void,
360
523
  ): void
361
- } = (firstMutationOrTxnFnOrOptions: any, ...restMutations: any[]) => {
362
- const { mutationsEvents, options } = this.getMutateArgs(firstMutationOrTxnFnOrOptions, restMutations)
524
+ } = (firstEventOrTxnFnOrOptions: any, ...restEvents: any[]) => {
525
+ const { events, options } = this.getCommitArgs(firstEventOrTxnFnOrOptions, restEvents)
363
526
 
364
- for (const mutationEvent of mutationsEvents) {
365
- replaceSessionIdSymbol(mutationEvent.args, this.clientSession.sessionId)
527
+ for (const event of events) {
528
+ replaceSessionIdSymbol(event.args, this.clientSession.sessionId)
366
529
  }
367
530
 
368
- if (mutationsEvents.length === 0) return
531
+ if (events.length === 0) return
369
532
 
370
- const label = options?.label ?? 'mutate'
371
533
  const skipRefresh = options?.skipRefresh ?? false
372
534
 
373
- const mutationsSpan = otel.trace.getSpan(this.otel.mutationsSpanContext)!
374
- mutationsSpan.addEvent('mutate')
535
+ const commitsSpan = otel.trace.getSpan(this.otel.commitsSpanContext)!
536
+ commitsSpan.addEvent('commit')
375
537
 
376
- // console.group('LiveStore.mutate', { skipRefresh, wasSyncMessage, label })
377
- // mutationsEvents.forEach((_) => console.debug(_.mutation, _.id, _.args))
538
+ // console.group('LiveStore.commit', { skipRefresh })
539
+ // events.forEach((_) => console.debug(_.name, _.args))
378
540
  // console.groupEnd()
379
541
 
380
542
  let durationMs: number
381
543
 
382
544
  return this.otel.tracer.startActiveSpan(
383
- 'LiveStore:mutate',
384
- { attributes: { 'livestore.mutateLabel': label }, links: options?.spanLinks },
385
- options?.otelContext ?? this.otel.mutationsSpanContext,
545
+ 'LiveStore:commit',
546
+ {
547
+ attributes: {
548
+ 'livestore.eventsCount': events.length,
549
+ 'livestore.eventTags': events.map((_) => _.name),
550
+ 'livestore.commitLabel': options?.label,
551
+ },
552
+ links: options?.spanLinks,
553
+ },
554
+ options?.otelContext ?? this.otel.commitsSpanContext,
386
555
  (span) => {
387
556
  const otelContext = otel.trace.setSpan(otel.context.active(), span)
388
557
 
389
558
  try {
390
- const { writeTables } = this.otel.tracer.startActiveSpan(
391
- 'LiveStore:mutate:applyMutations',
392
- { attributes: { 'livestore.mutateLabel': label } },
393
- otel.trace.setSpan(otel.context.active(), span),
394
- (span) => {
395
- try {
396
- const otelContext = otel.trace.setSpan(otel.context.active(), span)
397
- // 5
398
-
399
- const applyMutations = () => this.syncProcessor.push(mutationsEvents, { otelContext })
400
-
401
- if (mutationsEvents.length > 1) {
402
- // TODO: what to do about leader transaction here?
403
- return this.syncDbWrapper.txn(applyMutations)
404
- } else {
405
- return applyMutations()
406
- }
407
- } catch (e: any) {
408
- console.error(e)
409
- span.setStatus({ code: otel.SpanStatusCode.ERROR, message: e.toString() })
410
- throw e
411
- } finally {
412
- span.end()
559
+ const { writeTables } = (() => {
560
+ try {
561
+ const materializeEvents = () => this.syncProcessor.push(events, { otelContext })
562
+
563
+ if (events.length > 1) {
564
+ // TODO: what to do about leader transaction here?
565
+ return this.sqliteDbWrapper.txn(materializeEvents)
566
+ } else {
567
+ return materializeEvents()
413
568
  }
414
- },
415
- )
569
+ } catch (e: any) {
570
+ console.error(e)
571
+ span.setStatus({ code: otel.SpanStatusCode.ERROR, message: e.toString() })
572
+ throw e
573
+ } finally {
574
+ span.end()
575
+ }
576
+ })()
416
577
 
417
- const tablesToUpdate = [] as [Ref<null, QueryContext, RefreshReason>, null][]
578
+ const tablesToUpdate = [] as [Ref<null, ReactivityGraphContext, RefreshReason>, null][]
418
579
  for (const tableName of writeTables) {
419
580
  const tableRef = this.tableRefs[tableName]
420
581
  assertNever(tableRef !== undefined, `No table ref found for ${tableName}`)
@@ -422,8 +583,8 @@ export class Store<
422
583
  }
423
584
 
424
585
  const debugRefreshReason = {
425
- _tag: 'mutate' as const,
426
- mutations: mutationsEvents,
586
+ _tag: 'commit' as const,
587
+ events,
427
588
  writeTables: Array.from(writeTables),
428
589
  }
429
590
 
@@ -443,10 +604,36 @@ export class Store<
443
604
  },
444
605
  )
445
606
  }
446
- // #endregion mutate
607
+ // #endregion commit
447
608
 
448
609
  /**
449
- * This can be used in combination with `skipRefresh` when applying mutations.
610
+ * Returns an async iterable of events.
611
+ *
612
+ * @example
613
+ * ```ts
614
+ * for await (const event of store.events()) {
615
+ * console.log(event)
616
+ * }
617
+ * ```
618
+ *
619
+ * @example
620
+ * ```ts
621
+ * // Get all events from the beginning of time
622
+ * for await (const event of store.events({ cursor: EventSequenceNumber.ROOT })) {
623
+ * console.log(event)
624
+ * }
625
+ * ```
626
+ */
627
+ events = (_options?: StoreEventsOptions<TSchema>): AsyncIterable<LiveStoreEvent.ForSchema<TSchema>> => {
628
+ return notYetImplemented(`store.events() is not yet implemented but planned soon`)
629
+ }
630
+
631
+ eventsStream = (_options?: StoreEventsOptions<TSchema>): Stream.Stream<LiveStoreEvent.ForSchema<TSchema>> => {
632
+ return notYetImplemented(`store.eventsStream() is not yet implemented but planned soon`)
633
+ }
634
+
635
+ /**
636
+ * This can be used in combination with `skipRefresh` when committing events.
450
637
  * We might need a better solution for this. Let's see.
451
638
  */
452
639
  manualRefresh = (options?: { label?: string }) => {
@@ -454,7 +641,7 @@ export class Store<
454
641
  this.otel.tracer.startActiveSpan(
455
642
  'LiveStore:manualRefresh',
456
643
  { attributes: { 'livestore.manualRefreshLabel': label } },
457
- this.otel.mutationsSpanContext,
644
+ this.otel.commitsSpanContext,
458
645
  (span) => {
459
646
  const otelContext = otel.trace.setSpan(otel.context.active(), span)
460
647
  this.reactivityGraph.runDeferredEffects({ otelContext })
@@ -463,48 +650,74 @@ export class Store<
463
650
  )
464
651
  }
465
652
 
466
- private makeTableRef = (tableName: string) =>
467
- this.reactivityGraph.makeRef(null, {
468
- equal: () => false,
469
- label: `tableRef:${tableName}`,
470
- meta: { liveStoreRefType: 'table' },
471
- })
472
-
473
- __devDownloadDb = (source: 'local' | 'leader' = 'local') => {
474
- Effect.gen(this, function* () {
475
- const data = source === 'local' ? this.syncDbWrapper.export() : yield* this.clientSession.leaderThread.export
476
- downloadBlob(data, `livestore-${Date.now()}.db`)
477
- }).pipe(this.runEffectFork)
478
- }
479
-
480
- __devDownloadMutationLogDb = () => {
481
- Effect.gen(this, function* () {
482
- const data = yield* this.clientSession.leaderThread.getMutationLogData
483
- downloadBlob(data, `livestore-mutationlog-${Date.now()}.db`)
484
- }).pipe(this.runEffectFork)
485
- }
486
-
487
- __devHardReset = (mode: 'all-data' | 'only-app-db' = 'all-data') => {
488
- Effect.gen(this, function* () {
489
- yield* this.clientSession.leaderThread.sendDevtoolsMessage(
490
- Devtools.ResetAllDataReq.make({ liveStoreVersion, mode, requestId: nanoid() }),
491
- )
492
- }).pipe(this.runEffectFork)
493
- }
494
-
495
- __devSyncStates = () => {
496
- Effect.gen(this, function* () {
497
- const session = this.syncProcessor.syncStateRef.current
498
- console.log('Session sync state:', session)
499
- const leader = yield* this.clientSession.leaderThread.getSyncState
500
- console.log('Leader sync state:', leader)
501
- }).pipe(this.runEffectFork)
653
+ /**
654
+ * Shuts down the store and closes the client session.
655
+ *
656
+ * This is called automatically when the store was created using the React or Effect API.
657
+ */
658
+ shutdown = async (cause?: Cause.Cause<UnexpectedError>) => {
659
+ await this.clientSession
660
+ .shutdown(cause ?? Cause.fail(IntentionalShutdownCause.make({ reason: 'manual' })))
661
+ .pipe(this.runEffectFork, Fiber.join, Effect.runPromise)
502
662
  }
503
663
 
504
- __devShutdown = (cause?: Cause.Cause<UnexpectedError>) => {
505
- this.clientSession
506
- .shutdown(cause ?? Cause.fail(IntentionalShutdownCause.make({ reason: 'manual' })))
507
- .pipe(Effect.tapCauseLogPretty, Effect.provide(this.runtime), Effect.runFork)
664
+ /**
665
+ * Helper methods useful during development
666
+ *
667
+ * @internal
668
+ */
669
+ _dev = {
670
+ downloadDb: (source: 'local' | 'leader' = 'local') => {
671
+ Effect.gen(this, function* () {
672
+ const data = source === 'local' ? this.sqliteDbWrapper.export() : yield* this.clientSession.leaderThread.export
673
+ downloadBlob(data, `livestore-${Date.now()}.db`)
674
+ }).pipe(this.runEffectFork)
675
+ },
676
+
677
+ downloadEventlogDb: () => {
678
+ Effect.gen(this, function* () {
679
+ const data = yield* this.clientSession.leaderThread.getEventlogData
680
+ downloadBlob(data, `livestore-eventlog-${Date.now()}.db`)
681
+ }).pipe(this.runEffectFork)
682
+ },
683
+
684
+ hardReset: (mode: 'all-data' | 'only-app-db' = 'all-data') => {
685
+ Effect.gen(this, function* () {
686
+ const clientId = this.clientSession.clientId
687
+ yield* this.clientSession.leaderThread.sendDevtoolsMessage(
688
+ Devtools.Leader.ResetAllData.Request.make({ liveStoreVersion, mode, requestId: nanoid(), clientId }),
689
+ )
690
+ }).pipe(this.runEffectFork)
691
+ },
692
+
693
+ overrideNetworkStatus: (status: 'online' | 'offline') => {
694
+ const clientId = this.clientSession.clientId
695
+ this.clientSession.leaderThread
696
+ .sendDevtoolsMessage(
697
+ Devtools.Leader.SetSyncLatch.Request.make({
698
+ clientId,
699
+ closeLatch: status === 'offline',
700
+ liveStoreVersion,
701
+ requestId: nanoid(),
702
+ }),
703
+ )
704
+ .pipe(this.runEffectFork)
705
+ },
706
+
707
+ syncStates: () => {
708
+ Effect.gen(this, function* () {
709
+ const session = yield* this.syncProcessor.syncState
710
+ console.log('Session sync state:', session.toJSON())
711
+ const leader = yield* this.clientSession.leaderThread.getSyncState
712
+ console.log('Leader sync state:', leader.toJSON())
713
+ }).pipe(this.runEffectFork)
714
+ },
715
+
716
+ version: liveStoreVersion,
717
+
718
+ otel: {
719
+ rootSpanContext: () => otel.trace.getSpan(this.otel.rootSpanContext)?.spanContext(),
720
+ },
508
721
  }
509
722
 
510
723
  // NOTE This is needed because when booting a Store via Effect it seems to call `toJSON` in the error path
@@ -514,41 +727,46 @@ export class Store<
514
727
  })
515
728
 
516
729
  private runEffectFork = <A, E>(effect: Effect.Effect<A, E, Scope.Scope>) =>
517
- effect.pipe(Effect.forkIn(this.lifetimeScope), Effect.tapCauseLogPretty, Runtime.runFork(this.runtime))
730
+ effect.pipe(
731
+ Effect.forkIn(this.effectContext.lifetimeScope),
732
+ Effect.tapCauseLogPretty,
733
+ Runtime.runFork(this.effectContext.runtime),
734
+ )
518
735
 
519
- private getMutateArgs = (
520
- firstMutationOrTxnFnOrOptions: any,
521
- restMutations: any[],
736
+ private getCommitArgs = (
737
+ firstEventOrTxnFnOrOptions: any,
738
+ restEvents: any[],
522
739
  ): {
523
- mutationsEvents: (MutationEvent.ForSchema<TSchema> | MutationEvent.PartialForSchema<TSchema>)[]
524
- options: StoreMutateOptions | undefined
740
+ events: LiveStoreEvent.PartialForSchema<TSchema>[]
741
+ options: StoreCommitOptions | undefined
525
742
  } => {
526
- let mutationsEvents: (MutationEvent.ForSchema<TSchema> | MutationEvent.PartialForSchema<TSchema>)[]
527
- let options: StoreMutateOptions | undefined
743
+ let events: LiveStoreEvent.PartialForSchema<TSchema>[]
744
+ let options: StoreCommitOptions | undefined
528
745
 
529
- if (typeof firstMutationOrTxnFnOrOptions === 'function') {
746
+ if (typeof firstEventOrTxnFnOrOptions === 'function') {
530
747
  // TODO ensure that function is synchronous and isn't called in a async way (also write tests for this)
531
- mutationsEvents = firstMutationOrTxnFnOrOptions((arg: any) => mutationsEvents.push(arg))
748
+ events = firstEventOrTxnFnOrOptions((arg: any) => events.push(arg))
532
749
  } else if (
533
- firstMutationOrTxnFnOrOptions?.label !== undefined ||
534
- firstMutationOrTxnFnOrOptions?.skipRefresh !== undefined ||
535
- firstMutationOrTxnFnOrOptions?.otelContext !== undefined ||
536
- firstMutationOrTxnFnOrOptions?.spanLinks !== undefined
750
+ firstEventOrTxnFnOrOptions?.label !== undefined ||
751
+ firstEventOrTxnFnOrOptions?.skipRefresh !== undefined ||
752
+ firstEventOrTxnFnOrOptions?.otelContext !== undefined ||
753
+ firstEventOrTxnFnOrOptions?.spanLinks !== undefined
537
754
  ) {
538
- options = firstMutationOrTxnFnOrOptions
539
- mutationsEvents = restMutations
540
- } else if (firstMutationOrTxnFnOrOptions === undefined) {
541
- // When `mutate` is called with no arguments (which sometimes happens when dynamically filtering mutations)
542
- mutationsEvents = []
755
+ options = firstEventOrTxnFnOrOptions
756
+ events = restEvents
757
+ } else if (firstEventOrTxnFnOrOptions === undefined) {
758
+ // When `commit` is called with no arguments (which sometimes happens when dynamically filtering events)
759
+ events = []
543
760
  } else {
544
- mutationsEvents = [firstMutationOrTxnFnOrOptions, ...restMutations]
761
+ events = [firstEventOrTxnFnOrOptions, ...restEvents]
545
762
  }
546
763
 
547
- mutationsEvents = mutationsEvents.filter(
548
- // @ts-expect-error TODO
549
- (_) => _.id === undefined || !MutableHashMap.has(this.unsyncedMutationEvents, Data.struct(_.id)),
550
- )
764
+ // for (const event of events) {
765
+ // if (event.args.id === SessionIdSymbol) {
766
+ // event.args.id = this.clientSession.sessionId
767
+ // }
768
+ // }
551
769
 
552
- return { mutationsEvents, options }
770
+ return { events, options }
553
771
  }
554
772
  }