@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
@@ -9,8 +9,17 @@ import type { ShutdownDeferred, Store } from '@livestore/livestore'
9
9
  import { createStore, makeShutdownDeferred } from '@livestore/livestore'
10
10
  import { omitUndefineds } from '@livestore/utils'
11
11
  import { Vitest } from '@livestore/utils-dev/node-vitest'
12
- import type { OtelTracer, Scope } from '@livestore/utils/effect'
13
- import { Context, Effect, FetchHttpClient, Layer, Logger, LogLevel, Queue, Stream } from '@livestore/utils/effect'
12
+ import {
13
+ type OtelTracer,
14
+ type Scope,
15
+ Context,
16
+ Effect,
17
+ FetchHttpClient,
18
+ Layer,
19
+ Queue,
20
+ References,
21
+ Stream,
22
+ } from '@livestore/utils/effect'
14
23
  import { nanoid } from '@livestore/utils/nanoid'
15
24
  import { PlatformNode } from '@livestore/utils/node'
16
25
 
@@ -22,7 +31,7 @@ const withTestCtx = Vitest.makeWithTestCtx({
22
31
  TestContextLive,
23
32
  PlatformNode.NodeFileSystem.layer,
24
33
  FetchHttpClient.layer,
25
- Logger.minimumLogLevel(LogLevel.Debug),
34
+ Layer.succeed(References.MinimumLogLevel, 'Debug'),
26
35
  ),
27
36
  })
28
37
 
@@ -32,7 +41,7 @@ const withTestCtx = Vitest.makeWithTestCtx({
32
41
  * tests/package-common/src/leader-thread/stream-events.test.ts
33
42
  */
34
43
  Vitest.describe('Store events API', () => {
35
- Vitest.scopedLive('should resume when reconnected to sync backend', (test) =>
44
+ Vitest.live('should resume when reconnected to sync backend', (test) =>
36
45
  Effect.gen(function* () {
37
46
  const { makeStore, mockSyncBackend } = yield* TestContext
38
47
  const store = yield* makeStore()
@@ -69,7 +78,7 @@ Vitest.describe('Store events API', () => {
69
78
  )
70
79
  })
71
80
 
72
- class TestContext extends Context.Tag('TestContext')<
81
+ class TestContext extends Context.Service<
73
82
  TestContext,
74
83
  {
75
84
  makeStore: (args?: {
@@ -87,9 +96,9 @@ class TestContext extends Context.Tag('TestContext')<
87
96
  mockSyncBackend: MockSyncBackend
88
97
  shutdownDeferred: ShutdownDeferred
89
98
  }
90
- >() {}
99
+ >()('TestContext') {}
91
100
 
92
- const TestContextLive = Layer.scoped(
101
+ const TestContextLive = Layer.effect(
93
102
  TestContext,
94
103
  Effect.gen(function* () {
95
104
  const mockSyncBackend = yield* makeMockSyncBackend()
@@ -20,7 +20,7 @@ describe('isQueryable', () => {
20
20
  it('identifies live query definitions', () => {
21
21
  const def = queryDb({
22
22
  query: 'select 1 as value',
23
- schema: Schema.Array(Schema.Struct({ value: Schema.Number })),
23
+ schema: Schema.Array(Schema.Struct({ value: Schema.Finite })),
24
24
  })
25
25
 
26
26
  expect(isQueryable(def)).toBe(true)
@@ -1,21 +1,19 @@
1
1
  import type * as otel from '@opentelemetry/api'
2
2
 
3
3
  import {
4
+ type BackendIdMismatchError,
4
5
  type ClientSession,
5
6
  type ClientSessionSyncProcessor,
6
- type ClientSessionSyncProcessorSimulationParams,
7
7
  type IntentionalShutdownCause,
8
8
  isQueryBuilder,
9
9
  type MaterializeError,
10
10
  type QueryBuilder,
11
11
  type StoreInterrupted,
12
- type BackendIdMismatchError,
13
12
  type UnknownError,
14
13
  } from '@livestore/common'
15
14
  import type { StreamEventsOptions } from '@livestore/common/leader-thread'
16
15
  import type { LiveStoreEvent, LiveStoreSchema } from '@livestore/common/schema'
17
- import type { Effect, Runtime, Schema, Scope } from '@livestore/utils/effect'
18
- import { Deferred, Predicate } from '@livestore/utils/effect'
16
+ import { type Context, Deferred, type Effect, Predicate, type Schema, type Scope } from '@livestore/utils/effect'
19
17
 
20
18
  import type {
21
19
  LiveQuery,
@@ -24,7 +22,7 @@ import type {
24
22
  ReactivityGraphContext,
25
23
  SignalDef,
26
24
  } from '../live-queries/base-class.ts'
27
- import { TypeId } from '../live-queries/base-class.ts'
25
+ import { isLiveQueryDef, TypeId } from '../live-queries/base-class.ts'
28
26
  import type { DebugRefreshReasonBase, Ref } from '../reactive.ts'
29
27
  import type { SqliteDbWrapper } from '../SqliteDbWrapper.ts'
30
28
  import type { ReferenceCountedSet } from '../utils/data-structures.ts'
@@ -98,7 +96,7 @@ export type StoreInternals = {
98
96
  * Exposed primarily for Devtools (e.g. databrowser) to validate ad‑hoc
99
97
  * event payloads. Application code should not depend on it directly.
100
98
  */
101
- readonly eventSchema: Schema.Schema<LiveStoreEvent.Client.Decoded, LiveStoreEvent.Client.Encoded>
99
+ readonly eventSchema: Schema.Codec<LiveStoreEvent.Client.Decoded, LiveStoreEvent.Client.Encoded>
102
100
 
103
101
  /**
104
102
  * The active client session backing this Store. Provides access to the
@@ -115,14 +113,14 @@ export type StoreInternals = {
115
113
  readonly sqliteDbWrapper: SqliteDbWrapper
116
114
 
117
115
  /**
118
- * Effect runtime and scope used to fork background fibers for the Store.
116
+ * Effect context and scope used to fork background fibers for the Store.
119
117
  *
120
- * - `runtime` executes effects from imperative Store APIs.
118
+ * - `services` provides services when executing effects from imperative Store APIs.
121
119
  * - `lifetimeScope` owns forked fibers; closed during Store shutdown.
122
120
  */
123
121
  readonly effectContext: {
124
- /** Effect runtime to run Store effects with proper environment. */
125
- readonly runtime: Runtime.Runtime<Scope.Scope>
122
+ /** Effect context used to run Store effects with proper services. */
123
+ readonly services: Context.Context<Scope.Scope>
126
124
  /** Scope that owns all long‑lived fibers spawned by the Store. */
127
125
  readonly lifetimeScope: Scope.Scope
128
126
  }
@@ -189,7 +187,7 @@ export type StoreConstructorParams<TSchema extends LiveStoreSchema = LiveStoreSc
189
187
  context: TContext
190
188
  otelOptions: OtelOptions
191
189
  effectContext: {
192
- runtime: Runtime.Runtime<Scope.Scope>
190
+ services: Context.Context<Scope.Scope>
193
191
  lifetimeScope: Scope.Scope
194
192
  }
195
193
  confirmUnsavedChanges: boolean
@@ -197,9 +195,6 @@ export type StoreConstructorParams<TSchema extends LiveStoreSchema = LiveStoreSc
197
195
  params: {
198
196
  leaderPushBatchSize: number
199
197
  eventQueryBatchSize?: number
200
- simulation?: {
201
- clientSessionSyncProcessor: typeof ClientSessionSyncProcessorSimulationParams.Type
202
- }
203
198
  }
204
199
  __runningInDevtools: boolean
205
200
  }
@@ -335,48 +330,7 @@ export namespace Queryable {
335
330
  export type Result<TQueryable extends Queryable<any>> = TQueryable extends Queryable<infer TResult> ? TResult : never
336
331
  }
337
332
 
338
- /**
339
- * Type guard that checks if a value is a query or signal definition.
340
- *
341
- * Use this to distinguish between definitions (blueprints) and instances (live queries).
342
- * Definitions are created by `queryDb()`, `computed()`, and `signal()`.
343
- *
344
- * @example
345
- * ```ts
346
- * const todos$ = queryDb(tables.todos.all())
347
- *
348
- * if (isLiveQueryDef(todos$)) {
349
- * console.log('This is a definition:', todos$.label)
350
- * }
351
- * ```
352
- */
353
- export const isLiveQueryDef = (value: unknown): value is LiveQueryDef<any> | SignalDef<any> => {
354
- if (typeof value !== 'object' || value === null) {
355
- return false
356
- }
357
-
358
- if (!('_tag' in value)) {
359
- return false
360
- }
361
-
362
- const tag = (value as LiveQueryDef<any> | SignalDef<any>)._tag
363
- if (tag !== 'def' && tag !== 'signal-def') {
364
- return false
365
- }
366
-
367
- const candidate = value as LiveQueryDef<any>
368
- if (typeof candidate.make !== 'function') {
369
- // The store calls make() to turn the definition into a live query instance.
370
- return false
371
- }
372
-
373
- if (typeof candidate.hash !== 'string' || typeof candidate.label !== 'string') {
374
- // Both identifiers must be present so the store can cache and log the query.
375
- return false
376
- }
377
-
378
- return true
379
- }
333
+ export { isLiveQueryDef }
380
334
 
381
335
  /**
382
336
  * Type guard that checks if a value is a live query instance.
@@ -1,3 +1,5 @@
1
+ import * as otel from '@opentelemetry/api'
2
+
1
3
  import {
2
4
  type Bindable,
3
5
  type ClientSession,
@@ -11,10 +13,11 @@ import {
11
13
  MaterializeError,
12
14
  MaterializerHashMismatchError,
13
15
  makeClientSessionSyncProcessor,
14
- type PreparedBindValues,
15
16
  prepareBindValues,
16
17
  QueryBuilderAstSymbol,
17
- replaceSessionIdSymbol,
18
+ resolveSessionIdSymbolInBindValues,
19
+ SqliteDbHelper,
20
+ StateHead,
18
21
  type StorageMode,
19
22
  type SyncState,
20
23
  UnknownError,
@@ -23,8 +26,8 @@ import type { StreamEventsOptions } from '@livestore/common/leader-thread'
23
26
  import type { LiveStoreSchema } from '@livestore/common/schema'
24
27
  import { EventSequenceNumber, LiveStoreEvent, resolveEventDef, SystemTables } from '@livestore/common/schema'
25
28
  import { assertNever, isDevEnv, objectToString, omitUndefineds, shouldNeverHappen } from '@livestore/utils'
26
- import type { Scope } from '@livestore/utils/effect'
27
29
  import {
30
+ type Scope,
28
31
  Cause,
29
32
  Effect,
30
33
  Exit,
@@ -32,12 +35,12 @@ import {
32
35
  Inspectable,
33
36
  Option,
34
37
  OtelTracer,
35
- Runtime,
38
+ Queue,
39
+ Result,
36
40
  Schema,
37
41
  Stream,
38
42
  } from '@livestore/utils/effect'
39
43
  import { nanoid } from '@livestore/utils/nanoid'
40
- import * as otel from '@opentelemetry/api'
41
44
 
42
45
  import type { LiveQuery, ReactivityGraphContext, SignalDef } from '../live-queries/base-class.ts'
43
46
  import { makeReactivityGraph } from '../live-queries/base-class.ts'
@@ -206,14 +209,15 @@ export class Store<TSchema extends LiveStoreSchema = LiveStoreSchema.Any, TConte
206
209
  this.storageMode = clientSession.leaderThread.initialState.storageMode
207
210
 
208
211
  const reactivityGraph = makeReactivityGraph()
212
+ const stateHead = StateHead.make({ dbState: clientSession.sqliteDb })
209
213
 
210
214
  const syncProcessor = makeClientSessionSyncProcessor({
211
215
  schema,
212
216
  clientSession,
213
217
  materializeEvent: Effect.fn('client-session-sync-processor:materialize-event')(
214
- (eventEncoded, { withChangeset, materializerHashLeader }) =>
218
+ (eventEncoded, { materializerHashLeader }) =>
215
219
  // We need to use `Effect.gen` (even though we're using `Effect.fn`) so that we can pass `this` to the function
216
- Effect.gen(this, function* () {
220
+ Effect.gen({ self: this }, function* () {
217
221
  const resolution = yield* resolveEventDef(schema, {
218
222
  operation: '@livestore/livestore:store:materializeEvent',
219
223
  event: eventEncoded,
@@ -222,6 +226,7 @@ export class Store<TSchema extends LiveStoreSchema = LiveStoreSchema.Any, TConte
222
226
  if (resolution._tag === 'unknown') {
223
227
  // Runtime schema doesn't know this event yet; skip materialization but
224
228
  // keep the log entry so upgraded clients can replay it later.
229
+ yield* stateHead.set(eventEncoded.seqNum)
225
230
  return {
226
231
  writeTables: new Set<string>(),
227
232
  sessionChangeset: { _tag: 'no-op' as const },
@@ -286,18 +291,14 @@ export class Store<TSchema extends LiveStoreSchema = LiveStoreSchema.Any, TConte
286
291
  }
287
292
  }
288
293
 
289
- let sessionChangeset:
290
- | { _tag: 'sessionChangeset'; data: Uint8Array<ArrayBuffer>; debug: any }
291
- | { _tag: 'no-op' }
292
- | { _tag: 'unset' } = { _tag: 'unset' }
293
- if (withChangeset === true) {
294
- sessionChangeset = this[StoreInternalsSymbol].sqliteDbWrapper.withChangeset(exec).changeset
295
- } else {
296
- exec()
297
- }
294
+ const sessionChangeset = this[StoreInternalsSymbol].sqliteDbWrapper.withChangeset(exec).changeset
295
+ yield* stateHead.set(eventEncoded.seqNum)
298
296
 
299
297
  return { writeTables: writeTablesForEvent, sessionChangeset, materializerHash }
300
- }).pipe(Effect.mapError((cause) => MaterializeError.make({ cause }))),
298
+ }).pipe(
299
+ SqliteDbHelper.withSavepoint(clientSession.sqliteDb),
300
+ Effect.mapError((cause) => MaterializeError.make({ cause })),
301
+ ),
301
302
  ),
302
303
  rollback: (changeset) => {
303
304
  this[StoreInternalsSymbol].sqliteDbWrapper.rollback(changeset)
@@ -315,12 +316,9 @@ export class Store<TSchema extends LiveStoreSchema = LiveStoreSchema.Any, TConte
315
316
  ...omitUndefineds({
316
317
  leaderPushBatchSize: params.leaderPushBatchSize,
317
318
  }),
318
- ...(params.simulation?.clientSessionSyncProcessor !== undefined
319
- ? { simulation: params.simulation.clientSessionSyncProcessor }
320
- : {}),
321
319
  },
322
320
  confirmUnsavedChanges,
323
- }).pipe(Runtime.runSync(effectContext.runtime))
321
+ }).pipe(Effect.provideService(StateHead.StateHead, stateHead), Effect.runSyncWith(effectContext.services))
324
322
 
325
323
  // TODO generalize the `tableRefs` concept to allow finer-grained refs
326
324
  const tableRefs: { [key: string]: Ref<null, ReactivityGraphContext, RefreshReason> } = {}
@@ -370,7 +368,7 @@ export class Store<TSchema extends LiveStoreSchema = LiveStoreSchema.Any, TConte
370
368
  })
371
369
  }
372
370
 
373
- const boot = Effect.gen(this, function* () {
371
+ const boot = Effect.gen({ self: this }, function* () {
374
372
  yield* Effect.addFinalizer(() =>
375
373
  Effect.sync(() => {
376
374
  // Remove all table refs from the reactivity graph
@@ -394,7 +392,7 @@ export class Store<TSchema extends LiveStoreSchema = LiveStoreSchema.Any, TConte
394
392
 
395
393
  // Initialize internals bag
396
394
  this[StoreInternalsSymbol] = {
397
- eventSchema: LiveStoreEvent.Client.makeSchemaMemo(schema) as Schema.Schema<
395
+ eventSchema: LiveStoreEvent.Client.makeSchemaMemo(schema) as Schema.Codec<
398
396
  LiveStoreEvent.Client.Decoded,
399
397
  LiveStoreEvent.Client.Encoded
400
398
  >,
@@ -570,24 +568,111 @@ export class Store<TSchema extends LiveStoreSchema = LiveStoreSchema.Any, TConte
570
568
  ): AsyncIterable<TResult> => {
571
569
  this.checkShutdown('subscribe')
572
570
 
573
- return Stream.toAsyncIterable(this.subscribeStream(query, options))
571
+ // This used to delegate to `Stream.toAsyncIterable(this.subscribeStream(...))`.
572
+ // With Effect v4, the callback-backed stream producer is forked during stream startup, so a caller could request
573
+ // `next()`, commit an update, and race the actual subscription registration. Register directly here so the first
574
+ // pending `next()` observes post-subscription updates. In the future, this should be refactored back toward a shared
575
+ // stream/async-iterable bridge if Effect exposes a primitive with that synchronous registration guarantee.
576
+ return {
577
+ [Symbol.asyncIterator]: () => {
578
+ const services = this[StoreInternalsSymbol].effectContext.services
579
+ const runSync = Effect.runSyncWith(services)
580
+ const runPromiseExit = Effect.runPromiseExitWith(services)
581
+ let queue: Queue.Queue<TResult, Cause.Done> | undefined
582
+ let isDone = false
583
+ let unsubscribe: Unsubscribe | undefined
584
+
585
+ const done = (): IteratorResult<TResult> => ({ done: true, value: undefined })
586
+ const isClosed = () => isDone
587
+ const isQueueDone = (cause: Cause.Cause<Cause.Done>) => {
588
+ const error = Cause.findError(cause)
589
+ return Result.isSuccess(error) && Cause.isDone(error.success)
590
+ }
591
+
592
+ const ensureQueue = () => {
593
+ // Lazily allocate the queue so creating an async iterator without consuming it has no subscription-side work.
594
+ queue ??= Queue.unbounded<TResult, Cause.Done>().pipe(runSync)
595
+ return queue
596
+ }
597
+
598
+ const emit = (value: TResult) => {
599
+ if (isDone === true) return
600
+
601
+ Queue.offerUnsafe(ensureQueue(), value)
602
+ }
603
+
604
+ const ensureSubscribed = () => {
605
+ // Subscribe from the first `next()` call so callers that request a value before
606
+ // committing updates don't race the asynchronous Stream-to-iterator startup path.
607
+ unsubscribe ??= this.subscribeWithCallback(query, emit, options)
608
+ }
609
+
610
+ const close = (): IteratorResult<TResult> => {
611
+ if (isDone === false) {
612
+ isDone = true
613
+ unsubscribe?.()
614
+ unsubscribe = undefined
615
+
616
+ if (queue !== undefined) {
617
+ // Wake any pending `next()` call and prevent later callback emissions from being buffered.
618
+ Queue.shutdown(queue).pipe(runSync)
619
+ }
620
+ }
621
+
622
+ return done()
623
+ }
624
+
625
+ return {
626
+ next: async () => {
627
+ if (isDone === true) {
628
+ return done()
629
+ }
630
+
631
+ ensureSubscribed()
632
+
633
+ // Delegate buffering and pending-consumer wakeups to Effect's queue implementation.
634
+ const exit = await runPromiseExit(Queue.take(ensureQueue()))
635
+ if (Exit.isSuccess(exit) === true) {
636
+ return { done: false, value: exit.value }
637
+ }
638
+
639
+ if (isClosed() === true || isQueueDone(exit.cause) === true) {
640
+ return done()
641
+ }
642
+
643
+ throw Cause.squash(exit.cause)
644
+ },
645
+ return: () => Promise.resolve(close()),
646
+ throw: (cause) => {
647
+ close()
648
+ return Promise.reject(cause)
649
+ },
650
+ }
651
+ },
652
+ }
574
653
  }
575
654
 
576
655
  subscribeStream = <TResult>(query: Queryable<TResult>, options?: SubscribeOptions<TResult>): Stream.Stream<TResult> =>
577
- Stream.asyncPush<TResult>((emit) =>
578
- Effect.gen(this, function* () {
656
+ Stream.callback<TResult>((emit) =>
657
+ Effect.gen({ self: this }, function* () {
579
658
  const otelSpan = yield* OtelTracer.currentOtelSpan.pipe(
580
- Effect.catchTag('NoSuchElementException', () => Effect.succeed(undefined)),
659
+ Effect.catchTag('NoSuchElementError', () => Effect.void),
581
660
  )
582
661
  const otelContext =
583
662
  otelSpan !== undefined ? otel.trace.setSpan(otel.context.active(), otelSpan) : otel.context.active()
584
663
 
585
664
  yield* Effect.acquireRelease(
586
665
  Effect.sync(() =>
587
- this.subscribe(query, (result) => emit.single(result), {
588
- ...options,
589
- otelContext,
590
- }),
666
+ this.subscribe(
667
+ query,
668
+ (result) => {
669
+ Queue.offerUnsafe(emit, result)
670
+ },
671
+ {
672
+ ...options,
673
+ otelContext,
674
+ },
675
+ ),
591
676
  ),
592
677
  (unsub) => Effect.sync(() => unsub()),
593
678
  )
@@ -609,7 +694,7 @@ export class Store<TSchema extends LiveStoreSchema = LiveStoreSchema.Any, TConte
609
694
  * ```
610
695
  */
611
696
  query = <TResult>(
612
- query: Queryable<TResult> | { query: string; bindValues: Bindable; schema?: Schema.Schema<TResult> },
697
+ query: Queryable<TResult> | { query: string; bindValues: Bindable; schema?: Schema.Decoder<TResult> },
613
698
  options?: { otelContext?: otel.Context; debugRefreshReason?: RefreshReason },
614
699
  ): TResult => {
615
700
  this.checkShutdown('query')
@@ -623,7 +708,7 @@ export class Store<TSchema extends LiveStoreSchema = LiveStoreSchema.Any, TConte
623
708
  },
624
709
  ) as any
625
710
  if (query.schema !== undefined) {
626
- return Schema.decodeSync(query.schema)(res)
711
+ return Schema.decodeUnknownSync(query.schema)(res)
627
712
  }
628
713
  return res
629
714
  } else if (isQueryBuilder(query) === true) {
@@ -640,23 +725,25 @@ export class Store<TSchema extends LiveStoreSchema = LiveStoreSchema.Any, TConte
640
725
  const sqlRes = query.asSql()
641
726
  const schema = getResultSchema(query)
642
727
 
643
- // Replace SessionIdSymbol in bind values before executing the query
644
- if (sqlRes.bindValues !== undefined) {
645
- replaceSessionIdSymbol(sqlRes.bindValues, this[StoreInternalsSymbol].clientSession.sessionId)
646
- }
728
+ // Query builders preserve SessionIdSymbol so client-document queries can be reused across sessions.
729
+ // SQLite bind values must be concrete primitives, so resolve the symbol only at execution time.
730
+ const resolvedBindValues =
731
+ sqlRes.bindValues === undefined
732
+ ? undefined
733
+ : resolveSessionIdSymbolInBindValues(sqlRes.bindValues, this[StoreInternalsSymbol].clientSession.sessionId)
647
734
 
648
735
  const rawRes = this[StoreInternalsSymbol].sqliteDbWrapper.cachedSelect(
649
736
  sqlRes.query,
650
- sqlRes.bindValues as any as PreparedBindValues,
737
+ resolvedBindValues === undefined ? undefined : prepareBindValues(resolvedBindValues, sqlRes.query),
651
738
  {
652
739
  ...omitUndefineds({ otelContext: options?.otelContext }),
653
740
  queriedTables: new Set([query[QueryBuilderAstSymbol].tableDef.sqliteDef.name]),
654
741
  },
655
742
  )
656
743
 
657
- const decodeResult = Schema.decodeEither(schema)(rawRes)
658
- if (decodeResult._tag === 'Right') {
659
- return decodeResult.right
744
+ const decodeResult = Schema.decodeResult(schema)(rawRes)
745
+ if (Result.isSuccess(decodeResult) === true) {
746
+ return decodeResult.success
660
747
  } else {
661
748
  return shouldNeverHappen(
662
749
  'Failed to decode query result with for schema:',
@@ -664,7 +751,7 @@ export class Store<TSchema extends LiveStoreSchema = LiveStoreSchema.Any, TConte
664
751
  'raw result:',
665
752
  rawRes,
666
753
  'decode error:',
667
- decodeResult.left,
754
+ decodeResult.failure,
668
755
  )
669
756
  }
670
757
  } else if (query._tag === 'def') {
@@ -788,29 +875,25 @@ export class Store<TSchema extends LiveStoreSchema = LiveStoreSchema.Any, TConte
788
875
 
789
876
  const { events, options } = this.getCommitArgs(firstEventOrTxnFnOrOptions, restEvents)
790
877
 
791
- Effect.gen(this, function* () {
878
+ Effect.gen({ self: this }, function* () {
792
879
  const commitsSpan = otel.trace.getSpan(this[StoreInternalsSymbol].otel.commitsSpanContext)
793
880
  commitsSpan?.addEvent('commit')
794
881
  const currentSpan = yield* OtelTracer.currentOtelSpan.pipe(Effect.orDie)
795
882
  commitsSpan?.addLink({ context: currentSpan.spanContext() })
796
883
 
797
- for (const event of events) {
798
- replaceSessionIdSymbol(event.args, this[StoreInternalsSymbol].clientSession.sessionId)
799
- }
800
-
801
884
  if (events.length === 0) return
802
885
 
803
- const localRuntime = yield* Effect.runtime()
886
+ const localServices = yield* Effect.context()
804
887
 
805
888
  const encodedEvents = yield* this[StoreInternalsSymbol].syncProcessor.encodeEvents(events)
806
889
 
807
890
  const { writeTables } = yield* Effect.try({
808
891
  try: () => {
809
892
  const materialize = () =>
810
- this[StoreInternalsSymbol].syncProcessor.materializeEvents(encodedEvents).pipe(Runtime.runSync(localRuntime))
811
- return events.length > 1
812
- ? this[StoreInternalsSymbol].sqliteDbWrapper.txn(materialize)
813
- : materialize()
893
+ this[StoreInternalsSymbol].syncProcessor
894
+ .materializeEvents(encodedEvents)
895
+ .pipe(Effect.runSyncWith(localServices))
896
+ return events.length > 1 ? this[StoreInternalsSymbol].sqliteDbWrapper.txn(materialize) : materialize()
814
897
  },
815
898
  catch: (cause) => UnknownError.make({ cause }),
816
899
  })
@@ -854,9 +937,9 @@ export class Store<TSchema extends LiveStoreSchema = LiveStoreSchema.Any, TConte
854
937
  ...(options?.spanLinks?.map(OtelTracer.makeSpanLink) ?? []),
855
938
  ],
856
939
  }),
857
- Effect.tapErrorCause(Effect.logError),
858
- Effect.catchAllCause((cause) => Effect.fork(this.shutdown(cause))),
859
- Runtime.runSync(this[StoreInternalsSymbol].effectContext.runtime),
940
+ Effect.tapCause(Effect.logError),
941
+ Effect.catchCause((cause) => Effect.forkChild(this.shutdown(cause))),
942
+ Effect.runSyncWith(this[StoreInternalsSymbol].effectContext.services),
860
943
  )
861
944
  }
862
945
  //#endregion commit
@@ -934,8 +1017,8 @@ export class Store<TSchema extends LiveStoreSchema = LiveStoreSchema.Any, TConte
934
1017
  }
935
1018
 
936
1019
  return clientSession.leaderThread.events.stream(baseOptions).pipe(
937
- Stream.mapChunksEffect(Schema.decode(Schema.ChunkFromSelf(eventSchema))),
938
- Stream.catchTag('ParseError', (cause) => Stream.fail(UnknownError.make({ cause }))),
1020
+ Stream.mapEffect((event) => Schema.decodeEffect(eventSchema)(event)),
1021
+ Stream.catchTag('SchemaError', (cause) => Stream.fail(UnknownError.make({ cause }))),
939
1022
  Stream.tapError((error) => Effect.logError('Error in eventsStream', error)),
940
1023
  )
941
1024
  }
@@ -1028,7 +1111,7 @@ export class Store<TSchema extends LiveStoreSchema = LiveStoreSchema.Any, TConte
1028
1111
  )
1029
1112
 
1030
1113
  return () => {
1031
- Fiber.interrupt(fiber).pipe(Runtime.runFork(this[StoreInternalsSymbol].effectContext.runtime))
1114
+ Fiber.interrupt(fiber).pipe(Effect.runForkWith(this[StoreInternalsSymbol].effectContext.services))
1032
1115
  }
1033
1116
  }
1034
1117
 
@@ -1102,7 +1185,7 @@ export class Store<TSchema extends LiveStoreSchema = LiveStoreSchema.Any, TConte
1102
1185
  */
1103
1186
  _dev = {
1104
1187
  downloadDb: (source: 'local' | 'leader' = 'local') => {
1105
- Effect.gen(this, function* () {
1188
+ Effect.gen({ self: this }, function* () {
1106
1189
  const data =
1107
1190
  source === 'local'
1108
1191
  ? this[StoreInternalsSymbol].sqliteDbWrapper.export()
@@ -1112,14 +1195,14 @@ export class Store<TSchema extends LiveStoreSchema = LiveStoreSchema.Any, TConte
1112
1195
  },
1113
1196
 
1114
1197
  downloadEventlogDb: () => {
1115
- Effect.gen(this, function* () {
1198
+ Effect.gen({ self: this }, function* () {
1116
1199
  const data = yield* this[StoreInternalsSymbol].clientSession.leaderThread.getEventlogData
1117
1200
  downloadBlob(data, `livestore-eventlog-${Date.now()}.db`)
1118
1201
  }).pipe(this.runEffectFork)
1119
1202
  },
1120
1203
 
1121
1204
  hardReset: (mode: 'all-data' | 'only-app-db' = 'all-data') => {
1122
- Effect.gen(this, function* () {
1205
+ Effect.gen({ self: this }, function* () {
1123
1206
  const clientId = this[StoreInternalsSymbol].clientSession.clientId
1124
1207
  yield* this[StoreInternalsSymbol].clientSession.leaderThread.sendDevtoolsMessage(
1125
1208
  Devtools.Leader.ResetAllData.Request.make({ liveStoreVersion, mode, requestId: nanoid(), clientId }),
@@ -1143,14 +1226,14 @@ export class Store<TSchema extends LiveStoreSchema = LiveStoreSchema.Any, TConte
1143
1226
 
1144
1227
  // NOTE: Explicit return type needed to avoid TS2742 (inferred type references internal path)
1145
1228
  syncStates: (): Promise<{ session: SyncState.SyncState; leader: SyncState.SyncState }> =>
1146
- Effect.gen(this, function* () {
1229
+ Effect.gen({ self: this }, function* () {
1147
1230
  const session = yield* this[StoreInternalsSymbol].syncProcessor.syncState
1148
1231
  const leader = yield* this[StoreInternalsSymbol].clientSession.leaderThread.syncState
1149
1232
  return { session, leader }
1150
1233
  }).pipe(this.runEffectPromise),
1151
1234
 
1152
1235
  printSyncStates: () => {
1153
- Effect.gen(this, function* () {
1236
+ Effect.gen({ self: this }, function* () {
1154
1237
  const session = yield* this[StoreInternalsSymbol].syncProcessor.syncState
1155
1238
  yield* Effect.log(
1156
1239
  `Session sync state: ${objectToString(session.localHead)} (upstream: ${objectToString(session.upstreamHead)})`,
@@ -1181,11 +1264,11 @@ export class Store<TSchema extends LiveStoreSchema = LiveStoreSchema.Any, TConte
1181
1264
  effect.pipe(
1182
1265
  Effect.forkIn(this[StoreInternalsSymbol].effectContext.lifetimeScope),
1183
1266
  Effect.tapCauseLogPretty,
1184
- Runtime.runFork(this[StoreInternalsSymbol].effectContext.runtime),
1267
+ Effect.runForkWith(this[StoreInternalsSymbol].effectContext.services),
1185
1268
  )
1186
1269
 
1187
1270
  private runEffectPromise = <A, E>(effect: Effect.Effect<A, E, Scope.Scope>) =>
1188
- effect.pipe(Effect.tapCauseLogPretty, Runtime.runPromise(this[StoreInternalsSymbol].effectContext.runtime))
1271
+ effect.pipe(Effect.tapCauseLogPretty, Effect.runPromiseWith(this[StoreInternalsSymbol].effectContext.services))
1189
1272
 
1190
1273
  private getCommitArgs = (
1191
1274
  firstEventOrTxnFnOrOptions: any,
@@ -1215,12 +1298,6 @@ export class Store<TSchema extends LiveStoreSchema = LiveStoreSchema.Any, TConte
1215
1298
  events = [firstEventOrTxnFnOrOptions, ...restEvents]
1216
1299
  }
1217
1300
 
1218
- // for (const event of events) {
1219
- // if (event.args.id === SessionIdSymbol) {
1220
- // event.args.id = this.sessionId
1221
- // }
1222
- // }
1223
-
1224
1301
  return { events, options }
1225
1302
  }
1226
1303
  }
package/src/utils/dev.ts CHANGED
@@ -24,7 +24,7 @@ export const downloadURL = (data: string, fileName: string) => {
24
24
  const a = document.createElement('a')
25
25
  a.href = data
26
26
  a.download = fileName
27
- document.body.append(a)
27
+ document.body.appendChild(a)
28
28
  a.style.display = 'none'
29
29
  a.click()
30
30
  a.remove()
@@ -41,7 +41,10 @@ export const extractStackInfoFromStackTrace = (stackTrace: string): StackInfo =>
41
41
  // console.debug(name, filePath)
42
42
 
43
43
  // NOTE No idea where this `Module.` comes from - possibly a Vite thing?
44
- if ((name.startsWith('use') === true || name.startsWith('Module.use') === true) && name.endsWith('QueryRef') === false) {
44
+ if (
45
+ (name.startsWith('use') === true || name.startsWith('Module.use') === true) &&
46
+ name.endsWith('QueryRef') === false
47
+ ) {
45
48
  hasReachedStart = true
46
49
  // console.debug('hasReachedStart. adding one more frame.')
47
50