@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,17 +1,18 @@
1
+ import * as otel from '@opentelemetry/api'
2
+
1
3
  import {
2
4
  type Adapter,
5
+ type BackendIdMismatchError,
3
6
  type BootStatus,
4
7
  type ClientSession,
5
8
  type ClientSessionDevtoolsChannel,
6
- type ClientSessionSyncProcessorSimulationParams,
7
9
  type IntentionalShutdownCause,
8
- type InvalidPullError,
9
- type IsOfflineError,
10
10
  type MaterializeError,
11
11
  type MigrationsReport,
12
12
  provideOtel,
13
- type SyncError,
14
- UnexpectedError,
13
+ type ServerAheadError,
14
+ UnknownError,
15
+ type LogConfig,
15
16
  } from '@livestore/common'
16
17
  import type { LiveStoreSchema } from '@livestore/common/schema'
17
18
  import { isDevEnv, LS_DEV, omitUndefineds } from '@livestore/utils'
@@ -23,47 +24,76 @@ import {
23
24
  Fiber,
24
25
  identity,
25
26
  Layer,
26
- Logger,
27
- LogLevel,
28
27
  OtelTracer,
29
28
  Queue,
30
- Runtime,
31
- type Schema,
29
+ References,
30
+ Schema,
32
31
  Scope,
33
32
  TaskTracing,
34
33
  } from '@livestore/utils/effect'
35
34
  import { nanoid } from '@livestore/utils/nanoid'
36
- import * as otel from '@opentelemetry/api'
37
35
 
38
36
  import { connectDevtoolsToStore } from './devtools.ts'
39
- import { Store } from './store.ts'
40
37
  import type {
41
38
  LiveStoreContextRunning as LiveStoreContextRunning_,
42
39
  OtelOptions,
43
40
  ShutdownDeferred,
44
41
  } from './store-types.ts'
45
-
46
- export const DEFAULT_PARAMS = {
47
- leaderPushBatchSize: 100,
42
+ import { StoreInternalsSymbol } from './store-types.ts'
43
+ import { STORE_DEFAULT_PARAMS, Store } from './store.ts'
44
+
45
+ /**
46
+ * Hard upper bound (ms) for the detached shutdown drain. A dead/unresponsive leader must not keep
47
+ * the drain (and therefore the lifetime scope) alive forever; after this bound the scope is
48
+ * force-closed. Kept comfortably above the 1s caller-side soft wait so that a still-progressing
49
+ * in-flight leader push is allowed to finish rather than being interrupted.
50
+ */
51
+ const SHUTDOWN_DRAIN_HARD_TIMEOUT_MS = 30_000
52
+
53
+ declare global {
54
+ /** Store instances for console debugging */
55
+ var __debugLiveStore: Record<string, Store<any, any>> | undefined
48
56
  }
49
57
 
50
- export class LiveStoreContextRunning extends Context.Tag('@livestore/livestore/effect/LiveStoreContextRunning')<
51
- LiveStoreContextRunning,
52
- LiveStoreContextRunning_
53
- >() {
58
+ /**
59
+ * @deprecated Use `makeStoreContext()` from `@livestore/livestore/effect` instead.
60
+ * This service doesn't preserve schema types. See the Effect integration docs for migration.
61
+ *
62
+ * @example Migration
63
+ * ```ts
64
+ * // Before (untyped)
65
+ * import { LiveStoreContextRunning } from '@livestore/livestore/effect'
66
+ * const { store } = yield* LiveStoreContextRunning
67
+ *
68
+ * // After (typed)
69
+ * import { makeStoreContext } from '@livestore/livestore/effect'
70
+ * const AppStore = makeStoreContext<typeof schema>()('app')
71
+ * const { store } = yield* AppStore.Tag
72
+ * ```
73
+ */
74
+ export class LiveStoreContextRunning extends Context.Service<LiveStoreContextRunning, LiveStoreContextRunning_>()(
75
+ '@livestore/livestore/effect/LiveStoreContextRunning',
76
+ ) {
54
77
  static fromDeferred = Effect.gen(function* () {
55
78
  const deferred = yield* DeferredStoreContext
56
- const ctx = yield* deferred
57
- return Layer.succeed(LiveStoreContextRunning, ctx)
58
- }).pipe(Layer.unwrapScoped)
79
+ const ctx = yield* Deferred.await(deferred)
80
+ return Layer.succeed(LiveStoreContextRunning, LiveStoreContextRunning.of(ctx))
81
+ }).pipe(Layer.unwrap)
59
82
  }
60
83
 
61
- export class DeferredStoreContext extends Context.Tag('@livestore/livestore/effect/DeferredStoreContext')<
84
+ /**
85
+ * @deprecated Use `StoreContext.DeferredTag` from `makeStoreContext()` instead.
86
+ */
87
+ export class DeferredStoreContext extends Context.Service<
62
88
  DeferredStoreContext,
63
- Deferred.Deferred<LiveStoreContextRunning['Type'], UnexpectedError>
64
- >() {}
65
-
66
- export type LiveStoreContextProps<TSchema extends LiveStoreSchema, TContext = {}> = {
89
+ Deferred.Deferred<LiveStoreContextRunning['Service'], UnknownError>
90
+ >()('@livestore/livestore/effect/DeferredStoreContext') {}
91
+
92
+ export type LiveStoreContextProps<
93
+ TSchema extends LiveStoreSchema,
94
+ TContext = {},
95
+ TSyncPayloadSchema extends Schema.Codec<Schema.Json, Schema.Json> = typeof Schema.Json,
96
+ > = {
67
97
  schema: TSchema
68
98
  /**
69
99
  * The `storeId` can be used to isolate multiple stores from each other.
@@ -88,12 +118,48 @@ export type LiveStoreContextProps<TSchema extends LiveStoreSchema, TContext = {}
88
118
  disableDevtools?: boolean | 'auto'
89
119
  onBootStatus?: (status: BootStatus) => void
90
120
  batchUpdates: (run: () => void) => void
121
+ /**
122
+ * Schema describing the shape of the sync payload and used to encode it.
123
+ *
124
+ * - If omitted, `Schema.Json` is used (no additional typing/validation).
125
+ * - Prefer exporting a schema from your app (e.g. `export const SyncPayload = Schema.Struct({ authToken: Schema.String })`)
126
+ * and pass it here to get end-to-end type safety and validation.
127
+ */
128
+ syncPayloadSchema?: TSyncPayloadSchema
129
+ /**
130
+ * Payload that is sent to the sync backend when connecting
131
+ *
132
+ * - Its TypeScript type is inferred from `syncPayloadSchema` (i.e. `typeof SyncPayload.Type`).
133
+ * - At runtime this value is encoded with `syncPayloadSchema` before being handed to the adapter.
134
+ *
135
+ * Example:
136
+ * const SyncPayload = Schema.Struct({ authToken: Schema.String })
137
+ * useStore({ ..., syncPayloadSchema: SyncPayload, syncPayload: { authToken: '...' } })
138
+ */
139
+ syncPayload?: TSyncPayloadSchema['Type']
91
140
  }
92
141
 
93
- export interface CreateStoreOptions<TSchema extends LiveStoreSchema, TContext = {}> {
142
+ export interface CreateStoreOptions<
143
+ TSchema extends LiveStoreSchema,
144
+ TContext = {},
145
+ TSyncPayloadSchema extends Schema.Codec<Schema.Json, Schema.Json> = typeof Schema.Json,
146
+ >
147
+ extends LogConfig.LoggerOptions {
148
+ /** The LiveStore schema defining tables, events, and materializers. */
94
149
  schema: TSchema
150
+ /** Adapter used for data storage and synchronization. */
95
151
  adapter: Adapter
152
+ /**
153
+ * Unique identifier for the Store instance, stable for its lifetime.
154
+ *
155
+ * - **Valid characters**: Only alphanumeric characters, underscores (`_`), and hyphens (`-`)
156
+ * are allowed. Must match `/^[a-zA-Z0-9_-]+$/`.
157
+ * - **Globally unique**: Use globally unique IDs (e.g., nanoid) to prevent collisions across stores.
158
+ * - **Use namespaces**: Prefix to avoid collisions and for easier identification when debugging
159
+ * (e.g., `app-root`, `workspace-abc123`, `issue-456`)
160
+ */
96
161
  storeId: string
162
+ /** User-defined context that will be attached to the created Store (e.g. for dependency injection). */
97
163
  context?: TContext
98
164
  boot?: (
99
165
  store: Store<TSchema, TContext>,
@@ -102,6 +168,19 @@ export interface CreateStoreOptions<TSchema extends LiveStoreSchema, TContext =
102
168
  parentSpan: otel.Span
103
169
  },
104
170
  ) => Effect.SyncOrPromiseOrEffect<void, unknown, OtelTracer.OtelTracer | LiveStoreContextRunning>
171
+ onBootStatus?: (status: BootStatus) => void
172
+ /**
173
+ * Needed in React so LiveStore can apply multiple events in a single render.
174
+ *
175
+ * @example
176
+ * ```ts
177
+ * // With React DOM
178
+ * import { unstable_batchedUpdates as batchUpdates } from 'react-dom'
179
+ *
180
+ * // With React Native
181
+ * import { unstable_batchedUpdates as batchUpdates } from 'react-native'
182
+ * ```
183
+ */
105
184
  batchUpdates?: (run: () => void) => void
106
185
  /**
107
186
  * Whether to disable devtools.
@@ -109,7 +188,6 @@ export interface CreateStoreOptions<TSchema extends LiveStoreSchema, TContext =
109
188
  * @default 'auto'
110
189
  */
111
190
  disableDevtools?: boolean | 'auto'
112
- onBootStatus?: (status: BootStatus) => void
113
191
  shutdownDeferred?: ShutdownDeferred
114
192
  /**
115
193
  * Currently only used in the web adapter:
@@ -119,42 +197,65 @@ export interface CreateStoreOptions<TSchema extends LiveStoreSchema, TContext =
119
197
  */
120
198
  confirmUnsavedChanges?: boolean
121
199
  /**
122
- * Payload that will be passed to the sync backend when connecting
200
+ * Schema describing the shape of the sync payload and used to encode it.
201
+ *
202
+ * - If omitted, `Schema.Json` is used (no additional typing/validation).
203
+ * - Prefer exporting a schema from your app (e.g. `export const SyncPayload = Schema.Struct({ authToken: Schema.String })`)
204
+ * and pass it here to get end-to-end type safety and validation.
205
+ */
206
+ syncPayloadSchema?: TSyncPayloadSchema
207
+ /**
208
+ * Payload that is sent to the sync backend when connecting
209
+ *
210
+ * - Its TypeScript type is inferred from `syncPayloadSchema` (i.e. `typeof SyncPayload.Type`).
211
+ * - At runtime this value is encoded with `syncPayloadSchema` and carried through the adapter
212
+ * to the backend where it can be decoded with the same schema.
123
213
  *
124
214
  * @default undefined
125
215
  */
126
- syncPayload?: Schema.JsonValue
216
+ syncPayload?: TSyncPayloadSchema['Type']
217
+ /** Options provided to the Store constructor. */
127
218
  params?: {
219
+ /** Max events pushed to the leader per write batch. */
128
220
  leaderPushBatchSize?: number
129
- simulation?: {
130
- clientSessionSyncProcessor: typeof ClientSessionSyncProcessorSimulationParams.Type
131
- }
221
+ /** Chunk size used when the stream replays confirmed events. */
222
+ eventQueryBatchSize?: number
132
223
  }
133
224
  debug?: {
134
225
  instanceId?: string
135
226
  }
136
227
  }
137
228
 
229
+ export type CreateStoreOptionsPromise<
230
+ TSchema extends LiveStoreSchema = LiveStoreSchema.Any,
231
+ TContext = {},
232
+ TSyncPayloadSchema extends Schema.Codec<Schema.Json, Schema.Json> = typeof Schema.Json,
233
+ > = CreateStoreOptions<TSchema, TContext, TSyncPayloadSchema> & {
234
+ signal?: AbortSignal
235
+ otelOptions?: Partial<OtelOptions>
236
+ }
237
+
138
238
  /** Create a new LiveStore Store */
139
- export const createStorePromise = async <TSchema extends LiveStoreSchema = LiveStoreSchema.Any, TContext = {}>({
239
+ export const createStorePromise = async <
240
+ TSchema extends LiveStoreSchema = LiveStoreSchema.Any,
241
+ TContext = {},
242
+ TSyncPayloadSchema extends Schema.Codec<Schema.Json, Schema.Json> = typeof Schema.Json,
243
+ >({
140
244
  signal,
141
245
  otelOptions,
142
246
  ...options
143
- }: CreateStoreOptions<TSchema, TContext> & {
144
- signal?: AbortSignal
145
- otelOptions?: Partial<OtelOptions>
146
- }): Promise<Store<TSchema, TContext>> =>
247
+ }: CreateStoreOptionsPromise<TSchema, TContext, TSyncPayloadSchema>): Promise<Store<TSchema, TContext>> =>
147
248
  Effect.gen(function* () {
148
249
  const scope = yield* Scope.make()
149
- const runtime = yield* Effect.runtime()
250
+ const services = yield* Effect.context()
150
251
 
151
252
  if (signal !== undefined) {
152
253
  signal.addEventListener('abort', () => {
153
- Scope.close(scope, Exit.void).pipe(Effect.tapCauseLogPretty, Runtime.runFork(runtime))
254
+ Scope.close(scope, Exit.void).pipe(Effect.tapCauseLogPretty, Effect.runForkWith(services))
154
255
  })
155
256
  }
156
257
 
157
- return yield* createStore({ ...options }).pipe(Scope.extend(scope))
258
+ return yield* createStore({ ...options }).pipe(Scope.provide(scope))
158
259
  }).pipe(
159
260
  Effect.withSpan('createStore', {
160
261
  attributes: { storeId: options.storeId, disableDevtools: options.disableDevtools },
@@ -162,12 +263,20 @@ export const createStorePromise = async <TSchema extends LiveStoreSchema = LiveS
162
263
  provideOtel(omitUndefineds({ parentSpanContext: otelOptions?.rootSpanContext, otelTracer: otelOptions?.tracer })),
163
264
  Effect.tapCauseLogPretty,
164
265
  Effect.annotateLogs({ thread: 'window' }),
165
- Effect.provide(Logger.prettyWithThread('window')),
166
- Logger.withMinimumLogLevel(LogLevel.Debug),
266
+ Effect.provide(
267
+ Layer.mergeAll(
268
+ options.logger ?? Layer.empty,
269
+ Layer.succeed(References.MinimumLogLevel, options.logLevel ?? (isDevEnv() === true ? 'Debug' : 'Info')),
270
+ ),
271
+ ),
167
272
  Effect.runPromise,
168
273
  )
169
274
 
170
- export const createStore = <TSchema extends LiveStoreSchema = LiveStoreSchema.Any, TContext = {}>({
275
+ export const createStore = <
276
+ TSchema extends LiveStoreSchema = LiveStoreSchema.Any,
277
+ TContext = {},
278
+ TSyncPayloadSchema extends Schema.Codec<Schema.Json, Schema.Json> = typeof Schema.Json,
279
+ >({
171
280
  schema,
172
281
  adapter,
173
282
  storeId,
@@ -181,9 +290,10 @@ export const createStore = <TSchema extends LiveStoreSchema = LiveStoreSchema.An
181
290
  debug,
182
291
  confirmUnsavedChanges = true,
183
292
  syncPayload,
184
- }: CreateStoreOptions<TSchema, TContext>): Effect.Effect<
293
+ syncPayloadSchema,
294
+ }: CreateStoreOptions<TSchema, TContext, TSyncPayloadSchema>): Effect.Effect<
185
295
  Store<TSchema, TContext>,
186
- UnexpectedError,
296
+ UnknownError,
187
297
  Scope.Scope | OtelTracer.OtelTracer
188
298
  > =>
189
299
  Effect.gen(function* () {
@@ -194,17 +304,20 @@ export const createStore = <TSchema extends LiveStoreSchema = LiveStoreSchema.An
194
304
  yield* Effect.addFinalizer((_) => Scope.close(lifetimeScope, _))
195
305
 
196
306
  const debugInstanceId = debug?.instanceId ?? nanoid(10)
307
+ const resolvedSyncPayloadSchema = (syncPayloadSchema ?? Schema.Json) as TSyncPayloadSchema
197
308
 
198
309
  return yield* Effect.gen(function* () {
199
310
  const span = yield* OtelTracer.currentOtelSpan.pipe(Effect.orDie)
200
311
  const otelRootSpanContext = otel.trace.setSpan(otel.context.active(), span)
201
312
  const otelTracer = yield* OtelTracer.OtelTracer
202
313
 
203
- const bootStatusQueue = yield* Queue.unbounded<BootStatus>().pipe(Effect.acquireRelease(Queue.shutdown))
314
+ const bootStatusQueue = yield* Effect.acquireRelease(Queue.unbounded<BootStatus>(), Queue.shutdown)
204
315
 
205
316
  yield* Queue.take(bootStatusQueue).pipe(
206
317
  Effect.tapSync((status) => onBootStatus?.(status)),
207
- Effect.tap((status) => (status.stage === 'done' ? Queue.shutdown(bootStatusQueue) : Effect.void)),
318
+ Effect.tap((status) =>
319
+ status.stage === 'done' ? Queue.shutdown(bootStatusQueue).pipe(Effect.asVoid) : Effect.void,
320
+ ),
208
321
  Effect.forever,
209
322
  Effect.tapCauseLogPretty,
210
323
  Effect.forkScoped,
@@ -214,35 +327,52 @@ export const createStore = <TSchema extends LiveStoreSchema = LiveStoreSchema.An
214
327
 
215
328
  const connectDevtoolsToStore_ = (storeDevtoolsChannel: ClientSessionDevtoolsChannel) =>
216
329
  Effect.gen(function* () {
217
- const store = yield* storeDeferred
330
+ const store = yield* Deferred.await(storeDeferred)
218
331
  yield* connectDevtoolsToStore({ storeDevtoolsChannel, store })
219
332
  })
220
333
 
221
- const runtime = yield* Effect.runtime<Scope.Scope>()
334
+ const services = yield* Effect.context<Scope.Scope>()
335
+ let shutdownSyncProcessor: ((exit: Exit.Exit<unknown, unknown>) => Effect.Effect<void>) | undefined
222
336
 
223
337
  const shutdown = (
224
- exit: Exit.Exit<
225
- IntentionalShutdownCause,
226
- UnexpectedError | MaterializeError | SyncError | InvalidPullError | IsOfflineError
227
- >,
338
+ exit: Exit.Exit<IntentionalShutdownCause, UnknownError | MaterializeError | BackendIdMismatchError>,
228
339
  ) =>
229
340
  Effect.gen(function* () {
230
- yield* Scope.close(lifetimeScope, exit).pipe(
341
+ // Hard outer bound on the DETACHED teardown: the processor drain `awaitEmpty`s the
342
+ // leader-push worker, which never completes if the leader is dead/unresponsive. Without a
343
+ // bound the drain would block forever, so `Scope.close(lifetimeScope)` (in `ensuring`)
344
+ // would never run and the lifetime scope + its resources would leak indefinitely (a later
345
+ // `createStore`/registry dispose on the same `storeId` would observe a never-closed store).
346
+ // Drain up to the bound, then force-close the scope regardless. The bound exceeds the
347
+ // caller-side wait below so an in-flight (but progressing) push is not cut short.
348
+ const closeFiber = yield* (shutdownSyncProcessor?.(exit) ?? Effect.void).pipe(
349
+ Effect.timeout(SHUTDOWN_DRAIN_HARD_TIMEOUT_MS),
350
+ Effect.catchTag('TimeoutError', () =>
351
+ Effect.logError(
352
+ `@livestore/livestore:shutdown: drain exceeded hard bound of ${SHUTDOWN_DRAIN_HARD_TIMEOUT_MS}ms; forcing scope close`,
353
+ ),
354
+ ),
355
+ Effect.ensuring(Scope.close(lifetimeScope, exit)),
356
+ Effect.forkDetach,
357
+ )
358
+ // Caller-side soft wait: stop blocking the shutdown() caller after 1s without cancelling
359
+ // the detached teardown above (which remains bounded by SHUTDOWN_DRAIN_HARD_TIMEOUT_MS).
360
+ yield* Fiber.join(closeFiber).pipe(
231
361
  Effect.logWarnIfTakesLongerThan({ label: '@livestore/livestore:shutdown', duration: 500 }),
232
362
  Effect.timeout(1000),
233
- Effect.catchTag('TimeoutException', () =>
363
+ Effect.catchTag('TimeoutError', () =>
234
364
  Effect.logError('@livestore/livestore:shutdown: Timed out after 1 second'),
235
365
  ),
236
366
  )
237
367
 
238
- if (shutdownDeferred) {
368
+ if (shutdownDeferred !== undefined) {
239
369
  yield* Deferred.done(shutdownDeferred, exit)
240
370
  }
241
371
 
242
372
  yield* Effect.logDebug('LiveStore shutdown complete')
243
373
  }).pipe(
244
374
  Effect.withSpan('@livestore/livestore:shutdown'),
245
- Effect.provide(runtime),
375
+ Effect.provide(services),
246
376
  Effect.tapCauseLogPretty,
247
377
  // Given that the shutdown flow might also interrupt the effect that is calling the shutdown,
248
378
  // we want to detach the shutdown effect so it's not interrupted by itself
@@ -250,6 +380,11 @@ export const createStore = <TSchema extends LiveStoreSchema = LiveStoreSchema.An
250
380
  Fiber.join,
251
381
  )
252
382
 
383
+ const syncPayloadEncoded =
384
+ syncPayload === undefined
385
+ ? undefined
386
+ : yield* Schema.encodeEffect(resolvedSyncPayloadSchema)(syncPayload).pipe(UnknownError.mapToUnknownError)
387
+
253
388
  const clientSession: ClientSession = yield* adapter({
254
389
  schema,
255
390
  storeId,
@@ -258,10 +393,11 @@ export const createStore = <TSchema extends LiveStoreSchema = LiveStoreSchema.An
258
393
  shutdown,
259
394
  connectDevtoolsToStore: connectDevtoolsToStore_,
260
395
  debugInstanceId,
261
- syncPayload,
396
+ syncPayloadSchema: resolvedSyncPayloadSchema,
397
+ syncPayloadEncoded,
262
398
  }).pipe(Effect.withPerformanceMeasure('livestore:makeAdapter'), Effect.withSpan('createStore:makeAdapter'))
263
399
 
264
- if (LS_DEV && clientSession.leaderThread.initialState.migrationsReport.migrations.length > 0) {
400
+ if (LS_DEV === true && clientSession.leaderThread.initialState.migrationsReport.migrations.length > 0) {
265
401
  yield* Effect.logDebug(
266
402
  '[@livestore/livestore:createStore] migrationsReport',
267
403
  ...clientSession.leaderThread.initialState.migrationsReport.migrations.map((m) =>
@@ -277,51 +413,68 @@ export const createStore = <TSchema extends LiveStoreSchema = LiveStoreSchema.An
277
413
  schema,
278
414
  context,
279
415
  otelOptions: { tracer: otelTracer, rootSpanContext: otelRootSpanContext },
280
- effectContext: { lifetimeScope, runtime },
416
+ effectContext: { lifetimeScope, services },
281
417
  // TODO find a better way to detect if we're running LiveStore in the LiveStore devtools
282
418
  // But for now this is a good enough approximation with little downsides
283
- __runningInDevtools: getDevtoolsEnabled(disableDevtools) === false,
419
+ __runningInDevtools: !getDevtoolsEnabled(disableDevtools),
284
420
  confirmUnsavedChanges,
285
421
  // NOTE during boot we're not yet executing events in a batched context
286
422
  // but only set the provided `batchUpdates` function after boot
287
423
  batchUpdates: (run) => run(),
288
424
  storeId,
289
425
  params: {
290
- leaderPushBatchSize: params?.leaderPushBatchSize ?? DEFAULT_PARAMS.leaderPushBatchSize,
291
- ...omitUndefineds({ simulation: params?.simulation }),
426
+ leaderPushBatchSize: params?.leaderPushBatchSize ?? STORE_DEFAULT_PARAMS.leaderPushBatchSize,
427
+ eventQueryBatchSize: params?.eventQueryBatchSize ?? STORE_DEFAULT_PARAMS.eventQueryBatchSize,
292
428
  },
293
429
  })
430
+ shutdownSyncProcessor = store[StoreInternalsSymbol].syncProcessor.shutdown
294
431
 
295
432
  // Starts background fibers (syncing, event processing, etc) for store
296
- yield* store.boot
433
+ yield* store[StoreInternalsSymbol].boot
297
434
 
298
435
  if (boot !== undefined) {
299
436
  // TODO also incorporate `boot` function progress into `bootStatusQueue`
300
- yield* Effect.tryAll(() =>
437
+ // @effect-diagnostics-next-line anyUnknownInErrorContext:off -- user boot errors are immediately normalized to LiveStore UnknownError
438
+ yield* Effect.trySyncOrPromiseOrEffect(() =>
301
439
  boot(store, { migrationsReport: clientSession.leaderThread.initialState.migrationsReport, parentSpan: span }),
302
440
  ).pipe(
303
- UnexpectedError.mapToUnexpectedError,
304
- Effect.provide(Layer.succeed(LiveStoreContextRunning, { stage: 'running', store: store as any as Store })),
441
+ UnknownError.mapToUnknownError,
442
+ Effect.provide(
443
+ Layer.succeed(
444
+ LiveStoreContextRunning,
445
+ LiveStoreContextRunning.of({ stage: 'running', store: store as any as Store }),
446
+ ),
447
+ ),
305
448
  Effect.withSpan('createStore:boot'),
306
449
  )
307
450
  }
308
451
 
309
452
  // NOTE it's important to yield here to allow the forked Effect in the store constructor to run
310
- yield* Effect.yieldNow()
453
+ yield* Effect.yieldNow
311
454
 
312
455
  if (batchUpdates !== undefined) {
313
456
  // Replacing the default batchUpdates function with the provided one after boot
314
- store.reactivityGraph.context!.effectsWrapper = batchUpdates
457
+ store[StoreInternalsSymbol].reactivityGraph.context!.effectsWrapper = batchUpdates
315
458
  }
316
459
 
317
460
  yield* Deferred.succeed(storeDeferred, store as any as Store)
318
461
 
462
+ // Expose store on globalThis for console debugging
463
+ globalThis.__debugLiveStore ??= {}
464
+ globalThis.__debugLiveStore[storeId] = store
465
+
466
+ yield* Effect.addFinalizer(() =>
467
+ Effect.sync(() => {
468
+ delete globalThis.__debugLiveStore?.[storeId]
469
+ }),
470
+ )
471
+
319
472
  return store
320
473
  }).pipe(
321
474
  Effect.withSpan('createStore', { attributes: { debugInstanceId, storeId } }),
322
475
  Effect.annotateLogs({ debugInstanceId, storeId }),
323
- LS_DEV ? TaskTracing.withAsyncTaggingTracing((name) => (console as any).createTask(name)) : identity,
324
- Scope.extend(lifetimeScope),
476
+ LS_DEV === true ? TaskTracing.withAsyncTaggingTracing((name) => (console as any).createTask(name)) : identity,
477
+ Scope.provide(lifetimeScope),
325
478
  )
326
479
  })
327
480
 
@@ -329,8 +482,8 @@ const validateStoreId = (storeId: string) =>
329
482
  Effect.gen(function* () {
330
483
  const validChars = /^[a-zA-Z0-9_-]+$/
331
484
 
332
- if (!validChars.test(storeId)) {
333
- return yield* UnexpectedError.make({
485
+ if (validChars.test(storeId) === false) {
486
+ return yield* UnknownError.make({
334
487
  cause: `Invalid storeId: ${storeId}. Only alphanumeric characters, underscores, and hyphens are allowed.`,
335
488
  payload: { storeId },
336
489
  })