@livestore/livestore 0.0.58-dev.5 → 0.0.58-dev.7

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.
package/src/store.ts CHANGED
@@ -1,6 +1,7 @@
1
1
  import type {
2
2
  BootDb,
3
3
  BootStatus,
4
+ EventId,
4
5
  IntentionalShutdownCause,
5
6
  ParamsObject,
6
7
  PreparedBindValues,
@@ -10,10 +11,16 @@ import type {
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
+ makeMutationEventSchemaMemo,
16
+ SCHEMA_META_TABLE,
17
+ SCHEMA_MUTATIONS_META_TABLE,
18
+ SESSION_CHANGESET_META_TABLE,
19
+ } from '@livestore/common/schema'
14
20
  import { assertNever, makeNoopTracer, shouldNeverHappen } from '@livestore/utils'
15
21
  import {
16
22
  Cause,
23
+ Data,
17
24
  Deferred,
18
25
  Duration,
19
26
  Effect,
@@ -23,6 +30,7 @@ import {
23
30
  Layer,
24
31
  Logger,
25
32
  LogLevel,
33
+ MutableHashMap,
26
34
  OtelTracer,
27
35
  Queue,
28
36
  Runtime,
@@ -74,8 +82,8 @@ export type StoreOptions<
74
82
  fiberSet: FiberSet.FiberSet
75
83
  runtime: Runtime.Runtime<Scope.Scope>
76
84
  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>
85
+ currentMutationEventIdRef: { current: EventId }
86
+ unsyncedMutationEvents: MutableHashMap.MutableHashMap<EventId, MutationEvent.ForSchema<TSchema>>
79
87
  }
80
88
 
81
89
  export type RefreshReason =
@@ -109,9 +117,6 @@ export type StoreOtel = {
109
117
  queriesSpanContext: otel.Context
110
118
  }
111
119
 
112
- let storeCount = 0
113
- const uniqueStoreId = () => `store-${++storeCount}`
114
-
115
120
  export type StoreMutateOptions = {
116
121
  label?: string
117
122
  skipRefresh?: boolean
@@ -133,7 +138,7 @@ export class Store<
133
138
  TGraphQLContext extends BaseGraphQLContext = BaseGraphQLContext,
134
139
  TSchema extends LiveStoreSchema = LiveStoreSchema,
135
140
  > extends Inspectable.Class {
136
- id = uniqueStoreId()
141
+ readonly storeId: string
137
142
  reactivityGraph: ReactivityGraph
138
143
  syncDbWrapper: SynchronousDatabaseWrapper
139
144
  adapter: StoreAdapter
@@ -150,15 +155,15 @@ export class Store<
150
155
  private fiberSet: FiberSet.FiberSet
151
156
  private runtime: Runtime.Runtime<Scope.Scope>
152
157
 
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
158
  /** RC-based set to see which queries are currently subscribed to */
158
159
  activeQueries: ReferenceCountedSet<LiveQuery<any>>
159
160
 
161
+ // NOTE this is currently exposed for the Devtools databrowser to emit mutation events
160
162
  readonly __mutationEventSchema
161
163
 
164
+ private currentMutationEventIdRef
165
+ private unsyncedMutationEvents
166
+
162
167
  // #region constructor
163
168
  private constructor({
164
169
  adapter,
@@ -168,12 +173,19 @@ export class Store<
168
173
  otelOptions,
169
174
  disableDevtools,
170
175
  batchUpdates,
171
- __processedMutationIds,
176
+ currentMutationEventIdRef,
177
+ unsyncedMutationEvents,
178
+ storeId,
172
179
  fiberSet,
173
180
  runtime,
174
181
  }: StoreOptions<TGraphQLContext, TSchema>) {
175
182
  super()
176
183
 
184
+ this.storeId = storeId
185
+
186
+ this.currentMutationEventIdRef = currentMutationEventIdRef
187
+ this.unsyncedMutationEvents = unsyncedMutationEvents
188
+
177
189
  this.syncDbWrapper = new SynchronousDatabaseWrapper({ otel: otelOptions, db: adapter.syncDb })
178
190
  this.adapter = adapter
179
191
  this.schema = schema
@@ -184,9 +196,6 @@ export class Store<
184
196
  // TODO refactor
185
197
  this.__mutationEventSchema = makeMutationEventSchemaMemo(schema)
186
198
 
187
- // TODO remove this temporary solution and find a better way to avoid re-processing the same mutation
188
- this.__processedMutationIds = __processedMutationIds
189
-
190
199
  // TODO generalize the `tableRefs` concept to allow finer-grained refs
191
200
  this.tableRefs = {}
192
201
  this.activeQueries = new ReferenceCountedSet()
@@ -222,7 +231,7 @@ export class Store<
222
231
  isRunningInDevtools
223
232
  ? this.schema.tables.keys()
224
233
  : Array.from(this.schema.tables.keys()).filter(
225
- (_) => _ !== SCHEMA_META_TABLE && _ !== SCHEMA_MUTATIONS_META_TABLE,
234
+ (_) => _ !== SCHEMA_META_TABLE && _ !== SCHEMA_MUTATIONS_META_TABLE && _ !== SESSION_CHANGESET_META_TABLE,
226
235
  ),
227
236
  )
228
237
  const existingTableRefs = new Map(
@@ -241,9 +250,11 @@ export class Store<
241
250
 
242
251
  Effect.gen(this, function* () {
243
252
  yield* this.adapter.coordinator.syncMutations.pipe(
244
- Stream.tapSync((mutationEventDecoded) => {
245
- this.mutate({ wasSyncMessage: true }, mutationEventDecoded)
246
- }),
253
+ Stream.tapChunk((mutationsEventsDecodedChunk) =>
254
+ Effect.sync(() => {
255
+ this.mutate({ wasSyncMessage: true }, ...mutationsEventsDecodedChunk)
256
+ }),
257
+ ),
247
258
  Stream.runDrain,
248
259
  Effect.interruptible,
249
260
  Effect.withSpan('LiveStore:syncMutations'),
@@ -327,17 +338,21 @@ export class Store<
327
338
 
328
339
  // #region mutate
329
340
  mutate: {
330
- <const TMutationArg extends ReadonlyArray<MutationEvent.ForSchema<TSchema>>>(...list: TMutationArg): void
341
+ <const TMutationArg extends ReadonlyArray<MutationEvent.PartialForSchema<TSchema>>>(...list: TMutationArg): void
331
342
  (
332
- txn: <const TMutationArg extends ReadonlyArray<MutationEvent.ForSchema<TSchema>>>(...list: TMutationArg) => void,
343
+ txn: <const TMutationArg extends ReadonlyArray<MutationEvent.PartialForSchema<TSchema>>>(
344
+ ...list: TMutationArg
345
+ ) => void,
333
346
  ): void
334
- <const TMutationArg extends ReadonlyArray<MutationEvent.ForSchema<TSchema>>>(
347
+ <const TMutationArg extends ReadonlyArray<MutationEvent.PartialForSchema<TSchema>>>(
335
348
  options: StoreMutateOptions,
336
349
  ...list: TMutationArg
337
350
  ): void
338
351
  (
339
352
  options: StoreMutateOptions,
340
- txn: <const TMutationArg extends ReadonlyArray<MutationEvent.ForSchema<TSchema>>>(...list: TMutationArg) => void,
353
+ txn: <const TMutationArg extends ReadonlyArray<MutationEvent.PartialForSchema<TSchema>>>(
354
+ ...list: TMutationArg
355
+ ) => void,
341
356
  ): void
342
357
  } = (firstMutationOrTxnFnOrOptions: any, ...restMutations: any[]) => {
343
358
  let mutationsEvents: MutationEvent.ForSchema<TSchema>[]
@@ -361,16 +376,14 @@ export class Store<
361
376
  mutationsEvents = [firstMutationOrTxnFnOrOptions, ...restMutations]
362
377
  }
363
378
 
364
- mutationsEvents = mutationsEvents.filter((_) => !this.__processedMutationIds.has(_.id))
379
+ mutationsEvents = mutationsEvents.filter(
380
+ (_) => _.id === undefined || !MutableHashMap.has(this.unsyncedMutationEvents, Data.struct(_.id)),
381
+ )
365
382
 
366
383
  if (mutationsEvents.length === 0) {
367
384
  return
368
385
  }
369
386
 
370
- for (const mutationEvent of mutationsEvents) {
371
- this.__processedMutationIds.add(mutationEvent.id)
372
- }
373
-
374
387
  const label = options?.label ?? 'mutate'
375
388
  const skipRefresh = options?.skipRefresh ?? false
376
389
  const wasSyncMessage = options?.wasSyncMessage ?? false
@@ -385,7 +398,7 @@ export class Store<
385
398
 
386
399
  let durationMs: number
387
400
 
388
- return this.otel.tracer.startActiveSpan(
401
+ const res = this.otel.tracer.startActiveSpan(
389
402
  'LiveStore:mutate',
390
403
  { attributes: { 'livestore.mutateLabel': label } },
391
404
  this.otel.mutationsSpanContext,
@@ -465,6 +478,16 @@ export class Store<
465
478
  return { durationMs }
466
479
  },
467
480
  )
481
+
482
+ // NOTE we need to add the mutation events to the unsynced mutation events map only after running the code above
483
+ // so the short-circuiting in `mutateWithoutRefresh` doesn't kick in for those events
484
+ for (const mutationEvent of mutationsEvents) {
485
+ if (mutationEvent.id !== undefined) {
486
+ MutableHashMap.set(this.unsyncedMutationEvents, Data.struct(mutationEvent.id), mutationEvent)
487
+ }
488
+ }
489
+
490
+ return res
468
491
  }
469
492
 
470
493
  /**
@@ -492,19 +515,30 @@ export class Store<
492
515
  * the caller must refresh queries after calling this method.
493
516
  */
494
517
  mutateWithoutRefresh = (
495
- mutationEventDecoded: MutationEvent.ForSchema<TSchema>,
518
+ mutationEventDecoded_: MutationEvent.ForSchema<TSchema> | MutationEvent.PartialForSchema<TSchema>,
496
519
  options: {
497
520
  otelContext: otel.Context
498
521
  coordinatorMode: 'default' | 'skip-coordinator' | 'skip-persist'
499
522
  },
500
523
  ): { writeTables: ReadonlySet<string>; durationMs: number } => {
524
+ const mutationDef =
525
+ this.schema.mutations.get(mutationEventDecoded_.mutation) ??
526
+ shouldNeverHappen(`Unknown mutation type: ${mutationEventDecoded_.mutation}`)
527
+
528
+ const mutationEventDecoded: MutationEvent.ForSchema<TSchema> = mutationEventDecoded_.hasOwnProperty('id')
529
+ ? (mutationEventDecoded_ as MutationEvent.ForSchema<TSchema>)
530
+ : {
531
+ ...mutationEventDecoded_,
532
+ ...this.getNextMutationEventId({ localOnly: mutationDef.options.localOnly }),
533
+ }
534
+
501
535
  // NOTE we also need this temporary workaround here since some code-paths use `mutateWithoutRefresh` directly
502
536
  // e.g. the row-query functionality
503
- if (this.__processedMutationWithoutRefreshIds.has(mutationEventDecoded.id)) {
537
+ if (MutableHashMap.has(this.unsyncedMutationEvents, Data.struct(mutationEventDecoded.id))) {
504
538
  // NOTE this data should never be used
505
539
  return { writeTables: new Set(), durationMs: 0 }
506
540
  } else {
507
- this.__processedMutationWithoutRefreshIds.add(mutationEventDecoded.id)
541
+ MutableHashMap.set(this.unsyncedMutationEvents, Data.struct(mutationEventDecoded.id), mutationEventDecoded)
508
542
  }
509
543
 
510
544
  const { otelContext, coordinatorMode = 'default' } = options
@@ -524,10 +558,6 @@ export class Store<
524
558
  const allWriteTables = new Set<string>()
525
559
  let durationMsTotal = 0
526
560
 
527
- const mutationDef =
528
- this.schema.mutations.get(mutationEventDecoded.mutation) ??
529
- shouldNeverHappen(`Unknown mutation type: ${mutationEventDecoded.mutation}`)
530
-
531
561
  const execArgsArr = getExecArgsFromMutation({ mutationDef, mutationEventDecoded })
532
562
 
533
563
  for (const {
@@ -549,7 +579,7 @@ export class Store<
549
579
  // Asynchronously apply mutation to a persistent storage (we're not awaiting this promise here)
550
580
  this.adapter.coordinator
551
581
  .mutate(mutationEventEncoded as MutationEvent.AnyEncoded, { persisted: coordinatorMode !== 'skip-persist' })
552
- .pipe(Runtime.runFork(this.runtime))
582
+ .pipe(this.runEffectFork)
553
583
  }
554
584
 
555
585
  // Uncomment to print a list of queries currently registered on the store
@@ -568,7 +598,7 @@ export class Store<
568
598
  * This should only be used for framework-internal purposes;
569
599
  * all app writes should go through mutate.
570
600
  */
571
- execute = (
601
+ __execute = (
572
602
  query: string,
573
603
  params: ParamsObject = {},
574
604
  writeTables?: ReadonlySet<string>,
@@ -579,10 +609,21 @@ export class Store<
579
609
  this.adapter.coordinator.execute(query, prepareBindValues(params, query)).pipe(this.runEffectFork)
580
610
  }
581
611
 
582
- select = (query: string, params: ParamsObject = {}) => {
612
+ __select = (query: string, params: ParamsObject = {}) => {
583
613
  return this.syncDbWrapper.select(query, { bindValues: prepareBindValues(params, query) })
584
614
  }
585
615
 
616
+ private getNextMutationEventId = ({ localOnly }: { localOnly: boolean }): { id: EventId; parentId: EventId } => {
617
+ const id = this.adapter.coordinator.getNextMutationEventId({ localOnly }).pipe(Effect.runSync)
618
+ const parentId = localOnly
619
+ ? this.currentMutationEventIdRef.current
620
+ : { global: this.currentMutationEventIdRef.current.global, local: 0 }
621
+
622
+ this.currentMutationEventIdRef.current = id
623
+
624
+ return { id, parentId }
625
+ }
626
+
586
627
  private makeTableRef = (tableName: string) =>
587
628
  this.reactivityGraph.makeRef(null, {
588
629
  equal: () => false,
@@ -745,7 +786,13 @@ export const createStore = <
745
786
 
746
787
  const mutationEventSchema = makeMutationEventSchemaMemo(schema)
747
788
 
748
- const __processedMutationIds = new Set<string>()
789
+ // TODO get rid of this
790
+ // const __processedMutationIds = new Set<number>()
791
+
792
+ const currentMutationEventIdRef = { current: yield* adapter.coordinator.getCurrentMutationEventId }
793
+
794
+ // TODO fill up with unsynced mutation events from the coordinator
795
+ const unsyncedMutationEvents = MutableHashMap.empty<EventId, MutationEvent.ForSchema<TSchema>>()
749
796
 
750
797
  // TODO consider moving booting into the storage backend
751
798
  if (boot !== undefined) {
@@ -765,12 +812,24 @@ export const createStore = <
765
812
  }
766
813
  },
767
814
  mutate: (...list) => {
768
- for (const mutationEventDecoded of list) {
815
+ for (const mutationEventDecoded_ of list) {
769
816
  const mutationDef =
770
- schema.mutations.get(mutationEventDecoded.mutation) ??
771
- shouldNeverHappen(`Unknown mutation type: ${mutationEventDecoded.mutation}`)
817
+ schema.mutations.get(mutationEventDecoded_.mutation) ??
818
+ shouldNeverHappen(`Unknown mutation type: ${mutationEventDecoded_.mutation}`)
819
+
820
+ const parentId = mutationDef.options.localOnly
821
+ ? currentMutationEventIdRef.current
822
+ : { global: currentMutationEventIdRef.current.global, local: 0 }
823
+
824
+ currentMutationEventIdRef.current = adapter.coordinator
825
+ .getNextMutationEventId({ localOnly: mutationDef.options.localOnly })
826
+ .pipe(Effect.runSync)
827
+
828
+ const mutationEventDecoded = { ...mutationEventDecoded_, id: currentMutationEventIdRef.current, parentId }
829
+
830
+ MutableHashMap.set(unsyncedMutationEvents, Data.struct(mutationEventDecoded.id), mutationEventDecoded)
772
831
 
773
- __processedMutationIds.add(mutationEventDecoded.id)
832
+ // __processedMutationIds.add(mutationEventDecoded.id.global)
774
833
 
775
834
  const execArgsArr = getExecArgsFromMutation({ mutationDef, mutationEventDecoded })
776
835
  // const { bindValues, statementSql } = getExecArgsFromMutation({ mutationDef, mutationEventDecoded })
@@ -828,7 +887,8 @@ export const createStore = <
828
887
  otelOptions: { tracer: otelTracer, rootSpanContext: otelRootSpanContext },
829
888
  reactivityGraph,
830
889
  disableDevtools,
831
- __processedMutationIds,
890
+ currentMutationEventIdRef,
891
+ unsyncedMutationEvents,
832
892
  fiberSet,
833
893
  runtime,
834
894
  batchUpdates: batchUpdates ?? ((run) => run()),