@livestore/livestore 0.0.58-dev.0 → 0.0.58-dev.10

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (157) hide show
  1. package/README.md +1 -117
  2. package/dist/.tsbuildinfo +1 -1
  3. package/dist/effect/LiveStore.d.ts +3 -3
  4. package/dist/effect/LiveStore.d.ts.map +1 -1
  5. package/dist/effect/LiveStore.js +1 -1
  6. package/dist/effect/LiveStore.js.map +1 -1
  7. package/dist/global-state.d.ts.map +1 -1
  8. package/dist/global-state.js +2 -1
  9. package/dist/global-state.js.map +1 -1
  10. package/dist/index.d.ts +8 -6
  11. package/dist/index.d.ts.map +1 -1
  12. package/dist/index.js +4 -2
  13. package/dist/index.js.map +1 -1
  14. package/dist/reactiveQueries/base-class.d.ts +1 -1
  15. package/dist/reactiveQueries/base-class.d.ts.map +1 -1
  16. package/dist/reactiveQueries/base-class.js.map +1 -1
  17. package/dist/reactiveQueries/sql.d.ts +1 -1
  18. package/dist/reactiveQueries/sql.d.ts.map +1 -1
  19. package/dist/reactiveQueries/sql.js +4 -4
  20. package/dist/reactiveQueries/sql.js.map +1 -1
  21. package/dist/reactiveQueries/sql.test.js +2 -2
  22. package/dist/reactiveQueries/sql.test.js.map +1 -1
  23. package/dist/row-query.d.ts +3 -2
  24. package/dist/row-query.d.ts.map +1 -1
  25. package/dist/row-query.js +18 -10
  26. package/dist/row-query.js.map +1 -1
  27. package/dist/store-devtools.d.ts +2 -2
  28. package/dist/store-devtools.d.ts.map +1 -1
  29. package/dist/store-devtools.js +3 -3
  30. package/dist/store-devtools.js.map +1 -1
  31. package/dist/store.d.ts +23 -19
  32. package/dist/store.d.ts.map +1 -1
  33. package/dist/store.js +90 -59
  34. package/dist/store.js.map +1 -1
  35. package/dist/utils/dev.d.ts.map +1 -1
  36. package/dist/utils/dev.js +1 -0
  37. package/dist/utils/dev.js.map +1 -1
  38. package/dist/{react/utils → utils}/stack-info.d.ts +1 -2
  39. package/dist/utils/stack-info.d.ts.map +1 -0
  40. package/dist/{react/utils → utils}/stack-info.js +1 -9
  41. package/dist/utils/stack-info.js.map +1 -0
  42. package/dist/utils/stack-info.test.d.ts.map +1 -0
  43. package/dist/{__tests__/react/utils → utils}/stack-info.test.js +1 -1
  44. package/dist/utils/stack-info.test.js.map +1 -0
  45. package/dist/utils/tests/fixture.d.ts +259 -0
  46. package/dist/utils/tests/fixture.d.ts.map +1 -0
  47. package/dist/utils/tests/fixture.js +33 -0
  48. package/dist/utils/tests/fixture.js.map +1 -0
  49. package/dist/utils/tests/mod.d.ts +3 -0
  50. package/dist/utils/tests/mod.d.ts.map +1 -0
  51. package/dist/utils/tests/mod.js +3 -0
  52. package/dist/utils/tests/mod.js.map +1 -0
  53. package/dist/utils/tests/otel.d.ts.map +1 -0
  54. package/dist/utils/tests/otel.js.map +1 -0
  55. package/package.json +17 -24
  56. package/src/ambient.d.ts +3 -0
  57. package/src/effect/LiveStore.ts +4 -4
  58. package/src/global-state.ts +5 -1
  59. package/src/index.ts +17 -4
  60. package/src/reactiveQueries/base-class.ts +1 -1
  61. package/src/reactiveQueries/sql.test.ts +2 -2
  62. package/src/reactiveQueries/sql.ts +5 -5
  63. package/src/row-query.ts +36 -16
  64. package/src/store-devtools.ts +5 -5
  65. package/src/store.ts +146 -78
  66. package/src/utils/dev.ts +1 -0
  67. package/src/{__tests__/react/utils → utils}/stack-info.test.ts +1 -1
  68. package/src/{react/utils → utils}/stack-info.ts +2 -12
  69. package/src/utils/tests/fixture.ts +77 -0
  70. package/src/utils/tests/mod.ts +2 -0
  71. package/tsconfig.json +1 -2
  72. package/vitest.config.js +0 -8
  73. package/dist/__tests__/react/fixture.d.ts +0 -461
  74. package/dist/__tests__/react/fixture.d.ts.map +0 -1
  75. package/dist/__tests__/react/fixture.js +0 -68
  76. package/dist/__tests__/react/fixture.js.map +0 -1
  77. package/dist/__tests__/react/utils/otel.d.ts.map +0 -1
  78. package/dist/__tests__/react/utils/otel.js.map +0 -1
  79. package/dist/__tests__/react/utils/stack-info.test.d.ts.map +0 -1
  80. package/dist/__tests__/react/utils/stack-info.test.js.map +0 -1
  81. package/dist/react/LiveStoreContext.d.ts +0 -7
  82. package/dist/react/LiveStoreContext.d.ts.map +0 -1
  83. package/dist/react/LiveStoreContext.js +0 -13
  84. package/dist/react/LiveStoreContext.js.map +0 -1
  85. package/dist/react/LiveStoreProvider.d.ts +0 -47
  86. package/dist/react/LiveStoreProvider.d.ts.map +0 -1
  87. package/dist/react/LiveStoreProvider.js +0 -169
  88. package/dist/react/LiveStoreProvider.js.map +0 -1
  89. package/dist/react/LiveStoreProvider.test.d.ts +0 -2
  90. package/dist/react/LiveStoreProvider.test.d.ts.map +0 -1
  91. package/dist/react/LiveStoreProvider.test.js +0 -62
  92. package/dist/react/LiveStoreProvider.test.js.map +0 -1
  93. package/dist/react/components/LiveList.d.ts +0 -21
  94. package/dist/react/components/LiveList.d.ts.map +0 -1
  95. package/dist/react/components/LiveList.js +0 -31
  96. package/dist/react/components/LiveList.js.map +0 -1
  97. package/dist/react/index.d.ts +0 -11
  98. package/dist/react/index.d.ts.map +0 -1
  99. package/dist/react/index.js +0 -10
  100. package/dist/react/index.js.map +0 -1
  101. package/dist/react/useAtom.d.ts +0 -10
  102. package/dist/react/useAtom.d.ts.map +0 -1
  103. package/dist/react/useAtom.js +0 -37
  104. package/dist/react/useAtom.js.map +0 -1
  105. package/dist/react/useLocalId.d.ts +0 -10
  106. package/dist/react/useLocalId.d.ts.map +0 -1
  107. package/dist/react/useLocalId.js +0 -21
  108. package/dist/react/useLocalId.js.map +0 -1
  109. package/dist/react/useQuery.d.ts +0 -9
  110. package/dist/react/useQuery.d.ts.map +0 -1
  111. package/dist/react/useQuery.js +0 -69
  112. package/dist/react/useQuery.js.map +0 -1
  113. package/dist/react/useQuery.test.d.ts +0 -2
  114. package/dist/react/useQuery.test.d.ts.map +0 -1
  115. package/dist/react/useQuery.test.js +0 -51
  116. package/dist/react/useQuery.test.js.map +0 -1
  117. package/dist/react/useRow.d.ts +0 -46
  118. package/dist/react/useRow.d.ts.map +0 -1
  119. package/dist/react/useRow.js +0 -94
  120. package/dist/react/useRow.js.map +0 -1
  121. package/dist/react/useRow.test.d.ts +0 -2
  122. package/dist/react/useRow.test.d.ts.map +0 -1
  123. package/dist/react/useRow.test.js +0 -562
  124. package/dist/react/useRow.test.js.map +0 -1
  125. package/dist/react/useTemporaryQuery.d.ts +0 -22
  126. package/dist/react/useTemporaryQuery.d.ts.map +0 -1
  127. package/dist/react/useTemporaryQuery.js +0 -70
  128. package/dist/react/useTemporaryQuery.js.map +0 -1
  129. package/dist/react/useTemporaryQuery.test.d.ts +0 -2
  130. package/dist/react/useTemporaryQuery.test.d.ts.map +0 -1
  131. package/dist/react/useTemporaryQuery.test.js +0 -37
  132. package/dist/react/useTemporaryQuery.test.js.map +0 -1
  133. package/dist/react/utils/stack-info.d.ts.map +0 -1
  134. package/dist/react/utils/stack-info.js.map +0 -1
  135. package/dist/react/utils/useStateRefWithReactiveInput.d.ts +0 -13
  136. package/dist/react/utils/useStateRefWithReactiveInput.d.ts.map +0 -1
  137. package/dist/react/utils/useStateRefWithReactiveInput.js +0 -38
  138. package/dist/react/utils/useStateRefWithReactiveInput.js.map +0 -1
  139. package/src/__tests__/react/fixture.tsx +0 -126
  140. package/src/react/LiveStoreContext.ts +0 -20
  141. package/src/react/LiveStoreProvider.test.tsx +0 -109
  142. package/src/react/LiveStoreProvider.tsx +0 -289
  143. package/src/react/components/LiveList.tsx +0 -84
  144. package/src/react/index.ts +0 -19
  145. package/src/react/useAtom.ts +0 -55
  146. package/src/react/useLocalId.ts +0 -33
  147. package/src/react/useQuery.test.tsx +0 -82
  148. package/src/react/useQuery.ts +0 -105
  149. package/src/react/useRow.test.tsx +0 -699
  150. package/src/react/useRow.ts +0 -180
  151. package/src/react/useTemporaryQuery.test.tsx +0 -56
  152. package/src/react/useTemporaryQuery.ts +0 -121
  153. package/src/react/utils/useStateRefWithReactiveInput.ts +0 -51
  154. /package/dist/{__tests__/react/utils → utils}/stack-info.test.d.ts +0 -0
  155. /package/dist/{__tests__/react/utils → utils/tests}/otel.d.ts +0 -0
  156. /package/dist/{__tests__/react/utils → utils/tests}/otel.js +0 -0
  157. /package/src/{__tests__/react/utils → utils/tests}/otel.ts +0 -0
package/src/store.ts CHANGED
@@ -1,19 +1,27 @@
1
1
  import type {
2
+ Adapter,
2
3
  BootDb,
3
4
  BootStatus,
5
+ ClientSession,
6
+ EventId,
4
7
  IntentionalShutdownCause,
5
8
  ParamsObject,
6
9
  PreparedBindValues,
7
- StoreAdapter,
8
- StoreAdapterFactory,
9
10
  StoreDevtoolsChannel,
10
11
  } from '@livestore/common'
11
12
  import { getExecArgsFromMutation, prepareBindValues, UnexpectedError } from '@livestore/common'
12
13
  import type { LiveStoreSchema, MutationEvent } from '@livestore/common/schema'
13
- import { makeMutationEventSchemaMemo, SCHEMA_META_TABLE, SCHEMA_MUTATIONS_META_TABLE } from '@livestore/common/schema'
14
+ import {
15
+ isPartialMutationEvent,
16
+ makeMutationEventSchemaMemo,
17
+ SCHEMA_META_TABLE,
18
+ SCHEMA_MUTATIONS_META_TABLE,
19
+ SESSION_CHANGESET_META_TABLE,
20
+ } from '@livestore/common/schema'
14
21
  import { assertNever, makeNoopTracer, shouldNeverHappen } from '@livestore/utils'
15
22
  import {
16
23
  Cause,
24
+ Data,
17
25
  Deferred,
18
26
  Duration,
19
27
  Effect,
@@ -23,6 +31,7 @@ import {
23
31
  Layer,
24
32
  Logger,
25
33
  LogLevel,
34
+ MutableHashMap,
26
35
  OtelTracer,
27
36
  Queue,
28
37
  Runtime,
@@ -34,7 +43,6 @@ import * as otel from '@opentelemetry/api'
34
43
  import type { GraphQLSchema } from 'graphql'
35
44
 
36
45
  import { globalReactivityGraph } from './global-state.js'
37
- import type { StackInfo } from './react/utils/stack-info.js'
38
46
  import type { DebugRefreshReasonBase, Ref } from './reactive.js'
39
47
  import type { LiveQuery, QueryContext, ReactivityGraph } from './reactiveQueries/base-class.js'
40
48
  import { connectDevtoolsToStore } from './store-devtools.js'
@@ -42,6 +50,7 @@ import { SynchronousDatabaseWrapper } from './SynchronousDatabaseWrapper.js'
42
50
  import { ReferenceCountedSet } from './utils/data-structures.js'
43
51
  import { downloadBlob } from './utils/dev.js'
44
52
  import { getDurationMsFromSpan } from './utils/otel.js'
53
+ import type { StackInfo } from './utils/stack-info.js'
45
54
 
46
55
  export type BaseGraphQLContext = {
47
56
  queriedTables: Set<string>
@@ -51,7 +60,7 @@ export type BaseGraphQLContext = {
51
60
 
52
61
  export type GraphQLOptions<TContext> = {
53
62
  schema: GraphQLSchema
54
- makeContext: (db: SynchronousDatabaseWrapper, tracer: otel.Tracer) => TContext
63
+ makeContext: (db: SynchronousDatabaseWrapper, tracer: otel.Tracer, sessionId: string) => TContext
55
64
  }
56
65
 
57
66
  export type OtelOptions = {
@@ -63,7 +72,7 @@ export type StoreOptions<
63
72
  TGraphQLContext extends BaseGraphQLContext,
64
73
  TSchema extends LiveStoreSchema = LiveStoreSchema,
65
74
  > = {
66
- adapter: StoreAdapter
75
+ clientSession: ClientSession
67
76
  schema: TSchema
68
77
  storeId: string
69
78
  // TODO remove graphql-related stuff from store and move to GraphQL query directly
@@ -74,8 +83,8 @@ export type StoreOptions<
74
83
  fiberSet: FiberSet.FiberSet
75
84
  runtime: Runtime.Runtime<Scope.Scope>
76
85
  batchUpdates: (runUpdates: () => void) => void
77
- // TODO remove this temporary solution and find a better way to avoid re-processing the same mutation
78
- __processedMutationIds: Set<string>
86
+ currentMutationEventIdRef: { current: EventId }
87
+ unsyncedMutationEvents: MutableHashMap.MutableHashMap<EventId, MutationEvent.ForSchema<TSchema>>
79
88
  }
80
89
 
81
90
  export type RefreshReason =
@@ -89,6 +98,7 @@ export type RefreshReason =
89
98
  writeTables: ReadonlyArray<string>
90
99
  }
91
100
  | {
101
+ // TODO rename to a more appropriate name which is framework-agnostic
92
102
  _tag: 'react'
93
103
  api: string
94
104
  label?: string
@@ -109,9 +119,6 @@ export type StoreOtel = {
109
119
  queriesSpanContext: otel.Context
110
120
  }
111
121
 
112
- let storeCount = 0
113
- const uniqueStoreId = () => `store-${++storeCount}`
114
-
115
122
  export type StoreMutateOptions = {
116
123
  label?: string
117
124
  skipRefresh?: boolean
@@ -125,7 +132,9 @@ export type StoreMutateOptions = {
125
132
  persisted?: boolean
126
133
  }
127
134
 
128
- if (typeof window !== 'undefined') {
135
+ // eslint-disable-next-line unicorn/prefer-global-this
136
+ if (import.meta.env.DEV && typeof window !== 'undefined') {
137
+ // eslint-disable-next-line unicorn/prefer-global-this
129
138
  window.__debugDownloadBlob = downloadBlob
130
139
  }
131
140
 
@@ -133,10 +142,10 @@ export class Store<
133
142
  TGraphQLContext extends BaseGraphQLContext = BaseGraphQLContext,
134
143
  TSchema extends LiveStoreSchema = LiveStoreSchema,
135
144
  > extends Inspectable.Class {
136
- id = uniqueStoreId()
145
+ readonly storeId: string
137
146
  reactivityGraph: ReactivityGraph
138
147
  syncDbWrapper: SynchronousDatabaseWrapper
139
- adapter: StoreAdapter
148
+ clientSession: ClientSession
140
149
  schema: LiveStoreSchema
141
150
  graphQLSchema?: GraphQLSchema
142
151
  graphQLContext?: TGraphQLContext
@@ -150,32 +159,39 @@ export class Store<
150
159
  private fiberSet: FiberSet.FiberSet
151
160
  private runtime: Runtime.Runtime<Scope.Scope>
152
161
 
153
- // TODO remove this temporary solution and find a better way to avoid re-processing the same mutation
154
- private __processedMutationIds
155
- private __processedMutationWithoutRefreshIds = new Set<string>()
156
-
157
162
  /** RC-based set to see which queries are currently subscribed to */
158
163
  activeQueries: ReferenceCountedSet<LiveQuery<any>>
159
164
 
165
+ // NOTE this is currently exposed for the Devtools databrowser to emit mutation events
160
166
  readonly __mutationEventSchema
161
167
 
168
+ private currentMutationEventIdRef
169
+ private unsyncedMutationEvents
170
+
162
171
  // #region constructor
163
172
  private constructor({
164
- adapter,
173
+ clientSession,
165
174
  schema,
166
175
  graphQLOptions,
167
176
  reactivityGraph,
168
177
  otelOptions,
169
178
  disableDevtools,
170
179
  batchUpdates,
171
- __processedMutationIds,
180
+ currentMutationEventIdRef,
181
+ unsyncedMutationEvents,
182
+ storeId,
172
183
  fiberSet,
173
184
  runtime,
174
185
  }: StoreOptions<TGraphQLContext, TSchema>) {
175
186
  super()
176
187
 
177
- this.syncDbWrapper = new SynchronousDatabaseWrapper({ otel: otelOptions, db: adapter.syncDb })
178
- this.adapter = adapter
188
+ this.storeId = storeId
189
+
190
+ this.currentMutationEventIdRef = currentMutationEventIdRef
191
+ this.unsyncedMutationEvents = unsyncedMutationEvents
192
+
193
+ this.syncDbWrapper = new SynchronousDatabaseWrapper({ otel: otelOptions, db: clientSession.syncDb })
194
+ this.clientSession = clientSession
179
195
  this.schema = schema
180
196
 
181
197
  this.fiberSet = fiberSet
@@ -184,9 +200,6 @@ export class Store<
184
200
  // TODO refactor
185
201
  this.__mutationEventSchema = makeMutationEventSchemaMemo(schema)
186
202
 
187
- // TODO remove this temporary solution and find a better way to avoid re-processing the same mutation
188
- this.__processedMutationIds = __processedMutationIds
189
-
190
203
  // TODO generalize the `tableRefs` concept to allow finer-grained refs
191
204
  this.tableRefs = {}
192
205
  this.activeQueries = new ReferenceCountedSet()
@@ -222,7 +235,7 @@ export class Store<
222
235
  isRunningInDevtools
223
236
  ? this.schema.tables.keys()
224
237
  : Array.from(this.schema.tables.keys()).filter(
225
- (_) => _ !== SCHEMA_META_TABLE && _ !== SCHEMA_MUTATIONS_META_TABLE,
238
+ (_) => _ !== SCHEMA_META_TABLE && _ !== SCHEMA_MUTATIONS_META_TABLE && _ !== SESSION_CHANGESET_META_TABLE,
226
239
  ),
227
240
  )
228
241
  const existingTableRefs = new Map(
@@ -236,14 +249,20 @@ export class Store<
236
249
 
237
250
  if (graphQLOptions) {
238
251
  this.graphQLSchema = graphQLOptions.schema
239
- this.graphQLContext = graphQLOptions.makeContext(this.syncDbWrapper, this.otel.tracer)
252
+ this.graphQLContext = graphQLOptions.makeContext(
253
+ this.syncDbWrapper,
254
+ this.otel.tracer,
255
+ clientSession.coordinator.sessionId,
256
+ )
240
257
  }
241
258
 
242
259
  Effect.gen(this, function* () {
243
- yield* this.adapter.coordinator.syncMutations.pipe(
244
- Stream.tapSync((mutationEventDecoded) => {
245
- this.mutate({ wasSyncMessage: true }, mutationEventDecoded)
246
- }),
260
+ yield* this.clientSession.coordinator.syncMutations.pipe(
261
+ Stream.tapChunk((mutationsEventsDecodedChunk) =>
262
+ Effect.sync(() => {
263
+ this.mutate({ wasSyncMessage: true }, ...mutationsEventsDecodedChunk)
264
+ }),
265
+ ),
247
266
  Stream.runDrain,
248
267
  Effect.interruptible,
249
268
  Effect.withSpan('LiveStore:syncMutations'),
@@ -282,6 +301,10 @@ export class Store<
282
301
  })
283
302
  }
284
303
 
304
+ get sessionId(): string {
305
+ return this.clientSession.coordinator.sessionId
306
+ }
307
+
285
308
  /**
286
309
  * Subscribe to the results of a query
287
310
  * Returns a function to cancel the subscription.
@@ -297,7 +320,7 @@ export class Store<
297
320
  { attributes: { label: options?.label, queryLabel: query$.label } },
298
321
  options?.otelContext ?? this.otel.queriesSpanContext,
299
322
  (span) => {
300
- // console.log('store sub', query$.label)
323
+ // console.debug('store sub', query$.id, query$.label)
301
324
  const otelContext = otel.trace.setSpan(otel.context.active(), span)
302
325
 
303
326
  const label = `subscribe:${options?.label}`
@@ -311,7 +334,7 @@ export class Store<
311
334
  }
312
335
 
313
336
  const unsubscribe = () => {
314
- // console.log('store unsub', query$.label)
337
+ // console.debug('store unsub', query$.id, query$.label)
315
338
  try {
316
339
  this.reactivityGraph.destroyNode(effect)
317
340
  this.activeQueries.remove(query$ as LiveQuery<TResult>)
@@ -327,17 +350,21 @@ export class Store<
327
350
 
328
351
  // #region mutate
329
352
  mutate: {
330
- <const TMutationArg extends ReadonlyArray<MutationEvent.ForSchema<TSchema>>>(...list: TMutationArg): void
353
+ <const TMutationArg extends ReadonlyArray<MutationEvent.PartialForSchema<TSchema>>>(...list: TMutationArg): void
331
354
  (
332
- txn: <const TMutationArg extends ReadonlyArray<MutationEvent.ForSchema<TSchema>>>(...list: TMutationArg) => void,
355
+ txn: <const TMutationArg extends ReadonlyArray<MutationEvent.PartialForSchema<TSchema>>>(
356
+ ...list: TMutationArg
357
+ ) => void,
333
358
  ): void
334
- <const TMutationArg extends ReadonlyArray<MutationEvent.ForSchema<TSchema>>>(
359
+ <const TMutationArg extends ReadonlyArray<MutationEvent.PartialForSchema<TSchema>>>(
335
360
  options: StoreMutateOptions,
336
361
  ...list: TMutationArg
337
362
  ): void
338
363
  (
339
364
  options: StoreMutateOptions,
340
- txn: <const TMutationArg extends ReadonlyArray<MutationEvent.ForSchema<TSchema>>>(...list: TMutationArg) => void,
365
+ txn: <const TMutationArg extends ReadonlyArray<MutationEvent.PartialForSchema<TSchema>>>(
366
+ ...list: TMutationArg
367
+ ) => void,
341
368
  ): void
342
369
  } = (firstMutationOrTxnFnOrOptions: any, ...restMutations: any[]) => {
343
370
  let mutationsEvents: MutationEvent.ForSchema<TSchema>[]
@@ -361,16 +388,14 @@ export class Store<
361
388
  mutationsEvents = [firstMutationOrTxnFnOrOptions, ...restMutations]
362
389
  }
363
390
 
364
- mutationsEvents = mutationsEvents.filter((_) => !this.__processedMutationIds.has(_.id))
391
+ mutationsEvents = mutationsEvents.filter(
392
+ (_) => _.id === undefined || !MutableHashMap.has(this.unsyncedMutationEvents, Data.struct(_.id)),
393
+ )
365
394
 
366
395
  if (mutationsEvents.length === 0) {
367
396
  return
368
397
  }
369
398
 
370
- for (const mutationEvent of mutationsEvents) {
371
- this.__processedMutationIds.add(mutationEvent.id)
372
- }
373
-
374
399
  const label = options?.label ?? 'mutate'
375
400
  const skipRefresh = options?.skipRefresh ?? false
376
401
  const wasSyncMessage = options?.wasSyncMessage ?? false
@@ -380,12 +405,12 @@ export class Store<
380
405
  mutationsSpan.addEvent('mutate')
381
406
 
382
407
  // console.group('LiveStore.mutate', { skipRefresh, wasSyncMessage, label })
383
- // mutationsEvents.forEach((_) => console.log(_.mutation, _.id, _.args))
408
+ // mutationsEvents.forEach((_) => console.debug(_.mutation, _.id, _.args))
384
409
  // console.groupEnd()
385
410
 
386
411
  let durationMs: number
387
412
 
388
- return this.otel.tracer.startActiveSpan(
413
+ const res = this.otel.tracer.startActiveSpan(
389
414
  'LiveStore:mutate',
390
415
  { attributes: { 'livestore.mutateLabel': label } },
391
416
  this.otel.mutationsSpanContext,
@@ -465,6 +490,16 @@ export class Store<
465
490
  return { durationMs }
466
491
  },
467
492
  )
493
+
494
+ // NOTE we need to add the mutation events to the unsynced mutation events map only after running the code above
495
+ // so the short-circuiting in `mutateWithoutRefresh` doesn't kick in for those events
496
+ for (const mutationEvent of mutationsEvents) {
497
+ if (mutationEvent.id !== undefined) {
498
+ MutableHashMap.set(this.unsyncedMutationEvents, Data.struct(mutationEvent.id), mutationEvent)
499
+ }
500
+ }
501
+
502
+ return res
468
503
  }
469
504
 
470
505
  /**
@@ -492,19 +527,39 @@ export class Store<
492
527
  * the caller must refresh queries after calling this method.
493
528
  */
494
529
  mutateWithoutRefresh = (
495
- mutationEventDecoded: MutationEvent.ForSchema<TSchema>,
530
+ mutationEventDecoded_: MutationEvent.ForSchema<TSchema> | MutationEvent.PartialForSchema<TSchema>,
496
531
  options: {
497
532
  otelContext: otel.Context
533
+ // TODO adjust `skip-persist` with new rebase sync strategy
498
534
  coordinatorMode: 'default' | 'skip-coordinator' | 'skip-persist'
499
535
  },
500
536
  ): { writeTables: ReadonlySet<string>; durationMs: number } => {
537
+ const mutationDef =
538
+ this.schema.mutations.get(mutationEventDecoded_.mutation) ??
539
+ shouldNeverHappen(`Unknown mutation type: ${mutationEventDecoded_.mutation}`)
540
+
541
+ // Needs to happen only for partial mutation events (thus a function)
542
+ const nextMutationEventId = () => {
543
+ const { id, parentId } = this.clientSession.coordinator
544
+ .nextMutationEventIdPair({ localOnly: mutationDef.options.localOnly })
545
+ .pipe(Effect.runSync)
546
+
547
+ this.currentMutationEventIdRef.current = id
548
+
549
+ return { id, parentId }
550
+ }
551
+
552
+ const mutationEventDecoded: MutationEvent.ForSchema<TSchema> = isPartialMutationEvent(mutationEventDecoded_)
553
+ ? { ...mutationEventDecoded_, ...nextMutationEventId() }
554
+ : mutationEventDecoded_
555
+
501
556
  // NOTE we also need this temporary workaround here since some code-paths use `mutateWithoutRefresh` directly
502
557
  // e.g. the row-query functionality
503
- if (this.__processedMutationWithoutRefreshIds.has(mutationEventDecoded.id)) {
558
+ if (MutableHashMap.has(this.unsyncedMutationEvents, Data.struct(mutationEventDecoded.id))) {
504
559
  // NOTE this data should never be used
505
560
  return { writeTables: new Set(), durationMs: 0 }
506
561
  } else {
507
- this.__processedMutationWithoutRefreshIds.add(mutationEventDecoded.id)
562
+ MutableHashMap.set(this.unsyncedMutationEvents, Data.struct(mutationEventDecoded.id), mutationEventDecoded)
508
563
  }
509
564
 
510
565
  const { otelContext, coordinatorMode = 'default' } = options
@@ -524,10 +579,6 @@ export class Store<
524
579
  const allWriteTables = new Set<string>()
525
580
  let durationMsTotal = 0
526
581
 
527
- const mutationDef =
528
- this.schema.mutations.get(mutationEventDecoded.mutation) ??
529
- shouldNeverHappen(`Unknown mutation type: ${mutationEventDecoded.mutation}`)
530
-
531
582
  const execArgsArr = getExecArgsFromMutation({ mutationDef, mutationEventDecoded })
532
583
 
533
584
  for (const {
@@ -547,9 +598,9 @@ export class Store<
547
598
 
548
599
  if (coordinatorMode !== 'skip-coordinator') {
549
600
  // Asynchronously apply mutation to a persistent storage (we're not awaiting this promise here)
550
- this.adapter.coordinator
601
+ this.clientSession.coordinator
551
602
  .mutate(mutationEventEncoded as MutationEvent.AnyEncoded, { persisted: coordinatorMode !== 'skip-persist' })
552
- .pipe(Runtime.runFork(this.runtime))
603
+ .pipe(this.runEffectFork)
553
604
  }
554
605
 
555
606
  // Uncomment to print a list of queries currently registered on the store
@@ -568,7 +619,7 @@ export class Store<
568
619
  * This should only be used for framework-internal purposes;
569
620
  * all app writes should go through mutate.
570
621
  */
571
- execute = (
622
+ __execute = (
572
623
  query: string,
573
624
  params: ParamsObject = {},
574
625
  writeTables?: ReadonlySet<string>,
@@ -576,10 +627,10 @@ export class Store<
576
627
  ) => {
577
628
  this.syncDbWrapper.execute(query, prepareBindValues(params, query), writeTables, { otelContext })
578
629
 
579
- this.adapter.coordinator.execute(query, prepareBindValues(params, query)).pipe(this.runEffectFork)
630
+ this.clientSession.coordinator.execute(query, prepareBindValues(params, query)).pipe(this.runEffectFork)
580
631
  }
581
632
 
582
- select = (query: string, params: ParamsObject = {}) => {
633
+ __select = (query: string, params: ParamsObject = {}) => {
583
634
  return this.syncDbWrapper.select(query, { bindValues: prepareBindValues(params, query) })
584
635
  }
585
636
 
@@ -597,7 +648,7 @@ export class Store<
597
648
 
598
649
  __devDownloadMutationLogDb = () =>
599
650
  Effect.gen(this, function* () {
600
- const data = yield* this.adapter.coordinator.getMutationLogData
651
+ const data = yield* this.clientSession.coordinator.getMutationLogData
601
652
  downloadBlob(data, `livestore-mutationlog-${Date.now()}.db`)
602
653
  }).pipe(this.runEffectFork)
603
654
 
@@ -615,7 +666,7 @@ export class Store<
615
666
 
616
667
  export type CreateStoreOptions<TGraphQLContext extends BaseGraphQLContext, TSchema extends LiveStoreSchema> = {
617
668
  schema: TSchema
618
- adapter: StoreAdapterFactory
669
+ adapter: Adapter
619
670
  storeId: string
620
671
  reactivityGraph?: ReactivityGraph
621
672
  graphQLOptions?: GraphQLOptions<TGraphQLContext>
@@ -663,7 +714,7 @@ export const createStore = <
663
714
  TSchema extends LiveStoreSchema = LiveStoreSchema,
664
715
  >({
665
716
  schema,
666
- adapter: adapterFactory,
717
+ adapter,
667
718
  storeId,
668
719
  graphQLOptions,
669
720
  otelOptions,
@@ -734,7 +785,7 @@ export const createStore = <
734
785
  )
735
786
  }).pipe(Effect.withSpan('livestore:shutdown'))
736
787
 
737
- const adapter: StoreAdapter = yield* adapterFactory({
788
+ const clientSession: ClientSession = yield* adapter({
738
789
  schema,
739
790
  storeId,
740
791
  devtoolsEnabled: disableDevtools !== true,
@@ -745,7 +796,13 @@ export const createStore = <
745
796
 
746
797
  const mutationEventSchema = makeMutationEventSchemaMemo(schema)
747
798
 
748
- const __processedMutationIds = new Set<string>()
799
+ // TODO get rid of this
800
+ // const __processedMutationIds = new Set<number>()
801
+
802
+ const currentMutationEventIdRef = { current: yield* clientSession.coordinator.getCurrentMutationEventId }
803
+
804
+ // TODO fill up with unsynced mutation events from the coordinator
805
+ const unsyncedMutationEvents = MutableHashMap.empty<EventId, MutationEvent.ForSchema<TSchema>>()
749
806
 
750
807
  // TODO consider moving booting into the storage backend
751
808
  if (boot !== undefined) {
@@ -755,57 +812,67 @@ export const createStore = <
755
812
  const bootDbImpl: BootDb = {
756
813
  _tag: 'BootDb',
757
814
  execute: (queryStr, bindValues) => {
758
- const stmt = adapter.syncDb.prepare(queryStr)
815
+ const stmt = clientSession.syncDb.prepare(queryStr)
759
816
  stmt.execute(bindValues)
760
817
 
761
818
  if (isInTxn === true) {
762
819
  txnExecuteStmnts.push([queryStr, bindValues])
763
820
  } else {
764
- adapter.coordinator.execute(queryStr, bindValues).pipe(runEffectFork)
821
+ clientSession.coordinator.execute(queryStr, bindValues).pipe(runEffectFork)
765
822
  }
766
823
  },
767
824
  mutate: (...list) => {
768
- for (const mutationEventDecoded of list) {
825
+ for (const mutationEventDecoded_ of list) {
769
826
  const mutationDef =
770
- schema.mutations.get(mutationEventDecoded.mutation) ??
771
- shouldNeverHappen(`Unknown mutation type: ${mutationEventDecoded.mutation}`)
827
+ schema.mutations.get(mutationEventDecoded_.mutation) ??
828
+ shouldNeverHappen(`Unknown mutation type: ${mutationEventDecoded_.mutation}`)
829
+
830
+ const { id, parentId } = clientSession.coordinator
831
+ .nextMutationEventIdPair({ localOnly: mutationDef.options.localOnly })
832
+ .pipe(Effect.runSync)
833
+
834
+ currentMutationEventIdRef.current = id
835
+
836
+ const mutationEventDecoded = { ...mutationEventDecoded_, id, parentId }
837
+
838
+ MutableHashMap.set(unsyncedMutationEvents, Data.struct(mutationEventDecoded.id), mutationEventDecoded)
772
839
 
773
- __processedMutationIds.add(mutationEventDecoded.id)
840
+ // __processedMutationIds.add(mutationEventDecoded.id.global)
774
841
 
775
842
  const execArgsArr = getExecArgsFromMutation({ mutationDef, mutationEventDecoded })
776
843
  // const { bindValues, statementSql } = getExecArgsFromMutation({ mutationDef, mutationEventDecoded })
777
844
 
778
845
  for (const { statementSql, bindValues } of execArgsArr) {
779
- adapter.syncDb.execute(statementSql, bindValues)
846
+ clientSession.syncDb.execute(statementSql, bindValues)
780
847
  }
781
848
 
782
849
  const mutationEventEncoded = Schema.encodeUnknownSync(mutationEventSchema)(mutationEventDecoded)
783
850
 
784
- adapter.coordinator
851
+ clientSession.coordinator
785
852
  .mutate(mutationEventEncoded as MutationEvent.AnyEncoded, { persisted: true })
786
853
  .pipe(runEffectFork)
787
854
  }
788
855
  },
789
856
  select: (queryStr, bindValues) => {
790
- const stmt = adapter.syncDb.prepare(queryStr)
857
+ const stmt = clientSession.syncDb.prepare(queryStr)
791
858
  return stmt.select(bindValues)
792
859
  },
793
860
  txn: (callback) => {
794
861
  try {
795
862
  isInTxn = true
796
- // adapter.syncDb.execute('BEGIN TRANSACTION', undefined)
863
+ // clientSession.syncDb.execute('BEGIN TRANSACTION', undefined)
797
864
 
798
865
  callback()
799
866
 
800
- // adapter.syncDb.execute('COMMIT', undefined)
867
+ // clientSession.syncDb.execute('COMMIT', undefined)
801
868
 
802
- // adapter.coordinator.execute('BEGIN', undefined, undefined)
869
+ // clientSession.coordinator.execute('BEGIN', undefined, undefined)
803
870
  for (const [queryStr, bindValues] of txnExecuteStmnts) {
804
- adapter.coordinator.execute(queryStr, bindValues).pipe(runEffectFork)
871
+ clientSession.coordinator.execute(queryStr, bindValues).pipe(runEffectFork)
805
872
  }
806
- // adapter.coordinator.execute('COMMIT', undefined, undefined)
873
+ // clientSession.coordinator.execute('COMMIT', undefined, undefined)
807
874
  } catch (e: any) {
808
- // adapter.syncDb.execute('ROLLBACK', undefined)
875
+ // clientSession.syncDb.execute('ROLLBACK', undefined)
809
876
  throw e
810
877
  } finally {
811
878
  isInTxn = false
@@ -822,13 +889,14 @@ export const createStore = <
822
889
 
823
890
  const store = Store.createStore<TGraphQLContext, TSchema>(
824
891
  {
825
- adapter,
892
+ clientSession,
826
893
  schema,
827
894
  graphQLOptions,
828
895
  otelOptions: { tracer: otelTracer, rootSpanContext: otelRootSpanContext },
829
896
  reactivityGraph,
830
897
  disableDevtools,
831
- __processedMutationIds,
898
+ currentMutationEventIdRef,
899
+ unsyncedMutationEvents,
832
900
  fiberSet,
833
901
  runtime,
834
902
  batchUpdates: batchUpdates ?? ((run) => run()),
package/src/utils/dev.ts CHANGED
@@ -1,3 +1,4 @@
1
+ /* eslint-disable unicorn/prefer-global-this */
1
2
  export const downloadBlob = (
2
3
  data: Uint8Array | Blob | string,
3
4
  fileName: string,
@@ -1,6 +1,6 @@
1
1
  import { expect, it } from 'vitest'
2
2
 
3
- import { extractStackInfoFromStackTrace } from '../../../react/utils/stack-info.js'
3
+ import { extractStackInfoFromStackTrace } from './stack-info.js'
4
4
 
5
5
  it('RouteLink stacktrace', async () => {
6
6
  const stackTrace = `\
@@ -1,7 +1,3 @@
1
- import React from 'react'
2
-
3
- export const originalStackLimit = Error.stackTraceLimit
4
-
5
1
  export type StackInfo = {
6
2
  frames: StackFrame[]
7
3
  }
@@ -54,11 +50,5 @@ export const extractStackInfoFromStackTrace = (stackTrace: string): StackInfo =>
54
50
  return { frames }
55
51
  }
56
52
 
57
- export const useStackInfo = (): StackInfo =>
58
- React.useMemo(() => {
59
- Error.stackTraceLimit = 10
60
- // eslint-disable-next-line unicorn/error-message
61
- const stack = new Error().stack!
62
- Error.stackTraceLimit = originalStackLimit
63
- return extractStackInfoFromStackTrace(stack)
64
- }, [])
53
+ export const stackInfoToString = (stackInfo: StackInfo): string =>
54
+ stackInfo.frames.map((f) => `${f.name} (${f.filePath})`).join('\n')
@@ -0,0 +1,77 @@
1
+ import type { FromInputSchema } from '@livestore/common/schema'
2
+ import type { Store } from '@livestore/livestore'
3
+ import {
4
+ createStore,
5
+ DbSchema,
6
+ globalReactivityGraph,
7
+ makeReactivityGraph,
8
+ makeSchema,
9
+ sql,
10
+ } from '@livestore/livestore'
11
+ import { Effect, FiberSet } from '@livestore/utils/effect'
12
+ import { makeInMemoryAdapter } from '@livestore/web'
13
+ import type * as otel from '@opentelemetry/api'
14
+
15
+ export type Todo = {
16
+ id: string
17
+ text: string
18
+ completed: boolean
19
+ }
20
+
21
+ export type Filter = 'all' | 'active' | 'completed'
22
+
23
+ export type AppState = {
24
+ newTodoText: string
25
+ filter: Filter
26
+ }
27
+
28
+ export const todos = DbSchema.table(
29
+ 'todos',
30
+ {
31
+ id: DbSchema.text({ primaryKey: true }),
32
+ text: DbSchema.text({ default: '', nullable: false }),
33
+ completed: DbSchema.boolean({ default: false, nullable: false }),
34
+ },
35
+ { deriveMutations: true, isSingleton: false },
36
+ )
37
+
38
+ export const app = DbSchema.table('app', {
39
+ id: DbSchema.text({ primaryKey: true }),
40
+ newTodoText: DbSchema.text({ default: '', nullable: true }),
41
+ filter: DbSchema.text({ default: 'all', nullable: false }),
42
+ })
43
+
44
+ export const tables = { todos, app }
45
+ export const schema = makeSchema({ tables })
46
+
47
+ export interface FixtureSchema extends FromInputSchema.DeriveSchema<{ tables: typeof tables }> {}
48
+
49
+ export const makeTodoMvc = ({
50
+ otelTracer,
51
+ otelContext,
52
+ useGlobalReactivityGraph = true,
53
+ }: {
54
+ otelTracer?: otel.Tracer
55
+ otelContext?: otel.Context
56
+ useGlobalReactivityGraph?: boolean
57
+ } = {}) =>
58
+ Effect.gen(function* () {
59
+ const reactivityGraph = useGlobalReactivityGraph ? globalReactivityGraph : makeReactivityGraph()
60
+
61
+ const fiberSet = yield* FiberSet.make()
62
+
63
+ const store: Store<any, FixtureSchema> = yield* createStore({
64
+ schema,
65
+ storeId: 'default',
66
+ boot: (db) => db.execute(sql`INSERT OR IGNORE INTO app (id, newTodoText, filter) VALUES ('static', '', 'all');`),
67
+ adapter: makeInMemoryAdapter(),
68
+ reactivityGraph,
69
+ otelOptions: {
70
+ tracer: otelTracer,
71
+ rootSpanContext: otelContext,
72
+ },
73
+ fiberSet,
74
+ })
75
+
76
+ return { store, reactivityGraph }
77
+ })
@@ -0,0 +1,2 @@
1
+ export * from './fixture.js'
2
+ export * from './otel.js'