@livestore/livestore 0.4.0 → 0.5.0-dev.0

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 (89) hide show
  1. package/dist/.tsbuildinfo +1 -1
  2. package/dist/SqliteDbWrapper.d.ts.map +1 -1
  3. package/dist/SqliteDbWrapper.js +7 -5
  4. package/dist/SqliteDbWrapper.js.map +1 -1
  5. package/dist/SqliteDbWrapper.test.js +20 -3
  6. package/dist/SqliteDbWrapper.test.js.map +1 -1
  7. package/dist/effect/LiveStore.d.ts +15 -15
  8. package/dist/effect/LiveStore.d.ts.map +1 -1
  9. package/dist/effect/LiveStore.js +17 -17
  10. package/dist/effect/LiveStore.js.map +1 -1
  11. package/dist/effect/LiveStore.test.js +5 -6
  12. package/dist/effect/LiveStore.test.js.map +1 -1
  13. package/dist/live-queries/base-class.d.ts +19 -2
  14. package/dist/live-queries/base-class.d.ts.map +1 -1
  15. package/dist/live-queries/base-class.js +28 -2
  16. package/dist/live-queries/base-class.js.map +1 -1
  17. package/dist/live-queries/computed.d.ts.map +1 -1
  18. package/dist/live-queries/computed.js +4 -4
  19. package/dist/live-queries/computed.js.map +1 -1
  20. package/dist/live-queries/db-query.d.ts +2 -2
  21. package/dist/live-queries/db-query.d.ts.map +1 -1
  22. package/dist/live-queries/db-query.js +26 -17
  23. package/dist/live-queries/db-query.js.map +1 -1
  24. package/dist/live-queries/db-query.test.js +9 -9
  25. package/dist/live-queries/db-query.test.js.map +1 -1
  26. package/dist/live-queries/signal.d.ts.map +1 -1
  27. package/dist/live-queries/signal.js +2 -2
  28. package/dist/live-queries/signal.js.map +1 -1
  29. package/dist/live-queries/signal.test.js +2 -2
  30. package/dist/live-queries/signal.test.js.map +1 -1
  31. package/dist/reactive.d.ts +1 -1
  32. package/dist/reactive.d.ts.map +1 -1
  33. package/dist/reactive.js +20 -1
  34. package/dist/reactive.js.map +1 -1
  35. package/dist/store/StoreRegistry.d.ts +12 -12
  36. package/dist/store/StoreRegistry.d.ts.map +1 -1
  37. package/dist/store/StoreRegistry.js +24 -24
  38. package/dist/store/StoreRegistry.js.map +1 -1
  39. package/dist/store/StoreRegistry.test.js +62 -59
  40. package/dist/store/StoreRegistry.test.js.map +1 -1
  41. package/dist/store/create-store.d.ts +12 -15
  42. package/dist/store/create-store.d.ts.map +1 -1
  43. package/dist/store/create-store.js +43 -24
  44. package/dist/store/create-store.js.map +1 -1
  45. package/dist/store/devtools.d.ts +1 -2
  46. package/dist/store/devtools.d.ts.map +1 -1
  47. package/dist/store/devtools.js +3 -2
  48. package/dist/store/devtools.js.map +1 -1
  49. package/dist/store/store-eventstream.test.js +5 -5
  50. package/dist/store/store-eventstream.test.js.map +1 -1
  51. package/dist/store/store-types.d.ts +10 -28
  52. package/dist/store/store-types.d.ts.map +1 -1
  53. package/dist/store/store-types.js +2 -38
  54. package/dist/store/store-types.js.map +1 -1
  55. package/dist/store/store-types.test.js +1 -1
  56. package/dist/store/store.d.ts +2 -2
  57. package/dist/store/store.d.ts.map +1 -1
  58. package/dist/store/store.js +114 -56
  59. package/dist/store/store.js.map +1 -1
  60. package/dist/utils/dev.js +1 -1
  61. package/dist/utils/dev.js.map +1 -1
  62. package/dist/utils/stack-info.d.ts.map +1 -1
  63. package/dist/utils/stack-info.js +2 -1
  64. package/dist/utils/stack-info.js.map +1 -1
  65. package/dist/utils/tests/fixture.d.ts +26 -34
  66. package/dist/utils/tests/fixture.d.ts.map +1 -1
  67. package/package.json +14 -24
  68. package/src/SqliteDbWrapper.test.ts +25 -3
  69. package/src/SqliteDbWrapper.ts +12 -9
  70. package/src/effect/LiveStore.test.ts +16 -16
  71. package/src/effect/LiveStore.ts +79 -50
  72. package/src/live-queries/__snapshots__/db-query.test.ts.snap +273 -49
  73. package/src/live-queries/base-class.ts +38 -6
  74. package/src/live-queries/computed.ts +5 -5
  75. package/src/live-queries/db-query.test.ts +9 -9
  76. package/src/live-queries/db-query.ts +42 -30
  77. package/src/live-queries/signal.test.ts +2 -2
  78. package/src/live-queries/signal.ts +3 -3
  79. package/src/reactive.ts +45 -35
  80. package/src/store/StoreRegistry.test.ts +62 -59
  81. package/src/store/StoreRegistry.ts +49 -38
  82. package/src/store/create-store.ts +85 -51
  83. package/src/store/devtools.ts +283 -283
  84. package/src/store/store-eventstream.test.ts +16 -7
  85. package/src/store/store-types.test.ts +1 -1
  86. package/src/store/store-types.ts +10 -56
  87. package/src/store/store.ts +148 -71
  88. package/src/utils/dev.ts +1 -1
  89. package/src/utils/stack-info.ts +4 -1
@@ -54,7 +54,7 @@ Vitest.describe('otel', () => {
54
54
  }
55
55
  })
56
56
 
57
- Vitest.scopedLive('otel', () =>
57
+ Vitest.live('otel', () =>
58
58
  Effect.gen(function* () {
59
59
  const { store, exporter, span, provider } = yield* makeQuery
60
60
 
@@ -93,7 +93,7 @@ Vitest.describe('otel', () => {
93
93
  ),
94
94
  )
95
95
 
96
- Vitest.scopedLive('with thunks', () =>
96
+ Vitest.live('with thunks', () =>
97
97
  Effect.gen(function* () {
98
98
  const { store, exporter, span, provider } = yield* makeQuery
99
99
 
@@ -150,7 +150,7 @@ Vitest.describe('otel', () => {
150
150
  ),
151
151
  )
152
152
 
153
- Vitest.scopedLive('with thunks with query builder and without labels', () =>
153
+ Vitest.live('with thunks with query builder and without labels', () =>
154
154
  Effect.gen(function* () {
155
155
  const { store, exporter, span, provider } = yield* makeQuery
156
156
 
@@ -195,7 +195,7 @@ Vitest.describe('otel', () => {
195
195
  ),
196
196
  )
197
197
 
198
- Vitest.scopedLive('QueryBuilder subscription - basic functionality', () =>
198
+ Vitest.live('QueryBuilder subscription - basic functionality', () =>
199
199
  Effect.gen(function* () {
200
200
  const { store, exporter, span, provider } = yield* makeQuery
201
201
 
@@ -239,7 +239,7 @@ Vitest.describe('otel', () => {
239
239
  ),
240
240
  )
241
241
 
242
- Vitest.scopedLive('QueryBuilder subscription - skipInitialRun', () =>
242
+ Vitest.live('QueryBuilder subscription - skipInitialRun', () =>
243
243
  Effect.gen(function* () {
244
244
  const { store, exporter, span, provider } = yield* makeQuery
245
245
 
@@ -286,7 +286,7 @@ Vitest.describe('otel', () => {
286
286
  ),
287
287
  )
288
288
 
289
- Vitest.scopedLive('QueryBuilder subscription - unsubscribe functionality', () =>
289
+ Vitest.live('QueryBuilder subscription - unsubscribe functionality', () =>
290
290
  Effect.gen(function* () {
291
291
  const { store, exporter, span, provider } = yield* makeQuery
292
292
 
@@ -338,7 +338,7 @@ Vitest.describe('otel', () => {
338
338
  ),
339
339
  )
340
340
 
341
- Vitest.scopedLive('QueryBuilder subscription - async iterator', () =>
341
+ Vitest.live('QueryBuilder subscription - async iterator', () =>
342
342
  Effect.gen(function* () {
343
343
  const { store, exporter, span, provider } = yield* makeQuery
344
344
 
@@ -386,7 +386,7 @@ Vitest.describe('otel', () => {
386
386
  ),
387
387
  )
388
388
 
389
- Vitest.scopedLive('QueryBuilder subscription - async iterator with skipInitialRun', () =>
389
+ Vitest.live('QueryBuilder subscription - async iterator with skipInitialRun', () =>
390
390
  Effect.gen(function* () {
391
391
  const { store, exporter, span, provider } = yield* makeQuery
392
392
 
@@ -435,7 +435,7 @@ Vitest.describe('otel', () => {
435
435
  ),
436
436
  )
437
437
 
438
- Vitest.scopedLive('QueryBuilder subscription - direct table subscription', () =>
438
+ Vitest.live('QueryBuilder subscription - direct table subscription', () =>
439
439
  Effect.gen(function* () {
440
440
  const { store, exporter, span, provider } = yield* makeQuery
441
441
 
@@ -1,17 +1,17 @@
1
+ import * as otel from '@opentelemetry/api'
2
+
1
3
  import type { Bindable, QueryBuilder } from '@livestore/common'
2
4
  import {
3
- getDurationMsFromSpan,
4
5
  getResultSchema,
5
6
  isQueryBuilder,
6
7
  prepareBindValues,
7
8
  QueryBuilderAstSymbol,
8
- replaceSessionIdSymbol,
9
+ resolveSessionIdSymbolInBindValues,
9
10
  SessionIdSymbol,
10
11
  UnknownError,
11
12
  } from '@livestore/common'
12
13
  import { deepEqual, objectToString, omitUndefineds, shouldNeverHappen } from '@livestore/utils'
13
- import { Equal, Hash, Predicate, Schema, TreeFormatter } from '@livestore/utils/effect'
14
- import * as otel from '@opentelemetry/api'
14
+ import { Equal, Hash, Predicate, Result, Schema, SchemaIssue } from '@livestore/utils/effect'
15
15
 
16
16
  import type { Thunk } from '../reactive.ts'
17
17
  import { isThunk, NOT_REFRESHED_YET } from '../reactive.ts'
@@ -19,12 +19,12 @@ import type { RefreshReason } from '../store/store-types.ts'
19
19
  import { StoreInternalsSymbol } from '../store/store-types.ts'
20
20
  import { isValidFunctionString } from '../utils/function-string.ts'
21
21
  import type { DepKey, GetAtomResult, LiveQueryDef, ReactivityGraph, ReactivityGraphContext } from './base-class.ts'
22
- import { depsToString, LiveStoreQueryBase, makeGetAtomResult, withRCMap } from './base-class.ts'
22
+ import { depsToString, isLiveQueryDef, LiveStoreQueryBase, makeGetAtomResult, withRCMap } from './base-class.ts'
23
23
  import { makeExecBeforeFirstRun, rowQueryLabel } from './client-document-get-query.ts'
24
24
 
25
25
  export type QueryInputRaw<TDecoded, TEncoded> = {
26
26
  query: string
27
- schema: Schema.Schema<TDecoded, TEncoded>
27
+ schema: Schema.Codec<TDecoded, TEncoded>
28
28
  bindValues?: Bindable
29
29
  /**
30
30
  * Can be provided explicitly to slightly speed up initial query performance
@@ -103,7 +103,11 @@ export const queryDb: {
103
103
  } = (queryInput, options) => {
104
104
  const { queryString, extraDeps } = getQueryStringAndExtraDeps(queryInput)
105
105
 
106
- const hash = [queryString, options?.deps !== undefined ? depsToString(options.deps) : undefined, depsToString(extraDeps)]
106
+ const hash = [
107
+ queryString,
108
+ options?.deps !== undefined ? depsToString(options.deps) : undefined,
109
+ depsToString(extraDeps),
110
+ ]
107
111
  .filter(Boolean)
108
112
  .join('-')
109
113
 
@@ -112,7 +116,7 @@ export const queryDb: {
112
116
  }
113
117
 
114
118
  if (hash.trim() === '') {
115
- return shouldNeverHappen('Invalid query hash for query:', objectToString(queryInput))
119
+ return shouldNeverHappen('Invalid query hash for query:', objectToString(queryInput))
116
120
  }
117
121
 
118
122
  const label = options?.label ?? queryString
@@ -131,8 +135,8 @@ export const queryDb: {
131
135
  }),
132
136
  label,
133
137
  hash,
134
- [Equal.symbol](that: LiveQueryDef<any>): boolean {
135
- return this.hash === that.hash
138
+ [Equal.symbol](that: Equal.Equal): boolean {
139
+ return isLiveQueryDef(that) && that._tag === 'def' && this.hash === that.hash
136
140
  },
137
141
  [Hash.symbol](): number {
138
142
  return Hash.string(this.hash)
@@ -214,9 +218,13 @@ export class LiveStoreDbQuery<TResultSchema, TResult = TResultSchema> extends Li
214
218
 
215
219
  this.mapResult = map === undefined ? (rows: any) => rows as TResult : map
216
220
 
217
- const schemaRef: { current: Schema.Schema<any, any> | undefined } = {
221
+ const schemaRef: { current: Schema.Codec<any, any> | undefined } = {
218
222
  current:
219
- typeof queryInput === 'function' ? undefined : isQueryBuilder(queryInput) === true ? undefined : queryInput.schema,
223
+ typeof queryInput === 'function'
224
+ ? undefined
225
+ : isQueryBuilder(queryInput) === true
226
+ ? undefined
227
+ : queryInput.schema,
220
228
  }
221
229
 
222
230
  const execBeforeFirstRunRef: {
@@ -232,7 +240,7 @@ export class LiveStoreDbQuery<TResultSchema, TResult = TResultSchema> extends Li
232
240
  const fromQueryBuilder = (qb: QueryBuilder.Any, otelContext: otel.Context | undefined) => {
233
241
  try {
234
242
  const qbRes = qb.asSql()
235
- const schema = getResultSchema(qb) as Schema.Schema<TResultSchema, ReadonlyArray<any>>
243
+ const schema = getResultSchema(qb) as Schema.Codec<TResultSchema, ReadonlyArray<any>>
236
244
  const ast = qb[QueryBuilderAstSymbol]
237
245
 
238
246
  return {
@@ -320,9 +328,9 @@ export class LiveStoreDbQuery<TResultSchema, TResult = TResultSchema> extends Li
320
328
 
321
329
  const queriedTablesRef: { current: Set<string> | undefined } = { current: undefined }
322
330
 
323
- const makeResultsEqual = (resultSchema: Schema.Schema<any, any>) => {
331
+ const makeResultsEqual = (resultSchema: Schema.Codec<any, any>) => {
324
332
  // Creating the equivalence function eagerly in outer scope as it might be expensive
325
- const eq = Schema.equivalence(resultSchema)
333
+ const eq = Schema.toEquivalence(resultSchema)
326
334
  return (a: TResult, b: TResult) => (a === NOT_REFRESHED_YET || b === NOT_REFRESHED_YET ? false : eq(a, b))
327
335
  }
328
336
 
@@ -342,13 +350,15 @@ export class LiveStoreDbQuery<TResultSchema, TResult = TResultSchema> extends Li
342
350
  'db:...', // NOTE span name will be overridden further down
343
351
  {
344
352
  attributes: {
345
- 'livestore.debugRefreshReason': Predicate.hasProperty(debugRefreshReason, 'label') === true
346
- ? (debugRefreshReason.label as string)
347
- : debugRefreshReason?._tag,
353
+ 'livestore.debugRefreshReason':
354
+ Predicate.hasProperty(debugRefreshReason, 'label') === true
355
+ ? (debugRefreshReason.label as string)
356
+ : debugRefreshReason?._tag,
348
357
  },
349
358
  },
350
359
  otelContext ?? queryContext.rootOtelContext,
351
360
  (span) => {
361
+ const startTimePerfNow = performance.now()
352
362
  const otelContext = otel.trace.setSpan(otel.context.active(), span)
353
363
  const { store } = queryContext
354
364
 
@@ -357,9 +367,10 @@ export class LiveStoreDbQuery<TResultSchema, TResult = TResultSchema> extends Li
357
367
  execBeforeFirstRunRef.current = undefined
358
368
  }
359
369
 
360
- const queryInputResult = isThunk(queryInputRaw$OrQueryInputRaw) === true
361
- ? (get(queryInputRaw$OrQueryInputRaw, otelContext, debugRefreshReason) as TQueryInputRaw)
362
- : (queryInputRaw$OrQueryInputRaw as TQueryInputRaw)
370
+ const queryInputResult =
371
+ isThunk(queryInputRaw$OrQueryInputRaw) === true
372
+ ? (get(queryInputRaw$OrQueryInputRaw, otelContext, debugRefreshReason) as TQueryInputRaw)
373
+ : (queryInputRaw$OrQueryInputRaw as TQueryInputRaw)
363
374
 
364
375
  const sqlString = queryInputResult.query
365
376
  const bindValues = queryInputResult.bindValues
@@ -368,9 +379,10 @@ export class LiveStoreDbQuery<TResultSchema, TResult = TResultSchema> extends Li
368
379
  queriedTablesRef.current = store[StoreInternalsSymbol].sqliteDbWrapper.getTablesUsed(sqlString)
369
380
  }
370
381
 
371
- if (bindValues !== undefined) {
372
- replaceSessionIdSymbol(bindValues, store.sessionId)
373
- }
382
+ // Live query inputs may be cached and re-run as reactive dependencies change.
383
+ // Resolve SessionIdSymbol per execution so the cached input stays symbolic and session-agnostic.
384
+ const resolvedBindValues =
385
+ bindValues === undefined ? undefined : resolveSessionIdSymbolInBindValues(bindValues, store.sessionId)
374
386
 
375
387
  // Establish a reactive dependency on the tables used in the query
376
388
  for (const tableName of queriedTablesRef.current) {
@@ -385,7 +397,7 @@ export class LiveStoreDbQuery<TResultSchema, TResult = TResultSchema> extends Li
385
397
 
386
398
  const rawDbResults = store[StoreInternalsSymbol].sqliteDbWrapper.cachedSelect(
387
399
  sqlString,
388
- bindValues !== undefined ? prepareBindValues(bindValues, sqlString) : undefined,
400
+ resolvedBindValues !== undefined ? prepareBindValues(resolvedBindValues, sqlString) : undefined,
389
401
  {
390
402
  otelContext,
391
403
  ...(queriedTablesRef.current !== undefined ? { queriedTables: queriedTablesRef.current } : {}),
@@ -394,10 +406,10 @@ export class LiveStoreDbQuery<TResultSchema, TResult = TResultSchema> extends Li
394
406
 
395
407
  span.setAttribute('sql.rowsCount', rawDbResults.length)
396
408
 
397
- const parsedResult = Schema.decodeEither(schemaRef.current!)(rawDbResults)
409
+ const parsedResult = Schema.decodeResult(schemaRef.current!)(rawDbResults)
398
410
 
399
- if (parsedResult._tag === 'Left') {
400
- const parseErrorStr = TreeFormatter.formatErrorSync(parsedResult.left)
411
+ if (Result.isFailure(parsedResult) === true) {
412
+ const parseErrorStr = SchemaIssue.makeFormatterDefault()(parsedResult.failure.issue)
401
413
  const expectedSchemaStr = String(schemaRef.current!.ast)
402
414
  const bindValuesStr = bindValues === undefined ? '' : `\nBind values: ${JSON.stringify(bindValues)}`
403
415
 
@@ -418,11 +430,11 @@ Result:`,
418
430
  )
419
431
  }
420
432
 
421
- const result = this.mapResult(parsedResult.right)
433
+ const result = this.mapResult(parsedResult.success)
422
434
 
423
435
  span.end()
424
436
 
425
- const durationMs = getDurationMsFromSpan(span)
437
+ const durationMs = performance.now() - startTimePerfNow
426
438
 
427
439
  this.executionTimes.push(durationMs)
428
440
 
@@ -8,7 +8,7 @@ import { computed } from './computed.ts'
8
8
  import { signal } from './signal.ts'
9
9
 
10
10
  Vitest.describe('signal', () => {
11
- Vitest.scopedLive('should be able to create a signal', () =>
11
+ Vitest.live('should be able to create a signal', () =>
12
12
  Effect.gen(function* () {
13
13
  const num$ = signal(0, { label: 'num$' })
14
14
 
@@ -24,7 +24,7 @@ Vitest.describe('signal', () => {
24
24
  }),
25
25
  )
26
26
 
27
- Vitest.scopedLive('counter example', () =>
27
+ Vitest.live('counter example', () =>
28
28
  Effect.gen(function* () {
29
29
  const count$ = signal(0, { label: 'count$' })
30
30
 
@@ -4,7 +4,7 @@ import { nanoid } from '@livestore/utils/nanoid'
4
4
  import type * as RG from '../reactive.ts'
5
5
  import type { RefreshReason } from '../store/store-types.ts'
6
6
  import type { ISignal, ReactivityGraph, ReactivityGraphContext, SignalDef } from './base-class.ts'
7
- import { LiveStoreQueryBase, withRCMap } from './base-class.ts'
7
+ import { isSignalDef, LiveStoreQueryBase, withRCMap } from './base-class.ts'
8
8
 
9
9
  /**
10
10
  * Creates a reactive signal for ephemeral, local-only state that isn't persisted to the database.
@@ -70,8 +70,8 @@ export const signal = <T>(
70
70
  def,
71
71
  }),
72
72
  ),
73
- [Equal.symbol](that: SignalDef<T>): boolean {
74
- return this.hash === that.hash
73
+ [Equal.symbol](that: Equal.Equal): boolean {
74
+ return isSignalDef(that) && this.hash === that.hash
75
75
  },
76
76
  [Hash.symbol](): number {
77
77
  return Hash.string(this.hash)
package/src/reactive.ts CHANGED
@@ -25,17 +25,13 @@ import type * as otel from '@opentelemetry/api'
25
25
 
26
26
  import { BoundArray } from '@livestore/common'
27
27
  import { deepEqual, omitUndefineds, shouldNeverHappen } from '@livestore/utils'
28
- import type { Types } from '@livestore/utils/effect'
28
+ import { Schema, type Types } from '@livestore/utils/effect'
29
29
  // import { getDurationMsFromSpan } from './otel.ts'
30
30
 
31
31
  export const NOT_REFRESHED_YET = Symbol.for('NOT_REFRESHED_YET')
32
32
  export type NOT_REFRESHED_YET = typeof NOT_REFRESHED_YET
33
33
 
34
- export type GetAtom = <T>(
35
- atom: Atom<T, any, any>,
36
- otelContext?: otel.Context ,
37
- debugRefreshReason?: TODO ,
38
- ) => T
34
+ export type GetAtom = <T>(atom: Atom<T, any, any>, otelContext?: otel.Context, debugRefreshReason?: TODO) => T
39
35
 
40
36
  export type Ref<T, TContext, TDebugRefreshReason extends DebugRefreshReason> = {
41
37
  _tag: 'ref'
@@ -79,7 +75,7 @@ export type Effect<TDebugRefreshReason extends DebugRefreshReason> = {
79
75
  _tag: 'effect'
80
76
  id: string
81
77
  isDestroyed: boolean
82
- doEffect: (otelContext?: otel.Context , debugRefreshReason?: TDebugRefreshReason ) => void
78
+ doEffect: (otelContext?: otel.Context, debugRefreshReason?: TDebugRefreshReason) => void
83
79
  sub: Set<Atom<any, TODO, TODO>>
84
80
  label?: string | undefined
85
81
  invocations: number
@@ -246,13 +242,11 @@ export class ReactiveGraph<
246
242
  otelContext: otel.Context | undefined,
247
243
  debugRefreshReason: TDebugRefreshReason | undefined,
248
244
  ) => T,
249
- options?:
250
- | {
251
- label?: string
252
- meta?: any
253
- equal?: (a: T, b: T) => boolean
254
- }
255
- ,
245
+ options?: {
246
+ label?: string
247
+ meta?: any
248
+ equal?: (a: T, b: T) => boolean
249
+ },
256
250
  ): Thunk<T, TContext, TDebugRefreshReason> {
257
251
  const thunk: Thunk<T, TContext, TDebugRefreshReason> = {
258
252
  _tag: 'thunk',
@@ -291,7 +285,7 @@ export class ReactiveGraph<
291
285
  debugRefreshReason,
292
286
  )
293
287
 
294
- const resultChanged = ! thunk.equal(thunk.previousResult as T, result)
288
+ const resultChanged = !thunk.equal(thunk.previousResult as T, result)
295
289
 
296
290
  const debugInfoForAtom = {
297
291
  atom: serializeAtom(thunk, false),
@@ -384,7 +378,7 @@ export class ReactiveGraph<
384
378
  otelContext: otel.Context | undefined,
385
379
  debugRefreshReason: DebugRefreshReason | undefined,
386
380
  ) => void,
387
- options?: { label?: string } ,
381
+ options?: { label?: string },
388
382
  ): Effect<TDebugRefreshReason> {
389
383
  const effect: Effect<TDebugRefreshReason> = {
390
384
  _tag: 'effect',
@@ -422,26 +416,22 @@ export class ReactiveGraph<
422
416
  setRef<T>(
423
417
  ref: Ref<T, TContext, TDebugRefreshReason>,
424
418
  val: T,
425
- options?:
426
- | {
427
- skipRefresh?: boolean
428
- debugRefreshReason?: TDebugRefreshReason
429
- otelContext?: otel.Context
430
- }
431
- ,
419
+ options?: {
420
+ skipRefresh?: boolean
421
+ debugRefreshReason?: TDebugRefreshReason
422
+ otelContext?: otel.Context
423
+ },
432
424
  ) {
433
425
  this.setRefs([[ref, val]], options)
434
426
  }
435
427
 
436
428
  setRefs<T>(
437
429
  refs: [Ref<T, TContext, TDebugRefreshReason>, T][],
438
- options?:
439
- | {
440
- skipRefresh?: boolean
441
- debugRefreshReason?: TDebugRefreshReason
442
- otelContext?: otel.Context
443
- }
444
- ,
430
+ options?: {
431
+ skipRefresh?: boolean
432
+ debugRefreshReason?: TDebugRefreshReason
433
+ otelContext?: otel.Context
434
+ },
445
435
  ) {
446
436
  const effectsToRefresh = new Set<Effect<TDebugRefreshReason>>()
447
437
  for (const [ref, val] of refs) {
@@ -640,11 +630,31 @@ const serializeAtom = (atom: Atom<any, unknown, any>, includeResult: boolean): S
640
630
  super_.push(a.id)
641
631
  }
642
632
 
643
- const previousResult: EncodedOption<string> = includeResult === true
644
- ? encodedOptionSome(
645
- atom.previousResult === NOT_REFRESHED_YET ? '"SYMBOL_NOT_REFRESHED_YET"' : JSON.stringify(atom.previousResult),
646
- )
647
- : encodedOptionNone()
633
+ const previousResult: EncodedOption<string> =
634
+ includeResult === true
635
+ ? encodedOptionSome(
636
+ atom.previousResult === NOT_REFRESHED_YET
637
+ ? '"SYMBOL_NOT_REFRESHED_YET"'
638
+ : JSON.stringify(atom.previousResult, (key, nestedValue) => {
639
+ if (Schema.isSchema(nestedValue) === false) return nestedValue
640
+
641
+ // Query inputs carry schemas for result decoding, but graph snapshots use previousResult
642
+ // to explain reactive data flow. Omitting that field preserves the stable query shape
643
+ // without embedding Effect full schema object graph.
644
+ if (key === 'schema') return undefined
645
+
646
+ const annotations = Schema.resolveAnnotations(nestedValue)
647
+
648
+ return omitUndefineds({
649
+ _tag: 'Schema',
650
+ ast: nestedValue.ast._tag,
651
+ identifier: annotations?.identifier,
652
+ title: annotations?.title,
653
+ hash: Schema.hash(nestedValue),
654
+ })
655
+ }),
656
+ )
657
+ : encodedOptionNone()
648
658
 
649
659
  if (atom._tag === 'ref') {
650
660
  return {