@livestore/livestore 0.3.0-dev.5 → 0.3.0-dev.51

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 (170) hide show
  1. package/dist/.tsbuildinfo +1 -1
  2. package/dist/QueryCache.d.ts.map +1 -1
  3. package/dist/SqliteDbWrapper.d.ts +60 -0
  4. package/dist/SqliteDbWrapper.d.ts.map +1 -0
  5. package/dist/{SynchronousDatabaseWrapper.js → SqliteDbWrapper.js} +69 -34
  6. package/dist/SqliteDbWrapper.js.map +1 -0
  7. package/dist/effect/LiveStore.d.ts +6 -34
  8. package/dist/effect/LiveStore.d.ts.map +1 -1
  9. package/dist/effect/LiveStore.js +10 -12
  10. package/dist/effect/LiveStore.js.map +1 -1
  11. package/dist/effect/mod.d.ts +3 -0
  12. package/dist/effect/mod.d.ts.map +1 -0
  13. package/dist/effect/mod.js +3 -0
  14. package/dist/effect/mod.js.map +1 -0
  15. package/dist/internal/mod.d.ts +3 -0
  16. package/dist/internal/mod.d.ts.map +1 -0
  17. package/dist/internal/mod.js +3 -0
  18. package/dist/internal/mod.js.map +1 -0
  19. package/dist/live-queries/base-class.d.ts +69 -29
  20. package/dist/live-queries/base-class.d.ts.map +1 -1
  21. package/dist/live-queries/base-class.js +60 -14
  22. package/dist/live-queries/base-class.js.map +1 -1
  23. package/dist/live-queries/client-document-get-query.d.ts +12 -0
  24. package/dist/live-queries/client-document-get-query.d.ts.map +1 -0
  25. package/dist/live-queries/client-document-get-query.js +18 -0
  26. package/dist/live-queries/client-document-get-query.js.map +1 -0
  27. package/dist/live-queries/computed.d.ts +13 -15
  28. package/dist/live-queries/computed.d.ts.map +1 -1
  29. package/dist/live-queries/computed.js +37 -15
  30. package/dist/live-queries/computed.js.map +1 -1
  31. package/dist/live-queries/db-query.d.ts +93 -0
  32. package/dist/live-queries/db-query.d.ts.map +1 -0
  33. package/dist/live-queries/{db.js → db-query.js} +113 -40
  34. package/dist/live-queries/db-query.js.map +1 -0
  35. package/dist/live-queries/db-query.test.d.ts +2 -0
  36. package/dist/live-queries/db-query.test.d.ts.map +1 -0
  37. package/dist/live-queries/db-query.test.js +133 -0
  38. package/dist/live-queries/db-query.test.js.map +1 -0
  39. package/dist/live-queries/mod.d.ts +5 -0
  40. package/dist/live-queries/mod.d.ts.map +1 -0
  41. package/dist/live-queries/mod.js +5 -0
  42. package/dist/live-queries/mod.js.map +1 -0
  43. package/dist/live-queries/signal.d.ts +25 -0
  44. package/dist/live-queries/signal.d.ts.map +1 -0
  45. package/dist/live-queries/signal.js +50 -0
  46. package/dist/live-queries/signal.js.map +1 -0
  47. package/dist/live-queries/signal.test.d.ts +2 -0
  48. package/dist/live-queries/signal.test.d.ts.map +1 -0
  49. package/dist/live-queries/signal.test.js +25 -0
  50. package/dist/live-queries/signal.test.js.map +1 -0
  51. package/dist/mod.d.ts +14 -0
  52. package/dist/mod.d.ts.map +1 -0
  53. package/dist/mod.js +13 -0
  54. package/dist/mod.js.map +1 -0
  55. package/dist/reactive.d.ts +23 -17
  56. package/dist/reactive.d.ts.map +1 -1
  57. package/dist/reactive.js +23 -19
  58. package/dist/reactive.js.map +1 -1
  59. package/dist/reactive.test.js +1 -1
  60. package/dist/reactive.test.js.map +1 -1
  61. package/dist/store/create-store.d.ts +70 -12
  62. package/dist/store/create-store.d.ts.map +1 -1
  63. package/dist/store/create-store.js +68 -19
  64. package/dist/store/create-store.js.map +1 -1
  65. package/dist/store/devtools.d.ts +5 -4
  66. package/dist/store/devtools.d.ts.map +1 -1
  67. package/dist/store/devtools.js +92 -40
  68. package/dist/store/devtools.js.map +1 -1
  69. package/dist/store/store-types.d.ts +54 -42
  70. package/dist/store/store-types.d.ts.map +1 -1
  71. package/dist/store/store-types.js +2 -5
  72. package/dist/store/store-types.js.map +1 -1
  73. package/dist/store/store.d.ts +141 -35
  74. package/dist/store/store.d.ts.map +1 -1
  75. package/dist/store/store.js +322 -154
  76. package/dist/store/store.js.map +1 -1
  77. package/dist/utils/data-structures.d.ts.map +1 -1
  78. package/dist/utils/dev.d.ts.map +1 -1
  79. package/dist/utils/dev.js +6 -1
  80. package/dist/utils/dev.js.map +1 -1
  81. package/dist/utils/function-string.d.ts +7 -0
  82. package/dist/utils/function-string.d.ts.map +1 -0
  83. package/dist/utils/function-string.js +9 -0
  84. package/dist/utils/function-string.js.map +1 -0
  85. package/dist/utils/stack-info.d.ts.map +1 -1
  86. package/dist/utils/stack-info.js +6 -1
  87. package/dist/utils/stack-info.js.map +1 -1
  88. package/dist/utils/stack-info.test.js +54 -1
  89. package/dist/utils/stack-info.test.js.map +1 -1
  90. package/dist/utils/tests/fixture.d.ts +59 -216
  91. package/dist/utils/tests/fixture.d.ts.map +1 -1
  92. package/dist/utils/tests/fixture.js +23 -18
  93. package/dist/utils/tests/fixture.js.map +1 -1
  94. package/dist/utils/tests/mod.d.ts +1 -0
  95. package/dist/utils/tests/mod.d.ts.map +1 -1
  96. package/dist/utils/tests/mod.js +1 -0
  97. package/dist/utils/tests/mod.js.map +1 -1
  98. package/dist/utils/tests/otel.d.ts.map +1 -1
  99. package/dist/utils/tests/otel.js +8 -3
  100. package/dist/utils/tests/otel.js.map +1 -1
  101. package/package.json +29 -26
  102. package/src/{SynchronousDatabaseWrapper.ts → SqliteDbWrapper.ts} +92 -42
  103. package/src/effect/LiveStore.ts +27 -64
  104. package/src/effect/{index.ts → mod.ts} +2 -3
  105. package/src/internal/mod.ts +2 -0
  106. package/src/live-queries/__snapshots__/{db.test.ts.snap → db-query.test.ts.snap} +241 -45
  107. package/src/live-queries/base-class.ts +170 -53
  108. package/src/live-queries/client-document-get-query.ts +52 -0
  109. package/src/live-queries/computed.ts +51 -33
  110. package/src/live-queries/db-query.test.ts +192 -0
  111. package/src/live-queries/{db.ts → db-query.ts} +171 -82
  112. package/src/live-queries/mod.ts +4 -0
  113. package/src/live-queries/signal.test.ts +40 -0
  114. package/src/live-queries/signal.ts +81 -0
  115. package/src/mod.ts +51 -0
  116. package/src/reactive.test.ts +1 -1
  117. package/src/reactive.ts +66 -43
  118. package/src/store/create-store.ts +188 -62
  119. package/src/store/devtools.ts +124 -46
  120. package/src/store/store-types.ts +54 -43
  121. package/src/store/store.ts +457 -237
  122. package/src/utils/dev.ts +6 -1
  123. package/src/utils/function-string.ts +12 -0
  124. package/src/utils/stack-info.test.ts +58 -1
  125. package/src/utils/stack-info.ts +6 -1
  126. package/src/utils/tests/fixture.ts +22 -31
  127. package/src/utils/tests/mod.ts +1 -0
  128. package/src/utils/tests/otel.ts +10 -3
  129. package/dist/SynchronousDatabaseWrapper.d.ts +0 -41
  130. package/dist/SynchronousDatabaseWrapper.d.ts.map +0 -1
  131. package/dist/SynchronousDatabaseWrapper.js.map +0 -1
  132. package/dist/effect/index.d.ts +0 -2
  133. package/dist/effect/index.d.ts.map +0 -1
  134. package/dist/effect/index.js +0 -2
  135. package/dist/effect/index.js.map +0 -1
  136. package/dist/global-state.d.ts +0 -14
  137. package/dist/global-state.d.ts.map +0 -1
  138. package/dist/global-state.js +0 -16
  139. package/dist/global-state.js.map +0 -1
  140. package/dist/index.d.ts +0 -20
  141. package/dist/index.d.ts.map +0 -1
  142. package/dist/index.js +0 -16
  143. package/dist/index.js.map +0 -1
  144. package/dist/live-queries/db.d.ts +0 -66
  145. package/dist/live-queries/db.d.ts.map +0 -1
  146. package/dist/live-queries/db.js.map +0 -1
  147. package/dist/live-queries/db.test.d.ts +0 -2
  148. package/dist/live-queries/db.test.d.ts.map +0 -1
  149. package/dist/live-queries/db.test.js +0 -118
  150. package/dist/live-queries/db.test.js.map +0 -1
  151. package/dist/live-queries/graphql.d.ts +0 -49
  152. package/dist/live-queries/graphql.d.ts.map +0 -1
  153. package/dist/live-queries/graphql.js +0 -122
  154. package/dist/live-queries/graphql.js.map +0 -1
  155. package/dist/row-query-utils.d.ts +0 -17
  156. package/dist/row-query-utils.d.ts.map +0 -1
  157. package/dist/row-query-utils.js +0 -30
  158. package/dist/row-query-utils.js.map +0 -1
  159. package/dist/utils/otel.d.ts +0 -4
  160. package/dist/utils/otel.d.ts.map +0 -1
  161. package/dist/utils/otel.js +0 -6
  162. package/dist/utils/otel.js.map +0 -1
  163. package/src/global-state.ts +0 -20
  164. package/src/index.ts +0 -66
  165. package/src/live-queries/db.test.ts +0 -154
  166. package/src/live-queries/graphql.ts +0 -219
  167. package/src/row-query-utils.ts +0 -65
  168. package/src/utils/otel.ts +0 -9
  169. package/tsconfig.json +0 -18
  170. package/vitest.config.js +0 -9
@@ -2,21 +2,24 @@ import type {
2
2
  Adapter,
3
3
  BootStatus,
4
4
  ClientSession,
5
+ ClientSessionDevtoolsChannel,
5
6
  IntentionalShutdownCause,
6
- StoreDevtoolsChannel,
7
+ MigrationsReport,
7
8
  } from '@livestore/common'
8
9
  import { provideOtel, UnexpectedError } from '@livestore/common'
9
- import type { EventId, LiveStoreSchema, MutationEvent } from '@livestore/common/schema'
10
- import { LS_DEV } from '@livestore/utils'
11
- import type { Cause } from '@livestore/utils/effect'
10
+ import type { LiveStoreSchema } from '@livestore/common/schema'
11
+ import { isDevEnv, LS_DEV } from '@livestore/utils'
12
+ import type { Cause, Schema } from '@livestore/utils/effect'
12
13
  import {
14
+ Context,
13
15
  Deferred,
14
16
  Effect,
15
17
  Exit,
18
+ Fiber,
16
19
  identity,
20
+ Layer,
17
21
  Logger,
18
22
  LogLevel,
19
- MutableHashMap,
20
23
  OtelTracer,
21
24
  Queue,
22
25
  Runtime,
@@ -26,43 +29,112 @@ import {
26
29
  import { nanoid } from '@livestore/utils/nanoid'
27
30
  import * as otel from '@opentelemetry/api'
28
31
 
29
- import { globalReactivityGraph } from '../global-state.js'
30
- import type { ReactivityGraph } from '../live-queries/base-class.js'
31
32
  import { connectDevtoolsToStore } from './devtools.js'
32
33
  import { Store } from './store.js'
33
- import type { BaseGraphQLContext, GraphQLOptions, OtelOptions, ShutdownDeferred } from './store-types.js'
34
+ import type {
35
+ LiveStoreContextRunning as LiveStoreContextRunning_,
36
+ OtelOptions,
37
+ ShutdownDeferred,
38
+ } from './store-types.js'
39
+
40
+ export const DEFAULT_PARAMS = {
41
+ leaderPushBatchSize: 1,
42
+ }
43
+
44
+ export class LiveStoreContextRunning extends Context.Tag('@livestore/livestore/effect/LiveStoreContextRunning')<
45
+ LiveStoreContextRunning,
46
+ LiveStoreContextRunning_
47
+ >() {
48
+ static fromDeferred = Effect.gen(function* () {
49
+ const deferred = yield* DeferredStoreContext
50
+ const ctx = yield* deferred
51
+ return Layer.succeed(LiveStoreContextRunning, ctx)
52
+ }).pipe(Layer.unwrapScoped)
53
+ }
54
+
55
+ export class DeferredStoreContext extends Context.Tag('@livestore/livestore/effect/DeferredStoreContext')<
56
+ DeferredStoreContext,
57
+ Deferred.Deferred<LiveStoreContextRunning['Type'], UnexpectedError>
58
+ >() {}
59
+
60
+ export type LiveStoreContextProps<TSchema extends LiveStoreSchema, TContext = {}> = {
61
+ schema: TSchema
62
+ /**
63
+ * The `storeId` can be used to isolate multiple stores from each other.
64
+ * So it can be useful for multi-tenancy scenarios.
65
+ *
66
+ * The `storeId` is also used for persistence.
67
+ *
68
+ * @default 'default'
69
+ */
70
+ storeId?: string
71
+ /** Can be useful for custom live query implementations (e.g. see `@livestore/graphql`) */
72
+ context?: TContext
73
+ boot?: (
74
+ store: Store<TSchema, TContext>,
75
+ ) => Effect.Effect<void, unknown, OtelTracer.OtelTracer | LiveStoreContextRunning>
76
+ adapter: Adapter
77
+ /**
78
+ * Whether to disable devtools.
79
+ *
80
+ * @default 'auto'
81
+ */
82
+ disableDevtools?: boolean | 'auto'
83
+ onBootStatus?: (status: BootStatus) => void
84
+ batchUpdates: (run: () => void) => void
85
+ }
34
86
 
35
- export interface CreateStoreOptions<TGraphQLContext extends BaseGraphQLContext, TSchema extends LiveStoreSchema> {
87
+ export interface CreateStoreOptions<TSchema extends LiveStoreSchema, TContext = {}> {
36
88
  schema: TSchema
37
89
  adapter: Adapter
38
90
  storeId: string
39
- reactivityGraph?: ReactivityGraph
40
- graphQLOptions?: GraphQLOptions<TGraphQLContext>
91
+ context?: TContext
41
92
  boot?: (
42
- store: Store<TGraphQLContext, TSchema>,
43
- parentSpan: otel.Span,
44
- ) => void | Promise<void> | Effect.Effect<void, unknown, OtelTracer.OtelTracer>
93
+ store: Store<TSchema, TContext>,
94
+ ctx: {
95
+ migrationsReport: MigrationsReport
96
+ parentSpan: otel.Span
97
+ },
98
+ ) => void | Promise<void> | Effect.Effect<void, unknown, OtelTracer.OtelTracer | LiveStoreContextRunning>
45
99
  batchUpdates?: (run: () => void) => void
46
- disableDevtools?: boolean
100
+ /**
101
+ * Whether to disable devtools.
102
+ *
103
+ * @default 'auto'
104
+ */
105
+ disableDevtools?: boolean | 'auto'
47
106
  onBootStatus?: (status: BootStatus) => void
48
107
  shutdownDeferred?: ShutdownDeferred
108
+ /**
109
+ * Currently only used in the web adapter:
110
+ * If true, registers a beforeunload event listener to confirm unsaved changes.
111
+ *
112
+ * @default true
113
+ */
114
+ confirmUnsavedChanges?: boolean
115
+ /**
116
+ * Payload that will be passed to the sync backend when connecting
117
+ *
118
+ * @default undefined
119
+ */
120
+ syncPayload?: Schema.JsonValue
121
+ params?: {
122
+ leaderPushBatchSize?: number
123
+ }
49
124
  debug?: {
50
125
  instanceId?: string
51
126
  }
52
127
  }
53
128
 
54
129
  /** Create a new LiveStore Store */
55
- export const createStorePromise = async <
56
- TGraphQLContext extends BaseGraphQLContext,
57
- TSchema extends LiveStoreSchema = LiveStoreSchema,
58
- >({
130
+ export const createStorePromise = async <TSchema extends LiveStoreSchema = LiveStoreSchema, TContext = {}>({
59
131
  signal,
60
132
  otelOptions,
61
133
  ...options
62
- }: CreateStoreOptions<TGraphQLContext, TSchema> & {
134
+ }: CreateStoreOptions<TSchema, TContext> & {
63
135
  signal?: AbortSignal
64
136
  otelOptions?: Partial<OtelOptions>
65
- }): Promise<Store<TGraphQLContext, TSchema>> =>
137
+ }): Promise<Store<TSchema, TContext>> =>
66
138
  Effect.gen(function* () {
67
139
  const scope = yield* Scope.make()
68
140
  const runtime = yield* Effect.runtime()
@@ -81,34 +153,35 @@ export const createStorePromise = async <
81
153
  provideOtel({ parentSpanContext: otelOptions?.rootSpanContext, otelTracer: otelOptions?.tracer }),
82
154
  Effect.tapCauseLogPretty,
83
155
  Effect.annotateLogs({ thread: 'window' }),
84
- Effect.provide(Logger.pretty),
156
+ Effect.provide(Logger.prettyWithThread('window')),
85
157
  Logger.withMinimumLogLevel(LogLevel.Debug),
86
158
  Effect.runPromise,
87
159
  )
88
160
 
89
- export const createStore = <
90
- TGraphQLContext extends BaseGraphQLContext,
91
- TSchema extends LiveStoreSchema = LiveStoreSchema,
92
- >({
161
+ export const createStore = <TSchema extends LiveStoreSchema = LiveStoreSchema, TContext = {}>({
93
162
  schema,
94
163
  adapter,
95
164
  storeId,
96
- graphQLOptions,
165
+ context = {} as TContext,
97
166
  boot,
98
- reactivityGraph = globalReactivityGraph,
99
167
  batchUpdates,
100
168
  disableDevtools,
101
169
  onBootStatus,
102
170
  shutdownDeferred,
171
+ params,
103
172
  debug,
104
- }: CreateStoreOptions<TGraphQLContext, TSchema>): Effect.Effect<
105
- Store<TGraphQLContext, TSchema>,
173
+ confirmUnsavedChanges = true,
174
+ syncPayload,
175
+ }: CreateStoreOptions<TSchema, TContext>): Effect.Effect<
176
+ Store<TSchema, TContext>,
106
177
  UnexpectedError,
107
178
  Scope.Scope | OtelTracer.OtelTracer
108
179
  > =>
109
180
  Effect.gen(function* () {
110
181
  const lifetimeScope = yield* Scope.make()
111
182
 
183
+ yield* validateStoreId(storeId)
184
+
112
185
  yield* Effect.addFinalizer((_) => Scope.close(lifetimeScope, _))
113
186
 
114
187
  const debugInstanceId = debug?.instanceId ?? nanoid(10)
@@ -130,7 +203,7 @@ export const createStore = <
130
203
 
131
204
  const storeDeferred = yield* Deferred.make<Store>()
132
205
 
133
- const connectDevtoolsToStore_ = (storeDevtoolsChannel: StoreDevtoolsChannel) =>
206
+ const connectDevtoolsToStore_ = (storeDevtoolsChannel: ClientSessionDevtoolsChannel) =>
134
207
  Effect.gen(function* () {
135
208
  const store = yield* storeDeferred
136
209
  yield* connectDevtoolsToStore({ storeDevtoolsChannel, store })
@@ -139,53 +212,81 @@ export const createStore = <
139
212
  const runtime = yield* Effect.runtime<Scope.Scope>()
140
213
 
141
214
  const shutdown = (cause: Cause.Cause<UnexpectedError | IntentionalShutdownCause>) =>
142
- Scope.close(lifetimeScope, Exit.failCause(cause)).pipe(
143
- Effect.logWarnIfTakesLongerThan({ label: '@livestore/livestore:shutdown', duration: 500 }),
144
- Effect.timeout(1000),
145
- Effect.catchTag('TimeoutException', () =>
146
- Effect.logError('@livestore/livestore:shutdown: Timed out after 1 second'),
147
- ),
148
- Effect.tap(() => (shutdownDeferred ? Deferred.failCause(shutdownDeferred, cause) : Effect.void)),
149
- Effect.tap(() => Effect.logDebug('LiveStore shutdown complete')),
150
- Effect.withSpan('livestore:shutdown'),
215
+ Effect.gen(function* () {
216
+ yield* Scope.close(lifetimeScope, Exit.failCause(cause)).pipe(
217
+ Effect.logWarnIfTakesLongerThan({ label: '@livestore/livestore:shutdown', duration: 500 }),
218
+ Effect.timeout(1000),
219
+ Effect.catchTag('TimeoutException', () =>
220
+ Effect.logError('@livestore/livestore:shutdown: Timed out after 1 second'),
221
+ ),
222
+ )
223
+
224
+ if (shutdownDeferred) {
225
+ yield* Deferred.failCause(shutdownDeferred, cause)
226
+ }
227
+
228
+ yield* Effect.logDebug('LiveStore shutdown complete')
229
+ }).pipe(
230
+ Effect.withSpan('@livestore/livestore:shutdown'),
231
+ Effect.provide(runtime),
232
+ Effect.tapCauseLogPretty,
233
+ // Given that the shutdown flow might also interrupt the effect that is calling the shutdown,
234
+ // we want to detach the shutdown effect so it's not interrupted by itself
235
+ Effect.runFork,
236
+ Fiber.join,
151
237
  )
152
238
 
153
239
  const clientSession: ClientSession = yield* adapter({
154
240
  schema,
155
241
  storeId,
156
- devtoolsEnabled: disableDevtools !== true,
242
+ devtoolsEnabled: getDevtoolsEnabled(disableDevtools),
157
243
  bootStatusQueue,
158
244
  shutdown,
159
245
  connectDevtoolsToStore: connectDevtoolsToStore_,
160
246
  debugInstanceId,
247
+ syncPayload,
161
248
  }).pipe(Effect.withPerformanceMeasure('livestore:makeAdapter'), Effect.withSpan('createStore:makeAdapter'))
162
249
 
163
- // TODO fill up with unsynced mutation events from the client session
164
- const unsyncedMutationEvents = MutableHashMap.empty<EventId.EventId, MutationEvent.ForSchema<TSchema>>()
165
-
166
- const store = Store.createStore<TGraphQLContext, TSchema>(
167
- {
168
- clientSession,
169
- schema,
170
- graphQLOptions,
171
- otelOptions: { tracer: otelTracer, rootSpanContext: otelRootSpanContext },
172
- reactivityGraph,
173
- disableDevtools,
174
- unsyncedMutationEvents,
175
- lifetimeScope,
176
- runtime,
177
- // NOTE during boot we're not yet executing mutations in a batched context
178
- // but only set the provided `batchUpdates` function after boot
179
- batchUpdates: (run) => run(),
180
- storeId,
250
+ if (LS_DEV && clientSession.leaderThread.initialState.migrationsReport.migrations.length > 0) {
251
+ yield* Effect.logDebug(
252
+ '[@livestore/livestore:createStore] migrationsReport',
253
+ ...clientSession.leaderThread.initialState.migrationsReport.migrations.map((m) =>
254
+ m.hashes.actual === undefined
255
+ ? `Table '${m.tableName}' doesn't exist yet. Creating table...`
256
+ : `Schema hash mismatch for table '${m.tableName}' (DB: ${m.hashes.actual}, expected: ${m.hashes.expected}), migrating table...`,
257
+ ),
258
+ )
259
+ }
260
+
261
+ const store = new Store<TSchema, TContext>({
262
+ clientSession,
263
+ schema,
264
+ context,
265
+ otelOptions: { tracer: otelTracer, rootSpanContext: otelRootSpanContext },
266
+ effectContext: { lifetimeScope, runtime },
267
+ // TODO find a better way to detect if we're running LiveStore in the LiveStore devtools
268
+ // But for now this is a good enough approximation with little downsides
269
+ __runningInDevtools: getDevtoolsEnabled(disableDevtools) === false,
270
+ confirmUnsavedChanges,
271
+ // NOTE during boot we're not yet executing events in a batched context
272
+ // but only set the provided `batchUpdates` function after boot
273
+ batchUpdates: (run) => run(),
274
+ storeId,
275
+ params: {
276
+ leaderPushBatchSize: params?.leaderPushBatchSize ?? DEFAULT_PARAMS.leaderPushBatchSize,
181
277
  },
182
- span,
183
- )
278
+ })
279
+
280
+ // Starts background fibers (syncing, event processing, etc) for store
281
+ yield* store.boot
184
282
 
185
283
  if (boot !== undefined) {
186
284
  // TODO also incorporate `boot` function progress into `bootStatusQueue`
187
- yield* Effect.tryAll(() => boot(store, span)).pipe(
285
+ yield* Effect.tryAll(() =>
286
+ boot(store, { migrationsReport: clientSession.leaderThread.initialState.migrationsReport, parentSpan: span }),
287
+ ).pipe(
188
288
  UnexpectedError.mapToUnexpectedError,
289
+ Effect.provide(Layer.succeed(LiveStoreContextRunning, { stage: 'running', store: store as any as Store })),
189
290
  Effect.withSpan('createStore:boot'),
190
291
  )
191
292
  }
@@ -208,3 +309,28 @@ export const createStore = <
208
309
  Scope.extend(lifetimeScope),
209
310
  )
210
311
  })
312
+
313
+ const validateStoreId = (storeId: string) =>
314
+ Effect.gen(function* () {
315
+ const validChars = /^[a-zA-Z0-9_-]+$/
316
+
317
+ if (!validChars.test(storeId)) {
318
+ return yield* UnexpectedError.make({
319
+ cause: `Invalid storeId: ${storeId}. Only alphanumeric characters, underscores, and hyphens are allowed.`,
320
+ payload: { storeId },
321
+ })
322
+ }
323
+ })
324
+
325
+ const getDevtoolsEnabled = (disableDevtools: boolean | 'auto' | undefined) => {
326
+ // If an explicit value is provided, use that
327
+ if (disableDevtools === true || disableDevtools === false) {
328
+ return !disableDevtools
329
+ }
330
+
331
+ if (isDevEnv() === true) {
332
+ return true
333
+ }
334
+
335
+ return false
336
+ }
@@ -1,20 +1,22 @@
1
- import type { ClientSession, DebugInfo } from '@livestore/common'
1
+ import type { ClientSession, ClientSessionSyncProcessor, DebugInfo, SyncState } from '@livestore/common'
2
2
  import { Devtools, liveStoreVersion, UnexpectedError } from '@livestore/common'
3
3
  import { throttle } from '@livestore/utils'
4
4
  import type { WebChannel } from '@livestore/utils/effect'
5
5
  import { Effect, Stream } from '@livestore/utils/effect'
6
+ import { nanoid } from '@livestore/utils/nanoid'
6
7
 
7
8
  import type { LiveQuery, ReactivityGraph } from '../live-queries/base-class.js'
8
9
  import { NOT_REFRESHED_YET } from '../reactive.js'
9
- import type { SynchronousDatabaseWrapper } from '../SynchronousDatabaseWrapper.js'
10
- import { emptyDebugInfo as makeEmptyDebugInfo } from '../SynchronousDatabaseWrapper.js'
10
+ import type { SqliteDbWrapper } from '../SqliteDbWrapper.js'
11
+ import { emptyDebugInfo as makeEmptyDebugInfo } from '../SqliteDbWrapper.js'
11
12
  import type { ReferenceCountedSet } from '../utils/data-structures.js'
12
13
 
13
14
  type IStore = {
14
15
  clientSession: ClientSession
15
16
  reactivityGraph: ReactivityGraph
16
- syncDbWrapper: SynchronousDatabaseWrapper
17
+ sqliteDbWrapper: SqliteDbWrapper
17
18
  activeQueries: ReferenceCountedSet<LiveQuery<any>>
19
+ syncProcessor: ClientSessionSyncProcessor
18
20
  }
19
21
 
20
22
  type Unsub = () => void
@@ -34,13 +36,17 @@ export const connectDevtoolsToStore = ({
34
36
  storeDevtoolsChannel,
35
37
  store,
36
38
  }: {
37
- storeDevtoolsChannel: WebChannel.WebChannel<Devtools.MessageToAppClientSession, Devtools.MessageFromAppClientSession>
39
+ storeDevtoolsChannel: WebChannel.WebChannel<
40
+ Devtools.ClientSession.MessageToApp,
41
+ Devtools.ClientSession.MessageFromApp
42
+ >
38
43
  store: IStore
39
44
  }) =>
40
45
  Effect.gen(function* () {
41
46
  const reactivityGraphSubcriptions: SubMap = new Map()
42
47
  const liveQueriesSubscriptions: SubMap = new Map()
43
48
  const debugInfoHistorySubscriptions: SubMap = new Map()
49
+ const syncHeadClientSessionSubscriptions: SubMap = new Map()
44
50
 
45
51
  const { clientId, sessionId } = store.clientSession
46
52
 
@@ -49,13 +55,16 @@ export const connectDevtoolsToStore = ({
49
55
  reactivityGraphSubcriptions.forEach((unsub) => unsub())
50
56
  liveQueriesSubscriptions.forEach((unsub) => unsub())
51
57
  debugInfoHistorySubscriptions.forEach((unsub) => unsub())
58
+ syncHeadClientSessionSubscriptions.forEach((unsub) => unsub())
52
59
  }),
53
60
  )
54
61
 
55
- const sendToDevtools = (message: Devtools.MessageFromAppClientSession) =>
62
+ const handledRequestIds = new Set<RequestId>()
63
+
64
+ const sendToDevtools = (message: Devtools.ClientSession.MessageFromApp) =>
56
65
  storeDevtoolsChannel.send(message).pipe(Effect.tapCauseLogPretty, Effect.runFork)
57
66
 
58
- const onMessage = (decodedMessage: typeof Devtools.MessageToAppClientSession.Type) => {
67
+ const onMessage = (decodedMessage: typeof Devtools.ClientSession.MessageToApp.Type) => {
59
68
  // console.debug('@livestore/livestore:store:devtools:onMessage', decodedMessage)
60
69
 
61
70
  if (decodedMessage.clientId !== clientId || decodedMessage.sessionId !== sessionId) {
@@ -63,18 +72,29 @@ export const connectDevtoolsToStore = ({
63
72
  return
64
73
  }
65
74
 
66
- if (decodedMessage._tag === 'LSD.Disconnect') {
75
+ if (decodedMessage._tag === 'LSD.ClientSession.Disconnect') {
67
76
  // console.error('TODO handle disconnect properly in store')
68
77
  return
69
78
  }
70
79
 
71
80
  const requestId = decodedMessage.requestId
72
81
 
82
+ // TODO we should try to move the duplicate message handling on the webmesh layer
83
+ // So far I could only observe this problem with webmesh proxy channels (e.g. for Expo)
84
+ // Proof: https://share.cleanshot.com/V9G87B0B
85
+ // Also see `leader-worker-devtools.ts` for same problem
86
+ if (handledRequestIds.has(requestId)) {
87
+ return
88
+ }
89
+
90
+ handledRequestIds.add(requestId)
91
+
73
92
  const requestIdleCallback = globalThis.requestIdleCallback ?? ((cb: () => void) => cb())
74
93
 
75
94
  switch (decodedMessage._tag) {
76
- case 'LSD.ReactivityGraphSubscribe': {
95
+ case 'LSD.ClientSession.ReactivityGraphSubscribe': {
77
96
  const includeResults = decodedMessage.includeResults
97
+ const { subscriptionId } = decodedMessage
78
98
 
79
99
  const send = () =>
80
100
  // In order to not add more work to the current tick, we use requestIdleCallback
@@ -82,12 +102,13 @@ export const connectDevtoolsToStore = ({
82
102
  requestIdleCallback(
83
103
  () =>
84
104
  sendToDevtools(
85
- Devtools.ReactivityGraphRes.make({
105
+ Devtools.ClientSession.ReactivityGraphRes.make({
86
106
  reactivityGraph: store.reactivityGraph.getSnapshot({ includeResults }),
87
- requestId,
107
+ requestId: nanoid(10),
88
108
  clientId,
89
109
  sessionId,
90
110
  liveStoreVersion,
111
+ subscriptionId,
91
112
  }),
92
113
  ),
93
114
  { timeout: 500 },
@@ -100,14 +121,14 @@ export const connectDevtoolsToStore = ({
100
121
  // This might need to be tweaked further and possibly be exposed to the user in some way.
101
122
  const throttledSend = throttle(send, 20)
102
123
 
103
- reactivityGraphSubcriptions.set(requestId, store.reactivityGraph.subscribeToRefresh(throttledSend))
124
+ reactivityGraphSubcriptions.set(subscriptionId, store.reactivityGraph.subscribeToRefresh(throttledSend))
104
125
 
105
126
  break
106
127
  }
107
- case 'LSD.DebugInfoReq': {
128
+ case 'LSD.ClientSession.DebugInfoReq': {
108
129
  sendToDevtools(
109
- Devtools.DebugInfoRes.make({
110
- debugInfo: store.syncDbWrapper.debugInfo,
130
+ Devtools.ClientSession.DebugInfoRes.make({
131
+ debugInfo: store.sqliteDbWrapper.debugInfo,
111
132
  requestId,
112
133
  clientId,
113
134
  sessionId,
@@ -116,28 +137,30 @@ export const connectDevtoolsToStore = ({
116
137
  )
117
138
  break
118
139
  }
119
- case 'LSD.DebugInfoHistorySubscribe': {
140
+ case 'LSD.ClientSession.DebugInfoHistorySubscribe': {
141
+ const { subscriptionId } = decodedMessage
120
142
  const buffer: DebugInfo[] = []
121
143
  let hasStopped = false
122
144
  let tickHandle: number | undefined
123
145
 
124
146
  const tick = () => {
125
- buffer.push(store.syncDbWrapper.debugInfo)
147
+ buffer.push(store.sqliteDbWrapper.debugInfo)
126
148
 
127
149
  // NOTE this resets the debug info, so all other "readers" e.g. in other `requestAnimationFrame` loops,
128
150
  // will get the empty debug info
129
151
  // TODO We need to come up with a more graceful way to do store. Probably via a single global
130
152
  // `requestAnimationFrame` loop that is passed in somehow.
131
- store.syncDbWrapper.debugInfo = makeEmptyDebugInfo()
153
+ store.sqliteDbWrapper.debugInfo = makeEmptyDebugInfo()
132
154
 
133
155
  if (buffer.length > 10) {
134
156
  sendToDevtools(
135
- Devtools.DebugInfoHistoryRes.make({
157
+ Devtools.ClientSession.DebugInfoHistoryRes.make({
136
158
  debugInfoHistory: buffer,
137
- requestId,
159
+ requestId: nanoid(10),
138
160
  clientId,
139
161
  sessionId,
140
162
  liveStoreVersion,
163
+ subscriptionId,
141
164
  }),
142
165
  )
143
166
  buffer.length = 0
@@ -158,44 +181,53 @@ export const connectDevtoolsToStore = ({
158
181
  }
159
182
  }
160
183
 
161
- debugInfoHistorySubscriptions.set(requestId, unsub)
184
+ debugInfoHistorySubscriptions.set(subscriptionId, unsub)
162
185
 
163
186
  break
164
187
  }
165
- case 'LSD.DebugInfoHistoryUnsubscribe': {
166
- // NOTE given WebMesh channels have persistent retry behaviour, it can happen that a previous
167
- // WebMesh channel will send a unsubscribe message for an old requestId. Thus the `?.()` handling.
168
- debugInfoHistorySubscriptions.get(requestId)?.()
169
- debugInfoHistorySubscriptions.delete(requestId)
188
+ case 'LSD.ClientSession.DebugInfoHistoryUnsubscribe': {
189
+ const { subscriptionId } = decodedMessage
190
+ // NOTE given Webmesh channels have persistent retry behaviour, it can happen that a previous
191
+ // Webmesh channel will send a unsubscribe message for an old requestId. Thus the `?.()` handling.
192
+ debugInfoHistorySubscriptions.get(subscriptionId)?.()
193
+ debugInfoHistorySubscriptions.delete(subscriptionId)
170
194
  break
171
195
  }
172
- case 'LSD.DebugInfoResetReq': {
173
- store.syncDbWrapper.debugInfo.slowQueries.clear()
174
- sendToDevtools(Devtools.DebugInfoResetRes.make({ requestId, clientId, sessionId, liveStoreVersion }))
196
+ case 'LSD.ClientSession.DebugInfoResetReq': {
197
+ store.sqliteDbWrapper.debugInfo.slowQueries.clear()
198
+ sendToDevtools(
199
+ Devtools.ClientSession.DebugInfoResetRes.make({ requestId, clientId, sessionId, liveStoreVersion }),
200
+ )
175
201
  break
176
202
  }
177
- case 'LSD.DebugInfoRerunQueryReq': {
203
+ case 'LSD.ClientSession.DebugInfoRerunQueryReq': {
178
204
  const { queryStr, bindValues, queriedTables } = decodedMessage
179
- store.syncDbWrapper.select(queryStr, { bindValues, queriedTables, skipCache: true })
180
- sendToDevtools(Devtools.DebugInfoRerunQueryRes.make({ requestId, clientId, sessionId, liveStoreVersion }))
205
+ store.sqliteDbWrapper.select(queryStr, bindValues, { queriedTables, skipCache: true })
206
+ sendToDevtools(
207
+ Devtools.ClientSession.DebugInfoRerunQueryRes.make({ requestId, clientId, sessionId, liveStoreVersion }),
208
+ )
181
209
  break
182
210
  }
183
- case 'LSD.ReactivityGraphUnsubscribe': {
184
- // NOTE given WebMesh channels have persistent retry behaviour, it can happen that a previous
185
- // WebMesh channel will send a unsubscribe message for an old requestId. Thus the `?.()` handling.
186
- reactivityGraphSubcriptions.get(requestId)?.()
211
+ case 'LSD.ClientSession.ReactivityGraphUnsubscribe': {
212
+ const { subscriptionId } = decodedMessage
213
+ // NOTE given Webmesh channels have persistent retry behaviour, it can happen that a previous
214
+ // Webmesh channel will send a unsubscribe message for an old requestId. Thus the `?.()` handling.
215
+ reactivityGraphSubcriptions.get(subscriptionId)?.()
216
+ reactivityGraphSubcriptions.delete(subscriptionId)
187
217
  break
188
218
  }
189
- case 'LSD.LiveQueriesSubscribe': {
219
+ case 'LSD.ClientSession.LiveQueriesSubscribe': {
220
+ const { subscriptionId } = decodedMessage
190
221
  const send = () =>
191
222
  requestIdleCallback(
192
223
  () =>
193
224
  sendToDevtools(
194
- Devtools.LiveQueriesRes.make({
225
+ Devtools.ClientSession.LiveQueriesRes.make({
195
226
  liveQueries: [...store.activeQueries].map((q) => ({
196
227
  _tag: q._tag,
197
228
  id: q.id,
198
229
  label: q.label,
230
+ hash: q.def.hash,
199
231
  runs: q.runs,
200
232
  executionTimes: q.executionTimes.map((_) => Number(_.toString().slice(0, 5))),
201
233
  lastestResult:
@@ -204,10 +236,11 @@ export const connectDevtoolsToStore = ({
204
236
  : q.results$.previousResult,
205
237
  activeSubscriptions: Array.from(q.activeSubscriptions),
206
238
  })),
207
- requestId,
239
+ requestId: nanoid(10),
208
240
  liveStoreVersion,
209
241
  clientId,
210
242
  sessionId,
243
+ subscriptionId,
211
244
  }),
212
245
  ),
213
246
  { timeout: 500 },
@@ -218,18 +251,63 @@ export const connectDevtoolsToStore = ({
218
251
  // Same as in the reactivity graph subscription case above, we need to throttle the updates
219
252
  const throttledSend = throttle(send, 20)
220
253
 
221
- liveQueriesSubscriptions.set(requestId, store.reactivityGraph.subscribeToRefresh(throttledSend))
254
+ liveQueriesSubscriptions.set(subscriptionId, store.reactivityGraph.subscribeToRefresh(throttledSend))
255
+
256
+ break
257
+ }
258
+ case 'LSD.ClientSession.LiveQueriesUnsubscribe': {
259
+ const { subscriptionId } = decodedMessage
260
+ // NOTE given Webmesh channels have persistent retry behaviour, it can happen that a previous
261
+ // Webmesh channel will send a unsubscribe message for an old requestId. Thus the `?.()` handling.
262
+ liveQueriesSubscriptions.get(subscriptionId)?.()
263
+ liveQueriesSubscriptions.delete(subscriptionId)
264
+ break
265
+ }
266
+ case 'LSD.ClientSession.SyncHeadSubscribe': {
267
+ const { subscriptionId } = decodedMessage
268
+ const send = (syncState: SyncState.SyncState) =>
269
+ sendToDevtools(
270
+ Devtools.ClientSession.SyncHeadRes.make({
271
+ local: syncState.localHead,
272
+ upstream: syncState.upstreamHead,
273
+ requestId: nanoid(10),
274
+ clientId,
275
+ sessionId,
276
+ liveStoreVersion,
277
+ subscriptionId,
278
+ }),
279
+ )
280
+
281
+ send(store.syncProcessor.syncState.pipe(Effect.runSync))
282
+
283
+ syncHeadClientSessionSubscriptions.set(
284
+ subscriptionId,
285
+ store.syncProcessor.syncState.changes.pipe(
286
+ Stream.tap((syncState) => send(syncState)),
287
+ Stream.runDrain,
288
+ Effect.interruptible,
289
+ Effect.tapCauseLogPretty,
290
+ Effect.runCallback,
291
+ ),
292
+ )
222
293
 
223
294
  break
224
295
  }
225
- case 'LSD.LiveQueriesUnsubscribe': {
226
- // NOTE given WebMesh channels have persistent retry behaviour, it can happen that a previous
227
- // WebMesh channel will send a unsubscribe message for an old requestId. Thus the `?.()` handling.
228
- liveQueriesSubscriptions.get(requestId)?.()
229
- liveQueriesSubscriptions.delete(requestId)
296
+ case 'LSD.ClientSession.SyncHeadUnsubscribe': {
297
+ const { subscriptionId } = decodedMessage
298
+ // NOTE given Webmesh channels have persistent retry behaviour, it can happen that a previous
299
+ // Webmesh channel will send a unsubscribe message for an old requestId. Thus the `?.()` handling.
300
+ syncHeadClientSessionSubscriptions.get(subscriptionId)?.()
301
+ syncHeadClientSessionSubscriptions.delete(subscriptionId)
230
302
  break
231
303
  }
232
- // No default
304
+ case 'LSD.ClientSession.Ping': {
305
+ sendToDevtools(Devtools.ClientSession.Pong.make({ requestId, clientId, sessionId, liveStoreVersion }))
306
+ break
307
+ }
308
+ default: {
309
+ console.warn(`[LSD.ClientSession] Unknown message`, decodedMessage)
310
+ }
233
311
  }
234
312
  }
235
313