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

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 +15 -5
  3. package/dist/in-memory/in-memory-adapter.d.ts.map +1 -1
  4. package/dist/in-memory/in-memory-adapter.js +29 -15
  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 +67 -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 +114 -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 +103 -58
  31. package/dist/web-worker/common/worker-schema.d.ts.map +1 -1
  32. package/dist/web-worker/common/worker-schema.js +48 -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 +47 -21
  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 +65 -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 +36 -20
  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 +112 -59
  49. package/src/web-worker/client-session/sqlite-loader.ts +19 -0
  50. package/src/web-worker/common/persisted-sqlite.ts +219 -113
  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 +62 -35
  54. package/src/web-worker/leader-worker/make-leader-worker.ts +58 -33
  55. package/src/web-worker/shared-worker/make-shared-worker.ts +95 -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,7 +6,7 @@ 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
12
  import { configureConnection, Eventlog, LeaderThreadCtx, makeLeaderThreadLayer } from '@livestore/common/leader-thread'
@@ -16,19 +16,17 @@ import * as DevtoolsWeb from '@livestore/devtools-web-common/web-channel'
16
16
  import type * as WebmeshWorker from '@livestore/devtools-web-common/worker'
17
17
  import type { MakeWebSqliteDb } from '@livestore/sqlite-wasm/browser'
18
18
  import { sqliteDbFactory } from '@livestore/sqlite-wasm/browser'
19
- import { loadSqlite3Wasm } from '@livestore/sqlite-wasm/load-wasm'
20
19
  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'
20
+ import type { Scope } from '@livestore/utils/effect'
21
+ import { Effect, FetchHttpClient, Fiber, Layer, type Schema, SubscriptionRef, Worker } from '@livestore/utils/effect'
22
+ import { BrowserWorker } from '@livestore/utils/effect/browser'
23
23
  import { nanoid } from '@livestore/utils/nanoid'
24
24
  import * as Webmesh from '@livestore/webmesh'
25
25
 
26
26
  import { connectWebmeshNodeClientSession } from '../web-worker/client-session/client-session-devtools.ts'
27
+ import { loadSqlite3 } from '../web-worker/client-session/sqlite-loader.ts'
27
28
  import { makeShutdownChannel } from '../web-worker/common/shutdown-channel.ts'
28
29
 
29
- // NOTE we're starting to initialize the sqlite wasm binary here to speed things up
30
- const sqlite3Promise = loadSqlite3Wasm()
31
-
32
30
  export interface InMemoryAdapterOptions {
33
31
  importSnapshot?: Uint8Array<ArrayBuffer>
34
32
  sync?: SyncOptions
@@ -55,12 +53,22 @@ export interface InMemoryAdapterOptions {
55
53
  }
56
54
  }
57
55
 
56
+ /**
57
+ * Create a web-only in-memory LiveStore adapter.
58
+ *
59
+ * - Runs entirely in memory: fast, zero I/O, great for tests, sandboxes, or ephemeral sessions.
60
+ * - Works across browser execution contexts: Window, WebWorker, SharedWorker, and ServiceWorker.
61
+ * - DevTools: to inspect this adapter from the browser DevTools, provide a `sharedWorker` in `options.devtools`.
62
+ * (The shared worker is used to bridge the DevTools UI to the running session.)
63
+ * - No persistence support: nothing is written to OPFS/IndexedDB/localStorage. `importSnapshot`
64
+ * can bootstrap initial state only; subsequent changes are not persisted anywhere.
65
+ */
58
66
  export const makeInMemoryAdapter =
59
67
  (options: InMemoryAdapterOptions = {}): Adapter =>
60
68
  (adapterArgs) =>
61
69
  Effect.gen(function* () {
62
- const { schema, shutdown, syncPayload, storeId, devtoolsEnabled } = adapterArgs
63
- const sqlite3 = yield* Effect.promise(() => sqlite3Promise)
70
+ const { schema, shutdown, syncPayloadEncoded, syncPayloadSchema, storeId, devtoolsEnabled } = adapterArgs
71
+ const sqlite3 = yield* Effect.promise(() => loadSqlite3())
64
72
 
65
73
  const sqliteDb = yield* sqliteDbFactory({ sqlite3 })({ _tag: 'in-memory' })
66
74
 
@@ -80,7 +88,7 @@ export const makeInMemoryAdapter =
80
88
  }).pipe(
81
89
  Effect.provide(BrowserWorker.layer(() => sharedWebWorker)),
82
90
  Effect.tapCauseLogPretty,
83
- UnexpectedError.mapToUnexpectedError,
91
+ UnknownError.mapToUnknownError,
84
92
  Effect.forkScoped,
85
93
  )
86
94
  : undefined
@@ -91,7 +99,8 @@ export const makeInMemoryAdapter =
91
99
  clientId,
92
100
  makeSqliteDb: sqliteDbFactory({ sqlite3 }),
93
101
  syncOptions: options.sync,
94
- syncPayload,
102
+ syncPayloadEncoded,
103
+ syncPayloadSchema,
95
104
  importSnapshot: options.importSnapshot,
96
105
  devtoolsEnabled,
97
106
  sharedWorkerFiber,
@@ -111,6 +120,8 @@ export const makeInMemoryAdapter =
111
120
  lockStatus,
112
121
  shutdown,
113
122
  webmeshMode: 'direct',
123
+ // Can be undefined in Node.js
124
+ origin: globalThis.location?.origin,
114
125
  connectWebmeshNode: ({ sessionInfo, webmeshNode }) =>
115
126
  Effect.gen(function* () {
116
127
  if (sharedWorkerFiber === undefined || devtoolsEnabled === false) {
@@ -132,7 +143,7 @@ export const makeInMemoryAdapter =
132
143
  })
133
144
 
134
145
  return clientSession
135
- }).pipe(UnexpectedError.mapToUnexpectedError, Effect.provide(FetchHttpClient.layer))
146
+ }).pipe(UnknownError.mapToUnknownError, Effect.provide(FetchHttpClient.layer))
136
147
 
137
148
  export interface MakeLeaderThreadArgs {
138
149
  schema: LiveStoreSchema
@@ -140,7 +151,8 @@ export interface MakeLeaderThreadArgs {
140
151
  clientId: string
141
152
  makeSqliteDb: MakeWebSqliteDb
142
153
  syncOptions: SyncOptions | undefined
143
- syncPayload: Schema.JsonValue | undefined
154
+ syncPayloadEncoded: Schema.JsonValue | undefined
155
+ syncPayloadSchema: Schema.Schema<any> | undefined
144
156
  importSnapshot: Uint8Array<ArrayBuffer> | undefined
145
157
  devtoolsEnabled: boolean
146
158
  sharedWorkerFiber: SharedWorkerFiber | undefined
@@ -152,7 +164,8 @@ const makeLeaderThread = ({
152
164
  clientId,
153
165
  makeSqliteDb,
154
166
  syncOptions,
155
- syncPayload,
167
+ syncPayloadEncoded,
168
+ syncPayloadSchema,
156
169
  importSnapshot,
157
170
  devtoolsEnabled,
158
171
  sharedWorkerFiber,
@@ -199,12 +212,14 @@ const makeLeaderThread = ({
199
212
  dbEventlog,
200
213
  devtoolsOptions,
201
214
  shutdownChannel,
202
- syncPayload,
215
+ syncPayloadEncoded,
216
+ syncPayloadSchema,
203
217
  }),
204
218
  )
205
219
 
206
220
  return yield* Effect.gen(function* () {
207
- const { dbState, dbEventlog, syncProcessor, extraIncomingMessagesQueue, initialState } = yield* LeaderThreadCtx
221
+ const { dbState, dbEventlog, syncProcessor, extraIncomingMessagesQueue, initialState, networkStatus } =
222
+ yield* LeaderThreadCtx
208
223
 
209
224
  const initialLeaderHead = Eventlog.getClientHeadFromDb(dbEventlog)
210
225
 
@@ -213,15 +228,16 @@ const makeLeaderThread = ({
213
228
  pull: ({ cursor }) => syncProcessor.pull({ cursor }),
214
229
  push: (batch) =>
215
230
  syncProcessor.push(
216
- batch.map((item) => new LiveStoreEvent.EncodedWithMeta(item)),
231
+ batch.map((item) => new LiveStoreEvent.Client.EncodedWithMeta(item)),
217
232
  { waitForProcessing: true },
218
233
  ),
219
234
  },
220
235
  initialState: { leaderHead: initialLeaderHead, migrationsReport: initialState.migrationsReport },
221
236
  export: Effect.sync(() => dbState.export()),
222
237
  getEventlogData: Effect.sync(() => dbEventlog.export()),
223
- getSyncState: syncProcessor.syncState,
238
+ syncState: syncProcessor.syncState,
224
239
  sendDevtoolsMessage: (message) => extraIncomingMessagesQueue.offer(message),
240
+ networkStatus,
225
241
  })
226
242
 
227
243
  const initialSnapshot = dbState.export()
@@ -232,7 +248,7 @@ const makeLeaderThread = ({
232
248
 
233
249
  type SharedWorkerFiber = Fiber.Fiber<
234
250
  Worker.SerializedWorkerPool<typeof WebmeshWorker.Schema.Request.Type>,
235
- UnexpectedError
251
+ UnknownError
236
252
  >
237
253
 
238
254
  const makeDevtoolsOptions = ({
@@ -249,7 +265,7 @@ const makeDevtoolsOptions = ({
249
265
  dbEventlog: LeaderSqliteDb
250
266
  storeId: string
251
267
  clientId: string
252
- }): Effect.Effect<DevtoolsOptions, UnexpectedError, Scope.Scope> =>
268
+ }): Effect.Effect<DevtoolsOptions, UnknownError, Scope.Scope> =>
253
269
  Effect.gen(function* () {
254
270
  if (devtoolsEnabled === false || sharedWorkerFiber === undefined) {
255
271
  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
 
@@ -434,20 +479,27 @@ export const makePersistedAdapter =
434
479
 
435
480
  getEventlogData: runInWorker(new WorkerSchema.LeaderWorkerInnerExportEventlog()).pipe(
436
481
  Effect.timeout(10_000),
437
- UnexpectedError.mapToUnexpectedError,
482
+ UnknownError.mapToUnknownError,
438
483
  Effect.withSpan('@livestore/adapter-web:client-session:getEventlogData'),
439
484
  ),
440
485
 
441
- getSyncState: runInWorker(new WorkerSchema.LeaderWorkerInnerGetLeaderSyncState()).pipe(
442
- UnexpectedError.mapToUnexpectedError,
443
- Effect.withSpan('@livestore/adapter-web:client-session:getLeaderSyncState'),
444
- ),
486
+ syncState: Subscribable.make({
487
+ get: runInWorker(new WorkerSchema.LeaderWorkerInnerGetLeaderSyncState()).pipe(
488
+ UnknownError.mapToUnknownError,
489
+ Effect.withSpan('@livestore/adapter-web:client-session:getLeaderSyncState'),
490
+ ),
491
+ changes: runInWorkerStream(new WorkerSchema.LeaderWorkerInnerSyncStateStream()).pipe(Stream.orDie),
492
+ }),
445
493
 
446
494
  sendDevtoolsMessage: (message) =>
447
495
  runInWorker(new WorkerSchema.LeaderWorkerInnerExtraDevtoolsMessage({ message })).pipe(
448
- UnexpectedError.mapToUnexpectedError,
496
+ UnknownError.mapToUnknownError,
449
497
  Effect.withSpan('@livestore/adapter-web:client-session:devtoolsMessageForLeader'),
450
498
  ),
499
+ networkStatus: Subscribable.make({
500
+ get: runInWorker(new WorkerSchema.LeaderWorkerInnerGetNetworkStatus()).pipe(Effect.orDie),
501
+ changes: runInWorkerStream(new WorkerSchema.LeaderWorkerInnerNetworkStatusStream()).pipe(Stream.orDie),
502
+ }),
451
503
  }
452
504
 
453
505
  const sharedWorker = yield* Fiber.join(sharedWorkerFiber)
@@ -458,10 +510,11 @@ export const makePersistedAdapter =
458
510
  lockStatus,
459
511
  clientId,
460
512
  sessionId,
461
- // isLeader: gotLocky, // TODO update when leader is changing
462
- isLeader: true,
513
+ isLeader: gotLocky,
463
514
  leaderThread,
464
515
  webmeshMode: 'direct',
516
+ // Can be undefined in Node.js
517
+ origin: globalThis.location?.origin,
465
518
  connectWebmeshNode: ({ sessionInfo, webmeshNode }) =>
466
519
  connectWebmeshNodeClientSession({ webmeshNode, sessionInfo, sharedWorker, devtoolsEnabled, schema }),
467
520
  registerBeforeUnload: (onBeforeUnload) => {
@@ -475,7 +528,7 @@ export const makePersistedAdapter =
475
528
  })
476
529
 
477
530
  return clientSession
478
- }).pipe(UnexpectedError.mapToUnexpectedError)
531
+ }).pipe(Effect.provide(Opfs.Opfs.Default), UnknownError.mapToUnknownError)
479
532
 
480
533
  // NOTE for `local` storage we could also use the eventlog db to store the data
481
534
  const getPersistedId = (key: string, storageType: 'session' | 'local') => {
@@ -511,7 +564,7 @@ const ensureBrowserRequirements = Effect.gen(function* () {
511
564
  const validate = (condition: boolean, label: string) =>
512
565
  Effect.gen(function* () {
513
566
  if (condition) {
514
- return yield* UnexpectedError.make({
567
+ return yield* UnknownError.make({
515
568
  cause: `[@livestore/adapter-web] Browser not supported. The LiveStore web adapter needs '${label}' to work properly`,
516
569
  })
517
570
  }
@@ -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()))