@livestore/livestore 0.4.0-dev.1 → 0.4.0-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 (58) hide show
  1. package/dist/.tsbuildinfo +1 -1
  2. package/dist/effect/LiveStore.d.ts.map +1 -1
  3. package/dist/effect/LiveStore.js +2 -4
  4. package/dist/effect/LiveStore.js.map +1 -1
  5. package/dist/live-queries/db-query.d.ts.map +1 -1
  6. package/dist/live-queries/db-query.js +7 -4
  7. package/dist/live-queries/db-query.js.map +1 -1
  8. package/dist/live-queries/db-query.test.js +13 -7
  9. package/dist/live-queries/db-query.test.js.map +1 -1
  10. package/dist/mod.d.ts +1 -1
  11. package/dist/mod.d.ts.map +1 -1
  12. package/dist/mod.js +1 -1
  13. package/dist/mod.js.map +1 -1
  14. package/dist/reactive.d.ts +10 -10
  15. package/dist/reactive.d.ts.map +1 -1
  16. package/dist/reactive.js +36 -27
  17. package/dist/reactive.js.map +1 -1
  18. package/dist/reactive.test.js +115 -0
  19. package/dist/reactive.test.js.map +1 -1
  20. package/dist/store/create-store.d.ts.map +1 -1
  21. package/dist/store/create-store.js +3 -3
  22. package/dist/store/create-store.js.map +1 -1
  23. package/dist/store/store-types.d.ts +2 -2
  24. package/dist/store/store-types.d.ts.map +1 -1
  25. package/dist/store/store-types.js.map +1 -1
  26. package/dist/store/store.d.ts +21 -2
  27. package/dist/store/store.d.ts.map +1 -1
  28. package/dist/store/store.js +134 -88
  29. package/dist/store/store.js.map +1 -1
  30. package/dist/utils/dev.d.ts +3 -0
  31. package/dist/utils/dev.d.ts.map +1 -1
  32. package/dist/utils/dev.js.map +1 -1
  33. package/dist/utils/tests/fixture.d.ts.map +1 -1
  34. package/dist/utils/tests/fixture.js +2 -1
  35. package/dist/utils/tests/fixture.js.map +1 -1
  36. package/dist/utils/tests/otel.d.ts +15 -14
  37. package/dist/utils/tests/otel.d.ts.map +1 -1
  38. package/dist/utils/tests/otel.js +20 -15
  39. package/dist/utils/tests/otel.js.map +1 -1
  40. package/package.json +7 -7
  41. package/src/ambient.d.ts +3 -3
  42. package/src/effect/LiveStore.ts +2 -4
  43. package/src/live-queries/__snapshots__/db-query.test.ts.snap +268 -131
  44. package/src/live-queries/db-query.test.ts +13 -7
  45. package/src/live-queries/db-query.ts +7 -4
  46. package/src/mod.ts +2 -0
  47. package/src/reactive.test.ts +150 -1
  48. package/src/reactive.ts +47 -39
  49. package/src/store/create-store.ts +12 -4
  50. package/src/store/store-types.ts +5 -2
  51. package/src/store/store.ts +204 -145
  52. package/src/utils/dev.ts +5 -0
  53. package/src/utils/tests/fixture.ts +2 -1
  54. package/src/utils/tests/otel.ts +31 -20
  55. package/dist/store/store-shutdown.test.d.ts +0 -2
  56. package/dist/store/store-shutdown.test.d.ts.map +0 -1
  57. package/dist/store/store-shutdown.test.js +0 -103
  58. package/dist/store/store-shutdown.test.js.map +0 -1
@@ -9,7 +9,7 @@ import {
9
9
  SessionIdSymbol,
10
10
  UnexpectedError,
11
11
  } from '@livestore/common'
12
- import { deepEqual, shouldNeverHappen } from '@livestore/utils'
12
+ import { deepEqual, omitUndefineds, shouldNeverHappen } from '@livestore/utils'
13
13
  import { Equal, Hash, Predicate, Schema, TreeFormatter } from '@livestore/utils/effect'
14
14
  import * as otel from '@opentelemetry/api'
15
15
 
@@ -124,9 +124,8 @@ export const queryDb: {
124
124
  reactivityGraph: ctx.reactivityGraph.deref()!,
125
125
  queryInput,
126
126
  label,
127
- map: options?.map,
128
- otelContext,
129
127
  def,
128
+ ...omitUndefineds({ map: options?.map, otelContext }),
130
129
  })
131
130
  }),
132
131
  label,
@@ -429,7 +428,11 @@ Result:`,
429
428
  return result
430
429
  },
431
430
  ),
432
- { label: `${label}:results`, meta: { liveStoreThunkType: 'db.result' }, equal: resultsEqual },
431
+ {
432
+ label: `${label}:results`,
433
+ meta: { liveStoreThunkType: 'db.result' },
434
+ ...omitUndefineds({ equal: resultsEqual }),
435
+ },
433
436
  )
434
437
 
435
438
  this.results$ = results$
package/src/mod.ts CHANGED
@@ -4,6 +4,7 @@ export {
4
4
  type BootStatus,
5
5
  type DebugInfo,
6
6
  IntentionalShutdownCause,
7
+ liveStoreVersion,
7
8
  type MutableDebugInfo,
8
9
  type PreparedBindValues,
9
10
  prepareBindValues,
@@ -14,6 +15,7 @@ export {
14
15
  SessionIdSymbol,
15
16
  type SqliteDb,
16
17
  StoreInterrupted,
18
+ type SyncState,
17
19
  sql,
18
20
  } from '@livestore/common'
19
21
  export * from '@livestore/common/schema'
@@ -1,5 +1,5 @@
1
1
  import { describe, expect, it } from 'vitest'
2
-
2
+ import type { DebugRefreshReasonBase, DebugThunkInfo } from './reactive.ts'
3
3
  import { ReactiveGraph } from './reactive.ts'
4
4
 
5
5
  describe('a trivial graph', () => {
@@ -424,3 +424,152 @@ describe('error handling', () => {
424
424
  )
425
425
  })
426
426
  })
427
+
428
+ // Bug: When an effect calls setRef during execution, it triggers nested runEffects calls.
429
+ // The nested call would overwrite and clear the shared currentDebugRefresh state,
430
+ // causing a TypeError when the outer runEffects tried to access currentDebugRefresh.refreshedAtoms.
431
+ // Fix: Use local variables to capture debug state instead of relying on shared mutable state.
432
+ describe('bug fix: currentDebugRefresh race condition', () => {
433
+ it('keeps the debug refresh context when nested effect runs are triggered', () => {
434
+ type TestRefreshReason = DebugRefreshReasonBase | { _tag: 'outer' } | { _tag: 'nested' }
435
+
436
+ const graph = new ReactiveGraph<TestRefreshReason, DebugThunkInfo>()
437
+ graph.context = {}
438
+
439
+ const triggerRef = graph.makeRef(0, { label: 'trigger' })
440
+ const responseRef = graph.makeRef(0, { label: 'response' })
441
+
442
+ const triggerThunk = graph.makeThunk((get) => get(triggerRef), { label: 'triggerThunk' })
443
+ const laterThunk = graph.makeThunk((get) => get(triggerRef) + 1, { label: 'laterThunk' })
444
+ const responseThunk = graph.makeThunk((get) => get(responseRef), { label: 'responseThunk' })
445
+
446
+ const nestedEffect = graph.makeEffect((get) => {
447
+ get(responseThunk)
448
+ })
449
+
450
+ let observedLater: number | undefined
451
+ const outerEffect = graph.makeEffect((get) => {
452
+ const current = get(triggerThunk)
453
+
454
+ if (current !== 0) {
455
+ graph.setRef(responseRef, current, {
456
+ debugRefreshReason: { _tag: 'nested' },
457
+ })
458
+ }
459
+
460
+ observedLater = get(laterThunk)
461
+ })
462
+
463
+ nestedEffect.doEffect()
464
+ outerEffect.doEffect()
465
+ graph.debugRefreshInfos.clear()
466
+
467
+ graph.setRef(triggerRef, 1, { debugRefreshReason: { _tag: 'outer' } })
468
+
469
+ expect(observedLater).toBe(2)
470
+
471
+ const refreshInfos = Array.from(graph.debugRefreshInfos)
472
+ const outerRefresh = refreshInfos.find((info) => info.reason._tag === 'outer')
473
+ expect(outerRefresh).toBeDefined()
474
+ const refreshedLabels = outerRefresh!.refreshedAtoms.map((atomInfo) => atomInfo.atom.label)
475
+ expect(refreshedLabels).toContain('triggerThunk')
476
+ expect(refreshedLabels).toContain('laterThunk')
477
+
478
+ const makeThunkRefreshes = refreshInfos.filter((info) => info.reason._tag === 'makeThunk')
479
+ expect(makeThunkRefreshes).toHaveLength(0)
480
+ })
481
+
482
+ it('handles nested runEffects from effect calling setRef', () => {
483
+ const graph = new ReactiveGraph()
484
+ graph.context = {}
485
+
486
+ const a = graph.makeRef(1)
487
+ const b = graph.makeRef(2)
488
+
489
+ // Effect that calls setRef, triggering nested runEffects
490
+ graph
491
+ .makeEffect((get) => {
492
+ get(a)
493
+ graph.setRef(b, 3)
494
+ })
495
+ .doEffect()
496
+
497
+ // Effect observing b
498
+ graph.makeEffect((get) => get(b)).doEffect()
499
+
500
+ // Previously crashed with: Cannot read properties of undefined (reading 'refreshedAtoms')
501
+ // Now handles nested runEffects correctly
502
+ expect(() => graph.setRef(a, 2)).not.toThrow()
503
+ })
504
+
505
+ it('handles thunk calling setRef directly', () => {
506
+ const graph = new ReactiveGraph()
507
+ graph.context = {}
508
+
509
+ const a = graph.makeRef(1)
510
+ const b = graph.makeRef(2)
511
+
512
+ // Effect observing b so setRef(b) triggers runEffects
513
+ graph.makeEffect((get) => get(b)).doEffect()
514
+
515
+ // Thunk that calls setRef during its computation
516
+ const thunk = graph.makeThunk((get) => {
517
+ const val = get(a)
518
+ graph.setRef(b, val * 2) // This triggers nested runEffects
519
+ return val + get(b)
520
+ })
521
+
522
+ // With our fix, this handles nested currentDebugRefresh correctly
523
+ expect(() => thunk.computeResult()).not.toThrow()
524
+ expect(thunk.computeResult()).toBe(3) // 1 + 2 (b was already set to 2)
525
+ })
526
+
527
+ it('handles nested thunk computations', () => {
528
+ const graph = new ReactiveGraph()
529
+ graph.context = {}
530
+
531
+ const a = graph.makeRef(1)
532
+
533
+ // Outer thunk that creates and computes inner thunk
534
+ const outerThunk = graph.makeThunk((get) => {
535
+ const val = get(a)
536
+
537
+ // Create and compute inner thunk during outer computation
538
+ const innerThunk = graph.makeThunk((innerGet) => innerGet(a) * 2)
539
+
540
+ // Nested thunk computation - previously could corrupt currentDebugRefresh
541
+ return val + innerThunk.computeResult()
542
+ })
543
+
544
+ // With our fix, nested thunk computations work correctly
545
+ expect(() => outerThunk.computeResult()).not.toThrow()
546
+ expect(outerThunk.computeResult()).toBe(3) // 1 + (1 * 2)
547
+ })
548
+ })
549
+
550
+ // Bug: Nodes could have undefined or deleted super/sub properties in certain edge cases,
551
+ // causing crashes with "Cannot read properties of undefined" errors.
552
+ // Fix: Added validation checks in destroyNode to handle corrupted nodes gracefully.
553
+ // Note: Full addEdge protection was removed, so some scenarios still crash with native errors.
554
+ describe('bug fix: node corruption protection', () => {
555
+ it('handles node destruction during effect execution', () => {
556
+ const graph = new ReactiveGraph()
557
+ graph.context = {}
558
+
559
+ const a = graph.makeRef(1)
560
+ const thunk1 = graph.makeThunk((get) => get(a) * 2)
561
+ const thunk2 = graph.makeThunk((get) => get(thunk1))
562
+
563
+ let firstRun = true
564
+ const effect = graph.makeEffect((get) => {
565
+ if (firstRun) {
566
+ firstRun = false
567
+ graph.destroyNode(thunk1) // Destroy dependency mid-execution
568
+ }
569
+ get(thunk2)
570
+ })
571
+
572
+ // Should detect the destroyed node
573
+ expect(() => effect.doEffect()).toThrow('LiveStore Error: Attempted to compute destroyed')
574
+ })
575
+ })
package/src/reactive.ts CHANGED
@@ -22,7 +22,7 @@
22
22
  // - At every thunk we check value equality with the previous value and cutoff propagation if possible.
23
23
 
24
24
  import { BoundArray } from '@livestore/common'
25
- import { deepEqual, shouldNeverHappen } from '@livestore/utils'
25
+ import { deepEqual, omitUndefineds, shouldNeverHappen } from '@livestore/utils'
26
26
  import type { Types } from '@livestore/utils/effect'
27
27
  import type * as otel from '@opentelemetry/api'
28
28
  // import { getDurationMsFromSpan } from './otel.ts'
@@ -45,9 +45,9 @@ export type Ref<T, TContext, TDebugRefreshReason extends DebugRefreshReason> = {
45
45
  computeResult: () => T
46
46
  sub: Set<Atom<any, TContext, TDebugRefreshReason>> // always empty
47
47
  super: Set<Thunk<any, TContext, TDebugRefreshReason> | Effect<TDebugRefreshReason>>
48
- label?: string
48
+ label?: string | undefined
49
49
  /** Container for meta information (e.g. the LiveStore Store) */
50
- meta?: any
50
+ meta?: any | undefined
51
51
  equal: (a: T, b: T) => boolean
52
52
  refreshes: number
53
53
  }
@@ -61,9 +61,9 @@ export type Thunk<TResult, TContext, TDebugRefreshReason extends DebugRefreshRea
61
61
  previousResult: TResult | NOT_REFRESHED_YET
62
62
  sub: Set<Atom<any, TContext, TDebugRefreshReason>>
63
63
  super: Set<Thunk<any, TContext, TDebugRefreshReason> | Effect<TDebugRefreshReason>>
64
- label?: string
64
+ label?: string | undefined
65
65
  /** Container for meta information (e.g. the LiveStore Store) */
66
- meta?: any
66
+ meta?: any | undefined
67
67
  equal: (a: TResult, b: TResult) => boolean
68
68
  recomputations: number
69
69
 
@@ -80,7 +80,7 @@ export type Effect<TDebugRefreshReason extends DebugRefreshReason> = {
80
80
  isDestroyed: boolean
81
81
  doEffect: (otelContext?: otel.Context | undefined, debugRefreshReason?: TDebugRefreshReason | undefined) => void
82
82
  sub: Set<Atom<any, TODO, TODO>>
83
- label?: string
83
+ label?: string | undefined
84
84
  invocations: number
85
85
  }
86
86
 
@@ -103,10 +103,10 @@ export type DebugRefreshReasonBase =
103
103
  /** Usually in response to some `commit` calls with `skipRefresh: true` */
104
104
  | {
105
105
  _tag: 'runDeferredEffects'
106
- originalRefreshReasons?: ReadonlyArray<DebugRefreshReasonBase>
107
- manualRefreshReason?: DebugRefreshReasonBase
106
+ originalRefreshReasons?: ReadonlyArray<DebugRefreshReasonBase> | undefined
107
+ manualRefreshReason?: DebugRefreshReasonBase | undefined
108
108
  }
109
- | { _tag: 'makeThunk'; label?: string }
109
+ | { _tag: 'makeThunk'; label?: string | undefined }
110
110
  | { _tag: 'unknown' }
111
111
 
112
112
  export type DebugRefreshReason<T extends string = string> = DebugRefreshReasonBase | { _tag: T }
@@ -135,7 +135,7 @@ const unknownRefreshReason = () => {
135
135
  return { _tag: 'unknown' as const }
136
136
  }
137
137
 
138
- export type EncodedOption<A> = { _tag: 'Some'; value?: A } | { _tag: 'None' }
138
+ export type EncodedOption<A> = { _tag: 'Some'; value?: A | undefined } | { _tag: 'None' }
139
139
  const encodedOptionSome = <A>(value: A): EncodedOption<A> => ({ _tag: 'Some', value })
140
140
  const encodedOptionNone = <A>(): EncodedOption<A> => ({ _tag: 'None' })
141
141
 
@@ -192,7 +192,7 @@ export const __resetIds = () => {
192
192
  export class ReactiveGraph<
193
193
  TDebugRefreshReason extends DebugRefreshReason,
194
194
  TDebugThunkInfo extends DebugThunkInfo,
195
- TContext extends { effectsWrapper?: (runEffects: () => void) => void } = {},
195
+ TContext extends { effectsWrapper?: ((runEffects: () => void) => void) | undefined } = {},
196
196
  > {
197
197
  id = uniqueGraphId()
198
198
 
@@ -211,10 +211,8 @@ export class ReactiveGraph<
211
211
 
212
212
  private refreshCallbacks: Set<() => void> = new Set()
213
213
 
214
- // biome-ignore lint/correctness/noUnusedPrivateClassMembers: for debugging
215
214
  private nodeIdCounter = 0
216
215
  private uniqueNodeId = () => `node-${++this.nodeIdCounter}`
217
- // biome-ignore lint/correctness/noUnusedPrivateClassMembers: for debugging
218
216
  private refreshInfoIdCounter = 0
219
217
  private uniqueRefreshInfoId = () => `refresh-info-${++this.refreshInfoIdCounter}`
220
218
 
@@ -231,8 +229,7 @@ export class ReactiveGraph<
231
229
  computeResult: () => ref.previousResult,
232
230
  sub: new Set(),
233
231
  super: new Set(),
234
- label: options?.label,
235
- meta: options?.meta,
232
+ ...omitUndefineds({ label: options?.label, meta: options?.meta }),
236
233
  equal: options?.equal ?? deepEqual,
237
234
  refreshes: 0,
238
235
  }
@@ -267,8 +264,11 @@ export class ReactiveGraph<
267
264
  computeResult: (otelContext, debugRefreshReason) => {
268
265
  if (thunk.isDirty) {
269
266
  const neededCurrentRefresh = this.currentDebugRefresh === undefined
267
+ let localDebugRefresh: { refreshedAtoms: any[]; startMs: number } | undefined
270
268
  if (neededCurrentRefresh) {
271
- this.currentDebugRefresh = { refreshedAtoms: [], startMs: performance.now() }
269
+ // Use local variable to prevent corruption from nested computations
270
+ localDebugRefresh = { refreshedAtoms: [], startMs: performance.now() }
271
+ this.currentDebugRefresh = localDebugRefresh
272
272
  }
273
273
 
274
274
  // Reset previous subcomputations as we're about to re-add them as part of the `doEffect` call below
@@ -300,15 +300,20 @@ export class ReactiveGraph<
300
300
  debugInfo: debugInfo ?? (unknownRefreshReason() as TDebugThunkInfo),
301
301
  } satisfies AtomDebugInfo<TDebugThunkInfo>
302
302
 
303
- this.currentDebugRefresh!.refreshedAtoms.push(debugInfoForAtom)
303
+ // Use currentDebugRefresh if available (could be from parent or local)
304
+ const debugRefresh = localDebugRefresh ?? this.currentDebugRefresh
305
+ if (debugRefresh) {
306
+ debugRefresh.refreshedAtoms.push(debugInfoForAtom)
307
+ }
304
308
 
305
309
  thunk.isDirty = false
306
310
  thunk.previousResult = result
307
311
  thunk.recomputations++
308
312
 
309
- if (neededCurrentRefresh) {
310
- const refreshedAtoms = this.currentDebugRefresh!.refreshedAtoms
311
- const durationMs = performance.now() - this.currentDebugRefresh!.startMs
313
+ if (neededCurrentRefresh && localDebugRefresh) {
314
+ // Use local reference which can't be corrupted by nested calls
315
+ const refreshedAtoms = localDebugRefresh.refreshedAtoms
316
+ const durationMs = performance.now() - localDebugRefresh.startMs
312
317
  this.currentDebugRefresh = undefined
313
318
 
314
319
  this.debugRefreshInfos.push({
@@ -330,8 +335,7 @@ export class ReactiveGraph<
330
335
  sub: new Set(),
331
336
  super: new Set(),
332
337
  recomputations: 0,
333
- label: options?.label,
334
- meta: options?.meta,
338
+ ...omitUndefineds({ label: options?.label, meta: options?.meta }),
335
339
  equal: options?.equal ?? deepEqual,
336
340
  __getResult: getResult,
337
341
  }
@@ -407,7 +411,7 @@ export class ReactiveGraph<
407
411
  doEffect(getAtom as GetAtom, otelContext, debugRefreshReason)
408
412
  },
409
413
  sub: new Set(),
410
- label: options?.label,
414
+ ...omitUndefineds({ label: options?.label }),
411
415
  invocations: 0,
412
416
  }
413
417
 
@@ -461,7 +465,7 @@ export class ReactiveGraph<
461
465
  } else {
462
466
  this.runEffects(effectsToRefresh, {
463
467
  debugRefreshReason: options?.debugRefreshReason ?? (unknownRefreshReason() as TDebugRefreshReason),
464
- otelContext: options?.otelContext,
468
+ ...omitUndefineds({ otelContext: options?.otelContext }),
465
469
  })
466
470
  }
467
471
  }
@@ -475,15 +479,21 @@ export class ReactiveGraph<
475
479
  ) => {
476
480
  const effectsWrapper = this.context?.effectsWrapper ?? ((runEffects: () => void) => runEffects())
477
481
  effectsWrapper(() => {
478
- this.currentDebugRefresh = { refreshedAtoms: [], startMs: performance.now() }
479
-
480
- for (const effect of effectsToRefresh) {
481
- effect.doEffect(options?.otelContext, options.debugRefreshReason)
482
+ // Capture debug state in local variable to prevent corruption from nested runEffects
483
+ const previousDebugRefresh = this.currentDebugRefresh
484
+ const localDebugRefresh = { refreshedAtoms: [], startMs: performance.now() }
485
+ this.currentDebugRefresh = localDebugRefresh
486
+
487
+ try {
488
+ for (const effect of effectsToRefresh) {
489
+ effect.doEffect(options?.otelContext, options.debugRefreshReason)
490
+ }
491
+ } finally {
492
+ this.currentDebugRefresh = previousDebugRefresh
482
493
  }
483
494
 
484
- const refreshedAtoms = this.currentDebugRefresh.refreshedAtoms
485
- const durationMs = performance.now() - this.currentDebugRefresh.startMs
486
- this.currentDebugRefresh = undefined
495
+ const refreshedAtoms = localDebugRefresh.refreshedAtoms
496
+ const durationMs = performance.now() - localDebugRefresh.startMs
487
497
 
488
498
  const refreshDebugInfo: RefreshDebugInfo<TDebugRefreshReason, TDebugThunkInfo> = {
489
499
  id: this.uniqueRefreshInfoId(),
@@ -509,9 +519,9 @@ export class ReactiveGraph<
509
519
  debugRefreshReason: {
510
520
  _tag: 'runDeferredEffects',
511
521
  originalRefreshReasons: Array.from(debugRefreshReasons) as ReadonlyArray<DebugRefreshReasonBase>,
512
- manualRefreshReason: options?.debugRefreshReason,
513
- } as TDebugRefreshReason,
514
- otelContext: options?.otelContext,
522
+ ...omitUndefineds({ manualRefreshReason: options?.debugRefreshReason }),
523
+ } as unknown as TDebugRefreshReason,
524
+ ...omitUndefineds({ otelContext: options?.otelContext }),
515
525
  })
516
526
  }
517
527
  }
@@ -641,8 +651,7 @@ const serializeAtom = (atom: Atom<any, unknown, any>, includeResult: boolean): S
641
651
  return {
642
652
  _tag: atom._tag,
643
653
  id: atom.id,
644
- label: atom.label,
645
- meta: atom.meta,
654
+ ...omitUndefineds({ label: atom.label, meta: atom.meta }),
646
655
  isDirty: atom.isDirty,
647
656
  sub,
648
657
  super: super_,
@@ -655,8 +664,7 @@ const serializeAtom = (atom: Atom<any, unknown, any>, includeResult: boolean): S
655
664
  return {
656
665
  _tag: 'thunk',
657
666
  id: atom.id,
658
- label: atom.label,
659
- meta: atom.meta,
667
+ ...omitUndefineds({ label: atom.label, meta: atom.meta }),
660
668
  isDirty: atom.isDirty,
661
669
  sub,
662
670
  super: super_,
@@ -676,7 +684,7 @@ const serializeEffect = (effect: Effect<any>): SerializedEffect => {
676
684
  return {
677
685
  _tag: effect._tag,
678
686
  id: effect.id,
679
- label: effect.label,
687
+ ...omitUndefineds({ label: effect.label }),
680
688
  sub,
681
689
  invocations: effect.invocations,
682
690
  isDestroyed: effect.isDestroyed,
@@ -5,13 +5,16 @@ import {
5
5
  type ClientSessionDevtoolsChannel,
6
6
  type ClientSessionSyncProcessorSimulationParams,
7
7
  type IntentionalShutdownCause,
8
+ type InvalidPullError,
9
+ type IsOfflineError,
10
+ type MaterializeError,
8
11
  type MigrationsReport,
9
12
  provideOtel,
10
13
  type SyncError,
11
14
  UnexpectedError,
12
15
  } from '@livestore/common'
13
16
  import type { LiveStoreSchema } from '@livestore/common/schema'
14
- import { isDevEnv, LS_DEV } from '@livestore/utils'
17
+ import { isDevEnv, LS_DEV, omitUndefineds } from '@livestore/utils'
15
18
  import {
16
19
  Context,
17
20
  Deferred,
@@ -156,7 +159,7 @@ export const createStorePromise = async <TSchema extends LiveStoreSchema = LiveS
156
159
  Effect.withSpan('createStore', {
157
160
  attributes: { storeId: options.storeId, disableDevtools: options.disableDevtools },
158
161
  }),
159
- provideOtel({ parentSpanContext: otelOptions?.rootSpanContext, otelTracer: otelOptions?.tracer }),
162
+ provideOtel(omitUndefineds({ parentSpanContext: otelOptions?.rootSpanContext, otelTracer: otelOptions?.tracer })),
160
163
  Effect.tapCauseLogPretty,
161
164
  Effect.annotateLogs({ thread: 'window' }),
162
165
  Effect.provide(Logger.prettyWithThread('window')),
@@ -217,7 +220,12 @@ export const createStore = <TSchema extends LiveStoreSchema = LiveStoreSchema.An
217
220
 
218
221
  const runtime = yield* Effect.runtime<Scope.Scope>()
219
222
 
220
- const shutdown = (exit: Exit.Exit<IntentionalShutdownCause, UnexpectedError | SyncError>) =>
223
+ const shutdown = (
224
+ exit: Exit.Exit<
225
+ IntentionalShutdownCause,
226
+ UnexpectedError | MaterializeError | SyncError | InvalidPullError | IsOfflineError
227
+ >,
228
+ ) =>
221
229
  Effect.gen(function* () {
222
230
  yield* Scope.close(lifetimeScope, exit).pipe(
223
231
  Effect.logWarnIfTakesLongerThan({ label: '@livestore/livestore:shutdown', duration: 500 }),
@@ -280,7 +288,7 @@ export const createStore = <TSchema extends LiveStoreSchema = LiveStoreSchema.An
280
288
  storeId,
281
289
  params: {
282
290
  leaderPushBatchSize: params?.leaderPushBatchSize ?? DEFAULT_PARAMS.leaderPushBatchSize,
283
- simulation: params?.simulation,
291
+ ...omitUndefineds({ simulation: params?.simulation }),
284
292
  },
285
293
  })
286
294
 
@@ -2,6 +2,9 @@ import type {
2
2
  ClientSession,
3
3
  ClientSessionSyncProcessorSimulationParams,
4
4
  IntentionalShutdownCause,
5
+ InvalidPullError,
6
+ IsOfflineError,
7
+ MaterializeError,
5
8
  StoreInterrupted,
6
9
  SyncError,
7
10
  UnexpectedError,
@@ -28,11 +31,11 @@ export type LiveStoreContext =
28
31
 
29
32
  export type ShutdownDeferred = Deferred.Deferred<
30
33
  IntentionalShutdownCause,
31
- UnexpectedError | SyncError | StoreInterrupted
34
+ UnexpectedError | SyncError | StoreInterrupted | MaterializeError | InvalidPullError | IsOfflineError
32
35
  >
33
36
  export const makeShutdownDeferred: Effect.Effect<ShutdownDeferred> = Deferred.make<
34
37
  IntentionalShutdownCause,
35
- UnexpectedError | SyncError | StoreInterrupted
38
+ UnexpectedError | SyncError | StoreInterrupted | MaterializeError | InvalidPullError | IsOfflineError
36
39
  >()
37
40
 
38
41
  export type LiveStoreContextRunning = {