@livestore/adapter-web 0.4.0-dev.22 → 0.4.0-dev.23
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.js +3 -3
- package/dist/in-memory/in-memory-adapter.js.map +1 -1
- package/dist/single-tab/single-tab-adapter.d.ts.map +1 -1
- package/dist/single-tab/single-tab-adapter.js +17 -25
- package/dist/single-tab/single-tab-adapter.js.map +1 -1
- package/dist/web-worker/client-session/client-session-devtools.d.ts +1 -1
- package/dist/web-worker/client-session/client-session-devtools.d.ts.map +1 -1
- package/dist/web-worker/client-session/client-session-devtools.js +7 -7
- package/dist/web-worker/client-session/client-session-devtools.js.map +1 -1
- package/dist/web-worker/client-session/persisted-adapter.d.ts.map +1 -1
- package/dist/web-worker/client-session/persisted-adapter.js +25 -33
- package/dist/web-worker/client-session/persisted-adapter.js.map +1 -1
- package/dist/web-worker/client-session/sqlite-loader.d.ts.map +1 -1
- package/dist/web-worker/client-session/sqlite-loader.js +1 -1
- package/dist/web-worker/client-session/sqlite-loader.js.map +1 -1
- package/dist/web-worker/common/persisted-sqlite.d.ts.map +1 -1
- package/dist/web-worker/common/persisted-sqlite.js +12 -10
- package/dist/web-worker/common/persisted-sqlite.js.map +1 -1
- package/dist/web-worker/common/worker-schema.d.ts +26 -27
- package/dist/web-worker/common/worker-schema.d.ts.map +1 -1
- package/dist/web-worker/common/worker-schema.js +18 -19
- 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 +20 -20
- 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 +15 -15
- package/dist/web-worker/shared-worker/make-shared-worker.js.map +1 -1
- package/package.json +56 -17
- package/src/in-memory/in-memory-adapter.ts +4 -4
- package/src/single-tab/single-tab-adapter.ts +34 -52
- package/src/web-worker/client-session/client-session-devtools.ts +26 -27
- package/src/web-worker/client-session/persisted-adapter.ts +41 -60
- package/src/web-worker/client-session/sqlite-loader.ts +1 -1
- package/src/web-worker/common/persisted-sqlite.ts +12 -9
- package/src/web-worker/common/worker-schema.ts +19 -18
- package/src/web-worker/leader-worker/make-leader-worker.ts +39 -46
- package/src/web-worker/shared-worker/make-shared-worker.ts +26 -48
|
@@ -16,6 +16,7 @@
|
|
|
16
16
|
import type { Adapter, BootWarningReason, ClientSession, LockStatus } from '@livestore/common'
|
|
17
17
|
import {
|
|
18
18
|
IntentionalShutdownCause,
|
|
19
|
+
isWorkerTransportError,
|
|
19
20
|
makeClientSession,
|
|
20
21
|
StoreInterrupted,
|
|
21
22
|
sessionChangesetMetaTable,
|
|
@@ -30,17 +31,17 @@ import {
|
|
|
30
31
|
Exit,
|
|
31
32
|
Fiber,
|
|
32
33
|
Layer,
|
|
33
|
-
|
|
34
|
+
Option,
|
|
34
35
|
Queue,
|
|
35
36
|
Schema,
|
|
36
37
|
Stream,
|
|
37
38
|
Subscribable,
|
|
38
39
|
SubscriptionRef,
|
|
39
40
|
Worker,
|
|
40
|
-
WorkerError,
|
|
41
41
|
} from '@livestore/utils/effect'
|
|
42
42
|
import { BrowserWorker, Opfs, WebError } from '@livestore/utils/effect/browser'
|
|
43
43
|
import { nanoid } from '@livestore/utils/nanoid'
|
|
44
|
+
|
|
44
45
|
import { loadSqlite3 } from '../web-worker/client-session/sqlite-loader.ts'
|
|
45
46
|
import {
|
|
46
47
|
readPersistedStateDbFromClientSession,
|
|
@@ -193,7 +194,7 @@ export const makeSingleTabAdapter =
|
|
|
193
194
|
yield* shutdownChannel.listen.pipe(
|
|
194
195
|
Stream.flatten(),
|
|
195
196
|
Stream.tap((cause) =>
|
|
196
|
-
shutdown(cause._tag === '
|
|
197
|
+
shutdown(cause._tag === 'IntentionalShutdownCause' ? Exit.succeed(cause) : Exit.fail(cause)),
|
|
197
198
|
),
|
|
198
199
|
Stream.runDrain,
|
|
199
200
|
Effect.interruptible,
|
|
@@ -241,60 +242,43 @@ export const makeSingleTabAdapter =
|
|
|
241
242
|
}).pipe(
|
|
242
243
|
Effect.provide(innerWorkerContext),
|
|
243
244
|
Effect.tapCauseLogPretty,
|
|
244
|
-
|
|
245
|
+
Effect.orDie,
|
|
245
246
|
Effect.tapErrorCause((cause) => shutdown(Exit.failCause(cause))),
|
|
246
247
|
Effect.withSpan('@livestore/adapter-web:single-tab:setupInnerWorker'),
|
|
247
248
|
Effect.forkScoped,
|
|
248
249
|
)
|
|
249
250
|
|
|
250
251
|
// Helper to run requests against the worker
|
|
251
|
-
const runInWorker = <
|
|
252
|
-
req:
|
|
253
|
-
):
|
|
254
|
-
? Effect.Effect<A, UnknownError | E, R>
|
|
255
|
-
: never =>
|
|
252
|
+
const runInWorker = <A, I, E, EI, R>(
|
|
253
|
+
req: WorkerSchema.LeaderWorkerInnerRequest & Schema.WithResult<A, I, E, EI, R>,
|
|
254
|
+
): Effect.Effect<A, E, R> =>
|
|
256
255
|
Fiber.join(innerWorkerFiber).pipe(
|
|
257
|
-
Effect.flatMap((worker) => worker.executeEffect(req)
|
|
256
|
+
Effect.flatMap((worker) => worker.executeEffect(req)),
|
|
257
|
+
Effect.catchIf(isWorkerTransportError, (e) => Effect.die(e)),
|
|
258
258
|
Effect.logWarnIfTakesLongerThan({
|
|
259
259
|
label: `@livestore/adapter-web:single-tab:runInWorker:${req._tag}`,
|
|
260
260
|
duration: 2000,
|
|
261
261
|
}),
|
|
262
262
|
Effect.withSpan(`@livestore/adapter-web:single-tab:runInWorker:${req._tag}`),
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
: cause,
|
|
269
|
-
),
|
|
270
|
-
Effect.catchAllDefect((cause) => new UnknownError({ cause })),
|
|
271
|
-
) as any
|
|
272
|
-
|
|
273
|
-
const runInWorkerStream = <TReq extends WorkerSchema.LeaderWorkerInnerRequest>(
|
|
274
|
-
req: TReq,
|
|
275
|
-
): TReq extends Schema.WithResult<infer A, infer _I, infer _E, infer _EI, infer R>
|
|
276
|
-
? Stream.Stream<A, UnknownError, R>
|
|
277
|
-
: never =>
|
|
263
|
+
)
|
|
264
|
+
|
|
265
|
+
const runInWorkerStream = <A, I, E, EI, R>(
|
|
266
|
+
req: WorkerSchema.LeaderWorkerInnerRequest & Schema.WithResult<A, I, E, EI, R>,
|
|
267
|
+
): Stream.Stream<A, E, R> =>
|
|
278
268
|
Effect.gen(function* () {
|
|
279
269
|
const innerWorker = yield* Fiber.join(innerWorkerFiber)
|
|
280
|
-
return innerWorker.execute(req
|
|
281
|
-
Stream.
|
|
282
|
-
Schema.is(UnknownError)(cause)
|
|
283
|
-
? cause
|
|
284
|
-
: ParseResult.isParseError(cause) || Schema.is(WorkerError.WorkerError)(cause)
|
|
285
|
-
? new UnknownError({ cause })
|
|
286
|
-
: cause,
|
|
287
|
-
),
|
|
270
|
+
return innerWorker.execute(req).pipe(
|
|
271
|
+
Stream.refineOrDie((e) => isWorkerTransportError(e) === true ? Option.none() : Option.some(e)),
|
|
288
272
|
Stream.withSpan(`@livestore/adapter-web:single-tab:runInWorkerStream:${req._tag}`),
|
|
289
273
|
)
|
|
290
|
-
}).pipe(Stream.unwrap)
|
|
274
|
+
}).pipe(Stream.unwrap)
|
|
291
275
|
|
|
292
276
|
// Forward boot status from worker
|
|
293
277
|
const bootStatusFiber = yield* runInWorkerStream(new WorkerSchema.LeaderWorkerInnerBootStatusStream()).pipe(
|
|
294
278
|
Stream.tap((_) => Queue.offer(bootStatusQueue, _)),
|
|
295
279
|
Stream.runDrain,
|
|
296
280
|
Effect.tapErrorCause((cause) =>
|
|
297
|
-
Cause.isInterruptedOnly(cause) ? Effect.void : shutdown(Exit.failCause(cause)),
|
|
281
|
+
Cause.isInterruptedOnly(cause) === true ? Effect.void : shutdown(Exit.failCause(cause)),
|
|
298
282
|
),
|
|
299
283
|
Effect.interruptible,
|
|
300
284
|
Effect.tapCauseLogPretty,
|
|
@@ -347,18 +331,19 @@ export const makeSingleTabAdapter =
|
|
|
347
331
|
.first(),
|
|
348
332
|
)
|
|
349
333
|
|
|
350
|
-
const initialLeaderHead =
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
334
|
+
const initialLeaderHead =
|
|
335
|
+
initialLeaderHeadRes !== undefined
|
|
336
|
+
? EventSequenceNumber.Client.Composite.make({
|
|
337
|
+
global: initialLeaderHeadRes.seqNumGlobal,
|
|
338
|
+
client: initialLeaderHeadRes.seqNumClient,
|
|
339
|
+
rebaseGeneration: initialLeaderHeadRes.seqNumRebaseGeneration,
|
|
340
|
+
})
|
|
341
|
+
: EventSequenceNumber.Client.ROOT
|
|
357
342
|
|
|
358
343
|
yield* Effect.addFinalizer((ex) =>
|
|
359
344
|
Effect.gen(function* () {
|
|
360
345
|
if (
|
|
361
|
-
Exit.isFailure(ex) &&
|
|
346
|
+
Exit.isFailure(ex) === true &&
|
|
362
347
|
Exit.isInterrupted(ex) === false &&
|
|
363
348
|
Schema.is(IntentionalShutdownCause)(Cause.squash(ex.cause)) === false &&
|
|
364
349
|
Schema.is(StoreInterrupted)(Cause.squash(ex.cause)) === false
|
|
@@ -372,8 +357,7 @@ export const makeSingleTabAdapter =
|
|
|
372
357
|
|
|
373
358
|
const leaderThread: ClientSession['leaderThread'] = {
|
|
374
359
|
export: runInWorker(new WorkerSchema.LeaderWorkerInnerExport()).pipe(
|
|
375
|
-
Effect.
|
|
376
|
-
UnknownError.mapToUnknownError,
|
|
360
|
+
Effect.timeoutOrDie(10_000),
|
|
377
361
|
Effect.withSpan('@livestore/adapter-web:single-tab:export'),
|
|
378
362
|
),
|
|
379
363
|
|
|
@@ -400,14 +384,12 @@ export const makeSingleTabAdapter =
|
|
|
400
384
|
},
|
|
401
385
|
|
|
402
386
|
getEventlogData: runInWorker(new WorkerSchema.LeaderWorkerInnerExportEventlog()).pipe(
|
|
403
|
-
Effect.
|
|
404
|
-
UnknownError.mapToUnknownError,
|
|
387
|
+
Effect.timeoutOrDie(10_000),
|
|
405
388
|
Effect.withSpan('@livestore/adapter-web:single-tab:getEventlogData'),
|
|
406
389
|
),
|
|
407
390
|
|
|
408
391
|
syncState: Subscribable.make({
|
|
409
392
|
get: runInWorker(new WorkerSchema.LeaderWorkerInnerGetLeaderSyncState()).pipe(
|
|
410
|
-
UnknownError.mapToUnknownError,
|
|
411
393
|
Effect.withSpan('@livestore/adapter-web:single-tab:getLeaderSyncState'),
|
|
412
394
|
),
|
|
413
395
|
changes: runInWorkerStream(new WorkerSchema.LeaderWorkerInnerSyncStateStream()).pipe(Stream.orDie),
|
|
@@ -458,7 +440,7 @@ const getPersistedId = (key: string, storageType: 'session' | 'local') => {
|
|
|
458
440
|
? sessionStorage
|
|
459
441
|
: storageType === 'local'
|
|
460
442
|
? localStorage
|
|
461
|
-
: shouldNeverHappen(`[@livestore/adapter-web] Invalid storage type: ${storageType}`)
|
|
443
|
+
: shouldNeverHappen(`[@livestore/adapter-web] Invalid storage type: ${String(storageType)}`)
|
|
462
444
|
|
|
463
445
|
if (storage === undefined) {
|
|
464
446
|
return makeId()
|
|
@@ -467,7 +449,7 @@ const getPersistedId = (key: string, storageType: 'session' | 'local') => {
|
|
|
467
449
|
const fullKey = `livestore:${key}`
|
|
468
450
|
const storedKey = storage.getItem(fullKey)
|
|
469
451
|
|
|
470
|
-
if (storedKey) return storedKey
|
|
452
|
+
if (storedKey !== null) return storedKey
|
|
471
453
|
|
|
472
454
|
const newKey = makeId()
|
|
473
455
|
storage.setItem(fullKey, newKey)
|
|
@@ -478,7 +460,7 @@ const getPersistedId = (key: string, storageType: 'session' | 'local') => {
|
|
|
478
460
|
const ensureBrowserRequirements = Effect.gen(function* () {
|
|
479
461
|
const validate = (condition: boolean, label: string) =>
|
|
480
462
|
Effect.gen(function* () {
|
|
481
|
-
if (condition) {
|
|
463
|
+
if (condition === true) {
|
|
482
464
|
return yield* UnknownError.make({
|
|
483
465
|
cause: `[@livestore/adapter-web] Browser not supported. The LiveStore web adapter needs '${label}' to work properly`,
|
|
484
466
|
})
|
|
@@ -504,7 +486,7 @@ const checkOpfsAvailability = Effect.gen(function* () {
|
|
|
504
486
|
Effect.as(undefined),
|
|
505
487
|
Effect.catchAll((error) => {
|
|
506
488
|
const reason: BootWarningReason =
|
|
507
|
-
Schema.is(WebError.SecurityError)(error) || Schema.is(WebError.NotAllowedError)(error)
|
|
489
|
+
Schema.is(WebError.SecurityError)(error) === true || Schema.is(WebError.NotAllowedError)(error) === true
|
|
508
490
|
? 'private-browsing'
|
|
509
491
|
: 'storage-unavailable'
|
|
510
492
|
const message =
|
|
@@ -7,7 +7,7 @@ import { Effect, Stream } from '@livestore/utils/effect'
|
|
|
7
7
|
import { WebChannelBrowser } from '@livestore/utils/effect/browser'
|
|
8
8
|
import * as Webmesh from '@livestore/webmesh'
|
|
9
9
|
|
|
10
|
-
export const logDevtoolsUrl = ({
|
|
10
|
+
export const logDevtoolsUrl = Effect.fn('@livestore/adapter-web:client-session:devtools:logDevtoolsUrl')(function* ({
|
|
11
11
|
schema,
|
|
12
12
|
storeId,
|
|
13
13
|
clientId,
|
|
@@ -17,37 +17,36 @@ export const logDevtoolsUrl = ({
|
|
|
17
17
|
storeId: string
|
|
18
18
|
clientId: string
|
|
19
19
|
sessionId: string
|
|
20
|
-
})
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
const devtoolsBaseUrl = `${location.origin}${devtoolsPath}`
|
|
20
|
+
}) {
|
|
21
|
+
if (isDevEnv() === true) {
|
|
22
|
+
const devtoolsPath = (globalThis as unknown as { LIVESTORE_DEVTOOLS_PATH?: string }).LIVESTORE_DEVTOOLS_PATH ?? `/_livestore`
|
|
23
|
+
const devtoolsBaseUrl = `${location.origin}${devtoolsPath}`
|
|
25
24
|
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
25
|
+
// Check whether devtools are available and then log the URL
|
|
26
|
+
const response = yield* Effect.promise(() => fetch(devtoolsBaseUrl))
|
|
27
|
+
if (response.ok === true) {
|
|
28
|
+
const text = yield* Effect.promise(() => response.text())
|
|
29
|
+
if (text.includes('<meta name="livestore-devtools" content="true" />') === true) {
|
|
30
|
+
const url = `${devtoolsBaseUrl}/web/${storeId}/${clientId}/${sessionId}/${schema.devtools.alias}`
|
|
31
|
+
yield* Effect.log(`[@livestore/adapter-web] Devtools ready on ${url}`)
|
|
32
|
+
}
|
|
34
33
|
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
34
|
+
// Check for DevTools Chrome extension presence via iframe container the extension injects
|
|
35
|
+
const hasExt = document.querySelector('[id^="livestore-devtools-iframe-"]') !== null
|
|
36
|
+
if (hasExt === false) {
|
|
37
|
+
const g = globalThis as { __livestoreDevtoolsChromeNoticeShown?: boolean }
|
|
38
|
+
if (g.__livestoreDevtoolsChromeNoticeShown !== true) {
|
|
39
|
+
g.__livestoreDevtoolsChromeNoticeShown = true
|
|
41
40
|
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
}
|
|
41
|
+
const urlToLog = `https://github.com/livestorejs/livestore/releases/download/v${liveStoreVersion}/livestore-devtools-chrome-${liveStoreVersion}.zip`
|
|
42
|
+
yield* Effect.log(
|
|
43
|
+
`[@livestore/adapter-web] LiveStore DevTools Chrome extension not detected. Install v${liveStoreVersion}: ${urlToLog}`,
|
|
44
|
+
)
|
|
47
45
|
}
|
|
48
46
|
}
|
|
49
47
|
}
|
|
50
|
-
}
|
|
48
|
+
}
|
|
49
|
+
})
|
|
51
50
|
|
|
52
51
|
export const connectWebmeshNodeClientSession = Effect.fn(function* ({
|
|
53
52
|
webmeshNode,
|
|
@@ -62,7 +61,7 @@ export const connectWebmeshNodeClientSession = Effect.fn(function* ({
|
|
|
62
61
|
devtoolsEnabled: boolean
|
|
63
62
|
schema: LiveStoreSchema
|
|
64
63
|
}) {
|
|
65
|
-
if (devtoolsEnabled) {
|
|
64
|
+
if (devtoolsEnabled === true) {
|
|
66
65
|
const { clientId, sessionId, storeId } = sessionInfo
|
|
67
66
|
yield* logDevtoolsUrl({ clientId, sessionId, schema, storeId })
|
|
68
67
|
|
|
@@ -1,6 +1,7 @@
|
|
|
1
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,
|
|
@@ -20,17 +21,17 @@ import {
|
|
|
20
21
|
Exit,
|
|
21
22
|
Fiber,
|
|
22
23
|
Layer,
|
|
23
|
-
|
|
24
|
+
Option,
|
|
24
25
|
Queue,
|
|
25
26
|
Schema,
|
|
26
27
|
Stream,
|
|
27
28
|
Subscribable,
|
|
28
29
|
SubscriptionRef,
|
|
29
30
|
Worker,
|
|
30
|
-
WorkerError,
|
|
31
31
|
} from '@livestore/utils/effect'
|
|
32
32
|
import { BrowserWorker, Opfs, WebError, WebLock } from '@livestore/utils/effect/browser'
|
|
33
33
|
import { nanoid } from '@livestore/utils/nanoid'
|
|
34
|
+
|
|
34
35
|
import { makeSingleTabAdapter } from '../../single-tab/single-tab-adapter.ts'
|
|
35
36
|
import {
|
|
36
37
|
readPersistedStateDbFromClientSession,
|
|
@@ -52,7 +53,7 @@ import { loadSqlite3 } from './sqlite-loader.ts'
|
|
|
52
53
|
*/
|
|
53
54
|
export const canUseSharedWorker = (): boolean => typeof SharedWorker !== 'undefined'
|
|
54
55
|
|
|
55
|
-
if (isDevEnv()) {
|
|
56
|
+
if (isDevEnv() === true) {
|
|
56
57
|
globalThis.__debugLiveStoreUtils = {
|
|
57
58
|
...globalThis.__debugLiveStoreUtils,
|
|
58
59
|
opfs: Opfs.debugUtils,
|
|
@@ -158,7 +159,7 @@ export const makePersistedAdapter =
|
|
|
158
159
|
(adapterArgs) =>
|
|
159
160
|
Effect.gen(function* () {
|
|
160
161
|
// Check SharedWorker availability first and fall back to single-tab mode if unavailable
|
|
161
|
-
if (
|
|
162
|
+
if (canUseSharedWorker() === false) {
|
|
162
163
|
yield* Effect.logWarning(
|
|
163
164
|
'[@livestore/adapter-web] SharedWorker unavailable (e.g. Android Chrome). ' +
|
|
164
165
|
'Falling back to single-tab mode. Multi-tab synchronization and devtools are disabled. ' +
|
|
@@ -254,7 +255,7 @@ export const makePersistedAdapter =
|
|
|
254
255
|
yield* shutdownChannel.listen.pipe(
|
|
255
256
|
Stream.flatten(),
|
|
256
257
|
Stream.tap((cause) =>
|
|
257
|
-
shutdown(cause._tag === '
|
|
258
|
+
shutdown(cause._tag === 'IntentionalShutdownCause' ? Exit.succeed(cause) : Exit.fail(cause)),
|
|
258
259
|
),
|
|
259
260
|
Stream.runDrain,
|
|
260
261
|
Effect.interruptible,
|
|
@@ -264,7 +265,7 @@ export const makePersistedAdapter =
|
|
|
264
265
|
|
|
265
266
|
const sharedWebWorker = tryAsFunctionAndNew(options.sharedWorker, { name: `livestore-shared-worker-${storeId}` })
|
|
266
267
|
|
|
267
|
-
if (options.experimental?.awaitSharedWorkerTermination) {
|
|
268
|
+
if (options.experimental?.awaitSharedWorkerTermination === true) {
|
|
268
269
|
// Relying on the lock being available is currently the only mechanism we're aware of
|
|
269
270
|
// to know whether the shared worker has terminated.
|
|
270
271
|
yield* Effect.addFinalizer(() => WebLock.waitForLock(LIVESTORE_SHARED_WORKER_TERMINATION_LOCK))
|
|
@@ -277,7 +278,7 @@ export const makePersistedAdapter =
|
|
|
277
278
|
}).pipe(
|
|
278
279
|
Effect.provide(sharedWorkerContext),
|
|
279
280
|
Effect.tapCauseLogPretty,
|
|
280
|
-
|
|
281
|
+
Effect.orDie,
|
|
281
282
|
Effect.tapErrorCause((cause) => shutdown(Exit.failCause(cause))),
|
|
282
283
|
Effect.withSpan('@livestore/adapter-web:client-session:setupSharedWorker'),
|
|
283
284
|
Effect.forkScoped,
|
|
@@ -289,7 +290,7 @@ export const makePersistedAdapter =
|
|
|
289
290
|
//
|
|
290
291
|
// Sorry for this pun ...
|
|
291
292
|
let gotLocky = yield* WebLock.tryGetDeferredLock(lockDeferred, LIVESTORE_TAB_LOCK)
|
|
292
|
-
const lockStatus = yield* SubscriptionRef.make<LockStatus>(gotLocky ? 'has-lock' : 'no-lock')
|
|
293
|
+
const lockStatus = yield* SubscriptionRef.make<LockStatus>(gotLocky === true ? 'has-lock' : 'no-lock')
|
|
293
294
|
|
|
294
295
|
// Ideally we can come up with a simpler implementation that doesn't require this
|
|
295
296
|
const waitForSharedWorkerInitialized = yield* Deferred.make<void>()
|
|
@@ -374,15 +375,14 @@ export const makePersistedAdapter =
|
|
|
374
375
|
yield* runLocked.pipe(Effect.interruptible, Effect.tapCauseLogPretty, Effect.forkScoped)
|
|
375
376
|
}
|
|
376
377
|
|
|
377
|
-
const runInWorker = <
|
|
378
|
-
req:
|
|
379
|
-
):
|
|
380
|
-
? Effect.Effect<A, UnknownError | E, R>
|
|
381
|
-
: 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> =>
|
|
382
381
|
Fiber.join(sharedWorkerFiber).pipe(
|
|
383
382
|
// NOTE we need to wait for the shared worker to be initialized before we can send requests to it
|
|
384
383
|
Effect.tap(() => waitForSharedWorkerInitialized),
|
|
385
|
-
Effect.flatMap((worker) => worker.executeEffect(req)
|
|
384
|
+
Effect.flatMap((worker) => worker.executeEffect(req)),
|
|
385
|
+
Effect.catchIf(isWorkerTransportError, (e) => Effect.die(e)),
|
|
386
386
|
// NOTE we want to treat worker requests as atomic and therefore not allow them to be interrupted
|
|
387
387
|
// Interruption usually only happens during leader re-election or store shutdown
|
|
388
388
|
// Effect.uninterruptible,
|
|
@@ -391,40 +391,24 @@ export const makePersistedAdapter =
|
|
|
391
391
|
duration: 2000,
|
|
392
392
|
}),
|
|
393
393
|
Effect.withSpan(`@livestore/adapter-web:client-session:runInWorker:${req._tag}`),
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
: cause,
|
|
400
|
-
),
|
|
401
|
-
Effect.catchAllDefect((cause) => new UnknownError({ cause })),
|
|
402
|
-
) as any
|
|
403
|
-
|
|
404
|
-
const runInWorkerStream = <TReq extends typeof WorkerSchema.SharedWorkerRequest.Type>(
|
|
405
|
-
req: TReq,
|
|
406
|
-
): TReq extends Schema.WithResult<infer A, infer _I, infer _E, infer _EI, infer R>
|
|
407
|
-
? Stream.Stream<A, UnknownError, R>
|
|
408
|
-
: 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> =>
|
|
409
399
|
Effect.gen(function* () {
|
|
410
400
|
const sharedWorker = yield* Fiber.join(sharedWorkerFiber)
|
|
411
|
-
return sharedWorker.execute(req
|
|
412
|
-
Stream.
|
|
413
|
-
Schema.is(UnknownError)(cause)
|
|
414
|
-
? cause
|
|
415
|
-
: ParseResult.isParseError(cause) || Schema.is(WorkerError.WorkerError)(cause)
|
|
416
|
-
? new UnknownError({ cause })
|
|
417
|
-
: cause,
|
|
418
|
-
),
|
|
401
|
+
return sharedWorker.execute(req).pipe(
|
|
402
|
+
Stream.refineOrDie((e) => isWorkerTransportError(e) === true ? Option.none() : Option.some(e)),
|
|
419
403
|
Stream.withSpan(`@livestore/adapter-web:client-session:runInWorkerStream:${req._tag}`),
|
|
420
404
|
)
|
|
421
|
-
}).pipe(Stream.unwrap)
|
|
405
|
+
}).pipe(Stream.unwrap)
|
|
422
406
|
|
|
423
407
|
const bootStatusFiber = yield* runInWorkerStream(new WorkerSchema.LeaderWorkerInnerBootStatusStream()).pipe(
|
|
424
408
|
Stream.tap((_) => Queue.offer(bootStatusQueue, _)),
|
|
425
409
|
Stream.runDrain,
|
|
426
410
|
Effect.tapErrorCause((cause) =>
|
|
427
|
-
Cause.isInterruptedOnly(cause) ? Effect.void : shutdown(Exit.failCause(cause)),
|
|
411
|
+
Cause.isInterruptedOnly(cause) === true ? Effect.void : shutdown(Exit.failCause(cause)),
|
|
428
412
|
),
|
|
429
413
|
Effect.interruptible,
|
|
430
414
|
Effect.tapCauseLogPretty,
|
|
@@ -479,20 +463,21 @@ export const makePersistedAdapter =
|
|
|
479
463
|
.first(),
|
|
480
464
|
)
|
|
481
465
|
|
|
482
|
-
const initialLeaderHead =
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
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
|
|
489
474
|
|
|
490
475
|
// console.debug('[@livestore/adapter-web:client-session] initialLeaderHead', initialLeaderHead)
|
|
491
476
|
|
|
492
477
|
yield* Effect.addFinalizer((ex) =>
|
|
493
478
|
Effect.gen(function* () {
|
|
494
479
|
if (
|
|
495
|
-
Exit.isFailure(ex) &&
|
|
480
|
+
Exit.isFailure(ex) === true &&
|
|
496
481
|
Exit.isInterrupted(ex) === false &&
|
|
497
482
|
Schema.is(IntentionalShutdownCause)(Cause.squash(ex.cause)) === false &&
|
|
498
483
|
Schema.is(StoreInterrupted)(Cause.squash(ex.cause)) === false
|
|
@@ -502,7 +487,7 @@ export const makePersistedAdapter =
|
|
|
502
487
|
yield* Effect.logDebug('[@livestore/adapter-web:client-session] client-session shutdown', gotLocky, ex)
|
|
503
488
|
}
|
|
504
489
|
|
|
505
|
-
if (gotLocky) {
|
|
490
|
+
if (gotLocky === true) {
|
|
506
491
|
yield* Deferred.succeed(lockDeferred, undefined)
|
|
507
492
|
}
|
|
508
493
|
}).pipe(Effect.tapCauseLogPretty, Effect.orDie),
|
|
@@ -510,8 +495,7 @@ export const makePersistedAdapter =
|
|
|
510
495
|
|
|
511
496
|
const leaderThread: ClientSession['leaderThread'] = {
|
|
512
497
|
export: runInWorker(new WorkerSchema.LeaderWorkerInnerExport()).pipe(
|
|
513
|
-
Effect.
|
|
514
|
-
UnknownError.mapToUnknownError,
|
|
498
|
+
Effect.timeoutOrDie(10_000),
|
|
515
499
|
Effect.withSpan('@livestore/adapter-web:client-session:export'),
|
|
516
500
|
),
|
|
517
501
|
|
|
@@ -538,14 +522,12 @@ export const makePersistedAdapter =
|
|
|
538
522
|
},
|
|
539
523
|
|
|
540
524
|
getEventlogData: runInWorker(new WorkerSchema.LeaderWorkerInnerExportEventlog()).pipe(
|
|
541
|
-
Effect.
|
|
542
|
-
UnknownError.mapToUnknownError,
|
|
525
|
+
Effect.timeoutOrDie(10_000),
|
|
543
526
|
Effect.withSpan('@livestore/adapter-web:client-session:getEventlogData'),
|
|
544
527
|
),
|
|
545
528
|
|
|
546
529
|
syncState: Subscribable.make({
|
|
547
530
|
get: runInWorker(new WorkerSchema.LeaderWorkerInnerGetLeaderSyncState()).pipe(
|
|
548
|
-
UnknownError.mapToUnknownError,
|
|
549
531
|
Effect.withSpan('@livestore/adapter-web:client-session:getLeaderSyncState'),
|
|
550
532
|
),
|
|
551
533
|
changes: runInWorkerStream(new WorkerSchema.LeaderWorkerInnerSyncStateStream()).pipe(Stream.orDie),
|
|
@@ -553,12 +535,11 @@ export const makePersistedAdapter =
|
|
|
553
535
|
|
|
554
536
|
sendDevtoolsMessage: (message) =>
|
|
555
537
|
runInWorker(new WorkerSchema.LeaderWorkerInnerExtraDevtoolsMessage({ message })).pipe(
|
|
556
|
-
UnknownError.mapToUnknownError,
|
|
557
538
|
Effect.withSpan('@livestore/adapter-web:client-session:devtoolsMessageForLeader'),
|
|
558
539
|
),
|
|
559
540
|
networkStatus: Subscribable.make({
|
|
560
|
-
get: runInWorker(new WorkerSchema.LeaderWorkerInnerGetNetworkStatus())
|
|
561
|
-
changes: runInWorkerStream(new WorkerSchema.LeaderWorkerInnerNetworkStatusStream())
|
|
541
|
+
get: runInWorker(new WorkerSchema.LeaderWorkerInnerGetNetworkStatus()),
|
|
542
|
+
changes: runInWorkerStream(new WorkerSchema.LeaderWorkerInnerNetworkStatusStream()),
|
|
562
543
|
}),
|
|
563
544
|
}
|
|
564
545
|
|
|
@@ -601,7 +582,7 @@ const getPersistedId = (key: string, storageType: 'session' | 'local') => {
|
|
|
601
582
|
? sessionStorage
|
|
602
583
|
: storageType === 'local'
|
|
603
584
|
? localStorage
|
|
604
|
-
: shouldNeverHappen(`[@livestore/adapter-web] Invalid storage type: ${storageType}`)
|
|
585
|
+
: shouldNeverHappen(`[@livestore/adapter-web] Invalid storage type: ${String(storageType)}`)
|
|
605
586
|
|
|
606
587
|
// in case of a worker, we need the id of the parent window, to keep the id consistent
|
|
607
588
|
// we also need to handle the case where there are multiple workers being spawned by the same window
|
|
@@ -612,7 +593,7 @@ const getPersistedId = (key: string, storageType: 'session' | 'local') => {
|
|
|
612
593
|
const fullKey = `livestore:${key}`
|
|
613
594
|
const storedKey = storage.getItem(fullKey)
|
|
614
595
|
|
|
615
|
-
if (storedKey) return storedKey
|
|
596
|
+
if (storedKey !== null) return storedKey
|
|
616
597
|
|
|
617
598
|
const newKey = makeId()
|
|
618
599
|
storage.setItem(fullKey, newKey)
|
|
@@ -623,7 +604,7 @@ const getPersistedId = (key: string, storageType: 'session' | 'local') => {
|
|
|
623
604
|
const ensureBrowserRequirements = Effect.gen(function* () {
|
|
624
605
|
const validate = (condition: boolean, label: string) =>
|
|
625
606
|
Effect.gen(function* () {
|
|
626
|
-
if (condition) {
|
|
607
|
+
if (condition === true) {
|
|
627
608
|
return yield* UnknownError.make({
|
|
628
609
|
cause: `[@livestore/adapter-web] Browser not supported. The LiveStore web adapter needs '${label}' to work properly`,
|
|
629
610
|
})
|
|
@@ -654,7 +635,7 @@ const checkOpfsAvailability = Effect.gen(function* () {
|
|
|
654
635
|
Effect.as(undefined),
|
|
655
636
|
Effect.catchAll((error) => {
|
|
656
637
|
const reason: BootWarningReason =
|
|
657
|
-
Schema.is(WebError.SecurityError)(error) || Schema.is(WebError.NotAllowedError)(error)
|
|
638
|
+
Schema.is(WebError.SecurityError)(error) === true || Schema.is(WebError.NotAllowedError)(error) === true
|
|
658
639
|
? 'private-browsing'
|
|
659
640
|
: 'storage-unavailable'
|
|
660
641
|
const message =
|
|
@@ -16,4 +16,4 @@ if (isServerRuntime === false) {
|
|
|
16
16
|
sqlite3Promise = loadSqlite3Wasm()
|
|
17
17
|
}
|
|
18
18
|
|
|
19
|
-
export const loadSqlite3 = () => (isServerRuntime ? loadSqlite3Wasm() : (sqlite3Promise ?? loadSqlite3Wasm()))
|
|
19
|
+
export const loadSqlite3 = () => (isServerRuntime === true ? loadSqlite3Wasm() : (sqlite3Promise ?? loadSqlite3Wasm()))
|
|
@@ -8,9 +8,10 @@ import {
|
|
|
8
8
|
import { isDevEnv } from '@livestore/utils'
|
|
9
9
|
import { Chunk, Effect, Option, Order, Schedule, Schema, Stream } from '@livestore/utils/effect'
|
|
10
10
|
import { Opfs, type WebError } from '@livestore/utils/effect/browser'
|
|
11
|
+
|
|
11
12
|
import type * as WorkerSchema from './worker-schema.ts'
|
|
12
13
|
|
|
13
|
-
export class PersistedSqliteError extends Schema.TaggedError<PersistedSqliteError>()('PersistedSqliteError', {
|
|
14
|
+
export class PersistedSqliteError extends Schema.TaggedError<PersistedSqliteError>('~@livestore/adapter-web/PersistedSqliteError')('PersistedSqliteError', {
|
|
14
15
|
message: Schema.String,
|
|
15
16
|
cause: Schema.optional(Schema.Defect),
|
|
16
17
|
}) {}
|
|
@@ -56,7 +57,7 @@ export const readPersistedStateDbFromClientSession: (args: {
|
|
|
56
57
|
Stream.runHead,
|
|
57
58
|
)
|
|
58
59
|
|
|
59
|
-
if (Option.isNone(stateDbFileOption)) {
|
|
60
|
+
if (Option.isNone(stateDbFileOption) === true) {
|
|
60
61
|
return yield* new PersistedSqliteError({
|
|
61
62
|
message: `State database file not found in client session (expected '${stateDbFileName}' in '${accessHandlePoolDirString}')`,
|
|
62
63
|
})
|
|
@@ -90,7 +91,7 @@ export const resetPersistedDataFromClientSession = Effect.fn(
|
|
|
90
91
|
const directory = yield* sanitizeOpfsDir(storageOptions.directory, storeId)
|
|
91
92
|
yield* Opfs.remove(directory, { recursive: true }).pipe(
|
|
92
93
|
// We ignore NotFoundError here as it may not exist or have already been deleted
|
|
93
|
-
Effect.catchTag('
|
|
94
|
+
Effect.catchTag('NotFoundError', () => Effect.void),
|
|
94
95
|
)
|
|
95
96
|
},
|
|
96
97
|
Effect.retry({
|
|
@@ -106,7 +107,7 @@ export const sanitizeOpfsDir = Effect.fn('@livestore/adapter-web:sanitizeOpfsDir
|
|
|
106
107
|
return `livestore-${storeId}@${liveStoreStorageFormatVersion}`
|
|
107
108
|
}
|
|
108
109
|
|
|
109
|
-
if (directory.includes('/')) {
|
|
110
|
+
if (directory.includes('/') === true) {
|
|
110
111
|
return yield* new PersistedSqliteError({
|
|
111
112
|
message: `Nested directories are not yet supported ('${directory}')`,
|
|
112
113
|
})
|
|
@@ -183,12 +184,13 @@ export const cleanupOldStateDbFiles: (options: {
|
|
|
183
184
|
}
|
|
184
185
|
|
|
185
186
|
const absoluteArchiveDirName = `${opfsDirectory}/${ARCHIVE_DIR_NAME}`
|
|
186
|
-
if (isDev &&
|
|
187
|
+
if (isDev === true && (yield* Opfs.exists(absoluteArchiveDirName)) === false)
|
|
188
|
+
yield* Opfs.makeDirectory(absoluteArchiveDirName)
|
|
187
189
|
|
|
188
190
|
for (const path of oldStateDbPaths) {
|
|
189
|
-
const fileName = path.startsWith('/') ? path.slice(1) : path
|
|
191
|
+
const fileName = path.startsWith('/') === true ? path.slice(1) : path
|
|
190
192
|
|
|
191
|
-
if (isDev) {
|
|
193
|
+
if (isDev === true) {
|
|
192
194
|
const archiveFileData = yield* vfs.readFilePayload(fileName)
|
|
193
195
|
|
|
194
196
|
const archiveFileName = `${Date.now()}-${fileName}`
|
|
@@ -201,7 +203,7 @@ export const cleanupOldStateDbFiles: (options: {
|
|
|
201
203
|
const supportsCreateWritable =
|
|
202
204
|
typeof FileSystemFileHandle !== 'undefined' && 'createWritable' in FileSystemFileHandle.prototype
|
|
203
205
|
|
|
204
|
-
if (supportsCreateWritable) {
|
|
206
|
+
if (supportsCreateWritable === true) {
|
|
205
207
|
yield* Opfs.writeFile(archivePath, archiveData)
|
|
206
208
|
} else {
|
|
207
209
|
yield* Opfs.syncWriteFile(archivePath, archiveData)
|
|
@@ -225,7 +227,7 @@ export const cleanupOldStateDbFiles: (options: {
|
|
|
225
227
|
yield* Effect.logDebug(`Deleted old state database file: ${fileName}`)
|
|
226
228
|
}
|
|
227
229
|
|
|
228
|
-
if (isDev) {
|
|
230
|
+
if (isDev === true) {
|
|
229
231
|
yield* pruneArchiveDirectory({
|
|
230
232
|
archiveDirectory: absoluteArchiveDirName,
|
|
231
233
|
keep: MAX_ARCHIVED_STATE_DBS_IN_DEV,
|
|
@@ -248,6 +250,7 @@ const pruneArchiveDirectory = Effect.fn('@livestore/adapter-web:pruneArchiveDire
|
|
|
248
250
|
Stream.runCollect,
|
|
249
251
|
)
|
|
250
252
|
const filesToDelete = filesWithMetadata.pipe(
|
|
253
|
+
// oxlint-disable-next-line unicorn/no-array-sort -- false positive: Effect Chunk.sort is immutable, not Array#sort (https://github.com/oxc-project/oxc/issues/19110)
|
|
251
254
|
Chunk.sort(Order.mapInput(Order.number, (entry: { lastModified: number }) => entry.lastModified)),
|
|
252
255
|
Chunk.drop(keep),
|
|
253
256
|
Chunk.toReadonlyArray,
|