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

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