@livestore/adapter-web 0.4.0-dev.2 → 0.4.0-dev.20
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 +15 -5
- package/dist/in-memory/in-memory-adapter.d.ts.map +1 -1
- package/dist/in-memory/in-memory-adapter.js +29 -15
- package/dist/in-memory/in-memory-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 +14 -3
- package/dist/web-worker/client-session/client-session-devtools.js.map +1 -1
- package/dist/web-worker/client-session/persisted-adapter.d.ts +15 -0
- package/dist/web-worker/client-session/persisted-adapter.d.ts.map +1 -1
- package/dist/web-worker/client-session/persisted-adapter.js +67 -46
- 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 +23 -7
- package/dist/web-worker/common/persisted-sqlite.d.ts.map +1 -1
- package/dist/web-worker/common/persisted-sqlite.js +114 -76
- 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 +103 -58
- package/dist/web-worker/common/worker-schema.d.ts.map +1 -1
- package/dist/web-worker/common/worker-schema.js +48 -36
- package/dist/web-worker/common/worker-schema.js.map +1 -1
- package/dist/web-worker/leader-worker/make-leader-worker.d.ts +4 -2
- package/dist/web-worker/leader-worker/make-leader-worker.d.ts.map +1 -1
- package/dist/web-worker/leader-worker/make-leader-worker.js +47 -21
- 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 +65 -49
- package/dist/web-worker/shared-worker/make-shared-worker.js.map +1 -1
- package/dist/web-worker/vite-dev-polyfill.js +1 -0
- package/dist/web-worker/vite-dev-polyfill.js.map +1 -1
- package/package.json +8 -9
- package/src/in-memory/in-memory-adapter.ts +36 -20
- package/src/web-worker/ambient.d.ts +7 -24
- package/src/web-worker/client-session/client-session-devtools.ts +18 -3
- package/src/web-worker/client-session/persisted-adapter.ts +112 -59
- package/src/web-worker/client-session/sqlite-loader.ts +19 -0
- package/src/web-worker/common/persisted-sqlite.ts +219 -113
- 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 +62 -35
- package/src/web-worker/leader-worker/make-leader-worker.ts +58 -33
- package/src/web-worker/shared-worker/make-shared-worker.ts +95 -75
- package/src/web-worker/vite-dev-polyfill.ts +1 -0
- 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
|
@@ -6,7 +6,7 @@ import {
|
|
|
6
6
|
makeClientSession,
|
|
7
7
|
migrateDb,
|
|
8
8
|
type SyncOptions,
|
|
9
|
-
|
|
9
|
+
UnknownError,
|
|
10
10
|
} from '@livestore/common'
|
|
11
11
|
import type { DevtoolsOptions, LeaderSqliteDb } from '@livestore/common/leader-thread'
|
|
12
12
|
import { configureConnection, Eventlog, LeaderThreadCtx, makeLeaderThreadLayer } from '@livestore/common/leader-thread'
|
|
@@ -16,19 +16,17 @@ import * as DevtoolsWeb from '@livestore/devtools-web-common/web-channel'
|
|
|
16
16
|
import type * as WebmeshWorker from '@livestore/devtools-web-common/worker'
|
|
17
17
|
import type { MakeWebSqliteDb } from '@livestore/sqlite-wasm/browser'
|
|
18
18
|
import { sqliteDbFactory } from '@livestore/sqlite-wasm/browser'
|
|
19
|
-
import { loadSqlite3Wasm } from '@livestore/sqlite-wasm/load-wasm'
|
|
20
19
|
import { tryAsFunctionAndNew } from '@livestore/utils'
|
|
21
|
-
import type {
|
|
22
|
-
import {
|
|
20
|
+
import type { Scope } from '@livestore/utils/effect'
|
|
21
|
+
import { Effect, FetchHttpClient, Fiber, Layer, type Schema, SubscriptionRef, Worker } from '@livestore/utils/effect'
|
|
22
|
+
import { BrowserWorker } from '@livestore/utils/effect/browser'
|
|
23
23
|
import { nanoid } from '@livestore/utils/nanoid'
|
|
24
24
|
import * as Webmesh from '@livestore/webmesh'
|
|
25
25
|
|
|
26
26
|
import { connectWebmeshNodeClientSession } from '../web-worker/client-session/client-session-devtools.ts'
|
|
27
|
+
import { loadSqlite3 } from '../web-worker/client-session/sqlite-loader.ts'
|
|
27
28
|
import { makeShutdownChannel } from '../web-worker/common/shutdown-channel.ts'
|
|
28
29
|
|
|
29
|
-
// NOTE we're starting to initialize the sqlite wasm binary here to speed things up
|
|
30
|
-
const sqlite3Promise = loadSqlite3Wasm()
|
|
31
|
-
|
|
32
30
|
export interface InMemoryAdapterOptions {
|
|
33
31
|
importSnapshot?: Uint8Array<ArrayBuffer>
|
|
34
32
|
sync?: SyncOptions
|
|
@@ -55,12 +53,22 @@ export interface InMemoryAdapterOptions {
|
|
|
55
53
|
}
|
|
56
54
|
}
|
|
57
55
|
|
|
56
|
+
/**
|
|
57
|
+
* Create a web-only in-memory LiveStore adapter.
|
|
58
|
+
*
|
|
59
|
+
* - Runs entirely in memory: fast, zero I/O, great for tests, sandboxes, or ephemeral sessions.
|
|
60
|
+
* - Works across browser execution contexts: Window, WebWorker, SharedWorker, and ServiceWorker.
|
|
61
|
+
* - DevTools: to inspect this adapter from the browser DevTools, provide a `sharedWorker` in `options.devtools`.
|
|
62
|
+
* (The shared worker is used to bridge the DevTools UI to the running session.)
|
|
63
|
+
* - No persistence support: nothing is written to OPFS/IndexedDB/localStorage. `importSnapshot`
|
|
64
|
+
* can bootstrap initial state only; subsequent changes are not persisted anywhere.
|
|
65
|
+
*/
|
|
58
66
|
export const makeInMemoryAdapter =
|
|
59
67
|
(options: InMemoryAdapterOptions = {}): Adapter =>
|
|
60
68
|
(adapterArgs) =>
|
|
61
69
|
Effect.gen(function* () {
|
|
62
|
-
const { schema, shutdown,
|
|
63
|
-
const sqlite3 = yield* Effect.promise(() =>
|
|
70
|
+
const { schema, shutdown, syncPayloadEncoded, syncPayloadSchema, storeId, devtoolsEnabled } = adapterArgs
|
|
71
|
+
const sqlite3 = yield* Effect.promise(() => loadSqlite3())
|
|
64
72
|
|
|
65
73
|
const sqliteDb = yield* sqliteDbFactory({ sqlite3 })({ _tag: 'in-memory' })
|
|
66
74
|
|
|
@@ -80,7 +88,7 @@ export const makeInMemoryAdapter =
|
|
|
80
88
|
}).pipe(
|
|
81
89
|
Effect.provide(BrowserWorker.layer(() => sharedWebWorker)),
|
|
82
90
|
Effect.tapCauseLogPretty,
|
|
83
|
-
|
|
91
|
+
UnknownError.mapToUnknownError,
|
|
84
92
|
Effect.forkScoped,
|
|
85
93
|
)
|
|
86
94
|
: undefined
|
|
@@ -91,7 +99,8 @@ export const makeInMemoryAdapter =
|
|
|
91
99
|
clientId,
|
|
92
100
|
makeSqliteDb: sqliteDbFactory({ sqlite3 }),
|
|
93
101
|
syncOptions: options.sync,
|
|
94
|
-
|
|
102
|
+
syncPayloadEncoded,
|
|
103
|
+
syncPayloadSchema,
|
|
95
104
|
importSnapshot: options.importSnapshot,
|
|
96
105
|
devtoolsEnabled,
|
|
97
106
|
sharedWorkerFiber,
|
|
@@ -111,6 +120,8 @@ export const makeInMemoryAdapter =
|
|
|
111
120
|
lockStatus,
|
|
112
121
|
shutdown,
|
|
113
122
|
webmeshMode: 'direct',
|
|
123
|
+
// Can be undefined in Node.js
|
|
124
|
+
origin: globalThis.location?.origin,
|
|
114
125
|
connectWebmeshNode: ({ sessionInfo, webmeshNode }) =>
|
|
115
126
|
Effect.gen(function* () {
|
|
116
127
|
if (sharedWorkerFiber === undefined || devtoolsEnabled === false) {
|
|
@@ -132,7 +143,7 @@ export const makeInMemoryAdapter =
|
|
|
132
143
|
})
|
|
133
144
|
|
|
134
145
|
return clientSession
|
|
135
|
-
}).pipe(
|
|
146
|
+
}).pipe(UnknownError.mapToUnknownError, Effect.provide(FetchHttpClient.layer))
|
|
136
147
|
|
|
137
148
|
export interface MakeLeaderThreadArgs {
|
|
138
149
|
schema: LiveStoreSchema
|
|
@@ -140,7 +151,8 @@ export interface MakeLeaderThreadArgs {
|
|
|
140
151
|
clientId: string
|
|
141
152
|
makeSqliteDb: MakeWebSqliteDb
|
|
142
153
|
syncOptions: SyncOptions | undefined
|
|
143
|
-
|
|
154
|
+
syncPayloadEncoded: Schema.JsonValue | undefined
|
|
155
|
+
syncPayloadSchema: Schema.Schema<any> | undefined
|
|
144
156
|
importSnapshot: Uint8Array<ArrayBuffer> | undefined
|
|
145
157
|
devtoolsEnabled: boolean
|
|
146
158
|
sharedWorkerFiber: SharedWorkerFiber | undefined
|
|
@@ -152,7 +164,8 @@ const makeLeaderThread = ({
|
|
|
152
164
|
clientId,
|
|
153
165
|
makeSqliteDb,
|
|
154
166
|
syncOptions,
|
|
155
|
-
|
|
167
|
+
syncPayloadEncoded,
|
|
168
|
+
syncPayloadSchema,
|
|
156
169
|
importSnapshot,
|
|
157
170
|
devtoolsEnabled,
|
|
158
171
|
sharedWorkerFiber,
|
|
@@ -199,12 +212,14 @@ const makeLeaderThread = ({
|
|
|
199
212
|
dbEventlog,
|
|
200
213
|
devtoolsOptions,
|
|
201
214
|
shutdownChannel,
|
|
202
|
-
|
|
215
|
+
syncPayloadEncoded,
|
|
216
|
+
syncPayloadSchema,
|
|
203
217
|
}),
|
|
204
218
|
)
|
|
205
219
|
|
|
206
220
|
return yield* Effect.gen(function* () {
|
|
207
|
-
const { dbState, dbEventlog, syncProcessor, extraIncomingMessagesQueue, initialState } =
|
|
221
|
+
const { dbState, dbEventlog, syncProcessor, extraIncomingMessagesQueue, initialState, networkStatus } =
|
|
222
|
+
yield* LeaderThreadCtx
|
|
208
223
|
|
|
209
224
|
const initialLeaderHead = Eventlog.getClientHeadFromDb(dbEventlog)
|
|
210
225
|
|
|
@@ -213,15 +228,16 @@ const makeLeaderThread = ({
|
|
|
213
228
|
pull: ({ cursor }) => syncProcessor.pull({ cursor }),
|
|
214
229
|
push: (batch) =>
|
|
215
230
|
syncProcessor.push(
|
|
216
|
-
batch.map((item) => new LiveStoreEvent.EncodedWithMeta(item)),
|
|
231
|
+
batch.map((item) => new LiveStoreEvent.Client.EncodedWithMeta(item)),
|
|
217
232
|
{ waitForProcessing: true },
|
|
218
233
|
),
|
|
219
234
|
},
|
|
220
235
|
initialState: { leaderHead: initialLeaderHead, migrationsReport: initialState.migrationsReport },
|
|
221
236
|
export: Effect.sync(() => dbState.export()),
|
|
222
237
|
getEventlogData: Effect.sync(() => dbEventlog.export()),
|
|
223
|
-
|
|
238
|
+
syncState: syncProcessor.syncState,
|
|
224
239
|
sendDevtoolsMessage: (message) => extraIncomingMessagesQueue.offer(message),
|
|
240
|
+
networkStatus,
|
|
225
241
|
})
|
|
226
242
|
|
|
227
243
|
const initialSnapshot = dbState.export()
|
|
@@ -232,7 +248,7 @@ const makeLeaderThread = ({
|
|
|
232
248
|
|
|
233
249
|
type SharedWorkerFiber = Fiber.Fiber<
|
|
234
250
|
Worker.SerializedWorkerPool<typeof WebmeshWorker.Schema.Request.Type>,
|
|
235
|
-
|
|
251
|
+
UnknownError
|
|
236
252
|
>
|
|
237
253
|
|
|
238
254
|
const makeDevtoolsOptions = ({
|
|
@@ -249,7 +265,7 @@ const makeDevtoolsOptions = ({
|
|
|
249
265
|
dbEventlog: LeaderSqliteDb
|
|
250
266
|
storeId: string
|
|
251
267
|
clientId: string
|
|
252
|
-
}): Effect.Effect<DevtoolsOptions,
|
|
268
|
+
}): Effect.Effect<DevtoolsOptions, UnknownError, Scope.Scope> =>
|
|
253
269
|
Effect.gen(function* () {
|
|
254
270
|
if (devtoolsEnabled === false || sharedWorkerFiber === undefined) {
|
|
255
271
|
return { enabled: false }
|
|
@@ -1,23 +1,3 @@
|
|
|
1
|
-
// TODO remove OPFS stuff
|
|
2
|
-
// https://developer.mozilla.org/en-US/docs/Web/API/FileSystemSyncAccessHandle
|
|
3
|
-
interface FileSystemSyncAccessHandle {
|
|
4
|
-
close: () => void
|
|
5
|
-
flush: () => Promise<void>
|
|
6
|
-
getSize: () => number
|
|
7
|
-
read: (buffer: ArrayBuffer, options?: FileSystemReadWriteOptions) => number
|
|
8
|
-
truncate: (newSize: number) => void
|
|
9
|
-
write: (buffer: ArrayBuffer, options?: FileSystemReadWriteOptions) => number
|
|
10
|
-
seek: (offset: number) => void
|
|
11
|
-
}
|
|
12
|
-
|
|
13
|
-
interface FileSystemReadWriteOptions {
|
|
14
|
-
at?: number
|
|
15
|
-
}
|
|
16
|
-
|
|
17
|
-
interface FileSystemFileHandle {
|
|
18
|
-
createSyncAccessHandle: () => Promise<FileSystemSyncAccessHandle>
|
|
19
|
-
}
|
|
20
|
-
|
|
21
1
|
// TODO bring back when Vite limitation is resolved https://github.com/vitejs/vite/issues/8427
|
|
22
2
|
// declare module '*?sharedworker' {
|
|
23
3
|
// const sharedWorkerConstructor: {
|
|
@@ -26,11 +6,14 @@ interface FileSystemFileHandle {
|
|
|
26
6
|
// export default sharedWorkerConstructor
|
|
27
7
|
// }
|
|
28
8
|
|
|
9
|
+
declare interface ImportMetaEnv {
|
|
10
|
+
readonly SSR?: string | boolean
|
|
11
|
+
readonly DEV: boolean | undefined
|
|
12
|
+
readonly VITE_LIVESTORE_EXPERIMENTAL_SYNC_NEXT: boolean | undefined
|
|
13
|
+
}
|
|
14
|
+
|
|
29
15
|
declare interface ImportMeta {
|
|
30
|
-
env:
|
|
31
|
-
DEV: boolean | undefined
|
|
32
|
-
VITE_LIVESTORE_EXPERIMENTAL_SYNC_NEXT: boolean | undefined
|
|
33
|
-
}
|
|
16
|
+
readonly env: ImportMetaEnv
|
|
34
17
|
}
|
|
35
18
|
|
|
36
19
|
declare var __debugLiveStoreUtils: any
|
|
@@ -1,9 +1,10 @@
|
|
|
1
|
-
import { Devtools } from '@livestore/common'
|
|
1
|
+
import { Devtools, liveStoreVersion } from '@livestore/common'
|
|
2
2
|
import type { LiveStoreSchema } from '@livestore/common/schema'
|
|
3
3
|
import * as DevtoolsWeb from '@livestore/devtools-web-common/web-channel'
|
|
4
4
|
import { isDevEnv } from '@livestore/utils'
|
|
5
5
|
import type { Worker } from '@livestore/utils/effect'
|
|
6
|
-
import { Effect, Stream
|
|
6
|
+
import { Effect, Stream } from '@livestore/utils/effect'
|
|
7
|
+
import { WebChannelBrowser } from '@livestore/utils/effect/browser'
|
|
7
8
|
import * as Webmesh from '@livestore/webmesh'
|
|
8
9
|
|
|
9
10
|
export const logDevtoolsUrl = ({
|
|
@@ -30,6 +31,20 @@ export const logDevtoolsUrl = ({
|
|
|
30
31
|
const url = `${devtoolsBaseUrl}/web/${storeId}/${clientId}/${sessionId}/${schema.devtools.alias}`
|
|
31
32
|
yield* Effect.log(`[@livestore/adapter-web] Devtools ready on ${url}`)
|
|
32
33
|
}
|
|
34
|
+
|
|
35
|
+
// Check for DevTools Chrome extension presence via iframe container the extension injects
|
|
36
|
+
const hasExt = document.querySelector('[id^="livestore-devtools-iframe-"]') !== null
|
|
37
|
+
if (!hasExt) {
|
|
38
|
+
const g = globalThis as { __livestoreDevtoolsChromeNoticeShown?: boolean }
|
|
39
|
+
if (g.__livestoreDevtoolsChromeNoticeShown !== true) {
|
|
40
|
+
g.__livestoreDevtoolsChromeNoticeShown = true
|
|
41
|
+
|
|
42
|
+
const urlToLog = `https://github.com/livestorejs/livestore/releases/download/v${liveStoreVersion}/livestore-devtools-chrome-${liveStoreVersion}.zip`
|
|
43
|
+
yield* Effect.log(
|
|
44
|
+
`[@livestore/adapter-web] LiveStore DevTools Chrome extension not detected. Install v${liveStoreVersion}: ${urlToLog}`,
|
|
45
|
+
)
|
|
46
|
+
}
|
|
47
|
+
}
|
|
33
48
|
}
|
|
34
49
|
}
|
|
35
50
|
}).pipe(Effect.withSpan('@livestore/adapter-web:client-session:devtools:logDevtoolsUrl'))
|
|
@@ -69,7 +84,7 @@ export const connectWebmeshNodeClientSession = Effect.fn(function* ({
|
|
|
69
84
|
|
|
70
85
|
const contentscriptMainNodeName = DevtoolsWeb.makeNodeName.browserExtension.contentscriptMain(tabId)
|
|
71
86
|
|
|
72
|
-
const contentscriptMainChannel = yield*
|
|
87
|
+
const contentscriptMainChannel = yield* WebChannelBrowser.windowChannel({
|
|
73
88
|
listenWindow: window,
|
|
74
89
|
sendWindow: window,
|
|
75
90
|
schema: Webmesh.WebmeshSchema.Packet,
|
|
@@ -5,48 +5,46 @@ import {
|
|
|
5
5
|
makeClientSession,
|
|
6
6
|
StoreInterrupted,
|
|
7
7
|
sessionChangesetMetaTable,
|
|
8
|
-
|
|
8
|
+
UnknownError,
|
|
9
9
|
} from '@livestore/common'
|
|
10
10
|
// TODO bring back - this currently doesn't work due to https://github.com/vitejs/vite/issues/8427
|
|
11
11
|
// NOTE We're using a non-relative import here for Vite to properly resolve the import during app builds
|
|
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 { loadSqlite3Wasm } from '@livestore/sqlite-wasm/load-wasm'
|
|
16
15
|
import { isDevEnv, shouldNeverHappen, tryAsFunctionAndNew } from '@livestore/utils'
|
|
17
16
|
import {
|
|
18
|
-
BrowserWorker,
|
|
19
17
|
Cause,
|
|
20
18
|
Deferred,
|
|
21
19
|
Effect,
|
|
22
20
|
Exit,
|
|
23
21
|
Fiber,
|
|
22
|
+
Layer,
|
|
24
23
|
ParseResult,
|
|
25
24
|
Queue,
|
|
26
25
|
Schema,
|
|
27
26
|
Stream,
|
|
27
|
+
Subscribable,
|
|
28
28
|
SubscriptionRef,
|
|
29
|
-
WebLock,
|
|
30
29
|
Worker,
|
|
31
30
|
WorkerError,
|
|
32
31
|
} from '@livestore/utils/effect'
|
|
32
|
+
import { BrowserWorker, Opfs, WebLock } from '@livestore/utils/effect/browser'
|
|
33
33
|
import { nanoid } from '@livestore/utils/nanoid'
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
34
|
+
import {
|
|
35
|
+
readPersistedStateDbFromClientSession,
|
|
36
|
+
resetPersistedDataFromClientSession,
|
|
37
|
+
} from '../common/persisted-sqlite.ts'
|
|
37
38
|
import { makeShutdownChannel } from '../common/shutdown-channel.ts'
|
|
38
39
|
import { DedicatedWorkerDisconnectBroadcast, makeWorkerDisconnectChannel } from '../common/worker-disconnect-channel.ts'
|
|
39
40
|
import * as WorkerSchema from '../common/worker-schema.ts'
|
|
40
41
|
import { connectWebmeshNodeClientSession } from './client-session-devtools.ts'
|
|
41
|
-
|
|
42
|
-
// NOTE we're starting to initialize the sqlite wasm binary here to speed things up
|
|
43
|
-
const sqlite3Promise = loadSqlite3Wasm()
|
|
42
|
+
import { loadSqlite3 } from './sqlite-loader.ts'
|
|
44
43
|
|
|
45
44
|
if (isDevEnv()) {
|
|
46
45
|
globalThis.__debugLiveStoreUtils = {
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
runFork: (effect: Effect.Effect<any, any, never>) => Effect.runFork(effect),
|
|
46
|
+
...globalThis.__debugLiveStoreUtils,
|
|
47
|
+
opfs: Opfs.debugUtils,
|
|
50
48
|
}
|
|
51
49
|
}
|
|
52
50
|
|
|
@@ -100,6 +98,21 @@ export type WebAdapterOptions = {
|
|
|
100
98
|
* @default false
|
|
101
99
|
*/
|
|
102
100
|
disableFastPath?: boolean
|
|
101
|
+
/**
|
|
102
|
+
* Controls whether to wait for the shared worker to be terminated when LiveStore gets shut down.
|
|
103
|
+
* This prevents a race condition where a new LiveStore instance connects to a shutting-down shared
|
|
104
|
+
* worker from a previous instance.
|
|
105
|
+
*
|
|
106
|
+
* @default false
|
|
107
|
+
*
|
|
108
|
+
* @remarks
|
|
109
|
+
*
|
|
110
|
+
* In multi-tab scenarios, we don't want to await shared worker termination because the shared worker
|
|
111
|
+
* won't actually shut down when one tab closes - it stays alive to serve other tabs. Awaiting
|
|
112
|
+
* termination would cause unnecessary blocking since the termination will never happen until all
|
|
113
|
+
* tabs are closed.
|
|
114
|
+
*/
|
|
115
|
+
awaitSharedWorkerTermination?: boolean
|
|
103
116
|
}
|
|
104
117
|
}
|
|
105
118
|
|
|
@@ -124,15 +137,32 @@ export const makePersistedAdapter =
|
|
|
124
137
|
(options: WebAdapterOptions): Adapter =>
|
|
125
138
|
(adapterArgs) =>
|
|
126
139
|
Effect.gen(function* () {
|
|
127
|
-
const {
|
|
140
|
+
const {
|
|
141
|
+
schema,
|
|
142
|
+
storeId,
|
|
143
|
+
devtoolsEnabled,
|
|
144
|
+
debugInstanceId,
|
|
145
|
+
bootStatusQueue,
|
|
146
|
+
shutdown,
|
|
147
|
+
syncPayloadSchema: _syncPayloadSchema,
|
|
148
|
+
syncPayloadEncoded,
|
|
149
|
+
} = adapterArgs
|
|
150
|
+
|
|
151
|
+
// NOTE: The schema travels with the worker bundle (developers call
|
|
152
|
+
// `makeWorker({ schema, syncPayloadSchema })`). We only keep the
|
|
153
|
+
// destructured value here to document availability on the client session
|
|
154
|
+
// side—structured cloning the Effect schema into the worker is not
|
|
155
|
+
// possible, so we intentionally do not forward it.
|
|
156
|
+
void _syncPayloadSchema
|
|
128
157
|
|
|
129
158
|
yield* ensureBrowserRequirements
|
|
130
159
|
|
|
131
160
|
yield* Queue.offer(bootStatusQueue, { stage: 'loading' })
|
|
132
161
|
|
|
133
|
-
const sqlite3 = yield* Effect.promise(() =>
|
|
162
|
+
const sqlite3 = yield* Effect.promise(() => loadSqlite3())
|
|
134
163
|
|
|
135
164
|
const LIVESTORE_TAB_LOCK = `livestore-tab-lock-${storeId}`
|
|
165
|
+
const LIVESTORE_SHARED_WORKER_TERMINATION_LOCK = `livestore-shared-worker-termination-lock-${storeId}`
|
|
136
166
|
|
|
137
167
|
const storageOptions = yield* Schema.decode(WorkerSchema.StorageType)(options.storage)
|
|
138
168
|
|
|
@@ -153,7 +183,15 @@ export const makePersistedAdapter =
|
|
|
153
183
|
const dataFromFile =
|
|
154
184
|
options.experimental?.disableFastPath === true
|
|
155
185
|
? undefined
|
|
156
|
-
: yield*
|
|
186
|
+
: yield* readPersistedStateDbFromClientSession({ storageOptions, storeId, schema }).pipe(
|
|
187
|
+
Effect.tapError((error) =>
|
|
188
|
+
Effect.logDebug('[@livestore/adapter-web:client-session] Could not read persisted state db', error, {
|
|
189
|
+
storeId,
|
|
190
|
+
}),
|
|
191
|
+
),
|
|
192
|
+
// If we get any error here, we return `undefined` to fall back to the slow path
|
|
193
|
+
Effect.orElseSucceed(() => undefined),
|
|
194
|
+
)
|
|
157
195
|
|
|
158
196
|
// The same across all client sessions (i.e. tabs, windows)
|
|
159
197
|
const clientId = options.clientId ?? getPersistedId(`clientId:${storeId}`, 'local')
|
|
@@ -175,28 +213,20 @@ export const makePersistedAdapter =
|
|
|
175
213
|
|
|
176
214
|
const sharedWebWorker = tryAsFunctionAndNew(options.sharedWorker, { name: `livestore-shared-worker-${storeId}` })
|
|
177
215
|
|
|
216
|
+
if (options.experimental?.awaitSharedWorkerTermination) {
|
|
217
|
+
// Relying on the lock being available is currently the only mechanism we're aware of
|
|
218
|
+
// to know whether the shared worker has terminated.
|
|
219
|
+
yield* Effect.addFinalizer(() => WebLock.waitForLock(LIVESTORE_SHARED_WORKER_TERMINATION_LOCK))
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
const sharedWorkerContext = yield* Layer.build(BrowserWorker.layer(() => sharedWebWorker))
|
|
178
223
|
const sharedWorkerFiber = yield* Worker.makePoolSerialized<typeof WorkerSchema.SharedWorkerRequest.Type>({
|
|
179
224
|
size: 1,
|
|
180
225
|
concurrency: 100,
|
|
181
|
-
initialMessage: () =>
|
|
182
|
-
new WorkerSchema.SharedWorkerInitialMessage({
|
|
183
|
-
liveStoreVersion,
|
|
184
|
-
payload: {
|
|
185
|
-
_tag: 'FromClientSession',
|
|
186
|
-
initialMessage: new WorkerSchema.LeaderWorkerInnerInitialMessage({
|
|
187
|
-
storageOptions,
|
|
188
|
-
storeId,
|
|
189
|
-
clientId,
|
|
190
|
-
devtoolsEnabled,
|
|
191
|
-
debugInstanceId,
|
|
192
|
-
syncPayload,
|
|
193
|
-
}),
|
|
194
|
-
},
|
|
195
|
-
}),
|
|
196
226
|
}).pipe(
|
|
197
|
-
Effect.provide(
|
|
227
|
+
Effect.provide(sharedWorkerContext),
|
|
198
228
|
Effect.tapCauseLogPretty,
|
|
199
|
-
|
|
229
|
+
UnknownError.mapToUnknownError,
|
|
200
230
|
Effect.tapErrorCause((cause) => shutdown(Exit.failCause(cause))),
|
|
201
231
|
Effect.withSpan('@livestore/adapter-web:client-session:setupSharedWorker'),
|
|
202
232
|
Effect.forkScoped,
|
|
@@ -238,7 +268,7 @@ export const makePersistedAdapter =
|
|
|
238
268
|
initialMessage: () => new WorkerSchema.LeaderWorkerOuterInitialMessage({ port: mc.port1, storeId, clientId }),
|
|
239
269
|
}).pipe(
|
|
240
270
|
Effect.provide(BrowserWorker.layer(() => worker)),
|
|
241
|
-
|
|
271
|
+
UnknownError.mapToUnknownError,
|
|
242
272
|
Effect.tapErrorCause((cause) => shutdown(Exit.failCause(cause))),
|
|
243
273
|
Effect.withSpan('@livestore/adapter-web:client-session:setupDedicatedWorker'),
|
|
244
274
|
Effect.tapCauseLogPretty,
|
|
@@ -248,10 +278,25 @@ export const makePersistedAdapter =
|
|
|
248
278
|
yield* workerDisconnectChannel.send(DedicatedWorkerDisconnectBroadcast.make({}))
|
|
249
279
|
|
|
250
280
|
const sharedWorker = yield* Fiber.join(sharedWorkerFiber)
|
|
251
|
-
yield* sharedWorker
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
281
|
+
yield* sharedWorker
|
|
282
|
+
.executeEffect(
|
|
283
|
+
new WorkerSchema.SharedWorkerUpdateMessagePort({
|
|
284
|
+
port: mc.port2,
|
|
285
|
+
liveStoreVersion,
|
|
286
|
+
initial: new WorkerSchema.LeaderWorkerInnerInitialMessage({
|
|
287
|
+
storageOptions,
|
|
288
|
+
storeId,
|
|
289
|
+
clientId,
|
|
290
|
+
devtoolsEnabled,
|
|
291
|
+
debugInstanceId,
|
|
292
|
+
syncPayloadEncoded,
|
|
293
|
+
}),
|
|
294
|
+
}),
|
|
295
|
+
)
|
|
296
|
+
.pipe(
|
|
297
|
+
UnknownError.mapToUnknownError,
|
|
298
|
+
Effect.tapErrorCause((cause) => shutdown(Exit.failCause(cause))),
|
|
299
|
+
)
|
|
255
300
|
|
|
256
301
|
yield* Deferred.succeed(waitForSharedWorkerInitialized, undefined)
|
|
257
302
|
|
|
@@ -281,7 +326,7 @@ export const makePersistedAdapter =
|
|
|
281
326
|
const runInWorker = <TReq extends typeof WorkerSchema.SharedWorkerRequest.Type>(
|
|
282
327
|
req: TReq,
|
|
283
328
|
): TReq extends Schema.WithResult<infer A, infer _I, infer E, infer _EI, infer R>
|
|
284
|
-
? Effect.Effect<A,
|
|
329
|
+
? Effect.Effect<A, UnknownError | E, R>
|
|
285
330
|
: never =>
|
|
286
331
|
Fiber.join(sharedWorkerFiber).pipe(
|
|
287
332
|
// NOTE we need to wait for the shared worker to be initialized before we can send requests to it
|
|
@@ -296,28 +341,28 @@ export const makePersistedAdapter =
|
|
|
296
341
|
}),
|
|
297
342
|
Effect.withSpan(`@livestore/adapter-web:client-session:runInWorker:${req._tag}`),
|
|
298
343
|
Effect.mapError((cause) =>
|
|
299
|
-
Schema.is(
|
|
344
|
+
Schema.is(UnknownError)(cause)
|
|
300
345
|
? cause
|
|
301
346
|
: ParseResult.isParseError(cause) || Schema.is(WorkerError.WorkerError)(cause)
|
|
302
|
-
? new
|
|
347
|
+
? new UnknownError({ cause })
|
|
303
348
|
: cause,
|
|
304
349
|
),
|
|
305
|
-
Effect.catchAllDefect((cause) => new
|
|
350
|
+
Effect.catchAllDefect((cause) => new UnknownError({ cause })),
|
|
306
351
|
) as any
|
|
307
352
|
|
|
308
353
|
const runInWorkerStream = <TReq extends typeof WorkerSchema.SharedWorkerRequest.Type>(
|
|
309
354
|
req: TReq,
|
|
310
355
|
): TReq extends Schema.WithResult<infer A, infer _I, infer _E, infer _EI, infer R>
|
|
311
|
-
? Stream.Stream<A,
|
|
356
|
+
? Stream.Stream<A, UnknownError, R>
|
|
312
357
|
: never =>
|
|
313
358
|
Effect.gen(function* () {
|
|
314
359
|
const sharedWorker = yield* Fiber.join(sharedWorkerFiber)
|
|
315
360
|
return sharedWorker.execute(req as any).pipe(
|
|
316
361
|
Stream.mapError((cause) =>
|
|
317
|
-
Schema.is(
|
|
362
|
+
Schema.is(UnknownError)(cause)
|
|
318
363
|
? cause
|
|
319
364
|
: ParseResult.isParseError(cause) || Schema.is(WorkerError.WorkerError)(cause)
|
|
320
|
-
? new
|
|
365
|
+
? new UnknownError({ cause })
|
|
321
366
|
: cause,
|
|
322
367
|
),
|
|
323
368
|
Stream.withSpan(`@livestore/adapter-web:client-session:runInWorkerStream:${req._tag}`),
|
|
@@ -365,7 +410,7 @@ export const makePersistedAdapter =
|
|
|
365
410
|
const numberOfTables =
|
|
366
411
|
sqliteDb.select<{ count: number }>(`select count(*) as count from sqlite_master`)[0]?.count ?? 0
|
|
367
412
|
if (numberOfTables === 0) {
|
|
368
|
-
return yield*
|
|
413
|
+
return yield* UnknownError.make({
|
|
369
414
|
cause: `Encountered empty or corrupted database`,
|
|
370
415
|
payload: { snapshotByteLength: initialResult.snapshot.byteLength, storageOptions: options.storage },
|
|
371
416
|
})
|
|
@@ -384,12 +429,12 @@ export const makePersistedAdapter =
|
|
|
384
429
|
)
|
|
385
430
|
|
|
386
431
|
const initialLeaderHead = initialLeaderHeadRes
|
|
387
|
-
? EventSequenceNumber.make({
|
|
432
|
+
? EventSequenceNumber.Client.Composite.make({
|
|
388
433
|
global: initialLeaderHeadRes.seqNumGlobal,
|
|
389
434
|
client: initialLeaderHeadRes.seqNumClient,
|
|
390
435
|
rebaseGeneration: initialLeaderHeadRes.seqNumRebaseGeneration,
|
|
391
436
|
})
|
|
392
|
-
: EventSequenceNumber.ROOT
|
|
437
|
+
: EventSequenceNumber.Client.ROOT
|
|
393
438
|
|
|
394
439
|
// console.debug('[@livestore/adapter-web:client-session] initialLeaderHead', initialLeaderHead)
|
|
395
440
|
|
|
@@ -415,7 +460,7 @@ export const makePersistedAdapter =
|
|
|
415
460
|
const leaderThread: ClientSession['leaderThread'] = {
|
|
416
461
|
export: runInWorker(new WorkerSchema.LeaderWorkerInnerExport()).pipe(
|
|
417
462
|
Effect.timeout(10_000),
|
|
418
|
-
|
|
463
|
+
UnknownError.mapToUnknownError,
|
|
419
464
|
Effect.withSpan('@livestore/adapter-web:client-session:export'),
|
|
420
465
|
),
|
|
421
466
|
|
|
@@ -434,20 +479,27 @@ export const makePersistedAdapter =
|
|
|
434
479
|
|
|
435
480
|
getEventlogData: runInWorker(new WorkerSchema.LeaderWorkerInnerExportEventlog()).pipe(
|
|
436
481
|
Effect.timeout(10_000),
|
|
437
|
-
|
|
482
|
+
UnknownError.mapToUnknownError,
|
|
438
483
|
Effect.withSpan('@livestore/adapter-web:client-session:getEventlogData'),
|
|
439
484
|
),
|
|
440
485
|
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
486
|
+
syncState: Subscribable.make({
|
|
487
|
+
get: runInWorker(new WorkerSchema.LeaderWorkerInnerGetLeaderSyncState()).pipe(
|
|
488
|
+
UnknownError.mapToUnknownError,
|
|
489
|
+
Effect.withSpan('@livestore/adapter-web:client-session:getLeaderSyncState'),
|
|
490
|
+
),
|
|
491
|
+
changes: runInWorkerStream(new WorkerSchema.LeaderWorkerInnerSyncStateStream()).pipe(Stream.orDie),
|
|
492
|
+
}),
|
|
445
493
|
|
|
446
494
|
sendDevtoolsMessage: (message) =>
|
|
447
495
|
runInWorker(new WorkerSchema.LeaderWorkerInnerExtraDevtoolsMessage({ message })).pipe(
|
|
448
|
-
|
|
496
|
+
UnknownError.mapToUnknownError,
|
|
449
497
|
Effect.withSpan('@livestore/adapter-web:client-session:devtoolsMessageForLeader'),
|
|
450
498
|
),
|
|
499
|
+
networkStatus: Subscribable.make({
|
|
500
|
+
get: runInWorker(new WorkerSchema.LeaderWorkerInnerGetNetworkStatus()).pipe(Effect.orDie),
|
|
501
|
+
changes: runInWorkerStream(new WorkerSchema.LeaderWorkerInnerNetworkStatusStream()).pipe(Stream.orDie),
|
|
502
|
+
}),
|
|
451
503
|
}
|
|
452
504
|
|
|
453
505
|
const sharedWorker = yield* Fiber.join(sharedWorkerFiber)
|
|
@@ -458,10 +510,11 @@ export const makePersistedAdapter =
|
|
|
458
510
|
lockStatus,
|
|
459
511
|
clientId,
|
|
460
512
|
sessionId,
|
|
461
|
-
|
|
462
|
-
isLeader: true,
|
|
513
|
+
isLeader: gotLocky,
|
|
463
514
|
leaderThread,
|
|
464
515
|
webmeshMode: 'direct',
|
|
516
|
+
// Can be undefined in Node.js
|
|
517
|
+
origin: globalThis.location?.origin,
|
|
465
518
|
connectWebmeshNode: ({ sessionInfo, webmeshNode }) =>
|
|
466
519
|
connectWebmeshNodeClientSession({ webmeshNode, sessionInfo, sharedWorker, devtoolsEnabled, schema }),
|
|
467
520
|
registerBeforeUnload: (onBeforeUnload) => {
|
|
@@ -475,7 +528,7 @@ export const makePersistedAdapter =
|
|
|
475
528
|
})
|
|
476
529
|
|
|
477
530
|
return clientSession
|
|
478
|
-
}).pipe(
|
|
531
|
+
}).pipe(Effect.provide(Opfs.Opfs.Default), UnknownError.mapToUnknownError)
|
|
479
532
|
|
|
480
533
|
// NOTE for `local` storage we could also use the eventlog db to store the data
|
|
481
534
|
const getPersistedId = (key: string, storageType: 'session' | 'local') => {
|
|
@@ -511,7 +564,7 @@ const ensureBrowserRequirements = Effect.gen(function* () {
|
|
|
511
564
|
const validate = (condition: boolean, label: string) =>
|
|
512
565
|
Effect.gen(function* () {
|
|
513
566
|
if (condition) {
|
|
514
|
-
return yield*
|
|
567
|
+
return yield* UnknownError.make({
|
|
515
568
|
cause: `[@livestore/adapter-web] Browser not supported. The LiveStore web adapter needs '${label}' to work properly`,
|
|
516
569
|
})
|
|
517
570
|
}
|
|
@@ -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 ? loadSqlite3Wasm() : (sqlite3Promise ?? loadSqlite3Wasm()))
|