@livestore/adapter-web 0.4.0-dev.21 → 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 (36) hide show
  1. package/dist/.tsbuildinfo +1 -1
  2. package/dist/in-memory/in-memory-adapter.d.ts.map +1 -1
  3. package/dist/in-memory/in-memory-adapter.js +5 -1
  4. package/dist/in-memory/in-memory-adapter.js.map +1 -1
  5. package/dist/index.d.ts +11 -1
  6. package/dist/index.d.ts.map +1 -1
  7. package/dist/index.js +11 -1
  8. package/dist/index.js.map +1 -1
  9. package/dist/single-tab/mod.d.ts +15 -0
  10. package/dist/single-tab/mod.d.ts.map +1 -0
  11. package/dist/single-tab/mod.js +15 -0
  12. package/dist/single-tab/mod.js.map +1 -0
  13. package/dist/single-tab/single-tab-adapter.d.ts +108 -0
  14. package/dist/single-tab/single-tab-adapter.d.ts.map +1 -0
  15. package/dist/single-tab/single-tab-adapter.js +279 -0
  16. package/dist/single-tab/single-tab-adapter.js.map +1 -0
  17. package/dist/web-worker/client-session/persisted-adapter.d.ts +18 -0
  18. package/dist/web-worker/client-session/persisted-adapter.d.ts.map +1 -1
  19. package/dist/web-worker/client-session/persisted-adapter.js +72 -5
  20. package/dist/web-worker/client-session/persisted-adapter.js.map +1 -1
  21. package/dist/web-worker/common/persisted-sqlite.js +1 -1
  22. package/dist/web-worker/common/persisted-sqlite.js.map +1 -1
  23. package/dist/web-worker/common/worker-schema.d.ts +8 -4
  24. package/dist/web-worker/common/worker-schema.d.ts.map +1 -1
  25. package/dist/web-worker/leader-worker/make-leader-worker.d.ts +1 -1
  26. package/dist/web-worker/leader-worker/make-leader-worker.d.ts.map +1 -1
  27. package/dist/web-worker/leader-worker/make-leader-worker.js +43 -9
  28. package/dist/web-worker/leader-worker/make-leader-worker.js.map +1 -1
  29. package/package.json +6 -6
  30. package/src/in-memory/in-memory-adapter.ts +5 -1
  31. package/src/index.ts +15 -1
  32. package/src/single-tab/mod.ts +15 -0
  33. package/src/single-tab/single-tab-adapter.ts +517 -0
  34. package/src/web-worker/client-session/persisted-adapter.ts +87 -6
  35. package/src/web-worker/common/persisted-sqlite.ts +1 -1
  36. package/src/web-worker/leader-worker/make-leader-worker.ts +59 -10
@@ -1,4 +1,4 @@
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
3
  import type { DevtoolsOptions, StreamEventsOptions } from '@livestore/common/leader-thread'
4
4
  import {
@@ -22,12 +22,12 @@ import {
22
22
  Layer,
23
23
  OtelTracer,
24
24
  Scheduler,
25
- type Schema,
25
+ Schema,
26
26
  Stream,
27
27
  TaskTracing,
28
28
  WorkerRunner,
29
29
  } from '@livestore/utils/effect'
30
- import { BrowserWorkerRunner, Opfs } from '@livestore/utils/effect/browser'
30
+ import { BrowserWorkerRunner, Opfs, WebError } from '@livestore/utils/effect/browser'
31
31
  import type * as otel from '@opentelemetry/api'
32
32
 
33
33
  import { cleanupOldStateDbFiles, getStateDbFileName, sanitizeOpfsDir } from '../common/persisted-sqlite.ts'
@@ -118,13 +118,28 @@ const makeWorkerRunnerInner = ({ schema, sync: syncOptions, syncPayloadSchema }:
118
118
  Effect.gen(function* () {
119
119
  const sqlite3 = yield* Effect.promise(() => loadSqlite3Wasm())
120
120
  const makeSqliteDb = sqliteDbFactory({ sqlite3 })
121
- const opfsDirectory = yield* sanitizeOpfsDir(storageOptions.directory, storeId)
122
121
  const runtime = yield* Effect.runtime<never>()
123
122
 
124
- 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') =>
125
140
  makeSqliteDb({
126
141
  _tag: 'opfs',
127
- opfsDirectory,
142
+ opfsDirectory: opfsDirectory!,
128
143
  fileName: kind === 'state' ? getStateDbFileName(schema) : 'eventlog.db',
129
144
  configureDb: (db) =>
130
145
  configureConnection(db, {
@@ -137,10 +152,17 @@ const makeWorkerRunnerInner = ({ schema, sync: syncOptions, syncPayloadSchema }:
137
152
  }).pipe(Effect.provide(runtime), Effect.runSync),
138
153
  }).pipe(Effect.acquireRelease((db) => Effect.try(() => db.close()).pipe(Effect.ignoreLogged)))
139
154
 
140
- // Might involve some async work, so we're running them concurrently
141
- const [dbState, dbEventlog] = yield* Effect.all([makeDb('state'), makeDb('eventlog')], {
142
- concurrency: 2,
143
- })
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 })
144
166
 
145
167
  // Clean up old state database files after successful database creation
146
168
  // This prevents OPFS file pool capacity exhaustion from accumulated state db files after schema changes/migrations
@@ -167,6 +189,7 @@ const makeWorkerRunnerInner = ({ schema, sync: syncOptions, syncPayloadSchema }:
167
189
  shutdownChannel,
168
190
  syncPayloadEncoded,
169
191
  syncPayloadSchema,
192
+ ...(bootWarning !== undefined ? { bootWarning } : {}),
170
193
  })
171
194
  }).pipe(
172
195
  Effect.tapCauseLogPretty,
@@ -301,3 +324,29 @@ const makeDevtoolsOptions = ({
301
324
  }),
302
325
  }
303
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
+ })