@livestore/livestore 0.4.0-dev.9 → 0.4.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 (129) 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 +8 -8
  8. package/dist/SqliteDbWrapper.js.map +1 -1
  9. package/dist/SqliteDbWrapper.test.js +4 -3
  10. package/dist/SqliteDbWrapper.test.js.map +1 -1
  11. package/dist/effect/LiveStore.d.ts +133 -5
  12. package/dist/effect/LiveStore.d.ts.map +1 -1
  13. package/dist/effect/LiveStore.js +187 -8
  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 +42 -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 +110 -7
  24. package/dist/live-queries/base-class.d.ts.map +1 -1
  25. package/dist/live-queries/base-class.js +2 -2
  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 +58 -2
  34. package/dist/live-queries/computed.js.map +1 -1
  35. package/dist/live-queries/db-query.d.ts.map +1 -1
  36. package/dist/live-queries/db-query.js +21 -19
  37. package/dist/live-queries/db-query.js.map +1 -1
  38. package/dist/live-queries/db-query.test.js +106 -23
  39. package/dist/live-queries/db-query.test.js.map +1 -1
  40. package/dist/live-queries/signal.d.ts +49 -0
  41. package/dist/live-queries/signal.d.ts.map +1 -1
  42. package/dist/live-queries/signal.js +49 -0
  43. package/dist/live-queries/signal.js.map +1 -1
  44. package/dist/live-queries/signal.test.js +2 -2
  45. package/dist/live-queries/signal.test.js.map +1 -1
  46. package/dist/mod.d.ts +3 -3
  47. package/dist/mod.d.ts.map +1 -1
  48. package/dist/mod.js +3 -2
  49. package/dist/mod.js.map +1 -1
  50. package/dist/reactive.d.ts +9 -9
  51. package/dist/reactive.d.ts.map +1 -1
  52. package/dist/reactive.js +9 -26
  53. package/dist/reactive.js.map +1 -1
  54. package/dist/reactive.test.js +2 -2
  55. package/dist/reactive.test.js.map +1 -1
  56. package/dist/store/StoreRegistry.d.ts +215 -0
  57. package/dist/store/StoreRegistry.d.ts.map +1 -0
  58. package/dist/store/StoreRegistry.js +267 -0
  59. package/dist/store/StoreRegistry.js.map +1 -0
  60. package/dist/store/StoreRegistry.test.d.ts +2 -0
  61. package/dist/store/StoreRegistry.test.d.ts.map +1 -0
  62. package/dist/store/StoreRegistry.test.js +381 -0
  63. package/dist/store/StoreRegistry.test.js.map +1 -0
  64. package/dist/store/create-store.d.ts +98 -18
  65. package/dist/store/create-store.d.ts.map +1 -1
  66. package/dist/store/create-store.js +49 -20
  67. package/dist/store/create-store.js.map +1 -1
  68. package/dist/store/devtools.d.ts +5 -16
  69. package/dist/store/devtools.d.ts.map +1 -1
  70. package/dist/store/devtools.js +59 -18
  71. package/dist/store/devtools.js.map +1 -1
  72. package/dist/store/store-eventstream.test.d.ts +2 -0
  73. package/dist/store/store-eventstream.test.d.ts.map +1 -0
  74. package/dist/store/store-eventstream.test.js +65 -0
  75. package/dist/store/store-eventstream.test.js.map +1 -0
  76. package/dist/store/store-types.d.ts +285 -27
  77. package/dist/store/store-types.d.ts.map +1 -1
  78. package/dist/store/store-types.js +77 -1
  79. package/dist/store/store-types.js.map +1 -1
  80. package/dist/store/store-types.test.d.ts +2 -0
  81. package/dist/store/store-types.test.d.ts.map +1 -0
  82. package/dist/store/store-types.test.js +39 -0
  83. package/dist/store/store-types.test.js.map +1 -0
  84. package/dist/store/store.d.ts +253 -66
  85. package/dist/store/store.d.ts.map +1 -1
  86. package/dist/store/store.js +442 -153
  87. package/dist/store/store.js.map +1 -1
  88. package/dist/utils/dev.d.ts.map +1 -1
  89. package/dist/utils/dev.js.map +1 -1
  90. package/dist/utils/stack-info.js +2 -2
  91. package/dist/utils/stack-info.js.map +1 -1
  92. package/dist/utils/tests/fixture.d.ts +20 -5
  93. package/dist/utils/tests/fixture.d.ts.map +1 -1
  94. package/dist/utils/tests/fixture.js +7 -0
  95. package/dist/utils/tests/fixture.js.map +1 -1
  96. package/dist/utils/tests/otel.d.ts.map +1 -1
  97. package/dist/utils/tests/otel.js +5 -5
  98. package/dist/utils/tests/otel.js.map +1 -1
  99. package/package.json +59 -17
  100. package/src/QueryCache.ts +1 -1
  101. package/src/SqliteDbWrapper.test.ts +5 -3
  102. package/src/SqliteDbWrapper.ts +12 -11
  103. package/src/ambient.d.ts +0 -7
  104. package/src/effect/LiveStore.test.ts +61 -0
  105. package/src/effect/LiveStore.ts +388 -13
  106. package/src/effect/mod.ts +13 -1
  107. package/src/live-queries/__snapshots__/db-query.test.ts.snap +604 -192
  108. package/src/live-queries/base-class.ts +126 -28
  109. package/src/live-queries/client-document-get-query.ts +6 -4
  110. package/src/live-queries/computed.ts +59 -2
  111. package/src/live-queries/db-query.test.ts +162 -24
  112. package/src/live-queries/db-query.ts +23 -20
  113. package/src/live-queries/signal.test.ts +3 -2
  114. package/src/live-queries/signal.ts +49 -0
  115. package/src/mod.ts +19 -2
  116. package/src/reactive.test.ts +3 -2
  117. package/src/reactive.ts +22 -23
  118. package/src/store/StoreRegistry.test.ts +540 -0
  119. package/src/store/StoreRegistry.ts +418 -0
  120. package/src/store/create-store.ts +158 -39
  121. package/src/store/devtools.ts +77 -33
  122. package/src/store/store-eventstream.test.ts +114 -0
  123. package/src/store/store-types.test.ts +52 -0
  124. package/src/store/store-types.ts +360 -40
  125. package/src/store/store.ts +571 -236
  126. package/src/utils/dev.ts +2 -3
  127. package/src/utils/stack-info.ts +2 -2
  128. package/src/utils/tests/fixture.ts +9 -1
  129. package/src/utils/tests/otel.ts +8 -7
@@ -1,3 +1,5 @@
1
+ import * as otel from '@opentelemetry/api'
2
+
1
3
  import {
2
4
  type Adapter,
3
5
  type BootStatus,
@@ -5,13 +7,13 @@ import {
5
7
  type ClientSessionDevtoolsChannel,
6
8
  type ClientSessionSyncProcessorSimulationParams,
7
9
  type IntentionalShutdownCause,
8
- type InvalidPullError,
9
- type IsOfflineError,
10
+ LogConfig,
10
11
  type MaterializeError,
12
+ type BackendIdMismatchError,
11
13
  type MigrationsReport,
12
14
  provideOtel,
13
- type SyncError,
14
- UnexpectedError,
15
+ type ServerAheadError,
16
+ UnknownError,
15
17
  } from '@livestore/common'
16
18
  import type { LiveStoreSchema } from '@livestore/common/schema'
17
19
  import { isDevEnv, LS_DEV, omitUndefineds } from '@livestore/utils'
@@ -23,30 +25,45 @@ import {
23
25
  Fiber,
24
26
  identity,
25
27
  Layer,
26
- Logger,
27
- LogLevel,
28
28
  OtelTracer,
29
29
  Queue,
30
30
  Runtime,
31
- type Schema,
31
+ Schema,
32
32
  Scope,
33
33
  TaskTracing,
34
34
  } from '@livestore/utils/effect'
35
35
  import { nanoid } from '@livestore/utils/nanoid'
36
- import * as otel from '@opentelemetry/api'
37
36
 
38
37
  import { connectDevtoolsToStore } from './devtools.ts'
39
- import { Store } from './store.ts'
40
38
  import type {
41
39
  LiveStoreContextRunning as LiveStoreContextRunning_,
42
40
  OtelOptions,
43
41
  ShutdownDeferred,
44
42
  } from './store-types.ts'
43
+ import { StoreInternalsSymbol } from './store-types.ts'
44
+ import { STORE_DEFAULT_PARAMS, Store } from './store.ts'
45
45
 
46
- export const DEFAULT_PARAMS = {
47
- leaderPushBatchSize: 100,
46
+ declare global {
47
+ /** Store instances for console debugging */
48
+ var __debugLiveStore: Record<string, Store<any, any>> | undefined
48
49
  }
49
50
 
51
+ /**
52
+ * @deprecated Use `makeStoreContext()` from `@livestore/livestore/effect` instead.
53
+ * This service doesn't preserve schema types. See the Effect integration docs for migration.
54
+ *
55
+ * @example Migration
56
+ * ```ts
57
+ * // Before (untyped)
58
+ * import { LiveStoreContextRunning } from '@livestore/livestore/effect'
59
+ * const { store } = yield* LiveStoreContextRunning
60
+ *
61
+ * // After (typed)
62
+ * import { makeStoreContext } from '@livestore/livestore/effect'
63
+ * const AppStore = makeStoreContext<typeof schema>()('app')
64
+ * const { store } = yield* AppStore.Tag
65
+ * ```
66
+ */
50
67
  export class LiveStoreContextRunning extends Context.Tag('@livestore/livestore/effect/LiveStoreContextRunning')<
51
68
  LiveStoreContextRunning,
52
69
  LiveStoreContextRunning_
@@ -58,12 +75,19 @@ export class LiveStoreContextRunning extends Context.Tag('@livestore/livestore/e
58
75
  }).pipe(Layer.unwrapScoped)
59
76
  }
60
77
 
78
+ /**
79
+ * @deprecated Use `StoreContext.DeferredTag` from `makeStoreContext()` instead.
80
+ */
61
81
  export class DeferredStoreContext extends Context.Tag('@livestore/livestore/effect/DeferredStoreContext')<
62
82
  DeferredStoreContext,
63
- Deferred.Deferred<LiveStoreContextRunning['Type'], UnexpectedError>
83
+ Deferred.Deferred<LiveStoreContextRunning['Type'], UnknownError>
64
84
  >() {}
65
85
 
66
- export type LiveStoreContextProps<TSchema extends LiveStoreSchema, TContext = {}> = {
86
+ export type LiveStoreContextProps<
87
+ TSchema extends LiveStoreSchema,
88
+ TContext = {},
89
+ TSyncPayloadSchema extends Schema.Schema<any> = typeof Schema.JsonValue,
90
+ > = {
67
91
  schema: TSchema
68
92
  /**
69
93
  * The `storeId` can be used to isolate multiple stores from each other.
@@ -88,12 +112,47 @@ export type LiveStoreContextProps<TSchema extends LiveStoreSchema, TContext = {}
88
112
  disableDevtools?: boolean | 'auto'
89
113
  onBootStatus?: (status: BootStatus) => void
90
114
  batchUpdates: (run: () => void) => void
115
+ /**
116
+ * Schema describing the shape of the sync payload and used to encode it.
117
+ *
118
+ * - If omitted, `Schema.JsonValue` is used (no additional typing/validation).
119
+ * - Prefer exporting a schema from your app (e.g. `export const SyncPayload = Schema.Struct({ authToken: Schema.String })`)
120
+ * and pass it here to get end-to-end type safety and validation.
121
+ */
122
+ syncPayloadSchema?: TSyncPayloadSchema
123
+ /**
124
+ * Payload that is sent to the sync backend when connecting
125
+ *
126
+ * - Its TypeScript type is inferred from `syncPayloadSchema` (i.e. `typeof SyncPayload.Type`).
127
+ * - At runtime this value is encoded with `syncPayloadSchema` before being handed to the adapter.
128
+ *
129
+ * Example:
130
+ * const SyncPayload = Schema.Struct({ authToken: Schema.String })
131
+ * useStore({ ..., syncPayloadSchema: SyncPayload, syncPayload: { authToken: '...' } })
132
+ */
133
+ syncPayload?: Schema.Schema.Type<TSyncPayloadSchema>
91
134
  }
92
135
 
93
- export interface CreateStoreOptions<TSchema extends LiveStoreSchema, TContext = {}> {
136
+ export interface CreateStoreOptions<
137
+ TSchema extends LiveStoreSchema,
138
+ TContext = {},
139
+ TSyncPayloadSchema extends Schema.Schema<any> = typeof Schema.JsonValue,
140
+ > extends LogConfig.WithLoggerOptions {
141
+ /** The LiveStore schema defining tables, events, and materializers. */
94
142
  schema: TSchema
143
+ /** Adapter used for data storage and synchronization. */
95
144
  adapter: Adapter
145
+ /**
146
+ * Unique identifier for the Store instance, stable for its lifetime.
147
+ *
148
+ * - **Valid characters**: Only alphanumeric characters, underscores (`_`), and hyphens (`-`)
149
+ * are allowed. Must match `/^[a-zA-Z0-9_-]+$/`.
150
+ * - **Globally unique**: Use globally unique IDs (e.g., nanoid) to prevent collisions across stores.
151
+ * - **Use namespaces**: Prefix to avoid collisions and for easier identification when debugging
152
+ * (e.g., `app-root`, `workspace-abc123`, `issue-456`)
153
+ */
96
154
  storeId: string
155
+ /** User-defined context that will be attached to the created Store (e.g. for dependency injection). */
97
156
  context?: TContext
98
157
  boot?: (
99
158
  store: Store<TSchema, TContext>,
@@ -102,6 +161,19 @@ export interface CreateStoreOptions<TSchema extends LiveStoreSchema, TContext =
102
161
  parentSpan: otel.Span
103
162
  },
104
163
  ) => Effect.SyncOrPromiseOrEffect<void, unknown, OtelTracer.OtelTracer | LiveStoreContextRunning>
164
+ onBootStatus?: (status: BootStatus) => void
165
+ /**
166
+ * Needed in React so LiveStore can apply multiple events in a single render.
167
+ *
168
+ * @example
169
+ * ```ts
170
+ * // With React DOM
171
+ * import { unstable_batchedUpdates as batchUpdates } from 'react-dom'
172
+ *
173
+ * // With React Native
174
+ * import { unstable_batchedUpdates as batchUpdates } from 'react-native'
175
+ * ```
176
+ */
105
177
  batchUpdates?: (run: () => void) => void
106
178
  /**
107
179
  * Whether to disable devtools.
@@ -109,7 +181,6 @@ export interface CreateStoreOptions<TSchema extends LiveStoreSchema, TContext =
109
181
  * @default 'auto'
110
182
  */
111
183
  disableDevtools?: boolean | 'auto'
112
- onBootStatus?: (status: BootStatus) => void
113
184
  shutdownDeferred?: ShutdownDeferred
114
185
  /**
115
186
  * Currently only used in the web adapter:
@@ -119,13 +190,29 @@ export interface CreateStoreOptions<TSchema extends LiveStoreSchema, TContext =
119
190
  */
120
191
  confirmUnsavedChanges?: boolean
121
192
  /**
122
- * Payload that will be passed to the sync backend when connecting
193
+ * Schema describing the shape of the sync payload and used to encode it.
194
+ *
195
+ * - If omitted, `Schema.JsonValue` is used (no additional typing/validation).
196
+ * - Prefer exporting a schema from your app (e.g. `export const SyncPayload = Schema.Struct({ authToken: Schema.String })`)
197
+ * and pass it here to get end-to-end type safety and validation.
198
+ */
199
+ syncPayloadSchema?: TSyncPayloadSchema
200
+ /**
201
+ * Payload that is sent to the sync backend when connecting
202
+ *
203
+ * - Its TypeScript type is inferred from `syncPayloadSchema` (i.e. `typeof SyncPayload.Type`).
204
+ * - At runtime this value is encoded with `syncPayloadSchema` and carried through the adapter
205
+ * to the backend where it can be decoded with the same schema.
123
206
  *
124
207
  * @default undefined
125
208
  */
126
- syncPayload?: Schema.JsonValue
209
+ syncPayload?: Schema.Schema.Type<TSyncPayloadSchema>
210
+ /** Options provided to the Store constructor. */
127
211
  params?: {
212
+ /** Max events pushed to the leader per write batch. */
128
213
  leaderPushBatchSize?: number
214
+ /** Chunk size used when the stream replays confirmed events. */
215
+ eventQueryBatchSize?: number
129
216
  simulation?: {
130
217
  clientSessionSyncProcessor: typeof ClientSessionSyncProcessorSimulationParams.Type
131
218
  }
@@ -135,15 +222,25 @@ export interface CreateStoreOptions<TSchema extends LiveStoreSchema, TContext =
135
222
  }
136
223
  }
137
224
 
225
+ export type CreateStoreOptionsPromise<
226
+ TSchema extends LiveStoreSchema = LiveStoreSchema.Any,
227
+ TContext = {},
228
+ TSyncPayloadSchema extends Schema.Schema<any> = typeof Schema.JsonValue,
229
+ > = CreateStoreOptions<TSchema, TContext, TSyncPayloadSchema> & {
230
+ signal?: AbortSignal
231
+ otelOptions?: Partial<OtelOptions>
232
+ }
233
+
138
234
  /** Create a new LiveStore Store */
139
- export const createStorePromise = async <TSchema extends LiveStoreSchema = LiveStoreSchema.Any, TContext = {}>({
235
+ export const createStorePromise = async <
236
+ TSchema extends LiveStoreSchema = LiveStoreSchema.Any,
237
+ TContext = {},
238
+ TSyncPayloadSchema extends Schema.Schema<any> = typeof Schema.JsonValue,
239
+ >({
140
240
  signal,
141
241
  otelOptions,
142
242
  ...options
143
- }: CreateStoreOptions<TSchema, TContext> & {
144
- signal?: AbortSignal
145
- otelOptions?: Partial<OtelOptions>
146
- }): Promise<Store<TSchema, TContext>> =>
243
+ }: CreateStoreOptionsPromise<TSchema, TContext, TSyncPayloadSchema>): Promise<Store<TSchema, TContext>> =>
147
244
  Effect.gen(function* () {
148
245
  const scope = yield* Scope.make()
149
246
  const runtime = yield* Effect.runtime()
@@ -162,12 +259,15 @@ export const createStorePromise = async <TSchema extends LiveStoreSchema = LiveS
162
259
  provideOtel(omitUndefineds({ parentSpanContext: otelOptions?.rootSpanContext, otelTracer: otelOptions?.tracer })),
163
260
  Effect.tapCauseLogPretty,
164
261
  Effect.annotateLogs({ thread: 'window' }),
165
- Effect.provide(Logger.prettyWithThread('window')),
166
- Logger.withMinimumLogLevel(LogLevel.Debug),
262
+ LogConfig.withLoggerConfig(options, { threadName: 'window' }),
167
263
  Effect.runPromise,
168
264
  )
169
265
 
170
- export const createStore = <TSchema extends LiveStoreSchema = LiveStoreSchema.Any, TContext = {}>({
266
+ export const createStore = <
267
+ TSchema extends LiveStoreSchema = LiveStoreSchema.Any,
268
+ TContext = {},
269
+ TSyncPayloadSchema extends Schema.Schema<any> = typeof Schema.JsonValue,
270
+ >({
171
271
  schema,
172
272
  adapter,
173
273
  storeId,
@@ -181,9 +281,10 @@ export const createStore = <TSchema extends LiveStoreSchema = LiveStoreSchema.An
181
281
  debug,
182
282
  confirmUnsavedChanges = true,
183
283
  syncPayload,
184
- }: CreateStoreOptions<TSchema, TContext>): Effect.Effect<
284
+ syncPayloadSchema,
285
+ }: CreateStoreOptions<TSchema, TContext, TSyncPayloadSchema>): Effect.Effect<
185
286
  Store<TSchema, TContext>,
186
- UnexpectedError,
287
+ UnknownError,
187
288
  Scope.Scope | OtelTracer.OtelTracer
188
289
  > =>
189
290
  Effect.gen(function* () {
@@ -194,6 +295,7 @@ export const createStore = <TSchema extends LiveStoreSchema = LiveStoreSchema.An
194
295
  yield* Effect.addFinalizer((_) => Scope.close(lifetimeScope, _))
195
296
 
196
297
  const debugInstanceId = debug?.instanceId ?? nanoid(10)
298
+ const resolvedSyncPayloadSchema = (syncPayloadSchema ?? Schema.JsonValue) as TSyncPayloadSchema
197
299
 
198
300
  return yield* Effect.gen(function* () {
199
301
  const span = yield* OtelTracer.currentOtelSpan.pipe(Effect.orDie)
@@ -223,7 +325,7 @@ export const createStore = <TSchema extends LiveStoreSchema = LiveStoreSchema.An
223
325
  const shutdown = (
224
326
  exit: Exit.Exit<
225
327
  IntentionalShutdownCause,
226
- UnexpectedError | MaterializeError | SyncError | InvalidPullError | IsOfflineError
328
+ UnknownError | MaterializeError | BackendIdMismatchError
227
329
  >,
228
330
  ) =>
229
331
  Effect.gen(function* () {
@@ -235,7 +337,7 @@ export const createStore = <TSchema extends LiveStoreSchema = LiveStoreSchema.An
235
337
  ),
236
338
  )
237
339
 
238
- if (shutdownDeferred) {
340
+ if (shutdownDeferred !== undefined) {
239
341
  yield* Deferred.done(shutdownDeferred, exit)
240
342
  }
241
343
 
@@ -250,6 +352,11 @@ export const createStore = <TSchema extends LiveStoreSchema = LiveStoreSchema.An
250
352
  Fiber.join,
251
353
  )
252
354
 
355
+ const syncPayloadEncoded =
356
+ syncPayload === undefined
357
+ ? undefined
358
+ : yield* Schema.encode(resolvedSyncPayloadSchema)(syncPayload).pipe(UnknownError.mapToUnknownError)
359
+
253
360
  const clientSession: ClientSession = yield* adapter({
254
361
  schema,
255
362
  storeId,
@@ -258,10 +365,11 @@ export const createStore = <TSchema extends LiveStoreSchema = LiveStoreSchema.An
258
365
  shutdown,
259
366
  connectDevtoolsToStore: connectDevtoolsToStore_,
260
367
  debugInstanceId,
261
- syncPayload,
368
+ syncPayloadSchema: resolvedSyncPayloadSchema,
369
+ syncPayloadEncoded,
262
370
  }).pipe(Effect.withPerformanceMeasure('livestore:makeAdapter'), Effect.withSpan('createStore:makeAdapter'))
263
371
 
264
- if (LS_DEV && clientSession.leaderThread.initialState.migrationsReport.migrations.length > 0) {
372
+ if (LS_DEV === true && clientSession.leaderThread.initialState.migrationsReport.migrations.length > 0) {
265
373
  yield* Effect.logDebug(
266
374
  '[@livestore/livestore:createStore] migrationsReport',
267
375
  ...clientSession.leaderThread.initialState.migrationsReport.migrations.map((m) =>
@@ -280,27 +388,28 @@ export const createStore = <TSchema extends LiveStoreSchema = LiveStoreSchema.An
280
388
  effectContext: { lifetimeScope, runtime },
281
389
  // TODO find a better way to detect if we're running LiveStore in the LiveStore devtools
282
390
  // But for now this is a good enough approximation with little downsides
283
- __runningInDevtools: getDevtoolsEnabled(disableDevtools) === false,
391
+ __runningInDevtools: ! getDevtoolsEnabled(disableDevtools),
284
392
  confirmUnsavedChanges,
285
393
  // NOTE during boot we're not yet executing events in a batched context
286
394
  // but only set the provided `batchUpdates` function after boot
287
395
  batchUpdates: (run) => run(),
288
396
  storeId,
289
397
  params: {
290
- leaderPushBatchSize: params?.leaderPushBatchSize ?? DEFAULT_PARAMS.leaderPushBatchSize,
398
+ leaderPushBatchSize: params?.leaderPushBatchSize ?? STORE_DEFAULT_PARAMS.leaderPushBatchSize,
399
+ eventQueryBatchSize: params?.eventQueryBatchSize ?? STORE_DEFAULT_PARAMS.eventQueryBatchSize,
291
400
  ...omitUndefineds({ simulation: params?.simulation }),
292
401
  },
293
402
  })
294
403
 
295
404
  // Starts background fibers (syncing, event processing, etc) for store
296
- yield* store.boot
405
+ yield* store[StoreInternalsSymbol].boot
297
406
 
298
407
  if (boot !== undefined) {
299
408
  // TODO also incorporate `boot` function progress into `bootStatusQueue`
300
409
  yield* Effect.tryAll(() =>
301
410
  boot(store, { migrationsReport: clientSession.leaderThread.initialState.migrationsReport, parentSpan: span }),
302
411
  ).pipe(
303
- UnexpectedError.mapToUnexpectedError,
412
+ UnknownError.mapToUnknownError,
304
413
  Effect.provide(Layer.succeed(LiveStoreContextRunning, { stage: 'running', store: store as any as Store })),
305
414
  Effect.withSpan('createStore:boot'),
306
415
  )
@@ -311,16 +420,26 @@ export const createStore = <TSchema extends LiveStoreSchema = LiveStoreSchema.An
311
420
 
312
421
  if (batchUpdates !== undefined) {
313
422
  // Replacing the default batchUpdates function with the provided one after boot
314
- store.reactivityGraph.context!.effectsWrapper = batchUpdates
423
+ store[StoreInternalsSymbol].reactivityGraph.context!.effectsWrapper = batchUpdates
315
424
  }
316
425
 
317
426
  yield* Deferred.succeed(storeDeferred, store as any as Store)
318
427
 
428
+ // Expose store on globalThis for console debugging
429
+ globalThis.__debugLiveStore ??= {}
430
+ globalThis.__debugLiveStore[storeId] = store
431
+
432
+ yield* Effect.addFinalizer(() =>
433
+ Effect.sync(() => {
434
+ delete globalThis.__debugLiveStore?.[storeId]
435
+ }),
436
+ )
437
+
319
438
  return store
320
439
  }).pipe(
321
440
  Effect.withSpan('createStore', { attributes: { debugInstanceId, storeId } }),
322
441
  Effect.annotateLogs({ debugInstanceId, storeId }),
323
- LS_DEV ? TaskTracing.withAsyncTaggingTracing((name) => (console as any).createTask(name)) : identity,
442
+ LS_DEV === true ? TaskTracing.withAsyncTaggingTracing((name) => (console as any).createTask(name)) : identity,
324
443
  Scope.extend(lifetimeScope),
325
444
  )
326
445
  })
@@ -329,8 +448,8 @@ const validateStoreId = (storeId: string) =>
329
448
  Effect.gen(function* () {
330
449
  const validChars = /^[a-zA-Z0-9_-]+$/
331
450
 
332
- if (!validChars.test(storeId)) {
333
- return yield* UnexpectedError.make({
451
+ if (validChars.test(storeId) === false) {
452
+ return yield* UnknownError.make({
334
453
  cause: `Invalid storeId: ${storeId}. Only alphanumeric characters, underscores, and hyphens are allowed.`,
335
454
  payload: { storeId },
336
455
  })
@@ -1,23 +1,21 @@
1
- import type { ClientSession, ClientSessionSyncProcessor, DebugInfo, SyncState } from '@livestore/common'
2
- import { Devtools, liveStoreVersion, UnexpectedError } from '@livestore/common'
1
+ import type { DebugInfo, SyncState } from '@livestore/common'
2
+ import {
3
+ Devtools,
4
+ devtoolsProtocolVersion,
5
+ isDevtoolsProtocolVersionSupported,
6
+ liveStoreVersion,
7
+ resolveDevtoolsProtocolVersion,
8
+ UnknownError,
9
+ } from '@livestore/common'
3
10
  import { throttle } from '@livestore/utils'
4
11
  import type { WebChannel } from '@livestore/utils/effect'
5
12
  import { Effect, Stream } from '@livestore/utils/effect'
6
13
  import { nanoid } from '@livestore/utils/nanoid'
7
14
 
8
- import type { LiveQuery, ReactivityGraph } from '../live-queries/base-class.ts'
9
15
  import { NOT_REFRESHED_YET } from '../reactive.ts'
10
- import type { SqliteDbWrapper } from '../SqliteDbWrapper.ts'
11
16
  import { emptyDebugInfo as makeEmptyDebugInfo } from '../SqliteDbWrapper.ts'
12
- import type { ReferenceCountedSet } from '../utils/data-structures.ts'
13
-
14
- type IStore = {
15
- clientSession: ClientSession
16
- reactivityGraph: ReactivityGraph
17
- sqliteDbWrapper: SqliteDbWrapper
18
- activeQueries: ReferenceCountedSet<LiveQuery<any>>
19
- syncProcessor: ClientSessionSyncProcessor
20
- }
17
+ import { StoreInternalsSymbol } from './store-types.ts'
18
+ import type { Store } from './store.ts'
21
19
 
22
20
  type Unsub = () => void
23
21
  type RequestId = string
@@ -32,7 +30,7 @@ const requestNextTick: (cb: () => void) => number =
32
30
  const cancelTick: (id: number) => void =
33
31
  globalThis.cancelAnimationFrame === undefined ? (id: number) => clearTimeout(id) : globalThis.cancelAnimationFrame
34
32
 
35
- export const connectDevtoolsToStore = ({
33
+ export const connectDevtoolsToStore = Effect.fn('LSD.devtools.connectStoreToDevtools')(function* ({
36
34
  storeDevtoolsChannel,
37
35
  store,
38
36
  }: {
@@ -40,15 +38,14 @@ export const connectDevtoolsToStore = ({
40
38
  Devtools.ClientSession.MessageToApp,
41
39
  Devtools.ClientSession.MessageFromApp
42
40
  >
43
- store: IStore
44
- }) =>
45
- Effect.gen(function* () {
41
+ store: Store
42
+ }) {
46
43
  const reactivityGraphSubcriptions: SubMap = new Map()
47
44
  const liveQueriesSubscriptions: SubMap = new Map()
48
45
  const debugInfoHistorySubscriptions: SubMap = new Map()
49
46
  const syncHeadClientSessionSubscriptions: SubMap = new Map()
50
47
 
51
- const { clientId, sessionId } = store.clientSession
48
+ const { clientId, sessionId } = store[StoreInternalsSymbol].clientSession
52
49
 
53
50
  yield* Effect.addFinalizer(() =>
54
51
  Effect.sync(() => {
@@ -73,7 +70,21 @@ export const connectDevtoolsToStore = ({
73
70
  }
74
71
 
75
72
  if (decodedMessage._tag === 'LSD.ClientSession.Disconnect') {
76
- // console.error('TODO handle disconnect properly in store')
73
+ // Gracefully tear down all DevTools subscriptions and close the channel.
74
+ // This prevents background fibers from lingering after DevTools closes
75
+ // (e.g. when a window is closed without sending explicit unsubs).
76
+ for (const unsub of reactivityGraphSubcriptions.values()) unsub()
77
+ reactivityGraphSubcriptions.clear()
78
+ for (const unsub of liveQueriesSubscriptions.values()) unsub()
79
+ liveQueriesSubscriptions.clear()
80
+ for (const unsub of debugInfoHistorySubscriptions.values()) unsub()
81
+ debugInfoHistorySubscriptions.clear()
82
+ for (const unsub of syncHeadClientSessionSubscriptions.values()) unsub()
83
+ syncHeadClientSessionSubscriptions.clear()
84
+
85
+ // Signal the WebChannel to shut down; this causes the `listen` stream
86
+ // to complete and allows the surrounding scoped fiber to exit.
87
+ storeDevtoolsChannel.shutdown.pipe(Effect.runFork)
77
88
  return
78
89
  }
79
90
 
@@ -83,7 +94,7 @@ export const connectDevtoolsToStore = ({
83
94
  // So far I could only observe this problem with webmesh proxy channels (e.g. for Expo)
84
95
  // Proof: https://share.cleanshot.com/V9G87B0B
85
96
  // Also see `leader-worker-devtools.ts` for same problem
86
- if (handledRequestIds.has(requestId)) {
97
+ if (handledRequestIds.has(requestId) === true) {
87
98
  return
88
99
  }
89
100
 
@@ -103,7 +114,7 @@ export const connectDevtoolsToStore = ({
103
114
  () =>
104
115
  sendToDevtools(
105
116
  Devtools.ClientSession.ReactivityGraphRes.make({
106
- reactivityGraph: store.reactivityGraph.getSnapshot({ includeResults }),
117
+ reactivityGraph: store[StoreInternalsSymbol].reactivityGraph.getSnapshot({ includeResults }),
107
118
  requestId: nanoid(10),
108
119
  clientId,
109
120
  sessionId,
@@ -121,14 +132,17 @@ export const connectDevtoolsToStore = ({
121
132
  // This might need to be tweaked further and possibly be exposed to the user in some way.
122
133
  const throttledSend = throttle(send, 20)
123
134
 
124
- reactivityGraphSubcriptions.set(subscriptionId, store.reactivityGraph.subscribeToRefresh(throttledSend))
135
+ reactivityGraphSubcriptions.set(
136
+ subscriptionId,
137
+ store[StoreInternalsSymbol].reactivityGraph.subscribeToRefresh(throttledSend),
138
+ )
125
139
 
126
140
  break
127
141
  }
128
142
  case 'LSD.ClientSession.DebugInfoReq': {
129
143
  sendToDevtools(
130
144
  Devtools.ClientSession.DebugInfoRes.make({
131
- debugInfo: store.sqliteDbWrapper.debugInfo,
145
+ debugInfo: store[StoreInternalsSymbol].sqliteDbWrapper.debugInfo,
132
146
  requestId,
133
147
  clientId,
134
148
  sessionId,
@@ -144,13 +158,13 @@ export const connectDevtoolsToStore = ({
144
158
  let tickHandle: number | undefined
145
159
 
146
160
  const tick = () => {
147
- buffer.push(store.sqliteDbWrapper.debugInfo)
161
+ buffer.push(store[StoreInternalsSymbol].sqliteDbWrapper.debugInfo)
148
162
 
149
163
  // NOTE this resets the debug info, so all other "readers" e.g. in other `requestAnimationFrame` loops,
150
164
  // will get the empty debug info
151
165
  // TODO We need to come up with a more graceful way to do store. Probably via a single global
152
166
  // `requestAnimationFrame` loop that is passed in somehow.
153
- store.sqliteDbWrapper.debugInfo = makeEmptyDebugInfo()
167
+ store[StoreInternalsSymbol].sqliteDbWrapper.debugInfo = makeEmptyDebugInfo()
154
168
 
155
169
  if (buffer.length > 10) {
156
170
  sendToDevtools(
@@ -194,7 +208,7 @@ export const connectDevtoolsToStore = ({
194
208
  break
195
209
  }
196
210
  case 'LSD.ClientSession.DebugInfoResetReq': {
197
- store.sqliteDbWrapper.debugInfo.slowQueries.clear()
211
+ store[StoreInternalsSymbol].sqliteDbWrapper.debugInfo.slowQueries.clear()
198
212
  sendToDevtools(
199
213
  Devtools.ClientSession.DebugInfoResetRes.make({ requestId, clientId, sessionId, liveStoreVersion }),
200
214
  )
@@ -202,7 +216,10 @@ export const connectDevtoolsToStore = ({
202
216
  }
203
217
  case 'LSD.ClientSession.DebugInfoRerunQueryReq': {
204
218
  const { queryStr, bindValues, queriedTables } = decodedMessage
205
- store.sqliteDbWrapper.cachedSelect(queryStr, bindValues, { queriedTables, skipCache: true })
219
+ store[StoreInternalsSymbol].sqliteDbWrapper.cachedSelect(queryStr, bindValues, {
220
+ queriedTables,
221
+ skipCache: true,
222
+ })
206
223
  sendToDevtools(
207
224
  Devtools.ClientSession.DebugInfoRerunQueryRes.make({ requestId, clientId, sessionId, liveStoreVersion }),
208
225
  )
@@ -223,7 +240,8 @@ export const connectDevtoolsToStore = ({
223
240
  () =>
224
241
  sendToDevtools(
225
242
  Devtools.ClientSession.LiveQueriesRes.make({
226
- liveQueries: [...store.activeQueries].map((q) => ({
243
+ liveQueries: [...store[StoreInternalsSymbol].activeQueries].map((q) => ({
244
+ /** TODO: include schema metadata for schema-aware rendering in devtools (e.g., schema AST/hash/identifier or table+columns for QueryBuilder-derived queries). */
227
245
  _tag: q._tag,
228
246
  id: q.id,
229
247
  label: q.label,
@@ -251,7 +269,10 @@ export const connectDevtoolsToStore = ({
251
269
  // Same as in the reactivity graph subscription case above, we need to throttle the updates
252
270
  const throttledSend = throttle(send, 20)
253
271
 
254
- liveQueriesSubscriptions.set(subscriptionId, store.reactivityGraph.subscribeToRefresh(throttledSend))
272
+ liveQueriesSubscriptions.set(
273
+ subscriptionId,
274
+ store[StoreInternalsSymbol].reactivityGraph.subscribeToRefresh(throttledSend),
275
+ )
255
276
 
256
277
  break
257
278
  }
@@ -278,11 +299,11 @@ export const connectDevtoolsToStore = ({
278
299
  }),
279
300
  )
280
301
 
281
- send(store.syncProcessor.syncState.pipe(Effect.runSync))
302
+ send(store[StoreInternalsSymbol].syncProcessor.syncState.pipe(Effect.runSync))
282
303
 
283
304
  syncHeadClientSessionSubscriptions.set(
284
305
  subscriptionId,
285
- store.syncProcessor.syncState.changes.pipe(
306
+ store[StoreInternalsSymbol].syncProcessor.syncState.changes.pipe(
286
307
  Stream.tap((syncState) => send(syncState)),
287
308
  Stream.runDrain,
288
309
  Effect.interruptible,
@@ -302,7 +323,30 @@ export const connectDevtoolsToStore = ({
302
323
  break
303
324
  }
304
325
  case 'LSD.ClientSession.Ping': {
305
- sendToDevtools(Devtools.ClientSession.Pong.make({ requestId, clientId, sessionId, liveStoreVersion }))
326
+ if (isDevtoolsProtocolVersionSupported(decodedMessage.devtoolsProtocolVersion) === false) {
327
+ sendToDevtools(
328
+ Devtools.ClientSession.VersionMismatch.make({
329
+ requestId,
330
+ clientId,
331
+ sessionId,
332
+ liveStoreVersion,
333
+ appVersion: liveStoreVersion,
334
+ receivedVersion: decodedMessage.liveStoreVersion,
335
+ appDevtoolsProtocolVersion: devtoolsProtocolVersion,
336
+ receivedDevtoolsProtocolVersion: resolveDevtoolsProtocolVersion(decodedMessage.devtoolsProtocolVersion),
337
+ }),
338
+ )
339
+ break
340
+ }
341
+ sendToDevtools(
342
+ Devtools.ClientSession.Pong.make({
343
+ requestId,
344
+ clientId,
345
+ sessionId,
346
+ liveStoreVersion,
347
+ devtoolsProtocolVersion,
348
+ }),
349
+ )
306
350
  break
307
351
  }
308
352
  default: {
@@ -318,4 +362,4 @@ export const connectDevtoolsToStore = ({
318
362
  Stream.runDrain,
319
363
  Effect.withSpan('LSD.devtools.onMessage'),
320
364
  )
321
- }).pipe(UnexpectedError.mapToUnexpectedError, Effect.withSpan('LSD.devtools.connectStoreToDevtools'))
365
+ }, UnknownError.mapToUnknownError)