@livestore/adapter-web 0.0.0-snapshot-13a84f2057a5823fa045fade064685c25519837a → 0.0.0-snapshot-12fc9f248d56344bafb1f0bd9fc50c25c21fd3f7

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.
@@ -1,62 +1,273 @@
1
- import type { Adapter, ClientSession, LockStatus, MigrationsReport } from '@livestore/common'
2
- import { migrateDb, UnexpectedError } from '@livestore/common'
3
- import { configureConnection } from '@livestore/common/leader-thread'
4
- import { EventSequenceNumber } from '@livestore/common/schema'
1
+ import type { Adapter, ClientSessionLeaderThreadProxy, LockStatus, SyncOptions } from '@livestore/common'
2
+ import { Devtools, makeClientSession, UnexpectedError } from '@livestore/common'
3
+ import type { DevtoolsOptions, LeaderSqliteDb } from '@livestore/common/leader-thread'
4
+ import { configureConnection, Eventlog, LeaderThreadCtx, makeLeaderThreadLayer } from '@livestore/common/leader-thread'
5
+ import type { LiveStoreSchema } from '@livestore/common/schema'
6
+ import { LiveStoreEvent } from '@livestore/common/schema'
7
+ import * as DevtoolsWeb from '@livestore/devtools-web-common/web-channel'
8
+ import type * as WebmeshWorker from '@livestore/devtools-web-common/worker'
9
+ import type { MakeWebSqliteDb } from '@livestore/sqlite-wasm/browser'
5
10
  import { sqliteDbFactory } from '@livestore/sqlite-wasm/browser'
6
11
  import { loadSqlite3Wasm } from '@livestore/sqlite-wasm/load-wasm'
7
- import { Effect, Stream, SubscriptionRef } from '@livestore/utils/effect'
12
+ import { tryAsFunctionAndNew } from '@livestore/utils'
13
+ import type { Schema, Scope } from '@livestore/utils/effect'
14
+ import { BrowserWorker, Effect, FetchHttpClient, Fiber, Layer, SubscriptionRef, Worker } from '@livestore/utils/effect'
8
15
  import { nanoid } from '@livestore/utils/nanoid'
16
+ import * as Webmesh from '@livestore/webmesh'
9
17
 
10
- // TODO unify in-memory adapter with other in-memory adapter implementations
18
+ import { connectWebmeshNodeClientSession } from '../web-worker/client-session/client-session-devtools.js'
19
+ import { makeShutdownChannel } from '../web-worker/common/shutdown-channel.js'
11
20
 
12
21
  // NOTE we're starting to initialize the sqlite wasm binary here to speed things up
13
22
  const sqlite3Promise = loadSqlite3Wasm()
14
23
 
15
- /** NOTE: This adapter is currently only used for testing */
24
+ export interface InMemoryAdapterOptions {
25
+ importSnapshot?: Uint8Array
26
+ sync?: SyncOptions
27
+ /**
28
+ * The client ID to use for the adapter.
29
+ *
30
+ * @default a random nanoid
31
+ */
32
+ clientId?: string
33
+ /**
34
+ * The session ID to use for the adapter.
35
+ *
36
+ * @default a random nanoid
37
+ */
38
+ sessionId?: string
39
+ // TODO make the in-memory adapter work with the browser extension
40
+ /** In order to use the devtools with the in-memory adapter, you need to provide the shared worker. */
41
+ devtools?: {
42
+ sharedWorker:
43
+ | ((options: { name: string }) => globalThis.SharedWorker)
44
+ | (new (options: { name: string }) => globalThis.SharedWorker)
45
+ }
46
+ }
47
+
16
48
  export const makeInMemoryAdapter =
17
- (initialData?: Uint8Array): Adapter =>
18
- ({
19
- schema,
20
- shutdown,
21
- // devtoolsEnabled, bootStatusQueue, shutdown, connectDevtoolsToStore
22
- }) =>
49
+ (options: InMemoryAdapterOptions = {}): Adapter =>
50
+ (adapterArgs) =>
23
51
  Effect.gen(function* () {
52
+ const { schema, shutdown, syncPayload, storeId, devtoolsEnabled } = adapterArgs
24
53
  const sqlite3 = yield* Effect.promise(() => sqlite3Promise)
25
54
 
26
55
  const sqliteDb = yield* sqliteDbFactory({ sqlite3 })({ _tag: 'in-memory' })
27
- let migrationsReport: MigrationsReport = { migrations: [] }
28
56
 
29
- if (initialData === undefined) {
30
- yield* configureConnection(sqliteDb, { foreignKeys: true })
57
+ const clientId = options.clientId ?? nanoid(6)
58
+ const sessionId = options.sessionId ?? nanoid(6)
59
+
60
+ const sharedWebWorker = options.devtools?.sharedWorker
61
+ ? tryAsFunctionAndNew(options.devtools.sharedWorker, {
62
+ name: `livestore-shared-worker-${storeId}`,
63
+ })
64
+ : undefined
31
65
 
32
- migrationsReport = yield* migrateDb({ db: sqliteDb, schema })
33
- } else {
34
- sqliteDb.import(initialData)
66
+ const sharedWorkerFiber = sharedWebWorker
67
+ ? yield* Worker.makePoolSerialized<typeof WebmeshWorker.Schema.Request.Type>({
68
+ size: 1,
69
+ concurrency: 100,
70
+ }).pipe(
71
+ Effect.provide(BrowserWorker.layer(() => sharedWebWorker)),
72
+ Effect.tapCauseLogPretty,
73
+ UnexpectedError.mapToUnexpectedError,
74
+ Effect.forkScoped,
75
+ )
76
+ : undefined
35
77
 
36
- yield* configureConnection(sqliteDb, { foreignKeys: true })
37
- }
78
+ const { leaderThread, initialSnapshot } = yield* makeLeaderThread({
79
+ schema,
80
+ storeId,
81
+ clientId,
82
+ makeSqliteDb: sqliteDbFactory({ sqlite3 }),
83
+ syncOptions: options.sync,
84
+ syncPayload,
85
+ importSnapshot: options.importSnapshot,
86
+ devtoolsEnabled,
87
+ sharedWorkerFiber,
88
+ })
38
89
 
39
- const lockStatus = SubscriptionRef.make<LockStatus>('has-lock').pipe(Effect.runSync)
90
+ sqliteDb.import(initialSnapshot)
40
91
 
41
- const clientSession = {
92
+ const lockStatus = yield* SubscriptionRef.make<LockStatus>('has-lock')
93
+
94
+ const clientSession = yield* makeClientSession({
95
+ ...adapterArgs,
42
96
  sqliteDb,
43
- devtools: { enabled: false },
44
- clientId: 'in-memory',
45
- sessionId: nanoid(6),
46
- leaderThread: {
47
- events: {
48
- pull: () => Stream.never,
49
- push: () => Effect.void,
50
- },
51
- initialState: { leaderHead: EventSequenceNumber.ROOT, migrationsReport },
52
- export: Effect.sync(() => sqliteDb.export()),
53
- getEventlogData: Effect.succeed(new Uint8Array()),
54
- getSyncState: Effect.dieMessage('Not implemented'),
55
- sendDevtoolsMessage: () => Effect.dieMessage('Not implemented'),
56
- },
97
+ clientId,
98
+ sessionId,
99
+ isLeader: true,
100
+ leaderThread,
57
101
  lockStatus,
58
102
  shutdown,
59
- } satisfies ClientSession
103
+ webmeshMode: 'direct',
104
+ connectWebmeshNode: ({ sessionInfo, webmeshNode }) =>
105
+ Effect.gen(function* () {
106
+ if (sharedWorkerFiber === undefined || devtoolsEnabled === false) {
107
+ return
108
+ }
109
+
110
+ const sharedWorker = yield* sharedWorkerFiber.pipe(Fiber.join)
111
+
112
+ yield* connectWebmeshNodeClientSession({ webmeshNode, sessionInfo, sharedWorker, devtoolsEnabled, schema })
113
+ }),
114
+ registerBeforeUnload: (onBeforeUnload) => {
115
+ if (typeof window !== 'undefined' && typeof window.addEventListener === 'function') {
116
+ window.addEventListener('beforeunload', onBeforeUnload)
117
+ return () => window.removeEventListener('beforeunload', onBeforeUnload)
118
+ }
119
+
120
+ return () => {}
121
+ },
122
+ })
60
123
 
61
124
  return clientSession
62
- }).pipe(UnexpectedError.mapToUnexpectedError)
125
+ }).pipe(UnexpectedError.mapToUnexpectedError, Effect.provide(FetchHttpClient.layer))
126
+
127
+ export interface MakeLeaderThreadArgs {
128
+ schema: LiveStoreSchema
129
+ storeId: string
130
+ clientId: string
131
+ makeSqliteDb: MakeWebSqliteDb
132
+ syncOptions: SyncOptions | undefined
133
+ syncPayload: Schema.JsonValue | undefined
134
+ importSnapshot: Uint8Array | undefined
135
+ devtoolsEnabled: boolean
136
+ sharedWorkerFiber: SharedWorkerFiber | undefined
137
+ }
138
+
139
+ const makeLeaderThread = ({
140
+ schema,
141
+ storeId,
142
+ clientId,
143
+ makeSqliteDb,
144
+ syncOptions,
145
+ syncPayload,
146
+ importSnapshot,
147
+ devtoolsEnabled,
148
+ sharedWorkerFiber,
149
+ }: MakeLeaderThreadArgs) =>
150
+ Effect.gen(function* () {
151
+ const runtime = yield* Effect.runtime<never>()
152
+
153
+ const makeDb = (_kind: 'state' | 'eventlog') => {
154
+ return makeSqliteDb({
155
+ _tag: 'in-memory',
156
+ configureDb: (db) =>
157
+ configureConnection(db, { foreignKeys: true }).pipe(Effect.provide(runtime), Effect.runSync),
158
+ })
159
+ }
160
+
161
+ const shutdownChannel = yield* makeShutdownChannel(storeId)
162
+
163
+ // Might involve some async work, so we're running them concurrently
164
+ const [dbState, dbEventlog] = yield* Effect.all([makeDb('state'), makeDb('eventlog')], { concurrency: 2 })
165
+
166
+ if (importSnapshot) {
167
+ dbState.import(importSnapshot)
168
+ }
169
+
170
+ const devtoolsOptions = yield* makeDevtoolsOptions({
171
+ devtoolsEnabled,
172
+ sharedWorkerFiber,
173
+ dbState,
174
+ dbEventlog,
175
+ storeId,
176
+ clientId,
177
+ })
178
+
179
+ const layer = yield* Layer.build(
180
+ makeLeaderThreadLayer({
181
+ schema,
182
+ storeId,
183
+ clientId,
184
+ makeSqliteDb,
185
+ syncOptions,
186
+ dbState,
187
+ dbEventlog,
188
+ devtoolsOptions,
189
+ shutdownChannel,
190
+ syncPayload,
191
+ }),
192
+ )
193
+
194
+ return yield* Effect.gen(function* () {
195
+ const { dbState, dbEventlog, syncProcessor, extraIncomingMessagesQueue, initialState } = yield* LeaderThreadCtx
196
+
197
+ const initialLeaderHead = Eventlog.getClientHeadFromDb(dbEventlog)
198
+
199
+ const leaderThread = {
200
+ events: {
201
+ pull: ({ cursor }) => syncProcessor.pull({ cursor }),
202
+ push: (batch) =>
203
+ syncProcessor.push(
204
+ batch.map((item) => new LiveStoreEvent.EncodedWithMeta(item)),
205
+ { waitForProcessing: true },
206
+ ),
207
+ },
208
+ initialState: { leaderHead: initialLeaderHead, migrationsReport: initialState.migrationsReport },
209
+ export: Effect.sync(() => dbState.export()),
210
+ getEventlogData: Effect.sync(() => dbEventlog.export()),
211
+ getSyncState: syncProcessor.syncState,
212
+ sendDevtoolsMessage: (message) => extraIncomingMessagesQueue.offer(message),
213
+ } satisfies ClientSessionLeaderThreadProxy
214
+
215
+ const initialSnapshot = dbState.export()
216
+
217
+ return { leaderThread, initialSnapshot }
218
+ }).pipe(Effect.provide(layer))
219
+ })
220
+
221
+ type SharedWorkerFiber = Fiber.Fiber<
222
+ Worker.SerializedWorkerPool<typeof WebmeshWorker.Schema.Request.Type>,
223
+ UnexpectedError
224
+ >
225
+
226
+ const makeDevtoolsOptions = ({
227
+ devtoolsEnabled,
228
+ sharedWorkerFiber,
229
+ dbState,
230
+ dbEventlog,
231
+ storeId,
232
+ clientId,
233
+ }: {
234
+ devtoolsEnabled: boolean
235
+ sharedWorkerFiber: SharedWorkerFiber | undefined
236
+ dbState: LeaderSqliteDb
237
+ dbEventlog: LeaderSqliteDb
238
+ storeId: string
239
+ clientId: string
240
+ }): Effect.Effect<DevtoolsOptions, UnexpectedError, Scope.Scope> =>
241
+ Effect.gen(function* () {
242
+ if (devtoolsEnabled === false || sharedWorkerFiber === undefined) {
243
+ return { enabled: false }
244
+ }
245
+
246
+ return {
247
+ enabled: true,
248
+ boot: Effect.gen(function* () {
249
+ const persistenceInfo = {
250
+ state: dbState.metadata.persistenceInfo,
251
+ eventlog: dbEventlog.metadata.persistenceInfo,
252
+ }
253
+
254
+ const node = yield* Webmesh.makeMeshNode(Devtools.makeNodeName.client.leader({ storeId, clientId }))
255
+ // @ts-expect-error TODO type this
256
+ globalThis.__debugWebmeshNodeLeader = node
257
+
258
+ const sharedWorker = yield* sharedWorkerFiber.pipe(Fiber.join)
259
+
260
+ // TODO also make this work with the browser extension
261
+ // basic idea: instead of also connecting to the shared worker,
262
+ // connect to the client session node above which will already connect to the shared worker + browser extension
263
+
264
+ yield* DevtoolsWeb.connectViaWorker({
265
+ node,
266
+ worker: sharedWorker,
267
+ target: DevtoolsWeb.makeNodeName.sharedWorker({ storeId }),
268
+ }).pipe(Effect.tapCauseLogPretty, Effect.forkScoped)
269
+
270
+ return { node, persistenceInfo, mode: 'direct' }
271
+ }),
272
+ }
273
+ })
@@ -1,6 +1,10 @@
1
+ import { Devtools } from '@livestore/common'
1
2
  import type { LiveStoreSchema } from '@livestore/common/schema'
3
+ import * as DevtoolsWeb from '@livestore/devtools-web-common/web-channel'
2
4
  import { isDevEnv } from '@livestore/utils'
3
- import { Effect } from '@livestore/utils/effect'
5
+ import type { Worker } from '@livestore/utils/effect'
6
+ import { Effect, Stream, WebChannel } from '@livestore/utils/effect'
7
+ import * as Webmesh from '@livestore/webmesh'
4
8
 
5
9
  export const logDevtoolsUrl = ({
6
10
  schema,
@@ -29,3 +33,60 @@ export const logDevtoolsUrl = ({
29
33
  }
30
34
  }
31
35
  }).pipe(Effect.withSpan('@livestore/adapter-web:client-session:devtools:logDevtoolsUrl'))
36
+
37
+ export const connectWebmeshNodeClientSession = Effect.fn(function* ({
38
+ webmeshNode,
39
+ sessionInfo,
40
+ sharedWorker,
41
+ devtoolsEnabled,
42
+ schema,
43
+ }: {
44
+ webmeshNode: Webmesh.MeshNode
45
+ sessionInfo: Devtools.SessionInfo.SessionInfo
46
+ sharedWorker: Worker.SerializedWorkerPool<typeof DevtoolsWeb.WorkerSchema.Request.Type>
47
+ devtoolsEnabled: boolean
48
+ schema: LiveStoreSchema
49
+ }) {
50
+ if (devtoolsEnabled) {
51
+ const { clientId, sessionId, storeId } = sessionInfo
52
+ yield* logDevtoolsUrl({ clientId, sessionId, schema, storeId })
53
+
54
+ // This additional sessioninfo broadcast channel is needed since we can't use the shared worker
55
+ // as it's currently storeId-specific
56
+ yield* Devtools.SessionInfo.provideSessionInfo({
57
+ webChannel: yield* DevtoolsWeb.makeSessionInfoBroadcastChannel,
58
+ sessionInfo,
59
+ }).pipe(Effect.tapCauseLogPretty, Effect.forkScoped)
60
+
61
+ yield* Effect.gen(function* () {
62
+ const clientSessionStaticChannel = yield* DevtoolsWeb.makeStaticClientSessionChannel.clientSession
63
+
64
+ yield* clientSessionStaticChannel.send(
65
+ DevtoolsWeb.ClientSessionContentscriptMainReq.make({ clientId, sessionId, storeId }),
66
+ )
67
+
68
+ const { tabId } = yield* clientSessionStaticChannel.listen.pipe(Stream.flatten(), Stream.runHead, Effect.flatten)
69
+
70
+ const contentscriptMainNodeName = DevtoolsWeb.makeNodeName.browserExtension.contentscriptMain(tabId)
71
+
72
+ const contentscriptMainChannel = yield* WebChannel.windowChannel({
73
+ listenWindow: window,
74
+ sendWindow: window,
75
+ schema: Webmesh.WebmeshSchema.Packet,
76
+ ids: { own: webmeshNode.nodeName, other: contentscriptMainNodeName },
77
+ })
78
+
79
+ yield* webmeshNode.addEdge({ target: contentscriptMainNodeName, edgeChannel: contentscriptMainChannel })
80
+ }).pipe(
81
+ Effect.withSpan('@livestore/adapter-web:client-session:devtools:browser-extension'),
82
+ Effect.tapCauseLogPretty,
83
+ Effect.forkScoped,
84
+ )
85
+
86
+ yield* DevtoolsWeb.connectViaWorker({
87
+ node: webmeshNode,
88
+ target: DevtoolsWeb.makeNodeName.sharedWorker({ storeId }),
89
+ worker: sharedWorker,
90
+ })
91
+ }
92
+ })
@@ -1,6 +1,5 @@
1
1
  import type { Adapter, ClientSession, LockStatus } from '@livestore/common'
2
2
  import {
3
- Devtools,
4
3
  IntentionalShutdownCause,
5
4
  liveStoreVersion,
6
5
  makeClientSession,
@@ -11,7 +10,6 @@ import {
11
10
  // NOTE We're using a non-relative import here for Vite to properly resolve the import during app builds
12
11
  // import LiveStoreSharedWorker from '@livestore/adapter-web/internal-shared-worker?sharedworker'
13
12
  import { EventSequenceNumber, SystemTables } from '@livestore/common/schema'
14
- import * as DevtoolsWeb from '@livestore/devtools-web-common/web-channel'
15
13
  import { sqliteDbFactory } from '@livestore/sqlite-wasm/browser'
16
14
  import { loadSqlite3Wasm } from '@livestore/sqlite-wasm/load-wasm'
17
15
  import { isDevEnv, shouldNeverHappen, tryAsFunctionAndNew } from '@livestore/utils'
@@ -27,20 +25,18 @@ import {
27
25
  Schema,
28
26
  Stream,
29
27
  SubscriptionRef,
30
- WebChannel,
31
28
  WebLock,
32
29
  Worker,
33
30
  WorkerError,
34
31
  } from '@livestore/utils/effect'
35
32
  import { nanoid } from '@livestore/utils/nanoid'
36
- import * as Webmesh from '@livestore/webmesh'
37
33
 
38
34
  import * as OpfsUtils from '../../opfs-utils.js'
39
35
  import { readPersistedAppDbFromClientSession, resetPersistedDataFromClientSession } from '../common/persisted-sqlite.js'
40
36
  import { makeShutdownChannel } from '../common/shutdown-channel.js'
41
37
  import { DedicatedWorkerDisconnectBroadcast, makeWorkerDisconnectChannel } from '../common/worker-disconnect-channel.js'
42
38
  import * as WorkerSchema from '../common/worker-schema.js'
43
- import { logDevtoolsUrl } from './client-session-devtools.js'
39
+ import { connectWebmeshNodeClientSession } from './client-session-devtools.js'
44
40
 
45
41
  // NOTE we're starting to initialize the sqlite wasm binary here to speed things up
46
42
  const sqlite3Promise = loadSqlite3Wasm()
@@ -426,6 +422,8 @@ export const makePersistedAdapter =
426
422
  ),
427
423
  }
428
424
 
425
+ const sharedWorker = yield* Fiber.join(sharedWorkerFiber)
426
+
429
427
  const clientSession = yield* makeClientSession({
430
428
  ...adapterArgs,
431
429
  sqliteDb,
@@ -436,55 +434,8 @@ export const makePersistedAdapter =
436
434
  isLeader: true,
437
435
  leaderThread,
438
436
  webmeshMode: 'direct',
439
- connectWebmeshNode: Effect.fnUntraced(function* ({ webmeshNode, sessionInfo }) {
440
- if (devtoolsEnabled) {
441
- yield* logDevtoolsUrl({ clientId, sessionId, schema, storeId })
442
-
443
- // This additional sessioninfo broadcast channel is needed since we can't use the shared worker
444
- // as it's currently storeId-specific
445
- yield* Devtools.SessionInfo.provideSessionInfo({
446
- webChannel: yield* DevtoolsWeb.makeSessionInfoBroadcastChannel,
447
- sessionInfo,
448
- }).pipe(Effect.tapCauseLogPretty, Effect.forkScoped)
449
-
450
- yield* Effect.gen(function* () {
451
- const clientSessionStaticChannel = yield* DevtoolsWeb.makeStaticClientSessionChannel.clientSession
452
-
453
- yield* clientSessionStaticChannel.send(
454
- DevtoolsWeb.ClientSessionContentscriptMainReq.make({ clientId, sessionId, storeId }),
455
- )
456
-
457
- const { tabId } = yield* clientSessionStaticChannel.listen.pipe(
458
- Stream.flatten(),
459
- Stream.runHead,
460
- Effect.flatten,
461
- )
462
-
463
- const contentscriptMainNodeName = DevtoolsWeb.makeNodeName.browserExtension.contentscriptMain(tabId)
464
-
465
- const contentscriptMainChannel = yield* WebChannel.windowChannel({
466
- listenWindow: window,
467
- sendWindow: window,
468
- schema: Webmesh.WebmeshSchema.Packet,
469
- ids: { own: webmeshNode.nodeName, other: contentscriptMainNodeName },
470
- })
471
-
472
- yield* webmeshNode.addEdge({ target: contentscriptMainNodeName, edgeChannel: contentscriptMainChannel })
473
- }).pipe(
474
- Effect.withSpan('@livestore/adapter-web:client-session:devtools:browser-extension'),
475
- Effect.tapCauseLogPretty,
476
- Effect.forkScoped,
477
- )
478
-
479
- const sharedWorker = yield* Fiber.join(sharedWorkerFiber)
480
-
481
- yield* DevtoolsWeb.connectViaWorker({
482
- node: webmeshNode,
483
- target: DevtoolsWeb.makeNodeName.sharedWorker({ storeId }),
484
- worker: sharedWorker,
485
- })
486
- }
487
- }),
437
+ connectWebmeshNode: ({ sessionInfo, webmeshNode }) =>
438
+ connectWebmeshNodeClientSession({ webmeshNode, sessionInfo, sharedWorker, devtoolsEnabled, schema }),
488
439
  registerBeforeUnload: (onBeforeUnload) => {
489
440
  if (typeof window !== 'undefined' && typeof window.addEventListener === 'function') {
490
441
  window.addEventListener('beforeunload', onBeforeUnload)