@livestore/adapter-web 0.4.0-dev.2 → 0.4.0-dev.21
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 +49 -5
- package/dist/in-memory/in-memory-adapter.d.ts.map +1 -1
- package/dist/in-memory/in-memory-adapter.js +69 -16
- 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 +68 -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 +125 -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 +147 -56
- package/dist/web-worker/common/worker-schema.d.ts.map +1 -1
- package/dist/web-worker/common/worker-schema.js +55 -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 +63 -27
- 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 +66 -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 +83 -21
- 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 +117 -59
- package/src/web-worker/client-session/sqlite-loader.ts +19 -0
- package/src/web-worker/common/persisted-sqlite.ts +225 -107
- 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 +74 -35
- package/src/web-worker/leader-worker/make-leader-worker.ts +86 -41
- package/src/web-worker/shared-worker/make-shared-worker.ts +96 -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,29 +6,33 @@ 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
|
-
import {
|
|
12
|
+
import {
|
|
13
|
+
configureConnection,
|
|
14
|
+
Eventlog,
|
|
15
|
+
LeaderThreadCtx,
|
|
16
|
+
makeLeaderThreadLayer,
|
|
17
|
+
streamEventsWithSyncState,
|
|
18
|
+
} from '@livestore/common/leader-thread'
|
|
13
19
|
import type { LiveStoreSchema } from '@livestore/common/schema'
|
|
14
20
|
import { LiveStoreEvent } from '@livestore/common/schema'
|
|
15
21
|
import * as DevtoolsWeb from '@livestore/devtools-web-common/web-channel'
|
|
16
22
|
import type * as WebmeshWorker from '@livestore/devtools-web-common/worker'
|
|
17
23
|
import type { MakeWebSqliteDb } from '@livestore/sqlite-wasm/browser'
|
|
18
24
|
import { sqliteDbFactory } from '@livestore/sqlite-wasm/browser'
|
|
19
|
-
import { loadSqlite3Wasm } from '@livestore/sqlite-wasm/load-wasm'
|
|
20
25
|
import { tryAsFunctionAndNew } from '@livestore/utils'
|
|
21
|
-
import type {
|
|
22
|
-
import {
|
|
26
|
+
import type { Scope } from '@livestore/utils/effect'
|
|
27
|
+
import { Effect, FetchHttpClient, Fiber, Layer, type Schema, SubscriptionRef, Worker } from '@livestore/utils/effect'
|
|
28
|
+
import { BrowserWorker } from '@livestore/utils/effect/browser'
|
|
23
29
|
import { nanoid } from '@livestore/utils/nanoid'
|
|
24
30
|
import * as Webmesh from '@livestore/webmesh'
|
|
25
31
|
|
|
26
32
|
import { connectWebmeshNodeClientSession } from '../web-worker/client-session/client-session-devtools.ts'
|
|
33
|
+
import { loadSqlite3 } from '../web-worker/client-session/sqlite-loader.ts'
|
|
27
34
|
import { makeShutdownChannel } from '../web-worker/common/shutdown-channel.ts'
|
|
28
35
|
|
|
29
|
-
// NOTE we're starting to initialize the sqlite wasm binary here to speed things up
|
|
30
|
-
const sqlite3Promise = loadSqlite3Wasm()
|
|
31
|
-
|
|
32
36
|
export interface InMemoryAdapterOptions {
|
|
33
37
|
importSnapshot?: Uint8Array<ArrayBuffer>
|
|
34
38
|
sync?: SyncOptions
|
|
@@ -55,12 +59,56 @@ export interface InMemoryAdapterOptions {
|
|
|
55
59
|
}
|
|
56
60
|
}
|
|
57
61
|
|
|
62
|
+
/**
|
|
63
|
+
* Creates a web-only in-memory LiveStore adapter.
|
|
64
|
+
*
|
|
65
|
+
* This adapter runs entirely in memory with no persistence. Ideal for:
|
|
66
|
+
* - Unit tests and integration tests
|
|
67
|
+
* - Sandboxes and demos
|
|
68
|
+
* - Ephemeral sessions where persistence isn't needed
|
|
69
|
+
*
|
|
70
|
+
* **Characteristics:**
|
|
71
|
+
* - Fast, zero I/O overhead
|
|
72
|
+
* - Works in all browser contexts: Window, WebWorker, SharedWorker, ServiceWorker
|
|
73
|
+
* - Supports optional sync backends for real-time collaboration
|
|
74
|
+
* - No data persists after page reload
|
|
75
|
+
*
|
|
76
|
+
* For persistent storage, use `makePersistedAdapter` instead.
|
|
77
|
+
*
|
|
78
|
+
* @example
|
|
79
|
+
* ```ts
|
|
80
|
+
* import { makeInMemoryAdapter } from '@livestore/adapter-web'
|
|
81
|
+
*
|
|
82
|
+
* const adapter = makeInMemoryAdapter()
|
|
83
|
+
* ```
|
|
84
|
+
*
|
|
85
|
+
* @example
|
|
86
|
+
* ```ts
|
|
87
|
+
* // With sync backend for real-time collaboration
|
|
88
|
+
* import { makeInMemoryAdapter } from '@livestore/adapter-web'
|
|
89
|
+
* import { makeWsSync } from '@livestore/sync-cf/client'
|
|
90
|
+
*
|
|
91
|
+
* const adapter = makeInMemoryAdapter({
|
|
92
|
+
* sync: {
|
|
93
|
+
* backend: makeWsSync({ url: 'wss://api.example.com/sync' }),
|
|
94
|
+
* },
|
|
95
|
+
* })
|
|
96
|
+
* ```
|
|
97
|
+
*
|
|
98
|
+
* @example
|
|
99
|
+
* ```ts
|
|
100
|
+
* // Pre-populate with existing data
|
|
101
|
+
* const adapter = makeInMemoryAdapter({
|
|
102
|
+
* importSnapshot: existingDbSnapshot,
|
|
103
|
+
* })
|
|
104
|
+
* ```
|
|
105
|
+
*/
|
|
58
106
|
export const makeInMemoryAdapter =
|
|
59
107
|
(options: InMemoryAdapterOptions = {}): Adapter =>
|
|
60
108
|
(adapterArgs) =>
|
|
61
109
|
Effect.gen(function* () {
|
|
62
|
-
const { schema, shutdown,
|
|
63
|
-
const sqlite3 = yield* Effect.promise(() =>
|
|
110
|
+
const { schema, shutdown, syncPayloadEncoded, syncPayloadSchema, storeId, devtoolsEnabled } = adapterArgs
|
|
111
|
+
const sqlite3 = yield* Effect.promise(() => loadSqlite3())
|
|
64
112
|
|
|
65
113
|
const sqliteDb = yield* sqliteDbFactory({ sqlite3 })({ _tag: 'in-memory' })
|
|
66
114
|
|
|
@@ -80,7 +128,7 @@ export const makeInMemoryAdapter =
|
|
|
80
128
|
}).pipe(
|
|
81
129
|
Effect.provide(BrowserWorker.layer(() => sharedWebWorker)),
|
|
82
130
|
Effect.tapCauseLogPretty,
|
|
83
|
-
|
|
131
|
+
UnknownError.mapToUnknownError,
|
|
84
132
|
Effect.forkScoped,
|
|
85
133
|
)
|
|
86
134
|
: undefined
|
|
@@ -91,7 +139,8 @@ export const makeInMemoryAdapter =
|
|
|
91
139
|
clientId,
|
|
92
140
|
makeSqliteDb: sqliteDbFactory({ sqlite3 }),
|
|
93
141
|
syncOptions: options.sync,
|
|
94
|
-
|
|
142
|
+
syncPayloadEncoded,
|
|
143
|
+
syncPayloadSchema,
|
|
95
144
|
importSnapshot: options.importSnapshot,
|
|
96
145
|
devtoolsEnabled,
|
|
97
146
|
sharedWorkerFiber,
|
|
@@ -111,6 +160,8 @@ export const makeInMemoryAdapter =
|
|
|
111
160
|
lockStatus,
|
|
112
161
|
shutdown,
|
|
113
162
|
webmeshMode: 'direct',
|
|
163
|
+
// Can be undefined in Node.js
|
|
164
|
+
origin: globalThis.location?.origin,
|
|
114
165
|
connectWebmeshNode: ({ sessionInfo, webmeshNode }) =>
|
|
115
166
|
Effect.gen(function* () {
|
|
116
167
|
if (sharedWorkerFiber === undefined || devtoolsEnabled === false) {
|
|
@@ -132,7 +183,7 @@ export const makeInMemoryAdapter =
|
|
|
132
183
|
})
|
|
133
184
|
|
|
134
185
|
return clientSession
|
|
135
|
-
}).pipe(
|
|
186
|
+
}).pipe(UnknownError.mapToUnknownError, Effect.provide(FetchHttpClient.layer))
|
|
136
187
|
|
|
137
188
|
export interface MakeLeaderThreadArgs {
|
|
138
189
|
schema: LiveStoreSchema
|
|
@@ -140,7 +191,8 @@ export interface MakeLeaderThreadArgs {
|
|
|
140
191
|
clientId: string
|
|
141
192
|
makeSqliteDb: MakeWebSqliteDb
|
|
142
193
|
syncOptions: SyncOptions | undefined
|
|
143
|
-
|
|
194
|
+
syncPayloadEncoded: Schema.JsonValue | undefined
|
|
195
|
+
syncPayloadSchema: Schema.Schema<any> | undefined
|
|
144
196
|
importSnapshot: Uint8Array<ArrayBuffer> | undefined
|
|
145
197
|
devtoolsEnabled: boolean
|
|
146
198
|
sharedWorkerFiber: SharedWorkerFiber | undefined
|
|
@@ -152,7 +204,8 @@ const makeLeaderThread = ({
|
|
|
152
204
|
clientId,
|
|
153
205
|
makeSqliteDb,
|
|
154
206
|
syncOptions,
|
|
155
|
-
|
|
207
|
+
syncPayloadEncoded,
|
|
208
|
+
syncPayloadSchema,
|
|
156
209
|
importSnapshot,
|
|
157
210
|
devtoolsEnabled,
|
|
158
211
|
sharedWorkerFiber,
|
|
@@ -199,12 +252,14 @@ const makeLeaderThread = ({
|
|
|
199
252
|
dbEventlog,
|
|
200
253
|
devtoolsOptions,
|
|
201
254
|
shutdownChannel,
|
|
202
|
-
|
|
255
|
+
syncPayloadEncoded,
|
|
256
|
+
syncPayloadSchema,
|
|
203
257
|
}),
|
|
204
258
|
)
|
|
205
259
|
|
|
206
260
|
return yield* Effect.gen(function* () {
|
|
207
|
-
const { dbState, dbEventlog, syncProcessor, extraIncomingMessagesQueue, initialState } =
|
|
261
|
+
const { dbState, dbEventlog, syncProcessor, extraIncomingMessagesQueue, initialState, networkStatus } =
|
|
262
|
+
yield* LeaderThreadCtx
|
|
208
263
|
|
|
209
264
|
const initialLeaderHead = Eventlog.getClientHeadFromDb(dbEventlog)
|
|
210
265
|
|
|
@@ -213,15 +268,22 @@ const makeLeaderThread = ({
|
|
|
213
268
|
pull: ({ cursor }) => syncProcessor.pull({ cursor }),
|
|
214
269
|
push: (batch) =>
|
|
215
270
|
syncProcessor.push(
|
|
216
|
-
batch.map((item) => new LiveStoreEvent.EncodedWithMeta(item)),
|
|
271
|
+
batch.map((item) => new LiveStoreEvent.Client.EncodedWithMeta(item)),
|
|
217
272
|
{ waitForProcessing: true },
|
|
218
273
|
),
|
|
274
|
+
stream: (options) =>
|
|
275
|
+
streamEventsWithSyncState({
|
|
276
|
+
dbEventlog,
|
|
277
|
+
syncState: syncProcessor.syncState,
|
|
278
|
+
options,
|
|
279
|
+
}),
|
|
219
280
|
},
|
|
220
281
|
initialState: { leaderHead: initialLeaderHead, migrationsReport: initialState.migrationsReport },
|
|
221
282
|
export: Effect.sync(() => dbState.export()),
|
|
222
283
|
getEventlogData: Effect.sync(() => dbEventlog.export()),
|
|
223
|
-
|
|
284
|
+
syncState: syncProcessor.syncState,
|
|
224
285
|
sendDevtoolsMessage: (message) => extraIncomingMessagesQueue.offer(message),
|
|
286
|
+
networkStatus,
|
|
225
287
|
})
|
|
226
288
|
|
|
227
289
|
const initialSnapshot = dbState.export()
|
|
@@ -232,7 +294,7 @@ const makeLeaderThread = ({
|
|
|
232
294
|
|
|
233
295
|
type SharedWorkerFiber = Fiber.Fiber<
|
|
234
296
|
Worker.SerializedWorkerPool<typeof WebmeshWorker.Schema.Request.Type>,
|
|
235
|
-
|
|
297
|
+
UnknownError
|
|
236
298
|
>
|
|
237
299
|
|
|
238
300
|
const makeDevtoolsOptions = ({
|
|
@@ -249,7 +311,7 @@ const makeDevtoolsOptions = ({
|
|
|
249
311
|
dbEventlog: LeaderSqliteDb
|
|
250
312
|
storeId: string
|
|
251
313
|
clientId: string
|
|
252
|
-
}): Effect.Effect<DevtoolsOptions,
|
|
314
|
+
}): Effect.Effect<DevtoolsOptions, UnknownError, Scope.Scope> =>
|
|
253
315
|
Effect.gen(function* () {
|
|
254
316
|
if (devtoolsEnabled === false || sharedWorkerFiber === undefined) {
|
|
255
317
|
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
|
|
|
@@ -428,26 +473,38 @@ export const makePersistedAdapter =
|
|
|
428
473
|
attributes: { batchSize: batch.length },
|
|
429
474
|
}),
|
|
430
475
|
),
|
|
476
|
+
stream: (options) =>
|
|
477
|
+
runInWorkerStream(new WorkerSchema.LeaderWorkerInnerStreamEvents(options)).pipe(
|
|
478
|
+
Stream.withSpan('@livestore/adapter-web:client-session:streamEvents'),
|
|
479
|
+
Stream.orDie,
|
|
480
|
+
),
|
|
431
481
|
},
|
|
432
482
|
|
|
433
483
|
initialState: { leaderHead: initialLeaderHead, migrationsReport },
|
|
434
484
|
|
|
435
485
|
getEventlogData: runInWorker(new WorkerSchema.LeaderWorkerInnerExportEventlog()).pipe(
|
|
436
486
|
Effect.timeout(10_000),
|
|
437
|
-
|
|
487
|
+
UnknownError.mapToUnknownError,
|
|
438
488
|
Effect.withSpan('@livestore/adapter-web:client-session:getEventlogData'),
|
|
439
489
|
),
|
|
440
490
|
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
491
|
+
syncState: Subscribable.make({
|
|
492
|
+
get: runInWorker(new WorkerSchema.LeaderWorkerInnerGetLeaderSyncState()).pipe(
|
|
493
|
+
UnknownError.mapToUnknownError,
|
|
494
|
+
Effect.withSpan('@livestore/adapter-web:client-session:getLeaderSyncState'),
|
|
495
|
+
),
|
|
496
|
+
changes: runInWorkerStream(new WorkerSchema.LeaderWorkerInnerSyncStateStream()).pipe(Stream.orDie),
|
|
497
|
+
}),
|
|
445
498
|
|
|
446
499
|
sendDevtoolsMessage: (message) =>
|
|
447
500
|
runInWorker(new WorkerSchema.LeaderWorkerInnerExtraDevtoolsMessage({ message })).pipe(
|
|
448
|
-
|
|
501
|
+
UnknownError.mapToUnknownError,
|
|
449
502
|
Effect.withSpan('@livestore/adapter-web:client-session:devtoolsMessageForLeader'),
|
|
450
503
|
),
|
|
504
|
+
networkStatus: Subscribable.make({
|
|
505
|
+
get: runInWorker(new WorkerSchema.LeaderWorkerInnerGetNetworkStatus()).pipe(Effect.orDie),
|
|
506
|
+
changes: runInWorkerStream(new WorkerSchema.LeaderWorkerInnerNetworkStatusStream()).pipe(Stream.orDie),
|
|
507
|
+
}),
|
|
451
508
|
}
|
|
452
509
|
|
|
453
510
|
const sharedWorker = yield* Fiber.join(sharedWorkerFiber)
|
|
@@ -458,10 +515,11 @@ export const makePersistedAdapter =
|
|
|
458
515
|
lockStatus,
|
|
459
516
|
clientId,
|
|
460
517
|
sessionId,
|
|
461
|
-
|
|
462
|
-
isLeader: true,
|
|
518
|
+
isLeader: gotLocky,
|
|
463
519
|
leaderThread,
|
|
464
520
|
webmeshMode: 'direct',
|
|
521
|
+
// Can be undefined in Node.js
|
|
522
|
+
origin: globalThis.location?.origin,
|
|
465
523
|
connectWebmeshNode: ({ sessionInfo, webmeshNode }) =>
|
|
466
524
|
connectWebmeshNodeClientSession({ webmeshNode, sessionInfo, sharedWorker, devtoolsEnabled, schema }),
|
|
467
525
|
registerBeforeUnload: (onBeforeUnload) => {
|
|
@@ -475,7 +533,7 @@ export const makePersistedAdapter =
|
|
|
475
533
|
})
|
|
476
534
|
|
|
477
535
|
return clientSession
|
|
478
|
-
}).pipe(
|
|
536
|
+
}).pipe(Effect.provide(Opfs.Opfs.Default), UnknownError.mapToUnknownError)
|
|
479
537
|
|
|
480
538
|
// NOTE for `local` storage we could also use the eventlog db to store the data
|
|
481
539
|
const getPersistedId = (key: string, storageType: 'session' | 'local') => {
|
|
@@ -511,7 +569,7 @@ const ensureBrowserRequirements = Effect.gen(function* () {
|
|
|
511
569
|
const validate = (condition: boolean, label: string) =>
|
|
512
570
|
Effect.gen(function* () {
|
|
513
571
|
if (condition) {
|
|
514
|
-
return yield*
|
|
572
|
+
return yield* UnknownError.make({
|
|
515
573
|
cause: `[@livestore/adapter-web] Browser not supported. The LiveStore web adapter needs '${label}' to work properly`,
|
|
516
574
|
})
|
|
517
575
|
}
|
|
@@ -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()))
|