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

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 (46) hide show
  1. package/dist/.tsbuildinfo +1 -1
  2. package/dist/in-memory/in-memory-adapter.d.ts +42 -8
  3. package/dist/in-memory/in-memory-adapter.d.ts.map +1 -1
  4. package/dist/in-memory/in-memory-adapter.js +52 -9
  5. package/dist/in-memory/in-memory-adapter.js.map +1 -1
  6. package/dist/index.d.ts +11 -1
  7. package/dist/index.d.ts.map +1 -1
  8. package/dist/index.js +11 -1
  9. package/dist/index.js.map +1 -1
  10. package/dist/single-tab/mod.d.ts +15 -0
  11. package/dist/single-tab/mod.d.ts.map +1 -0
  12. package/dist/single-tab/mod.js +15 -0
  13. package/dist/single-tab/mod.js.map +1 -0
  14. package/dist/single-tab/single-tab-adapter.d.ts +108 -0
  15. package/dist/single-tab/single-tab-adapter.d.ts.map +1 -0
  16. package/dist/single-tab/single-tab-adapter.js +279 -0
  17. package/dist/single-tab/single-tab-adapter.js.map +1 -0
  18. package/dist/web-worker/client-session/persisted-adapter.d.ts +18 -0
  19. package/dist/web-worker/client-session/persisted-adapter.d.ts.map +1 -1
  20. package/dist/web-worker/client-session/persisted-adapter.js +73 -5
  21. package/dist/web-worker/client-session/persisted-adapter.js.map +1 -1
  22. package/dist/web-worker/common/persisted-sqlite.d.ts +1 -1
  23. package/dist/web-worker/common/persisted-sqlite.d.ts.map +1 -1
  24. package/dist/web-worker/common/persisted-sqlite.js +13 -2
  25. package/dist/web-worker/common/persisted-sqlite.js.map +1 -1
  26. package/dist/web-worker/common/worker-schema.d.ts +56 -6
  27. package/dist/web-worker/common/worker-schema.d.ts.map +1 -1
  28. package/dist/web-worker/common/worker-schema.js +9 -2
  29. package/dist/web-worker/common/worker-schema.js.map +1 -1
  30. package/dist/web-worker/leader-worker/make-leader-worker.d.ts +1 -1
  31. package/dist/web-worker/leader-worker/make-leader-worker.d.ts.map +1 -1
  32. package/dist/web-worker/leader-worker/make-leader-worker.js +59 -15
  33. package/dist/web-worker/leader-worker/make-leader-worker.js.map +1 -1
  34. package/dist/web-worker/shared-worker/make-shared-worker.d.ts.map +1 -1
  35. package/dist/web-worker/shared-worker/make-shared-worker.js +1 -0
  36. package/dist/web-worker/shared-worker/make-shared-worker.js.map +1 -1
  37. package/package.json +6 -6
  38. package/src/in-memory/in-memory-adapter.ts +59 -9
  39. package/src/index.ts +15 -1
  40. package/src/single-tab/mod.ts +15 -0
  41. package/src/single-tab/single-tab-adapter.ts +517 -0
  42. package/src/web-worker/client-session/persisted-adapter.ts +92 -6
  43. package/src/web-worker/common/persisted-sqlite.ts +15 -3
  44. package/src/web-worker/common/worker-schema.ts +12 -0
  45. package/src/web-worker/leader-worker/make-leader-worker.ts +87 -18
  46. package/src/web-worker/shared-worker/make-shared-worker.ts +1 -0
@@ -1,4 +1,4 @@
1
- import type { Adapter, ClientSession, LockStatus } from '@livestore/common'
1
+ import type { Adapter, BootWarningReason, ClientSession, LockStatus } from '@livestore/common'
2
2
  import {
3
3
  IntentionalShutdownCause,
4
4
  liveStoreVersion,
@@ -12,7 +12,7 @@ import {
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 { isDevEnv, shouldNeverHappen, tryAsFunctionAndNew } from '@livestore/utils'
15
+ import { isDevEnv, omitUndefineds, shouldNeverHappen, tryAsFunctionAndNew } from '@livestore/utils'
16
16
  import {
17
17
  Cause,
18
18
  Deferred,
@@ -29,8 +29,9 @@ import {
29
29
  Worker,
30
30
  WorkerError,
31
31
  } from '@livestore/utils/effect'
32
- import { BrowserWorker, Opfs, WebLock } from '@livestore/utils/effect/browser'
32
+ import { BrowserWorker, Opfs, WebError, WebLock } from '@livestore/utils/effect/browser'
33
33
  import { nanoid } from '@livestore/utils/nanoid'
34
+ import { makeSingleTabAdapter } from '../../single-tab/single-tab-adapter.ts'
34
35
  import {
35
36
  readPersistedStateDbFromClientSession,
36
37
  resetPersistedDataFromClientSession,
@@ -41,6 +42,16 @@ import * as WorkerSchema from '../common/worker-schema.ts'
41
42
  import { connectWebmeshNodeClientSession } from './client-session-devtools.ts'
42
43
  import { loadSqlite3 } from './sqlite-loader.ts'
43
44
 
45
+ /**
46
+ * Checks if SharedWorker API is available in the current browser context.
47
+ *
48
+ * Returns false on Android Chrome and other browsers without SharedWorker support.
49
+ *
50
+ * @see https://github.com/livestorejs/livestore/issues/321
51
+ * @see https://issues.chromium.org/issues/40290702
52
+ */
53
+ export const canUseSharedWorker = (): boolean => typeof SharedWorker !== 'undefined'
54
+
44
55
  if (isDevEnv()) {
45
56
  globalThis.__debugLiveStoreUtils = {
46
57
  ...globalThis.__debugLiveStoreUtils,
@@ -120,6 +131,15 @@ export type WebAdapterOptions = {
120
131
  * Creates a web adapter with persistent storage (currently only supports OPFS).
121
132
  * Requires both a web worker and a shared worker.
122
133
  *
134
+ * On browsers without SharedWorker support (e.g. Android Chrome), this adapter
135
+ * automatically falls back to single-tab mode. In single-tab mode:
136
+ * - Each tab runs independently with its own leader worker
137
+ * - Multi-tab synchronization is not available
138
+ * - Devtools are not supported
139
+ *
140
+ * @see https://github.com/livestorejs/livestore/issues/321 - SharedWorker tracking issue
141
+ * @see https://issues.chromium.org/issues/40290702 - Chromium SharedWorker bug
142
+ *
123
143
  * @example
124
144
  * ```ts
125
145
  * import { makePersistedAdapter } from '@livestore/adapter-web'
@@ -137,6 +157,26 @@ export const makePersistedAdapter =
137
157
  (options: WebAdapterOptions): Adapter =>
138
158
  (adapterArgs) =>
139
159
  Effect.gen(function* () {
160
+ // Check SharedWorker availability first and fall back to single-tab mode if unavailable
161
+ if (!canUseSharedWorker()) {
162
+ yield* Effect.logWarning(
163
+ '[@livestore/adapter-web] SharedWorker unavailable (e.g. Android Chrome). ' +
164
+ 'Falling back to single-tab mode. Multi-tab synchronization and devtools are disabled. ' +
165
+ 'See: https://github.com/livestorejs/livestore/issues/321',
166
+ )
167
+
168
+ return yield* makeSingleTabAdapter({
169
+ worker: options.worker,
170
+ storage: options.storage,
171
+ ...omitUndefineds({
172
+ resetPersistence: options.resetPersistence,
173
+ clientId: options.clientId,
174
+ sessionId: options.sessionId,
175
+ experimental: options.experimental,
176
+ }),
177
+ })(adapterArgs)
178
+ }
179
+
140
180
  const {
141
181
  schema,
142
182
  storeId,
@@ -168,10 +208,21 @@ export const makePersistedAdapter =
168
208
 
169
209
  const shutdownChannel = yield* makeShutdownChannel(storeId)
170
210
 
171
- if (options.resetPersistence === true) {
211
+ // Check OPFS availability early and notify user if storage is unavailable (e.g. private browsing)
212
+ const opfsWarning = yield* checkOpfsAvailability
213
+ if (opfsWarning !== undefined) {
214
+ yield* Effect.logWarning('[@livestore/adapter-web:client-session] OPFS unavailable', opfsWarning)
215
+ }
216
+
217
+ if (options.resetPersistence === true && opfsWarning === undefined) {
172
218
  yield* shutdownChannel.send(IntentionalShutdownCause.make({ reason: 'adapter-reset' }))
173
219
 
174
220
  yield* resetPersistedDataFromClientSession({ storageOptions, storeId })
221
+ } else if (options.resetPersistence === true) {
222
+ yield* Effect.logWarning(
223
+ '[@livestore/adapter-web:client-session] Skipping persistence reset because storage is unavailable',
224
+ opfsWarning,
225
+ )
175
226
  }
176
227
 
177
228
  // Note on fast-path booting:
@@ -181,7 +232,7 @@ export const makePersistedAdapter =
181
232
  // We need to be extra careful though to not run into any race conditions or inconsistencies.
182
233
  // TODO also verify persisted data
183
234
  const dataFromFile =
184
- options.experimental?.disableFastPath === true
235
+ options.experimental?.disableFastPath === true || opfsWarning !== undefined
185
236
  ? undefined
186
237
  : yield* readPersistedStateDbFromClientSession({ storageOptions, storeId, schema }).pipe(
187
238
  Effect.tapError((error) =>
@@ -473,9 +524,18 @@ export const makePersistedAdapter =
473
524
  attributes: { batchSize: batch.length },
474
525
  }),
475
526
  ),
527
+ stream: (options) =>
528
+ runInWorkerStream(new WorkerSchema.LeaderWorkerInnerStreamEvents(options)).pipe(
529
+ Stream.withSpan('@livestore/adapter-web:client-session:streamEvents'),
530
+ Stream.orDie,
531
+ ),
476
532
  },
477
533
 
478
- initialState: { leaderHead: initialLeaderHead, migrationsReport },
534
+ initialState: {
535
+ leaderHead: initialLeaderHead,
536
+ migrationsReport,
537
+ storageMode: opfsWarning === undefined ? 'persisted' : 'in-memory',
538
+ },
479
539
 
480
540
  getEventlogData: runInWorker(new WorkerSchema.LeaderWorkerInnerExportEventlog()).pipe(
481
541
  Effect.timeout(10_000),
@@ -579,3 +639,29 @@ const ensureBrowserRequirements = Effect.gen(function* () {
579
639
  validate(typeof sessionStorage === 'undefined', 'sessionStorage'),
580
640
  ])
581
641
  })
642
+
643
+ /**
644
+ * Attempts to access OPFS and returns a warning if unavailable.
645
+ *
646
+ * Common failure scenarios:
647
+ * - Safari/Firefox private browsing: SecurityError or NotAllowedError
648
+ * - Permission denied: NotAllowedError
649
+ * - Quota exceeded: QuotaExceededError
650
+ */
651
+ const checkOpfsAvailability = Effect.gen(function* () {
652
+ const opfs = yield* Opfs.Opfs
653
+ return yield* opfs.getRootDirectoryHandle.pipe(
654
+ Effect.as(undefined),
655
+ Effect.catchAll((error) => {
656
+ const reason: BootWarningReason =
657
+ Schema.is(WebError.SecurityError)(error) || Schema.is(WebError.NotAllowedError)(error)
658
+ ? 'private-browsing'
659
+ : 'storage-unavailable'
660
+ const message =
661
+ reason === 'private-browsing'
662
+ ? 'Storage unavailable in private browsing mode. LiveStore will continue without persistence.'
663
+ : 'Storage access denied. LiveStore will continue without persistence.'
664
+ return Effect.succeed({ reason, message } as const)
665
+ }),
666
+ )
667
+ })
@@ -88,7 +88,7 @@ export const resetPersistedDataFromClientSession = Effect.fn(
88
88
  )(
89
89
  function* ({ storageOptions, storeId }: { storageOptions: WorkerSchema.StorageType; storeId: string }) {
90
90
  const directory = yield* sanitizeOpfsDir(storageOptions.directory, storeId)
91
- yield* Opfs.remove(directory).pipe(
91
+ yield* Opfs.remove(directory, { recursive: true }).pipe(
92
92
  // We ignore NotFoundError here as it may not exist or have already been deleted
93
93
  Effect.catchTag('@livestore/utils/Web/NotFoundError', () => Effect.void),
94
94
  )
@@ -192,8 +192,20 @@ export const cleanupOldStateDbFiles: (options: {
192
192
  const archiveFileData = yield* vfs.readFilePayload(fileName)
193
193
 
194
194
  const archiveFileName = `${Date.now()}-${fileName}`
195
-
196
- yield* Opfs.writeFile(`${opfsDirectory}/archive/${archiveFileName}`, new Uint8Array(archiveFileData))
195
+ const archivePath = `${opfsDirectory}/archive/${archiveFileName}`
196
+ const archiveData = new Uint8Array(archiveFileData)
197
+
198
+ // Prefer writeFile (atomic) when createWritable is available (Chrome, Firefox, Safari 26+),
199
+ // fall back to syncWriteFile (non-atomic) for Safari 18.x compatibility.
200
+ // TODO: Remove feature detection and use writeFile directly when Safari >= 26 is widely available.
201
+ const supportsCreateWritable =
202
+ typeof FileSystemFileHandle !== 'undefined' && 'createWritable' in FileSystemFileHandle.prototype
203
+
204
+ if (supportsCreateWritable) {
205
+ yield* Opfs.writeFile(archivePath, archiveData)
206
+ } else {
207
+ yield* Opfs.syncWriteFile(archivePath, archiveData)
208
+ }
197
209
  }
198
210
 
199
211
  const vfsResultCode = yield* Effect.try({
@@ -8,6 +8,7 @@ import {
8
8
  SyncState,
9
9
  UnknownError,
10
10
  } from '@livestore/common'
11
+ import { StreamEventsOptionsFields } from '@livestore/common/leader-thread'
11
12
  import { EventSequenceNumber, LiveStoreEvent } from '@livestore/common/schema'
12
13
  import * as WebmeshWorker from '@livestore/devtools-web-common/worker'
13
14
  import { Schema, Transferable } from '@livestore/utils/effect'
@@ -102,6 +103,15 @@ export class LeaderWorkerInnerPullStream extends Schema.TaggedRequest<LeaderWork
102
103
  failure: UnknownError,
103
104
  }) {}
104
105
 
106
+ export class LeaderWorkerInnerStreamEvents extends Schema.TaggedRequest<LeaderWorkerInnerStreamEvents>()(
107
+ 'StreamEvents',
108
+ {
109
+ payload: StreamEventsOptionsFields,
110
+ success: LiveStoreEvent.Client.Encoded,
111
+ failure: UnknownError,
112
+ },
113
+ ) {}
114
+
105
115
  export class LeaderWorkerInnerExport extends Schema.TaggedRequest<LeaderWorkerInnerExport>()('Export', {
106
116
  payload: {},
107
117
  success: Transferable.Uint8Array as Schema.Schema<Uint8Array<ArrayBuffer>>,
@@ -196,6 +206,7 @@ export const LeaderWorkerInnerRequest = Schema.Union(
196
206
  LeaderWorkerInnerBootStatusStream,
197
207
  LeaderWorkerInnerPushToLeader,
198
208
  LeaderWorkerInnerPullStream,
209
+ LeaderWorkerInnerStreamEvents,
199
210
  LeaderWorkerInnerExport,
200
211
  LeaderWorkerInnerExportEventlog,
201
212
  LeaderWorkerInnerGetRecreateSnapshot,
@@ -237,6 +248,7 @@ export class SharedWorkerRequest extends Schema.Union(
237
248
  LeaderWorkerInnerBootStatusStream,
238
249
  LeaderWorkerInnerPushToLeader,
239
250
  LeaderWorkerInnerPullStream,
251
+ LeaderWorkerInnerStreamEvents,
240
252
  LeaderWorkerInnerExport,
241
253
  LeaderWorkerInnerGetRecreateSnapshot,
242
254
  LeaderWorkerInnerExportEventlog,
@@ -1,7 +1,13 @@
1
- import type { SqliteDb, SyncOptions } from '@livestore/common'
1
+ import type { BootStatus, BootWarningReason, SqliteDb, SyncOptions } from '@livestore/common'
2
2
  import { Devtools, LogConfig, UnknownError } from '@livestore/common'
3
- import type { DevtoolsOptions } from '@livestore/common/leader-thread'
4
- import { configureConnection, Eventlog, LeaderThreadCtx, makeLeaderThreadLayer } from '@livestore/common/leader-thread'
3
+ import type { DevtoolsOptions, StreamEventsOptions } from '@livestore/common/leader-thread'
4
+ import {
5
+ configureConnection,
6
+ Eventlog,
7
+ LeaderThreadCtx,
8
+ makeLeaderThreadLayer,
9
+ streamEventsWithSyncState,
10
+ } from '@livestore/common/leader-thread'
5
11
  import type { LiveStoreSchema } from '@livestore/common/schema'
6
12
  import { LiveStoreEvent } from '@livestore/common/schema'
7
13
  import * as WebmeshWorker from '@livestore/devtools-web-common/worker'
@@ -16,12 +22,12 @@ import {
16
22
  Layer,
17
23
  OtelTracer,
18
24
  Scheduler,
19
- type Schema,
25
+ Schema,
20
26
  Stream,
21
27
  TaskTracing,
22
28
  WorkerRunner,
23
29
  } from '@livestore/utils/effect'
24
- import { BrowserWorkerRunner, Opfs } from '@livestore/utils/effect/browser'
30
+ import { BrowserWorkerRunner, Opfs, WebError } from '@livestore/utils/effect/browser'
25
31
  import type * as otel from '@opentelemetry/api'
26
32
 
27
33
  import { cleanupOldStateDbFiles, getStateDbFileName, sanitizeOpfsDir } from '../common/persisted-sqlite.ts'
@@ -112,13 +118,28 @@ const makeWorkerRunnerInner = ({ schema, sync: syncOptions, syncPayloadSchema }:
112
118
  Effect.gen(function* () {
113
119
  const sqlite3 = yield* Effect.promise(() => loadSqlite3Wasm())
114
120
  const makeSqliteDb = sqliteDbFactory({ sqlite3 })
115
- const opfsDirectory = yield* sanitizeOpfsDir(storageOptions.directory, storeId)
116
121
  const runtime = yield* Effect.runtime<never>()
117
122
 
118
- const makeDb = (kind: 'state' | 'eventlog') =>
123
+ // Check OPFS availability and determine storage mode
124
+ const opfsCheck = yield* checkOpfsAvailability
125
+ const useOpfs = opfsCheck === undefined
126
+
127
+ // Track boot warning to emit later
128
+ let bootWarning: BootStatus | undefined
129
+ if (!useOpfs) {
130
+ yield* Effect.logWarning(
131
+ '[@livestore/adapter-web:worker] OPFS unavailable, using in-memory storage',
132
+ opfsCheck,
133
+ )
134
+ bootWarning = { stage: 'warning', ...opfsCheck }
135
+ }
136
+
137
+ const opfsDirectory = useOpfs ? yield* sanitizeOpfsDir(storageOptions.directory, storeId) : undefined
138
+
139
+ const makeOpfsDb = (kind: 'state' | 'eventlog') =>
119
140
  makeSqliteDb({
120
141
  _tag: 'opfs',
121
- opfsDirectory,
142
+ opfsDirectory: opfsDirectory!,
122
143
  fileName: kind === 'state' ? getStateDbFileName(schema) : 'eventlog.db',
123
144
  configureDb: (db) =>
124
145
  configureConnection(db, {
@@ -131,10 +152,17 @@ const makeWorkerRunnerInner = ({ schema, sync: syncOptions, syncPayloadSchema }:
131
152
  }).pipe(Effect.provide(runtime), Effect.runSync),
132
153
  }).pipe(Effect.acquireRelease((db) => Effect.try(() => db.close()).pipe(Effect.ignoreLogged)))
133
154
 
134
- // Might involve some async work, so we're running them concurrently
135
- const [dbState, dbEventlog] = yield* Effect.all([makeDb('state'), makeDb('eventlog')], {
136
- concurrency: 2,
137
- })
155
+ const makeInMemoryDb = () =>
156
+ makeSqliteDb({
157
+ _tag: 'in-memory',
158
+ configureDb: (db) =>
159
+ configureConnection(db, { foreignKeys: true }).pipe(Effect.provide(runtime), Effect.runSync),
160
+ }).pipe(Effect.acquireRelease((db) => Effect.try(() => db.close()).pipe(Effect.ignoreLogged)))
161
+
162
+ // Use OPFS if available, otherwise fall back to in-memory
163
+ const [dbState, dbEventlog] = useOpfs
164
+ ? yield* Effect.all([makeOpfsDb('state'), makeOpfsDb('eventlog')], { concurrency: 2 })
165
+ : yield* Effect.all([makeInMemoryDb(), makeInMemoryDb()], { concurrency: 2 })
138
166
 
139
167
  // Clean up old state database files after successful database creation
140
168
  // This prevents OPFS file pool capacity exhaustion from accumulated state db files after schema changes/migrations
@@ -161,6 +189,7 @@ const makeWorkerRunnerInner = ({ schema, sync: syncOptions, syncPayloadSchema }:
161
189
  shutdownChannel,
162
190
  syncPayloadEncoded,
163
191
  syncPayloadSchema,
192
+ ...(bootWarning !== undefined ? { bootWarning } : {}),
164
193
  })
165
194
  }).pipe(
166
195
  Effect.tapCauseLogPretty,
@@ -185,7 +214,7 @@ const makeWorkerRunnerInner = ({ schema, sync: syncOptions, syncPayloadSchema }:
185
214
  }).pipe(UnknownError.mapToUnknownError, Effect.withSpan('@livestore/adapter-web:worker:GetRecreateSnapshot')),
186
215
  PullStream: ({ cursor }) =>
187
216
  Effect.gen(function* () {
188
- const { syncProcessor } = yield* LeaderThreadCtx
217
+ const { syncProcessor } = yield* LeaderThreadCtx // <- syncState comes from here
189
218
  return syncProcessor.pull({ cursor })
190
219
  }).pipe(
191
220
  Stream.unwrapScoped,
@@ -200,6 +229,20 @@ const makeWorkerRunnerInner = ({ schema, sync: syncOptions, syncPayloadSchema }:
200
229
  { waitForProcessing: true },
201
230
  ),
202
231
  ).pipe(Effect.uninterruptible, Effect.withSpan('@livestore/adapter-web:worker:PushToLeader')),
232
+ StreamEvents: (options) =>
233
+ LeaderThreadCtx.pipe(
234
+ Effect.map(({ dbEventlog, syncProcessor }) => {
235
+ const { _tag: _ignored, ...payload } = options as any
236
+ const streamOptions = payload as StreamEventsOptions
237
+ return streamEventsWithSyncState({
238
+ dbEventlog,
239
+ syncState: syncProcessor.syncState,
240
+ options: streamOptions,
241
+ })
242
+ }),
243
+ Stream.unwrapScoped,
244
+ Stream.withSpan('@livestore/adapter-web:worker:StreamEvents'),
245
+ ),
203
246
  Export: () =>
204
247
  Effect.andThen(LeaderThreadCtx, (_) => _.dbState.export()).pipe(
205
248
  UnknownError.mapToUnknownError,
@@ -271,13 +314,39 @@ const makeDevtoolsOptions = ({
271
314
 
272
315
  return {
273
316
  enabled: true,
274
- boot: Effect.gen(function* () {
275
- const persistenceInfo = {
317
+ boot: Effect.succeed({
318
+ node,
319
+ persistenceInfo: {
276
320
  state: dbState.metadata.persistenceInfo,
277
321
  eventlog: dbEventlog.metadata.persistenceInfo,
278
- }
279
-
280
- return { node, persistenceInfo, mode: 'direct' }
322
+ },
323
+ mode: 'direct' as const,
281
324
  }),
282
325
  }
283
326
  })
327
+
328
+ /**
329
+ * Attempts to access OPFS and returns a warning if unavailable.
330
+ *
331
+ * Common failure scenarios:
332
+ * - Safari/Firefox private browsing: SecurityError or NotAllowedError
333
+ * - Permission denied: NotAllowedError
334
+ * - Quota exceeded: QuotaExceededError
335
+ */
336
+ const checkOpfsAvailability = Effect.gen(function* () {
337
+ const opfs = yield* Opfs.Opfs
338
+ return yield* opfs.getRootDirectoryHandle.pipe(
339
+ Effect.as(undefined),
340
+ Effect.catchAll((error) => {
341
+ const reason: BootWarningReason =
342
+ Schema.is(WebError.SecurityError)(error) || Schema.is(WebError.NotAllowedError)(error)
343
+ ? 'private-browsing'
344
+ : 'storage-unavailable'
345
+ const message =
346
+ reason === 'private-browsing'
347
+ ? 'Storage unavailable in private browsing mode. LiveStore will continue without persistence.'
348
+ : 'Storage access denied. LiveStore will continue without persistence.'
349
+ return Effect.succeed({ reason, message } as const)
350
+ }),
351
+ )
352
+ })
@@ -243,6 +243,7 @@ const makeWorkerRunner = Effect.gen(function* () {
243
243
  BootStatusStream: forwardRequestStream,
244
244
  PushToLeader: forwardRequest,
245
245
  PullStream: forwardRequestStream,
246
+ StreamEvents: forwardRequestStream,
246
247
  Export: forwardRequest,
247
248
  GetRecreateSnapshot: forwardRequest,
248
249
  ExportEventlog: forwardRequest,