@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.
- package/dist/.tsbuildinfo +1 -1
- package/dist/in-memory/in-memory-adapter.d.ts +42 -8
- package/dist/in-memory/in-memory-adapter.d.ts.map +1 -1
- package/dist/in-memory/in-memory-adapter.js +52 -9
- package/dist/in-memory/in-memory-adapter.js.map +1 -1
- package/dist/index.d.ts +11 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +11 -1
- package/dist/index.js.map +1 -1
- package/dist/single-tab/mod.d.ts +15 -0
- package/dist/single-tab/mod.d.ts.map +1 -0
- package/dist/single-tab/mod.js +15 -0
- package/dist/single-tab/mod.js.map +1 -0
- package/dist/single-tab/single-tab-adapter.d.ts +108 -0
- package/dist/single-tab/single-tab-adapter.d.ts.map +1 -0
- package/dist/single-tab/single-tab-adapter.js +279 -0
- package/dist/single-tab/single-tab-adapter.js.map +1 -0
- package/dist/web-worker/client-session/persisted-adapter.d.ts +18 -0
- package/dist/web-worker/client-session/persisted-adapter.d.ts.map +1 -1
- package/dist/web-worker/client-session/persisted-adapter.js +73 -5
- package/dist/web-worker/client-session/persisted-adapter.js.map +1 -1
- package/dist/web-worker/common/persisted-sqlite.d.ts +1 -1
- package/dist/web-worker/common/persisted-sqlite.d.ts.map +1 -1
- package/dist/web-worker/common/persisted-sqlite.js +13 -2
- package/dist/web-worker/common/persisted-sqlite.js.map +1 -1
- package/dist/web-worker/common/worker-schema.d.ts +56 -6
- package/dist/web-worker/common/worker-schema.d.ts.map +1 -1
- package/dist/web-worker/common/worker-schema.js +9 -2
- package/dist/web-worker/common/worker-schema.js.map +1 -1
- package/dist/web-worker/leader-worker/make-leader-worker.d.ts +1 -1
- package/dist/web-worker/leader-worker/make-leader-worker.d.ts.map +1 -1
- package/dist/web-worker/leader-worker/make-leader-worker.js +59 -15
- package/dist/web-worker/leader-worker/make-leader-worker.js.map +1 -1
- package/dist/web-worker/shared-worker/make-shared-worker.d.ts.map +1 -1
- package/dist/web-worker/shared-worker/make-shared-worker.js +1 -0
- package/dist/web-worker/shared-worker/make-shared-worker.js.map +1 -1
- package/package.json +6 -6
- package/src/in-memory/in-memory-adapter.ts +59 -9
- package/src/index.ts +15 -1
- package/src/single-tab/mod.ts +15 -0
- package/src/single-tab/single-tab-adapter.ts +517 -0
- package/src/web-worker/client-session/persisted-adapter.ts +92 -6
- package/src/web-worker/common/persisted-sqlite.ts +15 -3
- package/src/web-worker/common/worker-schema.ts +12 -0
- package/src/web-worker/leader-worker/make-leader-worker.ts +87 -18
- 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 (
|
|
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: {
|
|
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
|
-
|
|
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 {
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
135
|
-
|
|
136
|
-
|
|
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.
|
|
275
|
-
|
|
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,
|