@livestore/livestore 0.3.0-dev.4 → 0.3.0-dev.40

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