@livestore/livestore 0.0.58-dev.6 → 0.0.58-dev.8

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 (44) hide show
  1. package/dist/.tsbuildinfo +1 -1
  2. package/dist/effect/LiveStore.js +1 -1
  3. package/dist/effect/LiveStore.js.map +1 -1
  4. package/dist/react/LiveStoreProvider.js +2 -2
  5. package/dist/react/LiveStoreProvider.js.map +1 -1
  6. package/dist/react/useLocalId.d.ts.map +1 -1
  7. package/dist/react/useLocalId.js +1 -0
  8. package/dist/react/useLocalId.js.map +1 -1
  9. package/dist/react/useQuery.d.ts.map +1 -1
  10. package/dist/react/useQuery.js +1 -0
  11. package/dist/react/useQuery.js.map +1 -1
  12. package/dist/react/useRow.test.js +5 -359
  13. package/dist/react/useRow.test.js.map +1 -1
  14. package/dist/react/useTemporaryQuery.d.ts.map +1 -1
  15. package/dist/react/useTemporaryQuery.js +12 -7
  16. package/dist/react/useTemporaryQuery.js.map +1 -1
  17. package/dist/react/useTemporaryQuery.test.js +23 -1
  18. package/dist/react/useTemporaryQuery.test.js.map +1 -1
  19. package/dist/reactiveQueries/sql.d.ts.map +1 -1
  20. package/dist/reactiveQueries/sql.js +2 -2
  21. package/dist/reactiveQueries/sql.js.map +1 -1
  22. package/dist/store-devtools.js +1 -1
  23. package/dist/store-devtools.js.map +1 -1
  24. package/dist/store.d.ts +17 -13
  25. package/dist/store.d.ts.map +1 -1
  26. package/dist/store.js +71 -38
  27. package/dist/store.js.map +1 -1
  28. package/dist/utils/dev.d.ts.map +1 -1
  29. package/dist/utils/dev.js +1 -0
  30. package/dist/utils/dev.js.map +1 -1
  31. package/package.json +14 -13
  32. package/src/ambient.d.ts +3 -0
  33. package/src/effect/LiveStore.ts +1 -1
  34. package/src/react/LiveStoreProvider.tsx +2 -2
  35. package/src/react/__snapshots__/useRow.test.tsx.snap +359 -0
  36. package/src/react/useLocalId.ts +1 -0
  37. package/src/react/useQuery.ts +1 -0
  38. package/src/react/useRow.test.tsx +5 -359
  39. package/src/react/useTemporaryQuery.test.tsx +44 -2
  40. package/src/react/useTemporaryQuery.ts +23 -13
  41. package/src/reactiveQueries/sql.ts +2 -2
  42. package/src/store-devtools.ts +1 -1
  43. package/src/store.ts +111 -49
  44. package/src/utils/dev.ts +1 -0
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
@@ -125,7 +130,9 @@ export type StoreMutateOptions = {
125
130
  persisted?: boolean
126
131
  }
127
132
 
128
- if (typeof window !== 'undefined') {
133
+ // eslint-disable-next-line unicorn/prefer-global-this
134
+ if (import.meta.env.DEV && typeof window !== 'undefined') {
135
+ // eslint-disable-next-line unicorn/prefer-global-this
129
136
  window.__debugDownloadBlob = downloadBlob
130
137
  }
131
138
 
@@ -133,7 +140,7 @@ export class Store<
133
140
  TGraphQLContext extends BaseGraphQLContext = BaseGraphQLContext,
134
141
  TSchema extends LiveStoreSchema = LiveStoreSchema,
135
142
  > extends Inspectable.Class {
136
- id = uniqueStoreId()
143
+ readonly storeId: string
137
144
  reactivityGraph: ReactivityGraph
138
145
  syncDbWrapper: SynchronousDatabaseWrapper
139
146
  adapter: StoreAdapter
@@ -150,15 +157,15 @@ export class Store<
150
157
  private fiberSet: FiberSet.FiberSet
151
158
  private runtime: Runtime.Runtime<Scope.Scope>
152
159
 
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
160
  /** RC-based set to see which queries are currently subscribed to */
158
161
  activeQueries: ReferenceCountedSet<LiveQuery<any>>
159
162
 
163
+ // NOTE this is currently exposed for the Devtools databrowser to emit mutation events
160
164
  readonly __mutationEventSchema
161
165
 
166
+ private currentMutationEventIdRef
167
+ private unsyncedMutationEvents
168
+
162
169
  // #region constructor
163
170
  private constructor({
164
171
  adapter,
@@ -168,12 +175,19 @@ export class Store<
168
175
  otelOptions,
169
176
  disableDevtools,
170
177
  batchUpdates,
171
- __processedMutationIds,
178
+ currentMutationEventIdRef,
179
+ unsyncedMutationEvents,
180
+ storeId,
172
181
  fiberSet,
173
182
  runtime,
174
183
  }: StoreOptions<TGraphQLContext, TSchema>) {
175
184
  super()
176
185
 
186
+ this.storeId = storeId
187
+
188
+ this.currentMutationEventIdRef = currentMutationEventIdRef
189
+ this.unsyncedMutationEvents = unsyncedMutationEvents
190
+
177
191
  this.syncDbWrapper = new SynchronousDatabaseWrapper({ otel: otelOptions, db: adapter.syncDb })
178
192
  this.adapter = adapter
179
193
  this.schema = schema
@@ -184,9 +198,6 @@ export class Store<
184
198
  // TODO refactor
185
199
  this.__mutationEventSchema = makeMutationEventSchemaMemo(schema)
186
200
 
187
- // TODO remove this temporary solution and find a better way to avoid re-processing the same mutation
188
- this.__processedMutationIds = __processedMutationIds
189
-
190
201
  // TODO generalize the `tableRefs` concept to allow finer-grained refs
191
202
  this.tableRefs = {}
192
203
  this.activeQueries = new ReferenceCountedSet()
@@ -222,7 +233,7 @@ export class Store<
222
233
  isRunningInDevtools
223
234
  ? this.schema.tables.keys()
224
235
  : Array.from(this.schema.tables.keys()).filter(
225
- (_) => _ !== SCHEMA_META_TABLE && _ !== SCHEMA_MUTATIONS_META_TABLE,
236
+ (_) => _ !== SCHEMA_META_TABLE && _ !== SCHEMA_MUTATIONS_META_TABLE && _ !== SESSION_CHANGESET_META_TABLE,
226
237
  ),
227
238
  )
228
239
  const existingTableRefs = new Map(
@@ -241,9 +252,11 @@ export class Store<
241
252
 
242
253
  Effect.gen(this, function* () {
243
254
  yield* this.adapter.coordinator.syncMutations.pipe(
244
- Stream.tapSync((mutationEventDecoded) => {
245
- this.mutate({ wasSyncMessage: true }, mutationEventDecoded)
246
- }),
255
+ Stream.tapChunk((mutationsEventsDecodedChunk) =>
256
+ Effect.sync(() => {
257
+ this.mutate({ wasSyncMessage: true }, ...mutationsEventsDecodedChunk)
258
+ }),
259
+ ),
247
260
  Stream.runDrain,
248
261
  Effect.interruptible,
249
262
  Effect.withSpan('LiveStore:syncMutations'),
@@ -297,7 +310,7 @@ export class Store<
297
310
  { attributes: { label: options?.label, queryLabel: query$.label } },
298
311
  options?.otelContext ?? this.otel.queriesSpanContext,
299
312
  (span) => {
300
- // console.log('store sub', query$.label)
313
+ // console.debug('store sub', query$.id, query$.label)
301
314
  const otelContext = otel.trace.setSpan(otel.context.active(), span)
302
315
 
303
316
  const label = `subscribe:${options?.label}`
@@ -311,7 +324,7 @@ export class Store<
311
324
  }
312
325
 
313
326
  const unsubscribe = () => {
314
- // console.log('store unsub', query$.label)
327
+ // console.debug('store unsub', query$.id, query$.label)
315
328
  try {
316
329
  this.reactivityGraph.destroyNode(effect)
317
330
  this.activeQueries.remove(query$ as LiveQuery<TResult>)
@@ -327,17 +340,21 @@ export class Store<
327
340
 
328
341
  // #region mutate
329
342
  mutate: {
330
- <const TMutationArg extends ReadonlyArray<MutationEvent.ForSchema<TSchema>>>(...list: TMutationArg): void
343
+ <const TMutationArg extends ReadonlyArray<MutationEvent.PartialForSchema<TSchema>>>(...list: TMutationArg): void
331
344
  (
332
- txn: <const TMutationArg extends ReadonlyArray<MutationEvent.ForSchema<TSchema>>>(...list: TMutationArg) => void,
345
+ txn: <const TMutationArg extends ReadonlyArray<MutationEvent.PartialForSchema<TSchema>>>(
346
+ ...list: TMutationArg
347
+ ) => void,
333
348
  ): void
334
- <const TMutationArg extends ReadonlyArray<MutationEvent.ForSchema<TSchema>>>(
349
+ <const TMutationArg extends ReadonlyArray<MutationEvent.PartialForSchema<TSchema>>>(
335
350
  options: StoreMutateOptions,
336
351
  ...list: TMutationArg
337
352
  ): void
338
353
  (
339
354
  options: StoreMutateOptions,
340
- 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,
341
358
  ): void
342
359
  } = (firstMutationOrTxnFnOrOptions: any, ...restMutations: any[]) => {
343
360
  let mutationsEvents: MutationEvent.ForSchema<TSchema>[]
@@ -361,16 +378,14 @@ export class Store<
361
378
  mutationsEvents = [firstMutationOrTxnFnOrOptions, ...restMutations]
362
379
  }
363
380
 
364
- mutationsEvents = mutationsEvents.filter((_) => !this.__processedMutationIds.has(_.id))
381
+ mutationsEvents = mutationsEvents.filter(
382
+ (_) => _.id === undefined || !MutableHashMap.has(this.unsyncedMutationEvents, Data.struct(_.id)),
383
+ )
365
384
 
366
385
  if (mutationsEvents.length === 0) {
367
386
  return
368
387
  }
369
388
 
370
- for (const mutationEvent of mutationsEvents) {
371
- this.__processedMutationIds.add(mutationEvent.id)
372
- }
373
-
374
389
  const label = options?.label ?? 'mutate'
375
390
  const skipRefresh = options?.skipRefresh ?? false
376
391
  const wasSyncMessage = options?.wasSyncMessage ?? false
@@ -380,12 +395,12 @@ export class Store<
380
395
  mutationsSpan.addEvent('mutate')
381
396
 
382
397
  // console.group('LiveStore.mutate', { skipRefresh, wasSyncMessage, label })
383
- // mutationsEvents.forEach((_) => console.log(_.mutation, _.id, _.args))
398
+ // mutationsEvents.forEach((_) => console.debug(_.mutation, _.id, _.args))
384
399
  // console.groupEnd()
385
400
 
386
401
  let durationMs: number
387
402
 
388
- return this.otel.tracer.startActiveSpan(
403
+ const res = this.otel.tracer.startActiveSpan(
389
404
  'LiveStore:mutate',
390
405
  { attributes: { 'livestore.mutateLabel': label } },
391
406
  this.otel.mutationsSpanContext,
@@ -465,6 +480,16 @@ export class Store<
465
480
  return { durationMs }
466
481
  },
467
482
  )
483
+
484
+ // NOTE we need to add the mutation events to the unsynced mutation events map only after running the code above
485
+ // so the short-circuiting in `mutateWithoutRefresh` doesn't kick in for those events
486
+ for (const mutationEvent of mutationsEvents) {
487
+ if (mutationEvent.id !== undefined) {
488
+ MutableHashMap.set(this.unsyncedMutationEvents, Data.struct(mutationEvent.id), mutationEvent)
489
+ }
490
+ }
491
+
492
+ return res
468
493
  }
469
494
 
470
495
  /**
@@ -492,19 +517,30 @@ export class Store<
492
517
  * the caller must refresh queries after calling this method.
493
518
  */
494
519
  mutateWithoutRefresh = (
495
- mutationEventDecoded: MutationEvent.ForSchema<TSchema>,
520
+ mutationEventDecoded_: MutationEvent.ForSchema<TSchema> | MutationEvent.PartialForSchema<TSchema>,
496
521
  options: {
497
522
  otelContext: otel.Context
498
523
  coordinatorMode: 'default' | 'skip-coordinator' | 'skip-persist'
499
524
  },
500
525
  ): { writeTables: ReadonlySet<string>; durationMs: number } => {
526
+ const mutationDef =
527
+ this.schema.mutations.get(mutationEventDecoded_.mutation) ??
528
+ shouldNeverHappen(`Unknown mutation type: ${mutationEventDecoded_.mutation}`)
529
+
530
+ const mutationEventDecoded: MutationEvent.ForSchema<TSchema> = mutationEventDecoded_.hasOwnProperty('id')
531
+ ? (mutationEventDecoded_ as MutationEvent.ForSchema<TSchema>)
532
+ : {
533
+ ...mutationEventDecoded_,
534
+ ...this.getNextMutationEventId({ localOnly: mutationDef.options.localOnly }),
535
+ }
536
+
501
537
  // NOTE we also need this temporary workaround here since some code-paths use `mutateWithoutRefresh` directly
502
538
  // e.g. the row-query functionality
503
- if (this.__processedMutationWithoutRefreshIds.has(mutationEventDecoded.id)) {
539
+ if (MutableHashMap.has(this.unsyncedMutationEvents, Data.struct(mutationEventDecoded.id))) {
504
540
  // NOTE this data should never be used
505
541
  return { writeTables: new Set(), durationMs: 0 }
506
542
  } else {
507
- this.__processedMutationWithoutRefreshIds.add(mutationEventDecoded.id)
543
+ MutableHashMap.set(this.unsyncedMutationEvents, Data.struct(mutationEventDecoded.id), mutationEventDecoded)
508
544
  }
509
545
 
510
546
  const { otelContext, coordinatorMode = 'default' } = options
@@ -524,10 +560,6 @@ export class Store<
524
560
  const allWriteTables = new Set<string>()
525
561
  let durationMsTotal = 0
526
562
 
527
- const mutationDef =
528
- this.schema.mutations.get(mutationEventDecoded.mutation) ??
529
- shouldNeverHappen(`Unknown mutation type: ${mutationEventDecoded.mutation}`)
530
-
531
563
  const execArgsArr = getExecArgsFromMutation({ mutationDef, mutationEventDecoded })
532
564
 
533
565
  for (const {
@@ -549,7 +581,7 @@ export class Store<
549
581
  // Asynchronously apply mutation to a persistent storage (we're not awaiting this promise here)
550
582
  this.adapter.coordinator
551
583
  .mutate(mutationEventEncoded as MutationEvent.AnyEncoded, { persisted: coordinatorMode !== 'skip-persist' })
552
- .pipe(Runtime.runFork(this.runtime))
584
+ .pipe(this.runEffectFork)
553
585
  }
554
586
 
555
587
  // Uncomment to print a list of queries currently registered on the store
@@ -568,7 +600,7 @@ export class Store<
568
600
  * This should only be used for framework-internal purposes;
569
601
  * all app writes should go through mutate.
570
602
  */
571
- execute = (
603
+ __execute = (
572
604
  query: string,
573
605
  params: ParamsObject = {},
574
606
  writeTables?: ReadonlySet<string>,
@@ -579,10 +611,21 @@ export class Store<
579
611
  this.adapter.coordinator.execute(query, prepareBindValues(params, query)).pipe(this.runEffectFork)
580
612
  }
581
613
 
582
- select = (query: string, params: ParamsObject = {}) => {
614
+ __select = (query: string, params: ParamsObject = {}) => {
583
615
  return this.syncDbWrapper.select(query, { bindValues: prepareBindValues(params, query) })
584
616
  }
585
617
 
618
+ private getNextMutationEventId = ({ localOnly }: { localOnly: boolean }): { id: EventId; parentId: EventId } => {
619
+ const id = this.adapter.coordinator.getNextMutationEventId({ localOnly }).pipe(Effect.runSync)
620
+ const parentId = localOnly
621
+ ? this.currentMutationEventIdRef.current
622
+ : { global: this.currentMutationEventIdRef.current.global, local: 0 }
623
+
624
+ this.currentMutationEventIdRef.current = id
625
+
626
+ return { id, parentId }
627
+ }
628
+
586
629
  private makeTableRef = (tableName: string) =>
587
630
  this.reactivityGraph.makeRef(null, {
588
631
  equal: () => false,
@@ -745,7 +788,13 @@ export const createStore = <
745
788
 
746
789
  const mutationEventSchema = makeMutationEventSchemaMemo(schema)
747
790
 
748
- const __processedMutationIds = new Set<string>()
791
+ // TODO get rid of this
792
+ // const __processedMutationIds = new Set<number>()
793
+
794
+ const currentMutationEventIdRef = { current: yield* adapter.coordinator.getCurrentMutationEventId }
795
+
796
+ // TODO fill up with unsynced mutation events from the coordinator
797
+ const unsyncedMutationEvents = MutableHashMap.empty<EventId, MutationEvent.ForSchema<TSchema>>()
749
798
 
750
799
  // TODO consider moving booting into the storage backend
751
800
  if (boot !== undefined) {
@@ -765,12 +814,24 @@ export const createStore = <
765
814
  }
766
815
  },
767
816
  mutate: (...list) => {
768
- for (const mutationEventDecoded of list) {
817
+ for (const mutationEventDecoded_ of list) {
769
818
  const mutationDef =
770
- schema.mutations.get(mutationEventDecoded.mutation) ??
771
- shouldNeverHappen(`Unknown mutation type: ${mutationEventDecoded.mutation}`)
819
+ schema.mutations.get(mutationEventDecoded_.mutation) ??
820
+ shouldNeverHappen(`Unknown mutation type: ${mutationEventDecoded_.mutation}`)
821
+
822
+ const parentId = mutationDef.options.localOnly
823
+ ? currentMutationEventIdRef.current
824
+ : { global: currentMutationEventIdRef.current.global, local: 0 }
825
+
826
+ currentMutationEventIdRef.current = adapter.coordinator
827
+ .getNextMutationEventId({ localOnly: mutationDef.options.localOnly })
828
+ .pipe(Effect.runSync)
829
+
830
+ const mutationEventDecoded = { ...mutationEventDecoded_, id: currentMutationEventIdRef.current, parentId }
831
+
832
+ MutableHashMap.set(unsyncedMutationEvents, Data.struct(mutationEventDecoded.id), mutationEventDecoded)
772
833
 
773
- __processedMutationIds.add(mutationEventDecoded.id)
834
+ // __processedMutationIds.add(mutationEventDecoded.id.global)
774
835
 
775
836
  const execArgsArr = getExecArgsFromMutation({ mutationDef, mutationEventDecoded })
776
837
  // const { bindValues, statementSql } = getExecArgsFromMutation({ mutationDef, mutationEventDecoded })
@@ -828,7 +889,8 @@ export const createStore = <
828
889
  otelOptions: { tracer: otelTracer, rootSpanContext: otelRootSpanContext },
829
890
  reactivityGraph,
830
891
  disableDevtools,
831
- __processedMutationIds,
892
+ currentMutationEventIdRef,
893
+ unsyncedMutationEvents,
832
894
  fiberSet,
833
895
  runtime,
834
896
  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,