@livestore/adapter-web 0.4.0-dev.2 → 0.4.0-dev.21

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 (61) hide show
  1. package/dist/.tsbuildinfo +1 -1
  2. package/dist/in-memory/in-memory-adapter.d.ts +49 -5
  3. package/dist/in-memory/in-memory-adapter.d.ts.map +1 -1
  4. package/dist/in-memory/in-memory-adapter.js +69 -16
  5. package/dist/in-memory/in-memory-adapter.js.map +1 -1
  6. package/dist/web-worker/client-session/client-session-devtools.d.ts +1 -1
  7. package/dist/web-worker/client-session/client-session-devtools.d.ts.map +1 -1
  8. package/dist/web-worker/client-session/client-session-devtools.js +14 -3
  9. package/dist/web-worker/client-session/client-session-devtools.js.map +1 -1
  10. package/dist/web-worker/client-session/persisted-adapter.d.ts +15 -0
  11. package/dist/web-worker/client-session/persisted-adapter.d.ts.map +1 -1
  12. package/dist/web-worker/client-session/persisted-adapter.js +68 -46
  13. package/dist/web-worker/client-session/persisted-adapter.js.map +1 -1
  14. package/dist/web-worker/client-session/sqlite-loader.d.ts +2 -0
  15. package/dist/web-worker/client-session/sqlite-loader.d.ts.map +1 -0
  16. package/dist/web-worker/client-session/sqlite-loader.js +16 -0
  17. package/dist/web-worker/client-session/sqlite-loader.js.map +1 -0
  18. package/dist/web-worker/common/persisted-sqlite.d.ts +23 -7
  19. package/dist/web-worker/common/persisted-sqlite.d.ts.map +1 -1
  20. package/dist/web-worker/common/persisted-sqlite.js +125 -76
  21. package/dist/web-worker/common/persisted-sqlite.js.map +1 -1
  22. package/dist/web-worker/common/shutdown-channel.d.ts +3 -2
  23. package/dist/web-worker/common/shutdown-channel.d.ts.map +1 -1
  24. package/dist/web-worker/common/shutdown-channel.js +2 -2
  25. package/dist/web-worker/common/shutdown-channel.js.map +1 -1
  26. package/dist/web-worker/common/worker-disconnect-channel.d.ts +2 -6
  27. package/dist/web-worker/common/worker-disconnect-channel.d.ts.map +1 -1
  28. package/dist/web-worker/common/worker-disconnect-channel.js +3 -2
  29. package/dist/web-worker/common/worker-disconnect-channel.js.map +1 -1
  30. package/dist/web-worker/common/worker-schema.d.ts +147 -56
  31. package/dist/web-worker/common/worker-schema.d.ts.map +1 -1
  32. package/dist/web-worker/common/worker-schema.js +55 -36
  33. package/dist/web-worker/common/worker-schema.js.map +1 -1
  34. package/dist/web-worker/leader-worker/make-leader-worker.d.ts +4 -2
  35. package/dist/web-worker/leader-worker/make-leader-worker.d.ts.map +1 -1
  36. package/dist/web-worker/leader-worker/make-leader-worker.js +63 -27
  37. package/dist/web-worker/leader-worker/make-leader-worker.js.map +1 -1
  38. package/dist/web-worker/shared-worker/make-shared-worker.d.ts +2 -1
  39. package/dist/web-worker/shared-worker/make-shared-worker.d.ts.map +1 -1
  40. package/dist/web-worker/shared-worker/make-shared-worker.js +66 -49
  41. package/dist/web-worker/shared-worker/make-shared-worker.js.map +1 -1
  42. package/dist/web-worker/vite-dev-polyfill.js +1 -0
  43. package/dist/web-worker/vite-dev-polyfill.js.map +1 -1
  44. package/package.json +8 -9
  45. package/src/in-memory/in-memory-adapter.ts +83 -21
  46. package/src/web-worker/ambient.d.ts +7 -24
  47. package/src/web-worker/client-session/client-session-devtools.ts +18 -3
  48. package/src/web-worker/client-session/persisted-adapter.ts +117 -59
  49. package/src/web-worker/client-session/sqlite-loader.ts +19 -0
  50. package/src/web-worker/common/persisted-sqlite.ts +225 -107
  51. package/src/web-worker/common/shutdown-channel.ts +10 -3
  52. package/src/web-worker/common/worker-disconnect-channel.ts +10 -3
  53. package/src/web-worker/common/worker-schema.ts +74 -35
  54. package/src/web-worker/leader-worker/make-leader-worker.ts +86 -41
  55. package/src/web-worker/shared-worker/make-shared-worker.ts +96 -75
  56. package/src/web-worker/vite-dev-polyfill.ts +1 -0
  57. package/dist/opfs-utils.d.ts +0 -5
  58. package/dist/opfs-utils.d.ts.map +0 -1
  59. package/dist/opfs-utils.js +0 -43
  60. package/dist/opfs-utils.js.map +0 -1
  61. package/src/opfs-utils.ts +0 -61
@@ -6,29 +6,33 @@ import {
6
6
  makeClientSession,
7
7
  migrateDb,
8
8
  type SyncOptions,
9
- UnexpectedError,
9
+ UnknownError,
10
10
  } from '@livestore/common'
11
11
  import type { DevtoolsOptions, LeaderSqliteDb } from '@livestore/common/leader-thread'
12
- import { configureConnection, Eventlog, LeaderThreadCtx, makeLeaderThreadLayer } from '@livestore/common/leader-thread'
12
+ import {
13
+ configureConnection,
14
+ Eventlog,
15
+ LeaderThreadCtx,
16
+ makeLeaderThreadLayer,
17
+ streamEventsWithSyncState,
18
+ } from '@livestore/common/leader-thread'
13
19
  import type { LiveStoreSchema } from '@livestore/common/schema'
14
20
  import { LiveStoreEvent } from '@livestore/common/schema'
15
21
  import * as DevtoolsWeb from '@livestore/devtools-web-common/web-channel'
16
22
  import type * as WebmeshWorker from '@livestore/devtools-web-common/worker'
17
23
  import type { MakeWebSqliteDb } from '@livestore/sqlite-wasm/browser'
18
24
  import { sqliteDbFactory } from '@livestore/sqlite-wasm/browser'
19
- import { loadSqlite3Wasm } from '@livestore/sqlite-wasm/load-wasm'
20
25
  import { tryAsFunctionAndNew } from '@livestore/utils'
21
- import type { Schema, Scope } from '@livestore/utils/effect'
22
- import { BrowserWorker, Effect, FetchHttpClient, Fiber, Layer, SubscriptionRef, Worker } from '@livestore/utils/effect'
26
+ import type { Scope } from '@livestore/utils/effect'
27
+ import { Effect, FetchHttpClient, Fiber, Layer, type Schema, SubscriptionRef, Worker } from '@livestore/utils/effect'
28
+ import { BrowserWorker } from '@livestore/utils/effect/browser'
23
29
  import { nanoid } from '@livestore/utils/nanoid'
24
30
  import * as Webmesh from '@livestore/webmesh'
25
31
 
26
32
  import { connectWebmeshNodeClientSession } from '../web-worker/client-session/client-session-devtools.ts'
33
+ import { loadSqlite3 } from '../web-worker/client-session/sqlite-loader.ts'
27
34
  import { makeShutdownChannel } from '../web-worker/common/shutdown-channel.ts'
28
35
 
29
- // NOTE we're starting to initialize the sqlite wasm binary here to speed things up
30
- const sqlite3Promise = loadSqlite3Wasm()
31
-
32
36
  export interface InMemoryAdapterOptions {
33
37
  importSnapshot?: Uint8Array<ArrayBuffer>
34
38
  sync?: SyncOptions
@@ -55,12 +59,56 @@ export interface InMemoryAdapterOptions {
55
59
  }
56
60
  }
57
61
 
62
+ /**
63
+ * Creates a web-only in-memory LiveStore adapter.
64
+ *
65
+ * This adapter runs entirely in memory with no persistence. Ideal for:
66
+ * - Unit tests and integration tests
67
+ * - Sandboxes and demos
68
+ * - Ephemeral sessions where persistence isn't needed
69
+ *
70
+ * **Characteristics:**
71
+ * - Fast, zero I/O overhead
72
+ * - Works in all browser contexts: Window, WebWorker, SharedWorker, ServiceWorker
73
+ * - Supports optional sync backends for real-time collaboration
74
+ * - No data persists after page reload
75
+ *
76
+ * For persistent storage, use `makePersistedAdapter` instead.
77
+ *
78
+ * @example
79
+ * ```ts
80
+ * import { makeInMemoryAdapter } from '@livestore/adapter-web'
81
+ *
82
+ * const adapter = makeInMemoryAdapter()
83
+ * ```
84
+ *
85
+ * @example
86
+ * ```ts
87
+ * // With sync backend for real-time collaboration
88
+ * import { makeInMemoryAdapter } from '@livestore/adapter-web'
89
+ * import { makeWsSync } from '@livestore/sync-cf/client'
90
+ *
91
+ * const adapter = makeInMemoryAdapter({
92
+ * sync: {
93
+ * backend: makeWsSync({ url: 'wss://api.example.com/sync' }),
94
+ * },
95
+ * })
96
+ * ```
97
+ *
98
+ * @example
99
+ * ```ts
100
+ * // Pre-populate with existing data
101
+ * const adapter = makeInMemoryAdapter({
102
+ * importSnapshot: existingDbSnapshot,
103
+ * })
104
+ * ```
105
+ */
58
106
  export const makeInMemoryAdapter =
59
107
  (options: InMemoryAdapterOptions = {}): Adapter =>
60
108
  (adapterArgs) =>
61
109
  Effect.gen(function* () {
62
- const { schema, shutdown, syncPayload, storeId, devtoolsEnabled } = adapterArgs
63
- const sqlite3 = yield* Effect.promise(() => sqlite3Promise)
110
+ const { schema, shutdown, syncPayloadEncoded, syncPayloadSchema, storeId, devtoolsEnabled } = adapterArgs
111
+ const sqlite3 = yield* Effect.promise(() => loadSqlite3())
64
112
 
65
113
  const sqliteDb = yield* sqliteDbFactory({ sqlite3 })({ _tag: 'in-memory' })
66
114
 
@@ -80,7 +128,7 @@ export const makeInMemoryAdapter =
80
128
  }).pipe(
81
129
  Effect.provide(BrowserWorker.layer(() => sharedWebWorker)),
82
130
  Effect.tapCauseLogPretty,
83
- UnexpectedError.mapToUnexpectedError,
131
+ UnknownError.mapToUnknownError,
84
132
  Effect.forkScoped,
85
133
  )
86
134
  : undefined
@@ -91,7 +139,8 @@ export const makeInMemoryAdapter =
91
139
  clientId,
92
140
  makeSqliteDb: sqliteDbFactory({ sqlite3 }),
93
141
  syncOptions: options.sync,
94
- syncPayload,
142
+ syncPayloadEncoded,
143
+ syncPayloadSchema,
95
144
  importSnapshot: options.importSnapshot,
96
145
  devtoolsEnabled,
97
146
  sharedWorkerFiber,
@@ -111,6 +160,8 @@ export const makeInMemoryAdapter =
111
160
  lockStatus,
112
161
  shutdown,
113
162
  webmeshMode: 'direct',
163
+ // Can be undefined in Node.js
164
+ origin: globalThis.location?.origin,
114
165
  connectWebmeshNode: ({ sessionInfo, webmeshNode }) =>
115
166
  Effect.gen(function* () {
116
167
  if (sharedWorkerFiber === undefined || devtoolsEnabled === false) {
@@ -132,7 +183,7 @@ export const makeInMemoryAdapter =
132
183
  })
133
184
 
134
185
  return clientSession
135
- }).pipe(UnexpectedError.mapToUnexpectedError, Effect.provide(FetchHttpClient.layer))
186
+ }).pipe(UnknownError.mapToUnknownError, Effect.provide(FetchHttpClient.layer))
136
187
 
137
188
  export interface MakeLeaderThreadArgs {
138
189
  schema: LiveStoreSchema
@@ -140,7 +191,8 @@ export interface MakeLeaderThreadArgs {
140
191
  clientId: string
141
192
  makeSqliteDb: MakeWebSqliteDb
142
193
  syncOptions: SyncOptions | undefined
143
- syncPayload: Schema.JsonValue | undefined
194
+ syncPayloadEncoded: Schema.JsonValue | undefined
195
+ syncPayloadSchema: Schema.Schema<any> | undefined
144
196
  importSnapshot: Uint8Array<ArrayBuffer> | undefined
145
197
  devtoolsEnabled: boolean
146
198
  sharedWorkerFiber: SharedWorkerFiber | undefined
@@ -152,7 +204,8 @@ const makeLeaderThread = ({
152
204
  clientId,
153
205
  makeSqliteDb,
154
206
  syncOptions,
155
- syncPayload,
207
+ syncPayloadEncoded,
208
+ syncPayloadSchema,
156
209
  importSnapshot,
157
210
  devtoolsEnabled,
158
211
  sharedWorkerFiber,
@@ -199,12 +252,14 @@ const makeLeaderThread = ({
199
252
  dbEventlog,
200
253
  devtoolsOptions,
201
254
  shutdownChannel,
202
- syncPayload,
255
+ syncPayloadEncoded,
256
+ syncPayloadSchema,
203
257
  }),
204
258
  )
205
259
 
206
260
  return yield* Effect.gen(function* () {
207
- const { dbState, dbEventlog, syncProcessor, extraIncomingMessagesQueue, initialState } = yield* LeaderThreadCtx
261
+ const { dbState, dbEventlog, syncProcessor, extraIncomingMessagesQueue, initialState, networkStatus } =
262
+ yield* LeaderThreadCtx
208
263
 
209
264
  const initialLeaderHead = Eventlog.getClientHeadFromDb(dbEventlog)
210
265
 
@@ -213,15 +268,22 @@ const makeLeaderThread = ({
213
268
  pull: ({ cursor }) => syncProcessor.pull({ cursor }),
214
269
  push: (batch) =>
215
270
  syncProcessor.push(
216
- batch.map((item) => new LiveStoreEvent.EncodedWithMeta(item)),
271
+ batch.map((item) => new LiveStoreEvent.Client.EncodedWithMeta(item)),
217
272
  { waitForProcessing: true },
218
273
  ),
274
+ stream: (options) =>
275
+ streamEventsWithSyncState({
276
+ dbEventlog,
277
+ syncState: syncProcessor.syncState,
278
+ options,
279
+ }),
219
280
  },
220
281
  initialState: { leaderHead: initialLeaderHead, migrationsReport: initialState.migrationsReport },
221
282
  export: Effect.sync(() => dbState.export()),
222
283
  getEventlogData: Effect.sync(() => dbEventlog.export()),
223
- getSyncState: syncProcessor.syncState,
284
+ syncState: syncProcessor.syncState,
224
285
  sendDevtoolsMessage: (message) => extraIncomingMessagesQueue.offer(message),
286
+ networkStatus,
225
287
  })
226
288
 
227
289
  const initialSnapshot = dbState.export()
@@ -232,7 +294,7 @@ const makeLeaderThread = ({
232
294
 
233
295
  type SharedWorkerFiber = Fiber.Fiber<
234
296
  Worker.SerializedWorkerPool<typeof WebmeshWorker.Schema.Request.Type>,
235
- UnexpectedError
297
+ UnknownError
236
298
  >
237
299
 
238
300
  const makeDevtoolsOptions = ({
@@ -249,7 +311,7 @@ const makeDevtoolsOptions = ({
249
311
  dbEventlog: LeaderSqliteDb
250
312
  storeId: string
251
313
  clientId: string
252
- }): Effect.Effect<DevtoolsOptions, UnexpectedError, Scope.Scope> =>
314
+ }): Effect.Effect<DevtoolsOptions, UnknownError, Scope.Scope> =>
253
315
  Effect.gen(function* () {
254
316
  if (devtoolsEnabled === false || sharedWorkerFiber === undefined) {
255
317
  return { enabled: false }
@@ -1,23 +1,3 @@
1
- // TODO remove OPFS stuff
2
- // https://developer.mozilla.org/en-US/docs/Web/API/FileSystemSyncAccessHandle
3
- interface FileSystemSyncAccessHandle {
4
- close: () => void
5
- flush: () => Promise<void>
6
- getSize: () => number
7
- read: (buffer: ArrayBuffer, options?: FileSystemReadWriteOptions) => number
8
- truncate: (newSize: number) => void
9
- write: (buffer: ArrayBuffer, options?: FileSystemReadWriteOptions) => number
10
- seek: (offset: number) => void
11
- }
12
-
13
- interface FileSystemReadWriteOptions {
14
- at?: number
15
- }
16
-
17
- interface FileSystemFileHandle {
18
- createSyncAccessHandle: () => Promise<FileSystemSyncAccessHandle>
19
- }
20
-
21
1
  // TODO bring back when Vite limitation is resolved https://github.com/vitejs/vite/issues/8427
22
2
  // declare module '*?sharedworker' {
23
3
  // const sharedWorkerConstructor: {
@@ -26,11 +6,14 @@ interface FileSystemFileHandle {
26
6
  // export default sharedWorkerConstructor
27
7
  // }
28
8
 
9
+ declare interface ImportMetaEnv {
10
+ readonly SSR?: string | boolean
11
+ readonly DEV: boolean | undefined
12
+ readonly VITE_LIVESTORE_EXPERIMENTAL_SYNC_NEXT: boolean | undefined
13
+ }
14
+
29
15
  declare interface ImportMeta {
30
- env: {
31
- DEV: boolean | undefined
32
- VITE_LIVESTORE_EXPERIMENTAL_SYNC_NEXT: boolean | undefined
33
- }
16
+ readonly env: ImportMetaEnv
34
17
  }
35
18
 
36
19
  declare var __debugLiveStoreUtils: any
@@ -1,9 +1,10 @@
1
- import { Devtools } from '@livestore/common'
1
+ import { Devtools, liveStoreVersion } from '@livestore/common'
2
2
  import type { LiveStoreSchema } from '@livestore/common/schema'
3
3
  import * as DevtoolsWeb from '@livestore/devtools-web-common/web-channel'
4
4
  import { isDevEnv } from '@livestore/utils'
5
5
  import type { Worker } from '@livestore/utils/effect'
6
- import { Effect, Stream, WebChannel } from '@livestore/utils/effect'
6
+ import { Effect, Stream } from '@livestore/utils/effect'
7
+ import { WebChannelBrowser } from '@livestore/utils/effect/browser'
7
8
  import * as Webmesh from '@livestore/webmesh'
8
9
 
9
10
  export const logDevtoolsUrl = ({
@@ -30,6 +31,20 @@ export const logDevtoolsUrl = ({
30
31
  const url = `${devtoolsBaseUrl}/web/${storeId}/${clientId}/${sessionId}/${schema.devtools.alias}`
31
32
  yield* Effect.log(`[@livestore/adapter-web] Devtools ready on ${url}`)
32
33
  }
34
+
35
+ // Check for DevTools Chrome extension presence via iframe container the extension injects
36
+ const hasExt = document.querySelector('[id^="livestore-devtools-iframe-"]') !== null
37
+ if (!hasExt) {
38
+ const g = globalThis as { __livestoreDevtoolsChromeNoticeShown?: boolean }
39
+ if (g.__livestoreDevtoolsChromeNoticeShown !== true) {
40
+ g.__livestoreDevtoolsChromeNoticeShown = true
41
+
42
+ const urlToLog = `https://github.com/livestorejs/livestore/releases/download/v${liveStoreVersion}/livestore-devtools-chrome-${liveStoreVersion}.zip`
43
+ yield* Effect.log(
44
+ `[@livestore/adapter-web] LiveStore DevTools Chrome extension not detected. Install v${liveStoreVersion}: ${urlToLog}`,
45
+ )
46
+ }
47
+ }
33
48
  }
34
49
  }
35
50
  }).pipe(Effect.withSpan('@livestore/adapter-web:client-session:devtools:logDevtoolsUrl'))
@@ -69,7 +84,7 @@ export const connectWebmeshNodeClientSession = Effect.fn(function* ({
69
84
 
70
85
  const contentscriptMainNodeName = DevtoolsWeb.makeNodeName.browserExtension.contentscriptMain(tabId)
71
86
 
72
- const contentscriptMainChannel = yield* WebChannel.windowChannel({
87
+ const contentscriptMainChannel = yield* WebChannelBrowser.windowChannel({
73
88
  listenWindow: window,
74
89
  sendWindow: window,
75
90
  schema: Webmesh.WebmeshSchema.Packet,
@@ -5,48 +5,46 @@ import {
5
5
  makeClientSession,
6
6
  StoreInterrupted,
7
7
  sessionChangesetMetaTable,
8
- UnexpectedError,
8
+ UnknownError,
9
9
  } from '@livestore/common'
10
10
  // TODO bring back - this currently doesn't work due to https://github.com/vitejs/vite/issues/8427
11
11
  // NOTE We're using a non-relative import here for Vite to properly resolve the import during app builds
12
12
  // import LiveStoreSharedWorker from '@livestore/adapter-web/internal-shared-worker?sharedworker'
13
13
  import { EventSequenceNumber } from '@livestore/common/schema'
14
14
  import { sqliteDbFactory } from '@livestore/sqlite-wasm/browser'
15
- import { loadSqlite3Wasm } from '@livestore/sqlite-wasm/load-wasm'
16
15
  import { isDevEnv, shouldNeverHappen, tryAsFunctionAndNew } from '@livestore/utils'
17
16
  import {
18
- BrowserWorker,
19
17
  Cause,
20
18
  Deferred,
21
19
  Effect,
22
20
  Exit,
23
21
  Fiber,
22
+ Layer,
24
23
  ParseResult,
25
24
  Queue,
26
25
  Schema,
27
26
  Stream,
27
+ Subscribable,
28
28
  SubscriptionRef,
29
- WebLock,
30
29
  Worker,
31
30
  WorkerError,
32
31
  } from '@livestore/utils/effect'
32
+ import { BrowserWorker, Opfs, WebLock } from '@livestore/utils/effect/browser'
33
33
  import { nanoid } from '@livestore/utils/nanoid'
34
-
35
- import * as OpfsUtils from '../../opfs-utils.ts'
36
- import { readPersistedAppDbFromClientSession, resetPersistedDataFromClientSession } from '../common/persisted-sqlite.ts'
34
+ import {
35
+ readPersistedStateDbFromClientSession,
36
+ resetPersistedDataFromClientSession,
37
+ } from '../common/persisted-sqlite.ts'
37
38
  import { makeShutdownChannel } from '../common/shutdown-channel.ts'
38
39
  import { DedicatedWorkerDisconnectBroadcast, makeWorkerDisconnectChannel } from '../common/worker-disconnect-channel.ts'
39
40
  import * as WorkerSchema from '../common/worker-schema.ts'
40
41
  import { connectWebmeshNodeClientSession } from './client-session-devtools.ts'
41
-
42
- // NOTE we're starting to initialize the sqlite wasm binary here to speed things up
43
- const sqlite3Promise = loadSqlite3Wasm()
42
+ import { loadSqlite3 } from './sqlite-loader.ts'
44
43
 
45
44
  if (isDevEnv()) {
46
45
  globalThis.__debugLiveStoreUtils = {
47
- opfs: OpfsUtils,
48
- runSync: (effect: Effect.Effect<any, any, never>) => Effect.runSync(effect),
49
- runFork: (effect: Effect.Effect<any, any, never>) => Effect.runFork(effect),
46
+ ...globalThis.__debugLiveStoreUtils,
47
+ opfs: Opfs.debugUtils,
50
48
  }
51
49
  }
52
50
 
@@ -100,6 +98,21 @@ export type WebAdapterOptions = {
100
98
  * @default false
101
99
  */
102
100
  disableFastPath?: boolean
101
+ /**
102
+ * Controls whether to wait for the shared worker to be terminated when LiveStore gets shut down.
103
+ * This prevents a race condition where a new LiveStore instance connects to a shutting-down shared
104
+ * worker from a previous instance.
105
+ *
106
+ * @default false
107
+ *
108
+ * @remarks
109
+ *
110
+ * In multi-tab scenarios, we don't want to await shared worker termination because the shared worker
111
+ * won't actually shut down when one tab closes - it stays alive to serve other tabs. Awaiting
112
+ * termination would cause unnecessary blocking since the termination will never happen until all
113
+ * tabs are closed.
114
+ */
115
+ awaitSharedWorkerTermination?: boolean
103
116
  }
104
117
  }
105
118
 
@@ -124,15 +137,32 @@ export const makePersistedAdapter =
124
137
  (options: WebAdapterOptions): Adapter =>
125
138
  (adapterArgs) =>
126
139
  Effect.gen(function* () {
127
- const { schema, storeId, devtoolsEnabled, debugInstanceId, bootStatusQueue, shutdown, syncPayload } = adapterArgs
140
+ const {
141
+ schema,
142
+ storeId,
143
+ devtoolsEnabled,
144
+ debugInstanceId,
145
+ bootStatusQueue,
146
+ shutdown,
147
+ syncPayloadSchema: _syncPayloadSchema,
148
+ syncPayloadEncoded,
149
+ } = adapterArgs
150
+
151
+ // NOTE: The schema travels with the worker bundle (developers call
152
+ // `makeWorker({ schema, syncPayloadSchema })`). We only keep the
153
+ // destructured value here to document availability on the client session
154
+ // side—structured cloning the Effect schema into the worker is not
155
+ // possible, so we intentionally do not forward it.
156
+ void _syncPayloadSchema
128
157
 
129
158
  yield* ensureBrowserRequirements
130
159
 
131
160
  yield* Queue.offer(bootStatusQueue, { stage: 'loading' })
132
161
 
133
- const sqlite3 = yield* Effect.promise(() => sqlite3Promise)
162
+ const sqlite3 = yield* Effect.promise(() => loadSqlite3())
134
163
 
135
164
  const LIVESTORE_TAB_LOCK = `livestore-tab-lock-${storeId}`
165
+ const LIVESTORE_SHARED_WORKER_TERMINATION_LOCK = `livestore-shared-worker-termination-lock-${storeId}`
136
166
 
137
167
  const storageOptions = yield* Schema.decode(WorkerSchema.StorageType)(options.storage)
138
168
 
@@ -153,7 +183,15 @@ export const makePersistedAdapter =
153
183
  const dataFromFile =
154
184
  options.experimental?.disableFastPath === true
155
185
  ? undefined
156
- : yield* readPersistedAppDbFromClientSession({ storageOptions, storeId, schema })
186
+ : yield* readPersistedStateDbFromClientSession({ storageOptions, storeId, schema }).pipe(
187
+ Effect.tapError((error) =>
188
+ Effect.logDebug('[@livestore/adapter-web:client-session] Could not read persisted state db', error, {
189
+ storeId,
190
+ }),
191
+ ),
192
+ // If we get any error here, we return `undefined` to fall back to the slow path
193
+ Effect.orElseSucceed(() => undefined),
194
+ )
157
195
 
158
196
  // The same across all client sessions (i.e. tabs, windows)
159
197
  const clientId = options.clientId ?? getPersistedId(`clientId:${storeId}`, 'local')
@@ -175,28 +213,20 @@ export const makePersistedAdapter =
175
213
 
176
214
  const sharedWebWorker = tryAsFunctionAndNew(options.sharedWorker, { name: `livestore-shared-worker-${storeId}` })
177
215
 
216
+ if (options.experimental?.awaitSharedWorkerTermination) {
217
+ // Relying on the lock being available is currently the only mechanism we're aware of
218
+ // to know whether the shared worker has terminated.
219
+ yield* Effect.addFinalizer(() => WebLock.waitForLock(LIVESTORE_SHARED_WORKER_TERMINATION_LOCK))
220
+ }
221
+
222
+ const sharedWorkerContext = yield* Layer.build(BrowserWorker.layer(() => sharedWebWorker))
178
223
  const sharedWorkerFiber = yield* Worker.makePoolSerialized<typeof WorkerSchema.SharedWorkerRequest.Type>({
179
224
  size: 1,
180
225
  concurrency: 100,
181
- initialMessage: () =>
182
- new WorkerSchema.SharedWorkerInitialMessage({
183
- liveStoreVersion,
184
- payload: {
185
- _tag: 'FromClientSession',
186
- initialMessage: new WorkerSchema.LeaderWorkerInnerInitialMessage({
187
- storageOptions,
188
- storeId,
189
- clientId,
190
- devtoolsEnabled,
191
- debugInstanceId,
192
- syncPayload,
193
- }),
194
- },
195
- }),
196
226
  }).pipe(
197
- Effect.provide(BrowserWorker.layer(() => sharedWebWorker)),
227
+ Effect.provide(sharedWorkerContext),
198
228
  Effect.tapCauseLogPretty,
199
- UnexpectedError.mapToUnexpectedError,
229
+ UnknownError.mapToUnknownError,
200
230
  Effect.tapErrorCause((cause) => shutdown(Exit.failCause(cause))),
201
231
  Effect.withSpan('@livestore/adapter-web:client-session:setupSharedWorker'),
202
232
  Effect.forkScoped,
@@ -238,7 +268,7 @@ export const makePersistedAdapter =
238
268
  initialMessage: () => new WorkerSchema.LeaderWorkerOuterInitialMessage({ port: mc.port1, storeId, clientId }),
239
269
  }).pipe(
240
270
  Effect.provide(BrowserWorker.layer(() => worker)),
241
- UnexpectedError.mapToUnexpectedError,
271
+ UnknownError.mapToUnknownError,
242
272
  Effect.tapErrorCause((cause) => shutdown(Exit.failCause(cause))),
243
273
  Effect.withSpan('@livestore/adapter-web:client-session:setupDedicatedWorker'),
244
274
  Effect.tapCauseLogPretty,
@@ -248,10 +278,25 @@ export const makePersistedAdapter =
248
278
  yield* workerDisconnectChannel.send(DedicatedWorkerDisconnectBroadcast.make({}))
249
279
 
250
280
  const sharedWorker = yield* Fiber.join(sharedWorkerFiber)
251
- yield* sharedWorker.executeEffect(new WorkerSchema.SharedWorkerUpdateMessagePort({ port: mc.port2 })).pipe(
252
- UnexpectedError.mapToUnexpectedError,
253
- Effect.tapErrorCause((cause) => shutdown(Exit.failCause(cause))),
254
- )
281
+ yield* sharedWorker
282
+ .executeEffect(
283
+ new WorkerSchema.SharedWorkerUpdateMessagePort({
284
+ port: mc.port2,
285
+ liveStoreVersion,
286
+ initial: new WorkerSchema.LeaderWorkerInnerInitialMessage({
287
+ storageOptions,
288
+ storeId,
289
+ clientId,
290
+ devtoolsEnabled,
291
+ debugInstanceId,
292
+ syncPayloadEncoded,
293
+ }),
294
+ }),
295
+ )
296
+ .pipe(
297
+ UnknownError.mapToUnknownError,
298
+ Effect.tapErrorCause((cause) => shutdown(Exit.failCause(cause))),
299
+ )
255
300
 
256
301
  yield* Deferred.succeed(waitForSharedWorkerInitialized, undefined)
257
302
 
@@ -281,7 +326,7 @@ export const makePersistedAdapter =
281
326
  const runInWorker = <TReq extends typeof WorkerSchema.SharedWorkerRequest.Type>(
282
327
  req: TReq,
283
328
  ): TReq extends Schema.WithResult<infer A, infer _I, infer E, infer _EI, infer R>
284
- ? Effect.Effect<A, UnexpectedError | E, R>
329
+ ? Effect.Effect<A, UnknownError | E, R>
285
330
  : never =>
286
331
  Fiber.join(sharedWorkerFiber).pipe(
287
332
  // NOTE we need to wait for the shared worker to be initialized before we can send requests to it
@@ -296,28 +341,28 @@ export const makePersistedAdapter =
296
341
  }),
297
342
  Effect.withSpan(`@livestore/adapter-web:client-session:runInWorker:${req._tag}`),
298
343
  Effect.mapError((cause) =>
299
- Schema.is(UnexpectedError)(cause)
344
+ Schema.is(UnknownError)(cause)
300
345
  ? cause
301
346
  : ParseResult.isParseError(cause) || Schema.is(WorkerError.WorkerError)(cause)
302
- ? new UnexpectedError({ cause })
347
+ ? new UnknownError({ cause })
303
348
  : cause,
304
349
  ),
305
- Effect.catchAllDefect((cause) => new UnexpectedError({ cause })),
350
+ Effect.catchAllDefect((cause) => new UnknownError({ cause })),
306
351
  ) as any
307
352
 
308
353
  const runInWorkerStream = <TReq extends typeof WorkerSchema.SharedWorkerRequest.Type>(
309
354
  req: TReq,
310
355
  ): TReq extends Schema.WithResult<infer A, infer _I, infer _E, infer _EI, infer R>
311
- ? Stream.Stream<A, UnexpectedError, R>
356
+ ? Stream.Stream<A, UnknownError, R>
312
357
  : never =>
313
358
  Effect.gen(function* () {
314
359
  const sharedWorker = yield* Fiber.join(sharedWorkerFiber)
315
360
  return sharedWorker.execute(req as any).pipe(
316
361
  Stream.mapError((cause) =>
317
- Schema.is(UnexpectedError)(cause)
362
+ Schema.is(UnknownError)(cause)
318
363
  ? cause
319
364
  : ParseResult.isParseError(cause) || Schema.is(WorkerError.WorkerError)(cause)
320
- ? new UnexpectedError({ cause })
365
+ ? new UnknownError({ cause })
321
366
  : cause,
322
367
  ),
323
368
  Stream.withSpan(`@livestore/adapter-web:client-session:runInWorkerStream:${req._tag}`),
@@ -365,7 +410,7 @@ export const makePersistedAdapter =
365
410
  const numberOfTables =
366
411
  sqliteDb.select<{ count: number }>(`select count(*) as count from sqlite_master`)[0]?.count ?? 0
367
412
  if (numberOfTables === 0) {
368
- return yield* UnexpectedError.make({
413
+ return yield* UnknownError.make({
369
414
  cause: `Encountered empty or corrupted database`,
370
415
  payload: { snapshotByteLength: initialResult.snapshot.byteLength, storageOptions: options.storage },
371
416
  })
@@ -384,12 +429,12 @@ export const makePersistedAdapter =
384
429
  )
385
430
 
386
431
  const initialLeaderHead = initialLeaderHeadRes
387
- ? EventSequenceNumber.make({
432
+ ? EventSequenceNumber.Client.Composite.make({
388
433
  global: initialLeaderHeadRes.seqNumGlobal,
389
434
  client: initialLeaderHeadRes.seqNumClient,
390
435
  rebaseGeneration: initialLeaderHeadRes.seqNumRebaseGeneration,
391
436
  })
392
- : EventSequenceNumber.ROOT
437
+ : EventSequenceNumber.Client.ROOT
393
438
 
394
439
  // console.debug('[@livestore/adapter-web:client-session] initialLeaderHead', initialLeaderHead)
395
440
 
@@ -415,7 +460,7 @@ export const makePersistedAdapter =
415
460
  const leaderThread: ClientSession['leaderThread'] = {
416
461
  export: runInWorker(new WorkerSchema.LeaderWorkerInnerExport()).pipe(
417
462
  Effect.timeout(10_000),
418
- UnexpectedError.mapToUnexpectedError,
463
+ UnknownError.mapToUnknownError,
419
464
  Effect.withSpan('@livestore/adapter-web:client-session:export'),
420
465
  ),
421
466
 
@@ -428,26 +473,38 @@ export const makePersistedAdapter =
428
473
  attributes: { batchSize: batch.length },
429
474
  }),
430
475
  ),
476
+ stream: (options) =>
477
+ runInWorkerStream(new WorkerSchema.LeaderWorkerInnerStreamEvents(options)).pipe(
478
+ Stream.withSpan('@livestore/adapter-web:client-session:streamEvents'),
479
+ Stream.orDie,
480
+ ),
431
481
  },
432
482
 
433
483
  initialState: { leaderHead: initialLeaderHead, migrationsReport },
434
484
 
435
485
  getEventlogData: runInWorker(new WorkerSchema.LeaderWorkerInnerExportEventlog()).pipe(
436
486
  Effect.timeout(10_000),
437
- UnexpectedError.mapToUnexpectedError,
487
+ UnknownError.mapToUnknownError,
438
488
  Effect.withSpan('@livestore/adapter-web:client-session:getEventlogData'),
439
489
  ),
440
490
 
441
- getSyncState: runInWorker(new WorkerSchema.LeaderWorkerInnerGetLeaderSyncState()).pipe(
442
- UnexpectedError.mapToUnexpectedError,
443
- Effect.withSpan('@livestore/adapter-web:client-session:getLeaderSyncState'),
444
- ),
491
+ syncState: Subscribable.make({
492
+ get: runInWorker(new WorkerSchema.LeaderWorkerInnerGetLeaderSyncState()).pipe(
493
+ UnknownError.mapToUnknownError,
494
+ Effect.withSpan('@livestore/adapter-web:client-session:getLeaderSyncState'),
495
+ ),
496
+ changes: runInWorkerStream(new WorkerSchema.LeaderWorkerInnerSyncStateStream()).pipe(Stream.orDie),
497
+ }),
445
498
 
446
499
  sendDevtoolsMessage: (message) =>
447
500
  runInWorker(new WorkerSchema.LeaderWorkerInnerExtraDevtoolsMessage({ message })).pipe(
448
- UnexpectedError.mapToUnexpectedError,
501
+ UnknownError.mapToUnknownError,
449
502
  Effect.withSpan('@livestore/adapter-web:client-session:devtoolsMessageForLeader'),
450
503
  ),
504
+ networkStatus: Subscribable.make({
505
+ get: runInWorker(new WorkerSchema.LeaderWorkerInnerGetNetworkStatus()).pipe(Effect.orDie),
506
+ changes: runInWorkerStream(new WorkerSchema.LeaderWorkerInnerNetworkStatusStream()).pipe(Stream.orDie),
507
+ }),
451
508
  }
452
509
 
453
510
  const sharedWorker = yield* Fiber.join(sharedWorkerFiber)
@@ -458,10 +515,11 @@ export const makePersistedAdapter =
458
515
  lockStatus,
459
516
  clientId,
460
517
  sessionId,
461
- // isLeader: gotLocky, // TODO update when leader is changing
462
- isLeader: true,
518
+ isLeader: gotLocky,
463
519
  leaderThread,
464
520
  webmeshMode: 'direct',
521
+ // Can be undefined in Node.js
522
+ origin: globalThis.location?.origin,
465
523
  connectWebmeshNode: ({ sessionInfo, webmeshNode }) =>
466
524
  connectWebmeshNodeClientSession({ webmeshNode, sessionInfo, sharedWorker, devtoolsEnabled, schema }),
467
525
  registerBeforeUnload: (onBeforeUnload) => {
@@ -475,7 +533,7 @@ export const makePersistedAdapter =
475
533
  })
476
534
 
477
535
  return clientSession
478
- }).pipe(UnexpectedError.mapToUnexpectedError)
536
+ }).pipe(Effect.provide(Opfs.Opfs.Default), UnknownError.mapToUnknownError)
479
537
 
480
538
  // NOTE for `local` storage we could also use the eventlog db to store the data
481
539
  const getPersistedId = (key: string, storageType: 'session' | 'local') => {
@@ -511,7 +569,7 @@ const ensureBrowserRequirements = Effect.gen(function* () {
511
569
  const validate = (condition: boolean, label: string) =>
512
570
  Effect.gen(function* () {
513
571
  if (condition) {
514
- return yield* UnexpectedError.make({
572
+ return yield* UnknownError.make({
515
573
  cause: `[@livestore/adapter-web] Browser not supported. The LiveStore web adapter needs '${label}' to work properly`,
516
574
  })
517
575
  }
@@ -0,0 +1,19 @@
1
+ import { loadSqlite3Wasm } from '@livestore/sqlite-wasm/load-wasm'
2
+
3
+ /**
4
+ * Browser sessions benefit from downloading and compiling the wasm binary as soon as
5
+ * possible to hide network and IO latency behind the rest of the boot process. We kick
6
+ * that work off eagerly on the client while still returning the shared promise.
7
+ *
8
+ * The Cloudflare / Workerd runtime has stricter rules: async fetches during module
9
+ * evaluation are blocked, so we defer loading until the worker asks for it.
10
+ */
11
+ const isServerRuntime = String(import.meta.env?.SSR) === 'true' || typeof window === 'undefined'
12
+
13
+ let sqlite3Promise: ReturnType<typeof loadSqlite3Wasm> | undefined
14
+
15
+ if (isServerRuntime === false) {
16
+ sqlite3Promise = loadSqlite3Wasm()
17
+ }
18
+
19
+ export const loadSqlite3 = () => (isServerRuntime ? loadSqlite3Wasm() : (sqlite3Promise ?? loadSqlite3Wasm()))