@livestore/livestore 0.4.0-dev.3 → 0.4.0-dev.6

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 (49) 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 +29 -24
  17. package/dist/reactive.js.map +1 -1
  18. package/dist/reactive.test.js +80 -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 +3 -2
  27. package/dist/store/store.d.ts.map +1 -1
  28. package/dist/store/store.js +94 -88
  29. package/dist/store/store.js.map +1 -1
  30. package/dist/utils/tests/fixture.d.ts.map +1 -1
  31. package/dist/utils/tests/fixture.js +2 -1
  32. package/dist/utils/tests/fixture.js.map +1 -1
  33. package/dist/utils/tests/otel.d.ts +15 -14
  34. package/dist/utils/tests/otel.d.ts.map +1 -1
  35. package/dist/utils/tests/otel.js +20 -15
  36. package/dist/utils/tests/otel.js.map +1 -1
  37. package/package.json +6 -6
  38. package/src/effect/LiveStore.ts +2 -4
  39. package/src/live-queries/__snapshots__/db-query.test.ts.snap +268 -131
  40. package/src/live-queries/db-query.test.ts +13 -7
  41. package/src/live-queries/db-query.ts +7 -4
  42. package/src/mod.ts +2 -0
  43. package/src/reactive.test.ts +100 -0
  44. package/src/reactive.ts +40 -35
  45. package/src/store/create-store.ts +12 -4
  46. package/src/store/store-types.ts +5 -2
  47. package/src/store/store.ts +160 -147
  48. package/src/utils/tests/fixture.ts +2 -1
  49. package/src/utils/tests/otel.ts +31 -20
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,14 +479,17 @@ 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() }
482
+ // Capture debug state in local variable to prevent corruption from nested runEffects
483
+ const localDebugRefresh = { refreshedAtoms: [], startMs: performance.now() }
484
+ this.currentDebugRefresh = localDebugRefresh
479
485
 
480
486
  for (const effect of effectsToRefresh) {
481
487
  effect.doEffect(options?.otelContext, options.debugRefreshReason)
482
488
  }
483
489
 
484
- const refreshedAtoms = this.currentDebugRefresh.refreshedAtoms
485
- const durationMs = performance.now() - this.currentDebugRefresh.startMs
490
+ // Use local reference which can't be corrupted by nested calls
491
+ const refreshedAtoms = localDebugRefresh.refreshedAtoms
492
+ const durationMs = performance.now() - localDebugRefresh.startMs
486
493
  this.currentDebugRefresh = undefined
487
494
 
488
495
  const refreshDebugInfo: RefreshDebugInfo<TDebugRefreshReason, TDebugThunkInfo> = {
@@ -509,9 +516,9 @@ export class ReactiveGraph<
509
516
  debugRefreshReason: {
510
517
  _tag: 'runDeferredEffects',
511
518
  originalRefreshReasons: Array.from(debugRefreshReasons) as ReadonlyArray<DebugRefreshReasonBase>,
512
- manualRefreshReason: options?.debugRefreshReason,
513
- } as TDebugRefreshReason,
514
- otelContext: options?.otelContext,
519
+ ...omitUndefineds({ manualRefreshReason: options?.debugRefreshReason }),
520
+ } as unknown as TDebugRefreshReason,
521
+ ...omitUndefineds({ otelContext: options?.otelContext }),
515
522
  })
516
523
  }
517
524
  }
@@ -641,8 +648,7 @@ const serializeAtom = (atom: Atom<any, unknown, any>, includeResult: boolean): S
641
648
  return {
642
649
  _tag: atom._tag,
643
650
  id: atom.id,
644
- label: atom.label,
645
- meta: atom.meta,
651
+ ...omitUndefineds({ label: atom.label, meta: atom.meta }),
646
652
  isDirty: atom.isDirty,
647
653
  sub,
648
654
  super: super_,
@@ -655,8 +661,7 @@ const serializeAtom = (atom: Atom<any, unknown, any>, includeResult: boolean): S
655
661
  return {
656
662
  _tag: 'thunk',
657
663
  id: atom.id,
658
- label: atom.label,
659
- meta: atom.meta,
664
+ ...omitUndefineds({ label: atom.label, meta: atom.meta }),
660
665
  isDirty: atom.isDirty,
661
666
  sub,
662
667
  super: super_,
@@ -676,7 +681,7 @@ const serializeEffect = (effect: Effect<any>): SerializedEffect => {
676
681
  return {
677
682
  _tag: effect._tag,
678
683
  id: effect.id,
679
- label: effect.label,
684
+ ...omitUndefineds({ label: effect.label }),
680
685
  sub,
681
686
  invocations: effect.invocations,
682
687
  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 = {