@livestore/adapter-web 0.4.0-dev.8 → 0.4.0
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/README.md +5 -5
- package/dist/.tsbuildinfo +1 -1
- package/dist/in-memory/in-memory-adapter.d.ts +49 -5
- package/dist/in-memory/in-memory-adapter.d.ts.map +1 -1
- package/dist/in-memory/in-memory-adapter.js +77 -20
- 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 +271 -0
- package/dist/single-tab/single-tab-adapter.js.map +1 -0
- package/dist/web-worker/client-session/client-session-devtools.d.ts +2 -2
- package/dist/web-worker/client-session/client-session-devtools.d.ts.map +1 -1
- package/dist/web-worker/client-session/client-session-devtools.js +20 -9
- package/dist/web-worker/client-session/client-session-devtools.js.map +1 -1
- 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 +141 -67
- package/dist/web-worker/client-session/persisted-adapter.js.map +1 -1
- package/dist/web-worker/client-session/sqlite-loader.d.ts +2 -0
- package/dist/web-worker/client-session/sqlite-loader.d.ts.map +1 -0
- package/dist/web-worker/client-session/sqlite-loader.js +16 -0
- package/dist/web-worker/client-session/sqlite-loader.js.map +1 -0
- package/dist/web-worker/common/persisted-sqlite.d.ts +13 -20
- package/dist/web-worker/common/persisted-sqlite.d.ts.map +1 -1
- package/dist/web-worker/common/persisted-sqlite.js +95 -102
- package/dist/web-worker/common/persisted-sqlite.js.map +1 -1
- package/dist/web-worker/common/shutdown-channel.d.ts +3 -2
- package/dist/web-worker/common/shutdown-channel.d.ts.map +1 -1
- package/dist/web-worker/common/shutdown-channel.js +2 -2
- package/dist/web-worker/common/shutdown-channel.js.map +1 -1
- package/dist/web-worker/common/worker-disconnect-channel.d.ts +2 -6
- package/dist/web-worker/common/worker-disconnect-channel.d.ts.map +1 -1
- package/dist/web-worker/common/worker-disconnect-channel.js +3 -2
- package/dist/web-worker/common/worker-disconnect-channel.js.map +1 -1
- package/dist/web-worker/common/worker-schema.d.ts +152 -58
- package/dist/web-worker/common/worker-schema.d.ts.map +1 -1
- package/dist/web-worker/common/worker-schema.js +55 -37
- package/dist/web-worker/common/worker-schema.js.map +1 -1
- package/dist/web-worker/leader-worker/make-leader-worker.d.ts +5 -3
- package/dist/web-worker/leader-worker/make-leader-worker.d.ts.map +1 -1
- package/dist/web-worker/leader-worker/make-leader-worker.js +99 -38
- package/dist/web-worker/leader-worker/make-leader-worker.js.map +1 -1
- package/dist/web-worker/shared-worker/make-shared-worker.d.ts +2 -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 +62 -52
- package/dist/web-worker/shared-worker/make-shared-worker.js.map +1 -1
- package/package.json +56 -18
- package/src/in-memory/in-memory-adapter.ts +92 -26
- package/src/index.ts +15 -1
- package/src/single-tab/mod.ts +15 -0
- package/src/single-tab/single-tab-adapter.ts +499 -0
- package/src/web-worker/ambient.d.ts +7 -24
- package/src/web-worker/client-session/client-session-devtools.ts +32 -18
- package/src/web-worker/client-session/persisted-adapter.ts +199 -103
- package/src/web-worker/client-session/sqlite-loader.ts +19 -0
- package/src/web-worker/common/persisted-sqlite.ts +215 -170
- package/src/web-worker/common/shutdown-channel.ts +10 -3
- package/src/web-worker/common/worker-disconnect-channel.ts +10 -3
- package/src/web-worker/common/worker-schema.ts +78 -38
- package/src/web-worker/leader-worker/make-leader-worker.ts +149 -71
- package/src/web-worker/shared-worker/make-shared-worker.ts +78 -90
- package/dist/opfs-utils.d.ts +0 -5
- package/dist/opfs-utils.d.ts.map +0 -1
- package/dist/opfs-utils.js +0 -43
- package/dist/opfs-utils.js.map +0 -1
- package/src/opfs-utils.ts +0 -61
|
@@ -1,53 +1,62 @@
|
|
|
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
|
+
isWorkerTransportError,
|
|
4
5
|
liveStoreVersion,
|
|
5
6
|
makeClientSession,
|
|
6
7
|
StoreInterrupted,
|
|
7
8
|
sessionChangesetMetaTable,
|
|
8
|
-
|
|
9
|
+
UnknownError,
|
|
9
10
|
} from '@livestore/common'
|
|
10
11
|
// TODO bring back - this currently doesn't work due to https://github.com/vitejs/vite/issues/8427
|
|
11
12
|
// NOTE We're using a non-relative import here for Vite to properly resolve the import during app builds
|
|
12
13
|
// import LiveStoreSharedWorker from '@livestore/adapter-web/internal-shared-worker?sharedworker'
|
|
13
14
|
import { EventSequenceNumber } from '@livestore/common/schema'
|
|
14
15
|
import { sqliteDbFactory } from '@livestore/sqlite-wasm/browser'
|
|
15
|
-
import {
|
|
16
|
-
import { isDevEnv, shouldNeverHappen, tryAsFunctionAndNew } from '@livestore/utils'
|
|
16
|
+
import { isDevEnv, omitUndefineds, shouldNeverHappen, tryAsFunctionAndNew } from '@livestore/utils'
|
|
17
17
|
import {
|
|
18
|
-
BrowserWorker,
|
|
19
18
|
Cause,
|
|
20
19
|
Deferred,
|
|
21
20
|
Effect,
|
|
22
21
|
Exit,
|
|
23
22
|
Fiber,
|
|
24
23
|
Layer,
|
|
25
|
-
|
|
24
|
+
Option,
|
|
26
25
|
Queue,
|
|
27
26
|
Schema,
|
|
28
27
|
Stream,
|
|
28
|
+
Subscribable,
|
|
29
29
|
SubscriptionRef,
|
|
30
|
-
WebLock,
|
|
31
30
|
Worker,
|
|
32
|
-
WorkerError,
|
|
33
31
|
} from '@livestore/utils/effect'
|
|
32
|
+
import { BrowserWorker, Opfs, WebError, WebLock } from '@livestore/utils/effect/browser'
|
|
34
33
|
import { nanoid } from '@livestore/utils/nanoid'
|
|
35
34
|
|
|
36
|
-
import
|
|
37
|
-
import {
|
|
35
|
+
import { makeSingleTabAdapter } from '../../single-tab/single-tab-adapter.ts'
|
|
36
|
+
import {
|
|
37
|
+
readPersistedStateDbFromClientSession,
|
|
38
|
+
resetPersistedDataFromClientSession,
|
|
39
|
+
} from '../common/persisted-sqlite.ts'
|
|
38
40
|
import { makeShutdownChannel } from '../common/shutdown-channel.ts'
|
|
39
41
|
import { DedicatedWorkerDisconnectBroadcast, makeWorkerDisconnectChannel } from '../common/worker-disconnect-channel.ts'
|
|
40
42
|
import * as WorkerSchema from '../common/worker-schema.ts'
|
|
41
43
|
import { connectWebmeshNodeClientSession } from './client-session-devtools.ts'
|
|
44
|
+
import { loadSqlite3 } from './sqlite-loader.ts'
|
|
42
45
|
|
|
43
|
-
|
|
44
|
-
|
|
46
|
+
/**
|
|
47
|
+
* Checks if SharedWorker API is available in the current browser context.
|
|
48
|
+
*
|
|
49
|
+
* Returns false on Android Chrome and other browsers without SharedWorker support.
|
|
50
|
+
*
|
|
51
|
+
* @see https://github.com/livestorejs/livestore/issues/321
|
|
52
|
+
* @see https://issues.chromium.org/issues/40290702
|
|
53
|
+
*/
|
|
54
|
+
export const canUseSharedWorker = (): boolean => typeof SharedWorker !== 'undefined'
|
|
45
55
|
|
|
46
|
-
if (isDevEnv()) {
|
|
56
|
+
if (isDevEnv() === true) {
|
|
47
57
|
globalThis.__debugLiveStoreUtils = {
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
runFork: (effect: Effect.Effect<any, any, never>) => Effect.runFork(effect),
|
|
58
|
+
...globalThis.__debugLiveStoreUtils,
|
|
59
|
+
opfs: Opfs.debugUtils,
|
|
51
60
|
}
|
|
52
61
|
}
|
|
53
62
|
|
|
@@ -123,6 +132,15 @@ export type WebAdapterOptions = {
|
|
|
123
132
|
* Creates a web adapter with persistent storage (currently only supports OPFS).
|
|
124
133
|
* Requires both a web worker and a shared worker.
|
|
125
134
|
*
|
|
135
|
+
* On browsers without SharedWorker support (e.g. Android Chrome), this adapter
|
|
136
|
+
* automatically falls back to single-tab mode. In single-tab mode:
|
|
137
|
+
* - Each tab runs independently with its own leader worker
|
|
138
|
+
* - Multi-tab synchronization is not available
|
|
139
|
+
* - Devtools are not supported
|
|
140
|
+
*
|
|
141
|
+
* @see https://github.com/livestorejs/livestore/issues/321 - SharedWorker tracking issue
|
|
142
|
+
* @see https://issues.chromium.org/issues/40290702 - Chromium SharedWorker bug
|
|
143
|
+
*
|
|
126
144
|
* @example
|
|
127
145
|
* ```ts
|
|
128
146
|
* import { makePersistedAdapter } from '@livestore/adapter-web'
|
|
@@ -140,13 +158,49 @@ export const makePersistedAdapter =
|
|
|
140
158
|
(options: WebAdapterOptions): Adapter =>
|
|
141
159
|
(adapterArgs) =>
|
|
142
160
|
Effect.gen(function* () {
|
|
143
|
-
|
|
161
|
+
// Check SharedWorker availability first and fall back to single-tab mode if unavailable
|
|
162
|
+
if (canUseSharedWorker() === false) {
|
|
163
|
+
yield* Effect.logWarning(
|
|
164
|
+
'[@livestore/adapter-web] SharedWorker unavailable (e.g. Android Chrome). ' +
|
|
165
|
+
'Falling back to single-tab mode. Multi-tab synchronization and devtools are disabled. ' +
|
|
166
|
+
'See: https://github.com/livestorejs/livestore/issues/321',
|
|
167
|
+
)
|
|
168
|
+
|
|
169
|
+
return yield* makeSingleTabAdapter({
|
|
170
|
+
worker: options.worker,
|
|
171
|
+
storage: options.storage,
|
|
172
|
+
...omitUndefineds({
|
|
173
|
+
resetPersistence: options.resetPersistence,
|
|
174
|
+
clientId: options.clientId,
|
|
175
|
+
sessionId: options.sessionId,
|
|
176
|
+
experimental: options.experimental,
|
|
177
|
+
}),
|
|
178
|
+
})(adapterArgs)
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
const {
|
|
182
|
+
schema,
|
|
183
|
+
storeId,
|
|
184
|
+
devtoolsEnabled,
|
|
185
|
+
debugInstanceId,
|
|
186
|
+
bootStatusQueue,
|
|
187
|
+
shutdown,
|
|
188
|
+
syncPayloadSchema: _syncPayloadSchema,
|
|
189
|
+
syncPayloadEncoded,
|
|
190
|
+
} = adapterArgs
|
|
191
|
+
|
|
192
|
+
// NOTE: The schema travels with the worker bundle (developers call
|
|
193
|
+
// `makeWorker({ schema, syncPayloadSchema })`). We only keep the
|
|
194
|
+
// destructured value here to document availability on the client session
|
|
195
|
+
// side—structured cloning the Effect schema into the worker is not
|
|
196
|
+
// possible, so we intentionally do not forward it.
|
|
197
|
+
void _syncPayloadSchema
|
|
144
198
|
|
|
145
199
|
yield* ensureBrowserRequirements
|
|
146
200
|
|
|
147
201
|
yield* Queue.offer(bootStatusQueue, { stage: 'loading' })
|
|
148
202
|
|
|
149
|
-
const sqlite3 = yield* Effect.promise(() =>
|
|
203
|
+
const sqlite3 = yield* Effect.promise(() => loadSqlite3())
|
|
150
204
|
|
|
151
205
|
const LIVESTORE_TAB_LOCK = `livestore-tab-lock-${storeId}`
|
|
152
206
|
const LIVESTORE_SHARED_WORKER_TERMINATION_LOCK = `livestore-shared-worker-termination-lock-${storeId}`
|
|
@@ -155,10 +209,21 @@ export const makePersistedAdapter =
|
|
|
155
209
|
|
|
156
210
|
const shutdownChannel = yield* makeShutdownChannel(storeId)
|
|
157
211
|
|
|
158
|
-
if (
|
|
212
|
+
// Check OPFS availability early and notify user if storage is unavailable (e.g. private browsing)
|
|
213
|
+
const opfsWarning = yield* checkOpfsAvailability
|
|
214
|
+
if (opfsWarning !== undefined) {
|
|
215
|
+
yield* Effect.logWarning('[@livestore/adapter-web:client-session] OPFS unavailable', opfsWarning)
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
if (options.resetPersistence === true && opfsWarning === undefined) {
|
|
159
219
|
yield* shutdownChannel.send(IntentionalShutdownCause.make({ reason: 'adapter-reset' }))
|
|
160
220
|
|
|
161
221
|
yield* resetPersistedDataFromClientSession({ storageOptions, storeId })
|
|
222
|
+
} else if (options.resetPersistence === true) {
|
|
223
|
+
yield* Effect.logWarning(
|
|
224
|
+
'[@livestore/adapter-web:client-session] Skipping persistence reset because storage is unavailable',
|
|
225
|
+
opfsWarning,
|
|
226
|
+
)
|
|
162
227
|
}
|
|
163
228
|
|
|
164
229
|
// Note on fast-path booting:
|
|
@@ -168,9 +233,17 @@ export const makePersistedAdapter =
|
|
|
168
233
|
// We need to be extra careful though to not run into any race conditions or inconsistencies.
|
|
169
234
|
// TODO also verify persisted data
|
|
170
235
|
const dataFromFile =
|
|
171
|
-
options.experimental?.disableFastPath === true
|
|
236
|
+
options.experimental?.disableFastPath === true || opfsWarning !== undefined
|
|
172
237
|
? undefined
|
|
173
|
-
: yield*
|
|
238
|
+
: yield* readPersistedStateDbFromClientSession({ storageOptions, storeId, schema }).pipe(
|
|
239
|
+
Effect.tapError((error) =>
|
|
240
|
+
Effect.logDebug('[@livestore/adapter-web:client-session] Could not read persisted state db', error, {
|
|
241
|
+
storeId,
|
|
242
|
+
}),
|
|
243
|
+
),
|
|
244
|
+
// If we get any error here, we return `undefined` to fall back to the slow path
|
|
245
|
+
Effect.orElseSucceed(() => undefined),
|
|
246
|
+
)
|
|
174
247
|
|
|
175
248
|
// The same across all client sessions (i.e. tabs, windows)
|
|
176
249
|
const clientId = options.clientId ?? getPersistedId(`clientId:${storeId}`, 'local')
|
|
@@ -182,7 +255,7 @@ export const makePersistedAdapter =
|
|
|
182
255
|
yield* shutdownChannel.listen.pipe(
|
|
183
256
|
Stream.flatten(),
|
|
184
257
|
Stream.tap((cause) =>
|
|
185
|
-
shutdown(cause._tag === '
|
|
258
|
+
shutdown(cause._tag === 'IntentionalShutdownCause' ? Exit.succeed(cause) : Exit.fail(cause)),
|
|
186
259
|
),
|
|
187
260
|
Stream.runDrain,
|
|
188
261
|
Effect.interruptible,
|
|
@@ -192,7 +265,7 @@ export const makePersistedAdapter =
|
|
|
192
265
|
|
|
193
266
|
const sharedWebWorker = tryAsFunctionAndNew(options.sharedWorker, { name: `livestore-shared-worker-${storeId}` })
|
|
194
267
|
|
|
195
|
-
if (options.experimental?.awaitSharedWorkerTermination) {
|
|
268
|
+
if (options.experimental?.awaitSharedWorkerTermination === true) {
|
|
196
269
|
// Relying on the lock being available is currently the only mechanism we're aware of
|
|
197
270
|
// to know whether the shared worker has terminated.
|
|
198
271
|
yield* Effect.addFinalizer(() => WebLock.waitForLock(LIVESTORE_SHARED_WORKER_TERMINATION_LOCK))
|
|
@@ -202,25 +275,10 @@ export const makePersistedAdapter =
|
|
|
202
275
|
const sharedWorkerFiber = yield* Worker.makePoolSerialized<typeof WorkerSchema.SharedWorkerRequest.Type>({
|
|
203
276
|
size: 1,
|
|
204
277
|
concurrency: 100,
|
|
205
|
-
initialMessage: () =>
|
|
206
|
-
new WorkerSchema.SharedWorkerInitialMessage({
|
|
207
|
-
liveStoreVersion,
|
|
208
|
-
payload: {
|
|
209
|
-
_tag: 'FromClientSession',
|
|
210
|
-
initialMessage: new WorkerSchema.LeaderWorkerInnerInitialMessage({
|
|
211
|
-
storageOptions,
|
|
212
|
-
storeId,
|
|
213
|
-
clientId,
|
|
214
|
-
devtoolsEnabled,
|
|
215
|
-
debugInstanceId,
|
|
216
|
-
syncPayload,
|
|
217
|
-
}),
|
|
218
|
-
},
|
|
219
|
-
}),
|
|
220
278
|
}).pipe(
|
|
221
279
|
Effect.provide(sharedWorkerContext),
|
|
222
280
|
Effect.tapCauseLogPretty,
|
|
223
|
-
|
|
281
|
+
Effect.orDie,
|
|
224
282
|
Effect.tapErrorCause((cause) => shutdown(Exit.failCause(cause))),
|
|
225
283
|
Effect.withSpan('@livestore/adapter-web:client-session:setupSharedWorker'),
|
|
226
284
|
Effect.forkScoped,
|
|
@@ -232,7 +290,7 @@ export const makePersistedAdapter =
|
|
|
232
290
|
//
|
|
233
291
|
// Sorry for this pun ...
|
|
234
292
|
let gotLocky = yield* WebLock.tryGetDeferredLock(lockDeferred, LIVESTORE_TAB_LOCK)
|
|
235
|
-
const lockStatus = yield* SubscriptionRef.make<LockStatus>(gotLocky ? 'has-lock' : 'no-lock')
|
|
293
|
+
const lockStatus = yield* SubscriptionRef.make<LockStatus>(gotLocky === true ? 'has-lock' : 'no-lock')
|
|
236
294
|
|
|
237
295
|
// Ideally we can come up with a simpler implementation that doesn't require this
|
|
238
296
|
const waitForSharedWorkerInitialized = yield* Deferred.make<void>()
|
|
@@ -262,7 +320,7 @@ export const makePersistedAdapter =
|
|
|
262
320
|
initialMessage: () => new WorkerSchema.LeaderWorkerOuterInitialMessage({ port: mc.port1, storeId, clientId }),
|
|
263
321
|
}).pipe(
|
|
264
322
|
Effect.provide(BrowserWorker.layer(() => worker)),
|
|
265
|
-
|
|
323
|
+
UnknownError.mapToUnknownError,
|
|
266
324
|
Effect.tapErrorCause((cause) => shutdown(Exit.failCause(cause))),
|
|
267
325
|
Effect.withSpan('@livestore/adapter-web:client-session:setupDedicatedWorker'),
|
|
268
326
|
Effect.tapCauseLogPretty,
|
|
@@ -272,10 +330,25 @@ export const makePersistedAdapter =
|
|
|
272
330
|
yield* workerDisconnectChannel.send(DedicatedWorkerDisconnectBroadcast.make({}))
|
|
273
331
|
|
|
274
332
|
const sharedWorker = yield* Fiber.join(sharedWorkerFiber)
|
|
275
|
-
yield* sharedWorker
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
333
|
+
yield* sharedWorker
|
|
334
|
+
.executeEffect(
|
|
335
|
+
new WorkerSchema.SharedWorkerUpdateMessagePort({
|
|
336
|
+
port: mc.port2,
|
|
337
|
+
liveStoreVersion,
|
|
338
|
+
initial: new WorkerSchema.LeaderWorkerInnerInitialMessage({
|
|
339
|
+
storageOptions,
|
|
340
|
+
storeId,
|
|
341
|
+
clientId,
|
|
342
|
+
devtoolsEnabled,
|
|
343
|
+
debugInstanceId,
|
|
344
|
+
syncPayloadEncoded,
|
|
345
|
+
}),
|
|
346
|
+
}),
|
|
347
|
+
)
|
|
348
|
+
.pipe(
|
|
349
|
+
UnknownError.mapToUnknownError,
|
|
350
|
+
Effect.tapErrorCause((cause) => shutdown(Exit.failCause(cause))),
|
|
351
|
+
)
|
|
279
352
|
|
|
280
353
|
yield* Deferred.succeed(waitForSharedWorkerInitialized, undefined)
|
|
281
354
|
|
|
@@ -302,15 +375,14 @@ export const makePersistedAdapter =
|
|
|
302
375
|
yield* runLocked.pipe(Effect.interruptible, Effect.tapCauseLogPretty, Effect.forkScoped)
|
|
303
376
|
}
|
|
304
377
|
|
|
305
|
-
const runInWorker = <
|
|
306
|
-
req:
|
|
307
|
-
):
|
|
308
|
-
? Effect.Effect<A, UnexpectedError | E, R>
|
|
309
|
-
: never =>
|
|
378
|
+
const runInWorker = <A, I, E, EI, R>(
|
|
379
|
+
req: WorkerSchema.SharedWorkerRequest & Schema.WithResult<A, I, E, EI, R>,
|
|
380
|
+
): Effect.Effect<A, E, R> =>
|
|
310
381
|
Fiber.join(sharedWorkerFiber).pipe(
|
|
311
382
|
// NOTE we need to wait for the shared worker to be initialized before we can send requests to it
|
|
312
383
|
Effect.tap(() => waitForSharedWorkerInitialized),
|
|
313
|
-
Effect.flatMap((worker) => worker.executeEffect(req)
|
|
384
|
+
Effect.flatMap((worker) => worker.executeEffect(req)),
|
|
385
|
+
Effect.catchIf(isWorkerTransportError, (e) => Effect.die(e)),
|
|
314
386
|
// NOTE we want to treat worker requests as atomic and therefore not allow them to be interrupted
|
|
315
387
|
// Interruption usually only happens during leader re-election or store shutdown
|
|
316
388
|
// Effect.uninterruptible,
|
|
@@ -319,40 +391,24 @@ export const makePersistedAdapter =
|
|
|
319
391
|
duration: 2000,
|
|
320
392
|
}),
|
|
321
393
|
Effect.withSpan(`@livestore/adapter-web:client-session:runInWorker:${req._tag}`),
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
: cause,
|
|
328
|
-
),
|
|
329
|
-
Effect.catchAllDefect((cause) => new UnexpectedError({ cause })),
|
|
330
|
-
) as any
|
|
331
|
-
|
|
332
|
-
const runInWorkerStream = <TReq extends typeof WorkerSchema.SharedWorkerRequest.Type>(
|
|
333
|
-
req: TReq,
|
|
334
|
-
): TReq extends Schema.WithResult<infer A, infer _I, infer _E, infer _EI, infer R>
|
|
335
|
-
? Stream.Stream<A, UnexpectedError, R>
|
|
336
|
-
: never =>
|
|
394
|
+
)
|
|
395
|
+
|
|
396
|
+
const runInWorkerStream = <A, I, E, EI, R>(
|
|
397
|
+
req: WorkerSchema.SharedWorkerRequest & Schema.WithResult<A, I, E, EI, R>,
|
|
398
|
+
): Stream.Stream<A, E, R> =>
|
|
337
399
|
Effect.gen(function* () {
|
|
338
400
|
const sharedWorker = yield* Fiber.join(sharedWorkerFiber)
|
|
339
|
-
return sharedWorker.execute(req
|
|
340
|
-
Stream.
|
|
341
|
-
Schema.is(UnexpectedError)(cause)
|
|
342
|
-
? cause
|
|
343
|
-
: ParseResult.isParseError(cause) || Schema.is(WorkerError.WorkerError)(cause)
|
|
344
|
-
? new UnexpectedError({ cause })
|
|
345
|
-
: cause,
|
|
346
|
-
),
|
|
401
|
+
return sharedWorker.execute(req).pipe(
|
|
402
|
+
Stream.refineOrDie((e) => isWorkerTransportError(e) === true ? Option.none() : Option.some(e)),
|
|
347
403
|
Stream.withSpan(`@livestore/adapter-web:client-session:runInWorkerStream:${req._tag}`),
|
|
348
404
|
)
|
|
349
|
-
}).pipe(Stream.unwrap)
|
|
405
|
+
}).pipe(Stream.unwrap)
|
|
350
406
|
|
|
351
407
|
const bootStatusFiber = yield* runInWorkerStream(new WorkerSchema.LeaderWorkerInnerBootStatusStream()).pipe(
|
|
352
408
|
Stream.tap((_) => Queue.offer(bootStatusQueue, _)),
|
|
353
409
|
Stream.runDrain,
|
|
354
410
|
Effect.tapErrorCause((cause) =>
|
|
355
|
-
Cause.isInterruptedOnly(cause) ? Effect.void : shutdown(Exit.failCause(cause)),
|
|
411
|
+
Cause.isInterruptedOnly(cause) === true ? Effect.void : shutdown(Exit.failCause(cause)),
|
|
356
412
|
),
|
|
357
413
|
Effect.interruptible,
|
|
358
414
|
Effect.tapCauseLogPretty,
|
|
@@ -389,7 +445,7 @@ export const makePersistedAdapter =
|
|
|
389
445
|
const numberOfTables =
|
|
390
446
|
sqliteDb.select<{ count: number }>(`select count(*) as count from sqlite_master`)[0]?.count ?? 0
|
|
391
447
|
if (numberOfTables === 0) {
|
|
392
|
-
return yield*
|
|
448
|
+
return yield* UnknownError.make({
|
|
393
449
|
cause: `Encountered empty or corrupted database`,
|
|
394
450
|
payload: { snapshotByteLength: initialResult.snapshot.byteLength, storageOptions: options.storage },
|
|
395
451
|
})
|
|
@@ -407,20 +463,21 @@ export const makePersistedAdapter =
|
|
|
407
463
|
.first(),
|
|
408
464
|
)
|
|
409
465
|
|
|
410
|
-
const initialLeaderHead =
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
466
|
+
const initialLeaderHead =
|
|
467
|
+
initialLeaderHeadRes !== undefined
|
|
468
|
+
? EventSequenceNumber.Client.Composite.make({
|
|
469
|
+
global: initialLeaderHeadRes.seqNumGlobal,
|
|
470
|
+
client: initialLeaderHeadRes.seqNumClient,
|
|
471
|
+
rebaseGeneration: initialLeaderHeadRes.seqNumRebaseGeneration,
|
|
472
|
+
})
|
|
473
|
+
: EventSequenceNumber.Client.ROOT
|
|
417
474
|
|
|
418
475
|
// console.debug('[@livestore/adapter-web:client-session] initialLeaderHead', initialLeaderHead)
|
|
419
476
|
|
|
420
477
|
yield* Effect.addFinalizer((ex) =>
|
|
421
478
|
Effect.gen(function* () {
|
|
422
479
|
if (
|
|
423
|
-
Exit.isFailure(ex) &&
|
|
480
|
+
Exit.isFailure(ex) === true &&
|
|
424
481
|
Exit.isInterrupted(ex) === false &&
|
|
425
482
|
Schema.is(IntentionalShutdownCause)(Cause.squash(ex.cause)) === false &&
|
|
426
483
|
Schema.is(StoreInterrupted)(Cause.squash(ex.cause)) === false
|
|
@@ -430,7 +487,7 @@ export const makePersistedAdapter =
|
|
|
430
487
|
yield* Effect.logDebug('[@livestore/adapter-web:client-session] client-session shutdown', gotLocky, ex)
|
|
431
488
|
}
|
|
432
489
|
|
|
433
|
-
if (gotLocky) {
|
|
490
|
+
if (gotLocky === true) {
|
|
434
491
|
yield* Deferred.succeed(lockDeferred, undefined)
|
|
435
492
|
}
|
|
436
493
|
}).pipe(Effect.tapCauseLogPretty, Effect.orDie),
|
|
@@ -438,8 +495,7 @@ export const makePersistedAdapter =
|
|
|
438
495
|
|
|
439
496
|
const leaderThread: ClientSession['leaderThread'] = {
|
|
440
497
|
export: runInWorker(new WorkerSchema.LeaderWorkerInnerExport()).pipe(
|
|
441
|
-
Effect.
|
|
442
|
-
UnexpectedError.mapToUnexpectedError,
|
|
498
|
+
Effect.timeoutOrDie(10_000),
|
|
443
499
|
Effect.withSpan('@livestore/adapter-web:client-session:export'),
|
|
444
500
|
),
|
|
445
501
|
|
|
@@ -452,26 +508,39 @@ export const makePersistedAdapter =
|
|
|
452
508
|
attributes: { batchSize: batch.length },
|
|
453
509
|
}),
|
|
454
510
|
),
|
|
511
|
+
stream: (options) =>
|
|
512
|
+
runInWorkerStream(new WorkerSchema.LeaderWorkerInnerStreamEvents(options)).pipe(
|
|
513
|
+
Stream.withSpan('@livestore/adapter-web:client-session:streamEvents'),
|
|
514
|
+
Stream.orDie,
|
|
515
|
+
),
|
|
455
516
|
},
|
|
456
517
|
|
|
457
|
-
initialState: {
|
|
518
|
+
initialState: {
|
|
519
|
+
leaderHead: initialLeaderHead,
|
|
520
|
+
migrationsReport,
|
|
521
|
+
storageMode: opfsWarning === undefined ? 'persisted' : 'in-memory',
|
|
522
|
+
},
|
|
458
523
|
|
|
459
524
|
getEventlogData: runInWorker(new WorkerSchema.LeaderWorkerInnerExportEventlog()).pipe(
|
|
460
|
-
Effect.
|
|
461
|
-
UnexpectedError.mapToUnexpectedError,
|
|
525
|
+
Effect.timeoutOrDie(10_000),
|
|
462
526
|
Effect.withSpan('@livestore/adapter-web:client-session:getEventlogData'),
|
|
463
527
|
),
|
|
464
528
|
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
529
|
+
syncState: Subscribable.make({
|
|
530
|
+
get: runInWorker(new WorkerSchema.LeaderWorkerInnerGetLeaderSyncState()).pipe(
|
|
531
|
+
Effect.withSpan('@livestore/adapter-web:client-session:getLeaderSyncState'),
|
|
532
|
+
),
|
|
533
|
+
changes: runInWorkerStream(new WorkerSchema.LeaderWorkerInnerSyncStateStream()).pipe(Stream.orDie),
|
|
534
|
+
}),
|
|
469
535
|
|
|
470
536
|
sendDevtoolsMessage: (message) =>
|
|
471
537
|
runInWorker(new WorkerSchema.LeaderWorkerInnerExtraDevtoolsMessage({ message })).pipe(
|
|
472
|
-
UnexpectedError.mapToUnexpectedError,
|
|
473
538
|
Effect.withSpan('@livestore/adapter-web:client-session:devtoolsMessageForLeader'),
|
|
474
539
|
),
|
|
540
|
+
networkStatus: Subscribable.make({
|
|
541
|
+
get: runInWorker(new WorkerSchema.LeaderWorkerInnerGetNetworkStatus()),
|
|
542
|
+
changes: runInWorkerStream(new WorkerSchema.LeaderWorkerInnerNetworkStatusStream()),
|
|
543
|
+
}),
|
|
475
544
|
}
|
|
476
545
|
|
|
477
546
|
const sharedWorker = yield* Fiber.join(sharedWorkerFiber)
|
|
@@ -482,10 +551,11 @@ export const makePersistedAdapter =
|
|
|
482
551
|
lockStatus,
|
|
483
552
|
clientId,
|
|
484
553
|
sessionId,
|
|
485
|
-
|
|
486
|
-
isLeader: true,
|
|
554
|
+
isLeader: gotLocky,
|
|
487
555
|
leaderThread,
|
|
488
556
|
webmeshMode: 'direct',
|
|
557
|
+
// Can be undefined in Node.js
|
|
558
|
+
origin: globalThis.location?.origin,
|
|
489
559
|
connectWebmeshNode: ({ sessionInfo, webmeshNode }) =>
|
|
490
560
|
connectWebmeshNodeClientSession({ webmeshNode, sessionInfo, sharedWorker, devtoolsEnabled, schema }),
|
|
491
561
|
registerBeforeUnload: (onBeforeUnload) => {
|
|
@@ -499,7 +569,7 @@ export const makePersistedAdapter =
|
|
|
499
569
|
})
|
|
500
570
|
|
|
501
571
|
return clientSession
|
|
502
|
-
}).pipe(
|
|
572
|
+
}).pipe(Effect.provide(Opfs.Opfs.Default), UnknownError.mapToUnknownError)
|
|
503
573
|
|
|
504
574
|
// NOTE for `local` storage we could also use the eventlog db to store the data
|
|
505
575
|
const getPersistedId = (key: string, storageType: 'session' | 'local') => {
|
|
@@ -512,7 +582,7 @@ const getPersistedId = (key: string, storageType: 'session' | 'local') => {
|
|
|
512
582
|
? sessionStorage
|
|
513
583
|
: storageType === 'local'
|
|
514
584
|
? localStorage
|
|
515
|
-
: shouldNeverHappen(`[@livestore/adapter-web] Invalid storage type: ${storageType}`)
|
|
585
|
+
: shouldNeverHappen(`[@livestore/adapter-web] Invalid storage type: ${String(storageType)}`)
|
|
516
586
|
|
|
517
587
|
// in case of a worker, we need the id of the parent window, to keep the id consistent
|
|
518
588
|
// we also need to handle the case where there are multiple workers being spawned by the same window
|
|
@@ -523,7 +593,7 @@ const getPersistedId = (key: string, storageType: 'session' | 'local') => {
|
|
|
523
593
|
const fullKey = `livestore:${key}`
|
|
524
594
|
const storedKey = storage.getItem(fullKey)
|
|
525
595
|
|
|
526
|
-
if (storedKey) return storedKey
|
|
596
|
+
if (storedKey !== null) return storedKey
|
|
527
597
|
|
|
528
598
|
const newKey = makeId()
|
|
529
599
|
storage.setItem(fullKey, newKey)
|
|
@@ -534,8 +604,8 @@ const getPersistedId = (key: string, storageType: 'session' | 'local') => {
|
|
|
534
604
|
const ensureBrowserRequirements = Effect.gen(function* () {
|
|
535
605
|
const validate = (condition: boolean, label: string) =>
|
|
536
606
|
Effect.gen(function* () {
|
|
537
|
-
if (condition) {
|
|
538
|
-
return yield*
|
|
607
|
+
if (condition === true) {
|
|
608
|
+
return yield* UnknownError.make({
|
|
539
609
|
cause: `[@livestore/adapter-web] Browser not supported. The LiveStore web adapter needs '${label}' to work properly`,
|
|
540
610
|
})
|
|
541
611
|
}
|
|
@@ -550,3 +620,29 @@ const ensureBrowserRequirements = Effect.gen(function* () {
|
|
|
550
620
|
validate(typeof sessionStorage === 'undefined', 'sessionStorage'),
|
|
551
621
|
])
|
|
552
622
|
})
|
|
623
|
+
|
|
624
|
+
/**
|
|
625
|
+
* Attempts to access OPFS and returns a warning if unavailable.
|
|
626
|
+
*
|
|
627
|
+
* Common failure scenarios:
|
|
628
|
+
* - Safari/Firefox private browsing: SecurityError or NotAllowedError
|
|
629
|
+
* - Permission denied: NotAllowedError
|
|
630
|
+
* - Quota exceeded: QuotaExceededError
|
|
631
|
+
*/
|
|
632
|
+
const checkOpfsAvailability = Effect.gen(function* () {
|
|
633
|
+
const opfs = yield* Opfs.Opfs
|
|
634
|
+
return yield* opfs.getRootDirectoryHandle.pipe(
|
|
635
|
+
Effect.as(undefined),
|
|
636
|
+
Effect.catchAll((error) => {
|
|
637
|
+
const reason: BootWarningReason =
|
|
638
|
+
Schema.is(WebError.SecurityError)(error) === true || Schema.is(WebError.NotAllowedError)(error) === true
|
|
639
|
+
? 'private-browsing'
|
|
640
|
+
: 'storage-unavailable'
|
|
641
|
+
const message =
|
|
642
|
+
reason === 'private-browsing'
|
|
643
|
+
? 'Storage unavailable in private browsing mode. LiveStore will continue without persistence.'
|
|
644
|
+
: 'Storage access denied. LiveStore will continue without persistence.'
|
|
645
|
+
return Effect.succeed({ reason, message } as const)
|
|
646
|
+
}),
|
|
647
|
+
)
|
|
648
|
+
})
|
|
@@ -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 === true ? loadSqlite3Wasm() : (sqlite3Promise ?? loadSqlite3Wasm()))
|