@livestore/livestore 0.4.0-dev.9 → 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 (132) hide show
  1. package/README.md +0 -1
  2. package/dist/.tsbuildinfo +1 -1
  3. package/dist/QueryCache.js +1 -1
  4. package/dist/QueryCache.js.map +1 -1
  5. package/dist/SqliteDbWrapper.d.ts +5 -5
  6. package/dist/SqliteDbWrapper.d.ts.map +1 -1
  7. package/dist/SqliteDbWrapper.js +15 -13
  8. package/dist/SqliteDbWrapper.js.map +1 -1
  9. package/dist/SqliteDbWrapper.test.js +24 -6
  10. package/dist/SqliteDbWrapper.test.js.map +1 -1
  11. package/dist/effect/LiveStore.d.ts +134 -6
  12. package/dist/effect/LiveStore.d.ts.map +1 -1
  13. package/dist/effect/LiveStore.js +190 -11
  14. package/dist/effect/LiveStore.js.map +1 -1
  15. package/dist/effect/LiveStore.test.d.ts +2 -0
  16. package/dist/effect/LiveStore.test.d.ts.map +1 -0
  17. package/dist/effect/LiveStore.test.js +41 -0
  18. package/dist/effect/LiveStore.test.js.map +1 -0
  19. package/dist/effect/mod.d.ts +1 -1
  20. package/dist/effect/mod.d.ts.map +1 -1
  21. package/dist/effect/mod.js +3 -1
  22. package/dist/effect/mod.js.map +1 -1
  23. package/dist/live-queries/base-class.d.ts +129 -9
  24. package/dist/live-queries/base-class.d.ts.map +1 -1
  25. package/dist/live-queries/base-class.js +30 -4
  26. package/dist/live-queries/base-class.js.map +1 -1
  27. package/dist/live-queries/client-document-get-query.d.ts +1 -1
  28. package/dist/live-queries/client-document-get-query.d.ts.map +1 -1
  29. package/dist/live-queries/client-document-get-query.js +4 -3
  30. package/dist/live-queries/client-document-get-query.js.map +1 -1
  31. package/dist/live-queries/computed.d.ts +56 -0
  32. package/dist/live-queries/computed.d.ts.map +1 -1
  33. package/dist/live-queries/computed.js +62 -6
  34. package/dist/live-queries/computed.js.map +1 -1
  35. package/dist/live-queries/db-query.d.ts +2 -2
  36. package/dist/live-queries/db-query.d.ts.map +1 -1
  37. package/dist/live-queries/db-query.js +41 -30
  38. package/dist/live-queries/db-query.js.map +1 -1
  39. package/dist/live-queries/db-query.test.js +112 -29
  40. package/dist/live-queries/db-query.test.js.map +1 -1
  41. package/dist/live-queries/signal.d.ts +49 -0
  42. package/dist/live-queries/signal.d.ts.map +1 -1
  43. package/dist/live-queries/signal.js +51 -2
  44. package/dist/live-queries/signal.js.map +1 -1
  45. package/dist/live-queries/signal.test.js +4 -4
  46. package/dist/live-queries/signal.test.js.map +1 -1
  47. package/dist/mod.d.ts +3 -3
  48. package/dist/mod.d.ts.map +1 -1
  49. package/dist/mod.js +3 -2
  50. package/dist/mod.js.map +1 -1
  51. package/dist/reactive.d.ts +10 -10
  52. package/dist/reactive.d.ts.map +1 -1
  53. package/dist/reactive.js +29 -27
  54. package/dist/reactive.js.map +1 -1
  55. package/dist/reactive.test.js +2 -2
  56. package/dist/reactive.test.js.map +1 -1
  57. package/dist/store/StoreRegistry.d.ts +215 -0
  58. package/dist/store/StoreRegistry.d.ts.map +1 -0
  59. package/dist/store/StoreRegistry.js +267 -0
  60. package/dist/store/StoreRegistry.js.map +1 -0
  61. package/dist/store/StoreRegistry.test.d.ts +2 -0
  62. package/dist/store/StoreRegistry.test.d.ts.map +1 -0
  63. package/dist/store/StoreRegistry.test.js +384 -0
  64. package/dist/store/StoreRegistry.test.js.map +1 -0
  65. package/dist/store/create-store.d.ts +99 -22
  66. package/dist/store/create-store.d.ts.map +1 -1
  67. package/dist/store/create-store.js +85 -37
  68. package/dist/store/create-store.js.map +1 -1
  69. package/dist/store/devtools.d.ts +6 -18
  70. package/dist/store/devtools.d.ts.map +1 -1
  71. package/dist/store/devtools.js +61 -19
  72. package/dist/store/devtools.js.map +1 -1
  73. package/dist/store/store-eventstream.test.d.ts +2 -0
  74. package/dist/store/store-eventstream.test.d.ts.map +1 -0
  75. package/dist/store/store-eventstream.test.js +65 -0
  76. package/dist/store/store-eventstream.test.js.map +1 -0
  77. package/dist/store/store-types.d.ts +271 -31
  78. package/dist/store/store-types.d.ts.map +1 -1
  79. package/dist/store/store-types.js +41 -1
  80. package/dist/store/store-types.js.map +1 -1
  81. package/dist/store/store-types.test.d.ts +2 -0
  82. package/dist/store/store-types.test.d.ts.map +1 -0
  83. package/dist/store/store-types.test.js +39 -0
  84. package/dist/store/store-types.test.js.map +1 -0
  85. package/dist/store/store.d.ts +255 -68
  86. package/dist/store/store.d.ts.map +1 -1
  87. package/dist/store/store.js +533 -186
  88. package/dist/store/store.js.map +1 -1
  89. package/dist/utils/dev.d.ts.map +1 -1
  90. package/dist/utils/dev.js +1 -1
  91. package/dist/utils/dev.js.map +1 -1
  92. package/dist/utils/stack-info.d.ts.map +1 -1
  93. package/dist/utils/stack-info.js +3 -2
  94. package/dist/utils/stack-info.js.map +1 -1
  95. package/dist/utils/tests/fixture.d.ts +46 -39
  96. package/dist/utils/tests/fixture.d.ts.map +1 -1
  97. package/dist/utils/tests/fixture.js +7 -0
  98. package/dist/utils/tests/fixture.js.map +1 -1
  99. package/dist/utils/tests/otel.d.ts.map +1 -1
  100. package/dist/utils/tests/otel.js +5 -5
  101. package/dist/utils/tests/otel.js.map +1 -1
  102. package/package.json +50 -18
  103. package/src/QueryCache.ts +1 -1
  104. package/src/SqliteDbWrapper.test.ts +30 -6
  105. package/src/SqliteDbWrapper.ts +21 -17
  106. package/src/ambient.d.ts +0 -7
  107. package/src/effect/LiveStore.test.ts +61 -0
  108. package/src/effect/LiveStore.ts +426 -22
  109. package/src/effect/mod.ts +13 -1
  110. package/src/live-queries/__snapshots__/db-query.test.ts.snap +814 -178
  111. package/src/live-queries/base-class.ts +162 -32
  112. package/src/live-queries/client-document-get-query.ts +6 -4
  113. package/src/live-queries/computed.ts +65 -8
  114. package/src/live-queries/db-query.test.ts +168 -30
  115. package/src/live-queries/db-query.ts +58 -43
  116. package/src/live-queries/signal.test.ts +5 -4
  117. package/src/live-queries/signal.ts +52 -3
  118. package/src/mod.ts +19 -2
  119. package/src/reactive.test.ts +3 -2
  120. package/src/reactive.ts +58 -49
  121. package/src/store/StoreRegistry.test.ts +543 -0
  122. package/src/store/StoreRegistry.ts +429 -0
  123. package/src/store/create-store.ts +227 -74
  124. package/src/store/devtools.ts +305 -261
  125. package/src/store/store-eventstream.test.ts +123 -0
  126. package/src/store/store-types.test.ts +52 -0
  127. package/src/store/store-types.ts +318 -44
  128. package/src/store/store.ts +695 -283
  129. package/src/utils/dev.ts +3 -4
  130. package/src/utils/stack-info.ts +5 -2
  131. package/src/utils/tests/fixture.ts +9 -1
  132. package/src/utils/tests/otel.ts +8 -7
@@ -1,11 +1,13 @@
1
- import { Effect, ReadonlyRecord, Schema } from '@livestore/utils/effect'
2
- import { Vitest } from '@livestore/utils-dev/node-vitest'
3
1
  import * as otel from '@opentelemetry/api'
4
2
  import { BasicTracerProvider, InMemorySpanExporter, SimpleSpanProcessor } from '@opentelemetry/sdk-trace-base'
5
- import { expect } from 'vitest'
3
+ import { assert, expect } from 'vitest'
4
+
5
+ import { Vitest } from '@livestore/utils-dev/node-vitest'
6
+ import { Effect, ReadonlyRecord, Schema } from '@livestore/utils/effect'
6
7
 
7
8
  import * as RG from '../reactive.ts'
8
- import { events, makeTodoMvc, tables } from '../utils/tests/fixture.ts'
9
+ import { StoreInternalsSymbol } from '../store/store-types.ts'
10
+ import { events, makeTodoMvc, type Todo, tables } from '../utils/tests/fixture.ts'
9
11
  import { getAllSimplifiedRootSpans, getSimplifiedRootSpan } from '../utils/tests/otel.ts'
10
12
  import { computed } from './computed.ts'
11
13
  import { queryDb } from './db-query.ts'
@@ -52,7 +54,7 @@ Vitest.describe('otel', () => {
52
54
  }
53
55
  })
54
56
 
55
- Vitest.scopedLive('otel', () =>
57
+ Vitest.live('otel', () =>
56
58
  Effect.gen(function* () {
57
59
  const { store, exporter, span, provider } = yield* makeQuery
58
60
 
@@ -91,7 +93,7 @@ Vitest.describe('otel', () => {
91
93
  ),
92
94
  )
93
95
 
94
- Vitest.scopedLive('with thunks', () =>
96
+ Vitest.live('with thunks', () =>
95
97
  Effect.gen(function* () {
96
98
  const { store, exporter, span, provider } = yield* makeQuery
97
99
 
@@ -106,7 +108,7 @@ Vitest.describe('otel', () => {
106
108
  { label: 'all todos' },
107
109
  )
108
110
 
109
- expect(store.reactivityGraph.getSnapshot({ includeResults: true })).toMatchSnapshot()
111
+ expect(store[StoreInternalsSymbol].reactivityGraph.getSnapshot({ includeResults: true })).toMatchSnapshot()
110
112
 
111
113
  expect(store.query(query$)).toMatchInlineSnapshot(`
112
114
  {
@@ -116,11 +118,11 @@ Vitest.describe('otel', () => {
116
118
  }
117
119
  `)
118
120
 
119
- expect(store.reactivityGraph.getSnapshot({ includeResults: true })).toMatchSnapshot()
121
+ expect(store[StoreInternalsSymbol].reactivityGraph.getSnapshot({ includeResults: true })).toMatchSnapshot()
120
122
 
121
123
  store.commit(events.todoCreated({ id: 't1', text: 'buy milk', completed: false }))
122
124
 
123
- expect(store.reactivityGraph.getSnapshot({ includeResults: true })).toMatchSnapshot()
125
+ expect(store[StoreInternalsSymbol].reactivityGraph.getSnapshot({ includeResults: true })).toMatchSnapshot()
124
126
 
125
127
  expect(store.query(query$)).toMatchInlineSnapshot(`
126
128
  {
@@ -130,7 +132,7 @@ Vitest.describe('otel', () => {
130
132
  }
131
133
  `)
132
134
 
133
- expect(store.reactivityGraph.getSnapshot({ includeResults: true })).toMatchSnapshot()
135
+ expect(store[StoreInternalsSymbol].reactivityGraph.getSnapshot({ includeResults: true })).toMatchSnapshot()
134
136
 
135
137
  span.end()
136
138
 
@@ -148,7 +150,7 @@ Vitest.describe('otel', () => {
148
150
  ),
149
151
  )
150
152
 
151
- Vitest.scopedLive('with thunks with query builder and without labels', () =>
153
+ Vitest.live('with thunks with query builder and without labels', () =>
152
154
  Effect.gen(function* () {
153
155
  const { store, exporter, span, provider } = yield* makeQuery
154
156
 
@@ -193,7 +195,7 @@ Vitest.describe('otel', () => {
193
195
  ),
194
196
  )
195
197
 
196
- Vitest.scopedLive('QueryBuilder subscription - basic functionality', () =>
198
+ Vitest.live('QueryBuilder subscription - basic functionality', () =>
197
199
  Effect.gen(function* () {
198
200
  const { store, exporter, span, provider } = yield* makeQuery
199
201
 
@@ -204,10 +206,8 @@ Vitest.describe('otel', () => {
204
206
  .where({ completed: false })
205
207
  .first({ behaviour: 'fallback', fallback: () => defaultTodo })
206
208
 
207
- const unsubscribe = store.subscribe(queryBuilder, {
208
- onUpdate: (result) => {
209
- callbackResults.push(result)
210
- },
209
+ const unsubscribe = store.subscribe(queryBuilder, (result) => {
210
+ callbackResults.push(result)
211
211
  })
212
212
 
213
213
  expect(callbackResults).toHaveLength(1)
@@ -239,7 +239,54 @@ Vitest.describe('otel', () => {
239
239
  ),
240
240
  )
241
241
 
242
- Vitest.scopedLive('QueryBuilder subscription - unsubscribe functionality', () =>
242
+ Vitest.live('QueryBuilder subscription - skipInitialRun', () =>
243
+ Effect.gen(function* () {
244
+ const { store, exporter, span, provider } = yield* makeQuery
245
+
246
+ const callbackResults: Todo[] = []
247
+ const defaultTodo: Todo = { id: '', text: '', completed: false }
248
+
249
+ const queryBuilder = tables.todos
250
+ .where({ completed: false })
251
+ .first({ behaviour: 'fallback', fallback: () => defaultTodo })
252
+
253
+ const unsubscribe = store.subscribe(
254
+ queryBuilder,
255
+ (result) => {
256
+ callbackResults.push(result)
257
+ },
258
+ { skipInitialRun: true },
259
+ )
260
+
261
+ expect(callbackResults).toHaveLength(0)
262
+
263
+ store.commit(events.todoCreated({ id: 't-skip', text: 'skip initial', completed: false }))
264
+
265
+ expect(callbackResults).toHaveLength(1)
266
+ expect(callbackResults[0]).toMatchObject({
267
+ id: 't-skip',
268
+ text: 'skip initial',
269
+ completed: false,
270
+ })
271
+
272
+ unsubscribe()
273
+ span.end()
274
+
275
+ return { exporter, provider }
276
+ }).pipe(
277
+ Effect.scoped,
278
+ Effect.tap(({ exporter, provider }) =>
279
+ Effect.promise(async () => {
280
+ await provider.forceFlush()
281
+ expect(getSimplifiedRootSpan(exporter, 'createStore', mapAttributes)).toMatchSnapshot()
282
+ expect(getAllSimplifiedRootSpans(exporter, 'LiveStore:commit', mapAttributes)).toMatchSnapshot()
283
+ await provider.shutdown()
284
+ }),
285
+ ),
286
+ ),
287
+ )
288
+
289
+ Vitest.live('QueryBuilder subscription - unsubscribe functionality', () =>
243
290
  Effect.gen(function* () {
244
291
  const { store, exporter, span, provider } = yield* makeQuery
245
292
 
@@ -251,16 +298,12 @@ Vitest.describe('otel', () => {
251
298
  .where({ completed: false })
252
299
  .first({ behaviour: 'fallback', fallback: () => defaultTodo })
253
300
 
254
- const unsubscribe1 = store.subscribe(queryBuilder, {
255
- onUpdate: (result) => {
256
- callbackResults1.push(result)
257
- },
301
+ const unsubscribe1 = store.subscribe(queryBuilder, (result) => {
302
+ callbackResults1.push(result)
258
303
  })
259
304
 
260
- const unsubscribe2 = store.subscribe(queryBuilder, {
261
- onUpdate: (result) => {
262
- callbackResults2.push(result)
263
- },
305
+ const unsubscribe2 = store.subscribe(queryBuilder, (result) => {
306
+ callbackResults2.push(result)
264
307
  })
265
308
 
266
309
  expect(callbackResults1).toHaveLength(1)
@@ -295,16 +338,111 @@ Vitest.describe('otel', () => {
295
338
  ),
296
339
  )
297
340
 
298
- Vitest.scopedLive('QueryBuilder subscription - direct table subscription', () =>
341
+ Vitest.live('QueryBuilder subscription - async iterator', () =>
342
+ Effect.gen(function* () {
343
+ const { store, exporter, span, provider } = yield* makeQuery
344
+
345
+ const defaultTodo: Todo = { id: '', text: '', completed: false }
346
+
347
+ const queryBuilder = tables.todos
348
+ .where({ completed: false })
349
+ .first({ behaviour: 'fallback', fallback: () => defaultTodo })
350
+
351
+ yield* Effect.promise(async () => {
352
+ const iterator = store.subscribe(queryBuilder)[Symbol.asyncIterator]()
353
+
354
+ const initial = await iterator.next()
355
+ expect(initial.done).toBe(false)
356
+ expect(initial.value).toMatchObject(defaultTodo)
357
+
358
+ store.commit(events.todoCreated({ id: 't-async', text: 'write tests', completed: false }))
359
+
360
+ const update = await iterator.next()
361
+ expect(update.done).toBe(false)
362
+ expect(update.value).toMatchObject({
363
+ id: 't-async',
364
+ text: 'write tests',
365
+ completed: false,
366
+ })
367
+
368
+ const doneResult = await iterator.return?.()
369
+ assert(doneResult)
370
+ expect(doneResult.done).toBe(true)
371
+ })
372
+
373
+ span.end()
374
+
375
+ return { exporter, provider }
376
+ }).pipe(
377
+ Effect.scoped,
378
+ Effect.tap(({ exporter, provider }) =>
379
+ Effect.promise(async () => {
380
+ await provider.forceFlush()
381
+ expect(getSimplifiedRootSpan(exporter, 'createStore', mapAttributes)).toMatchSnapshot()
382
+ expect(getAllSimplifiedRootSpans(exporter, 'LiveStore:commit', mapAttributes)).toMatchSnapshot()
383
+ await provider.shutdown()
384
+ }),
385
+ ),
386
+ ),
387
+ )
388
+
389
+ Vitest.live('QueryBuilder subscription - async iterator with skipInitialRun', () =>
390
+ Effect.gen(function* () {
391
+ const { store, exporter, span, provider } = yield* makeQuery
392
+
393
+ const defaultTodo: Todo = { id: '', text: '', completed: false }
394
+
395
+ const queryBuilder = tables.todos
396
+ .where({ completed: false })
397
+ .first({ behaviour: 'fallback', fallback: () => defaultTodo })
398
+
399
+ yield* Effect.promise(async () => {
400
+ const iterator = store.subscribe(queryBuilder, { skipInitialRun: true })[Symbol.asyncIterator]()
401
+
402
+ const pending = Symbol('pending')
403
+ const nextPromise = iterator.next()
404
+ const raceResult = await Promise.race([nextPromise, Promise.resolve(pending)])
405
+ expect(raceResult).toBe(pending)
406
+
407
+ store.commit(events.todoCreated({ id: 't-async-skip', text: 'write tests later', completed: false }))
408
+
409
+ const update = await nextPromise
410
+ expect(update.done).toBe(false)
411
+ expect(update.value).toMatchObject({
412
+ id: 't-async-skip',
413
+ text: 'write tests later',
414
+ completed: false,
415
+ })
416
+
417
+ const doneResult = await iterator.return?.()
418
+ assert(doneResult)
419
+ expect(doneResult.done).toBe(true)
420
+ })
421
+
422
+ span.end()
423
+
424
+ return { exporter, provider }
425
+ }).pipe(
426
+ Effect.scoped,
427
+ Effect.tap(({ exporter, provider }) =>
428
+ Effect.promise(async () => {
429
+ await provider.forceFlush()
430
+ expect(getSimplifiedRootSpan(exporter, 'createStore', mapAttributes)).toMatchSnapshot()
431
+ expect(getAllSimplifiedRootSpans(exporter, 'LiveStore:commit', mapAttributes)).toMatchSnapshot()
432
+ await provider.shutdown()
433
+ }),
434
+ ),
435
+ ),
436
+ )
437
+
438
+ Vitest.live('QueryBuilder subscription - direct table subscription', () =>
299
439
  Effect.gen(function* () {
300
440
  const { store, exporter, span, provider } = yield* makeQuery
301
441
 
302
442
  const callbackResults: any[] = []
303
443
 
304
- const unsubscribe = store.subscribe(tables.todos, {
305
- onUpdate: (result) => {
306
- callbackResults.push(result)
307
- },
444
+ const unsubscribe = store.subscribe(tables.todos, (result) => {
445
+ callbackResults.push(result)
308
446
  })
309
447
 
310
448
  expect(callbackResults).toHaveLength(1)
@@ -1,29 +1,30 @@
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
- UnexpectedError,
11
+ UnknownError,
11
12
  } from '@livestore/common'
12
- import { deepEqual, omitUndefineds, shouldNeverHappen } from '@livestore/utils'
13
- import { Equal, Hash, Predicate, Schema, TreeFormatter } from '@livestore/utils/effect'
14
- import * as otel from '@opentelemetry/api'
13
+ import { deepEqual, objectToString, omitUndefineds, shouldNeverHappen } from '@livestore/utils'
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'
18
18
  import type { RefreshReason } from '../store/store-types.ts'
19
+ import { StoreInternalsSymbol } from '../store/store-types.ts'
19
20
  import { isValidFunctionString } from '../utils/function-string.ts'
20
21
  import type { DepKey, GetAtomResult, LiveQueryDef, ReactivityGraph, ReactivityGraphContext } from './base-class.ts'
21
- import { depsToString, LiveStoreQueryBase, makeGetAtomResult, withRCMap } from './base-class.ts'
22
+ import { depsToString, isLiveQueryDef, LiveStoreQueryBase, makeGetAtomResult, withRCMap } from './base-class.ts'
22
23
  import { makeExecBeforeFirstRun, rowQueryLabel } from './client-document-get-query.ts'
23
24
 
24
25
  export type QueryInputRaw<TDecoded, TEncoded> = {
25
26
  query: string
26
- schema: Schema.Schema<TDecoded, TEncoded>
27
+ schema: Schema.Codec<TDecoded, TEncoded>
27
28
  bindValues?: Bindable
28
29
  /**
29
30
  * Can be provided explicitly to slightly speed up initial query performance
@@ -102,7 +103,11 @@ export const queryDb: {
102
103
  } = (queryInput, options) => {
103
104
  const { queryString, extraDeps } = getQueryStringAndExtraDeps(queryInput)
104
105
 
105
- const hash = [queryString, options?.deps ? depsToString(options.deps) : undefined, depsToString(extraDeps)]
106
+ const hash = [
107
+ queryString,
108
+ options?.deps !== undefined ? depsToString(options.deps) : undefined,
109
+ depsToString(extraDeps),
110
+ ]
106
111
  .filter(Boolean)
107
112
  .join('-')
108
113
 
@@ -111,7 +116,7 @@ export const queryDb: {
111
116
  }
112
117
 
113
118
  if (hash.trim() === '') {
114
- return shouldNeverHappen(`Invalid query hash for query: ${queryInput}`)
119
+ return shouldNeverHappen('Invalid query hash for query:', objectToString(queryInput))
115
120
  }
116
121
 
117
122
  const label = options?.label ?? queryString
@@ -130,8 +135,8 @@ export const queryDb: {
130
135
  }),
131
136
  label,
132
137
  hash,
133
- [Equal.symbol](that: LiveQueryDef<any>): boolean {
134
- return this.hash === that.hash
138
+ [Equal.symbol](that: Equal.Equal): boolean {
139
+ return isLiveQueryDef(that) && that._tag === 'def' && this.hash === that.hash
135
140
  },
136
141
  [Hash.symbol](): number {
137
142
  return Hash.string(this.hash)
@@ -154,12 +159,12 @@ const bindValuesToDepKey = (bindValues: Bindable | undefined): DepKey => {
154
159
  const getQueryStringAndExtraDeps = (
155
160
  queryInput: QueryInput<any, any> | ((get: GetAtomResult) => QueryInput<any, any>),
156
161
  ): { queryString: string; extraDeps: DepKey } => {
157
- if (isQueryBuilder(queryInput)) {
162
+ if (isQueryBuilder(queryInput) === true) {
158
163
  const { query, bindValues } = queryInput.asSql()
159
164
  return { queryString: query, extraDeps: bindValuesToDepKey(bindValues) }
160
165
  }
161
166
 
162
- if (isQueryInputRaw(queryInput)) {
167
+ if (isQueryInputRaw(queryInput) === true) {
163
168
  return { queryString: queryInput.query, extraDeps: bindValuesToDepKey(queryInput.bindValues) }
164
169
  }
165
170
 
@@ -167,7 +172,7 @@ const getQueryStringAndExtraDeps = (
167
172
  return { queryString: queryInput.toString(), extraDeps: [] }
168
173
  }
169
174
 
170
- return shouldNeverHappen(`Invalid query input: ${queryInput}`)
175
+ return shouldNeverHappen(`Invalid query input: ${String(queryInput)}`)
171
176
  }
172
177
 
173
178
  /* An object encapsulating a reactive SQL query */
@@ -213,9 +218,13 @@ export class LiveStoreDbQuery<TResultSchema, TResult = TResultSchema> extends Li
213
218
 
214
219
  this.mapResult = map === undefined ? (rows: any) => rows as TResult : map
215
220
 
216
- const schemaRef: { current: Schema.Schema<any, any> | undefined } = {
221
+ const schemaRef: { current: Schema.Codec<any, any> | undefined } = {
217
222
  current:
218
- typeof queryInput === 'function' ? undefined : isQueryBuilder(queryInput) ? undefined : queryInput.schema,
223
+ typeof queryInput === 'function'
224
+ ? undefined
225
+ : isQueryBuilder(queryInput) === true
226
+ ? undefined
227
+ : queryInput.schema,
219
228
  }
220
229
 
221
230
  const execBeforeFirstRunRef: {
@@ -231,7 +240,7 @@ export class LiveStoreDbQuery<TResultSchema, TResult = TResultSchema> extends Li
231
240
  const fromQueryBuilder = (qb: QueryBuilder.Any, otelContext: otel.Context | undefined) => {
232
241
  try {
233
242
  const qbRes = qb.asSql()
234
- const schema = getResultSchema(qb) as Schema.Schema<TResultSchema, ReadonlyArray<any>>
243
+ const schema = getResultSchema(qb) as Schema.Codec<TResultSchema, ReadonlyArray<any>>
235
244
  const ast = qb[QueryBuilderAstSymbol]
236
245
 
237
246
  return {
@@ -253,7 +262,7 @@ export class LiveStoreDbQuery<TResultSchema, TResult = TResultSchema> extends Li
253
262
  : undefined,
254
263
  }
255
264
  } catch (cause) {
256
- throw new UnexpectedError({ cause, note: `Error building query for ${qb.toString()}`, payload: { qb } })
265
+ throw new UnknownError({ cause, note: `Error building query for ${qb.toString()}`, payload: { qb } })
257
266
  }
258
267
  }
259
268
 
@@ -269,7 +278,7 @@ export class LiveStoreDbQuery<TResultSchema, TResult = TResultSchema> extends Li
269
278
 
270
279
  let queryInputRaw: TQueryInputRaw
271
280
 
272
- if (isQueryBuilder(queryInputResult)) {
281
+ if (isQueryBuilder(queryInputResult) === true) {
273
282
  const res = fromQueryBuilder(queryInputResult, otelContext)
274
283
  queryInputRaw = res.queryInputRaw
275
284
  // setting label dynamically here
@@ -296,7 +305,7 @@ export class LiveStoreDbQuery<TResultSchema, TResult = TResultSchema> extends Li
296
305
  this.queryInput$ = queryInputRaw$OrQueryInputRaw
297
306
  } else {
298
307
  let queryInputRaw: TQueryInputRaw
299
- if (isQueryBuilder(queryInput)) {
308
+ if (isQueryBuilder(queryInput) === true) {
300
309
  const res = fromQueryBuilder(queryInput, otelContext)
301
310
  queryInputRaw = res.queryInputRaw
302
311
  label = res.label
@@ -309,7 +318,7 @@ export class LiveStoreDbQuery<TResultSchema, TResult = TResultSchema> extends Li
309
318
  queryInputRaw$OrQueryInputRaw = queryInputRaw
310
319
 
311
320
  // this.label = inputLabel ? this.label : `db(${})`
312
- if (inputLabel === undefined && isQueryBuilder(queryInput)) {
321
+ if (inputLabel === undefined && isQueryBuilder(queryInput) === true) {
313
322
  const ast = queryInput[QueryBuilderAstSymbol]
314
323
  if (ast._tag === 'RowQuery') {
315
324
  label = `db(${rowQueryLabel(ast.tableDef, ast.id)})`
@@ -319,9 +328,9 @@ export class LiveStoreDbQuery<TResultSchema, TResult = TResultSchema> extends Li
319
328
 
320
329
  const queriedTablesRef: { current: Set<string> | undefined } = { current: undefined }
321
330
 
322
- const makeResultsEqual = (resultSchema: Schema.Schema<any, any>) => {
331
+ const makeResultsEqual = (resultSchema: Schema.Codec<any, any>) => {
323
332
  // Creating the equivalence function eagerly in outer scope as it might be expensive
324
- const eq = Schema.equivalence(resultSchema)
333
+ const eq = Schema.toEquivalence(resultSchema)
325
334
  return (a: TResult, b: TResult) => (a === NOT_REFRESHED_YET || b === NOT_REFRESHED_YET ? false : eq(a, b))
326
335
  }
327
336
 
@@ -341,13 +350,15 @@ export class LiveStoreDbQuery<TResultSchema, TResult = TResultSchema> extends Li
341
350
  'db:...', // NOTE span name will be overridden further down
342
351
  {
343
352
  attributes: {
344
- 'livestore.debugRefreshReason': Predicate.hasProperty(debugRefreshReason, 'label')
345
- ? (debugRefreshReason.label as string)
346
- : debugRefreshReason?._tag,
353
+ 'livestore.debugRefreshReason':
354
+ Predicate.hasProperty(debugRefreshReason, 'label') === true
355
+ ? (debugRefreshReason.label as string)
356
+ : debugRefreshReason?._tag,
347
357
  },
348
358
  },
349
359
  otelContext ?? queryContext.rootOtelContext,
350
360
  (span) => {
361
+ const startTimePerfNow = performance.now()
351
362
  const otelContext = otel.trace.setSpan(otel.context.active(), span)
352
363
  const { store } = queryContext
353
364
 
@@ -356,45 +367,49 @@ export class LiveStoreDbQuery<TResultSchema, TResult = TResultSchema> extends Li
356
367
  execBeforeFirstRunRef.current = undefined
357
368
  }
358
369
 
359
- const queryInputResult = isThunk(queryInputRaw$OrQueryInputRaw)
360
- ? (get(queryInputRaw$OrQueryInputRaw, otelContext, debugRefreshReason) as TQueryInputRaw)
361
- : (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)
362
374
 
363
375
  const sqlString = queryInputResult.query
364
376
  const bindValues = queryInputResult.bindValues
365
377
 
366
378
  if (queriedTablesRef.current === undefined) {
367
- queriedTablesRef.current = store.sqliteDbWrapper.getTablesUsed(sqlString)
379
+ queriedTablesRef.current = store[StoreInternalsSymbol].sqliteDbWrapper.getTablesUsed(sqlString)
368
380
  }
369
381
 
370
- if (bindValues !== undefined) {
371
- replaceSessionIdSymbol(bindValues, store.clientSession.sessionId)
372
- }
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)
373
386
 
374
387
  // Establish a reactive dependency on the tables used in the query
375
388
  for (const tableName of queriedTablesRef.current) {
376
- const tableRef = store.tableRefs[tableName] ?? shouldNeverHappen(`No table ref found for ${tableName}`)
389
+ const tableRef =
390
+ store[StoreInternalsSymbol].tableRefs[tableName] ??
391
+ shouldNeverHappen(`No table ref found for ${tableName}`)
377
392
  get(tableRef, otelContext, debugRefreshReason)
378
393
  }
379
394
 
380
395
  span.setAttribute('sql.query', sqlString)
381
396
  span.updateName(`db:${sqlString.slice(0, 50)}`)
382
397
 
383
- const rawDbResults = store.sqliteDbWrapper.cachedSelect<any>(
398
+ const rawDbResults = store[StoreInternalsSymbol].sqliteDbWrapper.cachedSelect(
384
399
  sqlString,
385
- bindValues ? prepareBindValues(bindValues, sqlString) : undefined,
400
+ resolvedBindValues !== undefined ? prepareBindValues(resolvedBindValues, sqlString) : undefined,
386
401
  {
387
- queriedTables: queriedTablesRef.current,
388
402
  otelContext,
403
+ ...(queriedTablesRef.current !== undefined ? { queriedTables: queriedTablesRef.current } : {}),
389
404
  },
390
405
  )
391
406
 
392
407
  span.setAttribute('sql.rowsCount', rawDbResults.length)
393
408
 
394
- const parsedResult = Schema.decodeEither(schemaRef.current!)(rawDbResults)
409
+ const parsedResult = Schema.decodeResult(schemaRef.current!)(rawDbResults)
395
410
 
396
- if (parsedResult._tag === 'Left') {
397
- const parseErrorStr = TreeFormatter.formatErrorSync(parsedResult.left)
411
+ if (Result.isFailure(parsedResult) === true) {
412
+ const parseErrorStr = SchemaIssue.makeFormatterDefault()(parsedResult.failure.issue)
398
413
  const expectedSchemaStr = String(schemaRef.current!.ast)
399
414
  const bindValuesStr = bindValues === undefined ? '' : `\nBind values: ${JSON.stringify(bindValues)}`
400
415
 
@@ -415,11 +430,11 @@ Result:`,
415
430
  )
416
431
  }
417
432
 
418
- const result = this.mapResult(parsedResult.right)
433
+ const result = this.mapResult(parsedResult.success)
419
434
 
420
435
  span.end()
421
436
 
422
- const durationMs = getDurationMsFromSpan(span)
437
+ const durationMs = performance.now() - startTimePerfNow
423
438
 
424
439
  this.executionTimes.push(durationMs)
425
440
 
@@ -1,13 +1,14 @@
1
- import { Effect } from '@livestore/utils/effect'
2
- import { Vitest } from '@livestore/utils-dev/node-vitest'
3
1
  import { expect } from 'vitest'
4
2
 
3
+ import { Vitest } from '@livestore/utils-dev/node-vitest'
4
+ import { Effect } from '@livestore/utils/effect'
5
+
5
6
  import { makeTodoMvc } from '../utils/tests/fixture.ts'
6
7
  import { computed } from './computed.ts'
7
8
  import { signal } from './signal.ts'
8
9
 
9
10
  Vitest.describe('signal', () => {
10
- Vitest.scopedLive('should be able to create a signal', () =>
11
+ Vitest.live('should be able to create a signal', () =>
11
12
  Effect.gen(function* () {
12
13
  const num$ = signal(0, { label: 'num$' })
13
14
 
@@ -23,7 +24,7 @@ Vitest.describe('signal', () => {
23
24
  }),
24
25
  )
25
26
 
26
- Vitest.scopedLive('counter example', () =>
27
+ Vitest.live('counter example', () =>
27
28
  Effect.gen(function* () {
28
29
  const count$ = signal(0, { label: 'count$' })
29
30
 
@@ -4,8 +4,50 @@ 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
+ /**
10
+ * Creates a reactive signal for ephemeral, local-only state that isn't persisted to the database.
11
+ *
12
+ * Signals are useful for UI state that needs to trigger query re-evaluation but shouldn't be
13
+ * synced across clients or stored permanently—such as search filters, selected items, or
14
+ * temporary form values.
15
+ *
16
+ * Unlike database-backed state (via events), signals:
17
+ * - Are not persisted or synced
18
+ * - Exist only for the lifetime of the Store
19
+ * - Can hold any value type (primitives, objects, functions)
20
+ *
21
+ * @example
22
+ * ```ts
23
+ * // Create a signal for search text
24
+ * const searchText$ = signal('', { label: 'searchText' })
25
+ *
26
+ * // Create a query that depends on the signal
27
+ * const filteredTodos$ = queryDb(
28
+ * (get) => tables.todos.where({ text: { $like: `%${get(searchText$)}%` } }),
29
+ * { deps: [searchText$] }
30
+ * )
31
+ *
32
+ * // Update the signal (triggers query re-evaluation)
33
+ * store.setSignal(searchText$, 'buy')
34
+ *
35
+ * // Read the current value
36
+ * const results = store.query(filteredTodos$)
37
+ * ```
38
+ *
39
+ * @example
40
+ * ```ts
41
+ * // Counter with functional updates
42
+ * const count$ = signal(0, { label: 'count' })
43
+ *
44
+ * store.setSignal(count$, (prev) => prev + 1)
45
+ * ```
46
+ *
47
+ * @param defaultValue - Initial value of the signal
48
+ * @param options.label - Human-readable label for debugging and devtools
49
+ * @returns A signal definition that can be used with `store.query()`, `store.setSignal()`, and as a dependency in other queries
50
+ */
9
51
  export const signal = <T>(
10
52
  defaultValue: T,
11
53
  options?: {
@@ -28,8 +70,8 @@ export const signal = <T>(
28
70
  def,
29
71
  }),
30
72
  ),
31
- [Equal.symbol](that: SignalDef<T>): boolean {
32
- return this.hash === that.hash
73
+ [Equal.symbol](that: Equal.Equal): boolean {
74
+ return isSignalDef(that) && this.hash === that.hash
33
75
  },
34
76
  [Hash.symbol](): number {
35
77
  return Hash.string(this.hash)
@@ -39,6 +81,13 @@ export const signal = <T>(
39
81
  return def
40
82
  }
41
83
 
84
+ /**
85
+ * A live signal instance bound to a specific Store.
86
+ *
87
+ * Signal instances are created internally when you use a `SignalDef` with the Store.
88
+ * You typically don't construct these directly—use {@link signal} to create definitions
89
+ * and `store.setSignal()` / `store.query()` to interact with them.
90
+ */
42
91
  export class Signal<T> extends LiveStoreQueryBase<T> implements ISignal<T> {
43
92
  _tag = 'signal' as const
44
93
  readonly ref: RG.Ref<T, ReactivityGraphContext, RefreshReason>