@livestore/adapter-node 0.3.0-dev.33 → 0.3.0-dev.34

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.
Files changed (54) hide show
  1. package/dist/.tsbuildinfo +1 -1
  2. package/dist/client-session/adapter.d.ts +44 -0
  3. package/dist/client-session/adapter.d.ts.map +1 -0
  4. package/dist/client-session/adapter.js +202 -0
  5. package/dist/client-session/adapter.js.map +1 -0
  6. package/dist/client-session/in-memory-adapter.d.ts +145 -10
  7. package/dist/client-session/in-memory-adapter.d.ts.map +1 -1
  8. package/dist/client-session/in-memory-adapter.js +6 -7
  9. package/dist/client-session/in-memory-adapter.js.map +1 -1
  10. package/dist/client-session/persisted-adapter.d.ts +26 -14
  11. package/dist/client-session/persisted-adapter.d.ts.map +1 -1
  12. package/dist/client-session/persisted-adapter.js +81 -24
  13. package/dist/client-session/persisted-adapter.js.map +1 -1
  14. package/dist/devtools/devtools-server.d.ts.map +1 -1
  15. package/dist/devtools/devtools-server.js +3 -1
  16. package/dist/devtools/devtools-server.js.map +1 -1
  17. package/dist/index.d.ts +1 -2
  18. package/dist/index.d.ts.map +1 -1
  19. package/dist/index.js +1 -2
  20. package/dist/index.js.map +1 -1
  21. package/dist/leader-shared.d.ts +29 -0
  22. package/dist/leader-shared.d.ts.map +1 -0
  23. package/dist/leader-shared.js +87 -0
  24. package/dist/leader-shared.js.map +1 -0
  25. package/dist/leader-thread-lazy.js +4 -0
  26. package/dist/leader-thread-lazy.js.map +1 -1
  27. package/dist/leader-thread-shared.d.ts +29 -0
  28. package/dist/leader-thread-shared.d.ts.map +1 -0
  29. package/dist/leader-thread-shared.js +88 -0
  30. package/dist/leader-thread-shared.js.map +1 -0
  31. package/dist/make-leader-worker.d.ts +5 -1
  32. package/dist/make-leader-worker.d.ts.map +1 -1
  33. package/dist/make-leader-worker.js +16 -83
  34. package/dist/make-leader-worker.js.map +1 -1
  35. package/dist/webchannel.d.ts.map +1 -1
  36. package/dist/webchannel.js +13 -3
  37. package/dist/webchannel.js.map +1 -1
  38. package/dist/worker-schema.d.ts +35 -19
  39. package/dist/worker-schema.d.ts.map +1 -1
  40. package/dist/worker-schema.js +14 -19
  41. package/dist/worker-schema.js.map +1 -1
  42. package/package.json +12 -11
  43. package/src/client-session/{persisted-adapter.ts → adapter.ts} +193 -57
  44. package/src/devtools/devtools-server.ts +3 -0
  45. package/src/index.ts +1 -2
  46. package/src/leader-thread-lazy.ts +5 -0
  47. package/src/leader-thread-shared.ts +167 -0
  48. package/src/make-leader-worker.ts +22 -138
  49. package/src/webchannel.ts +16 -3
  50. package/src/worker-schema.ts +23 -26
  51. package/rollup.config.mjs +0 -24
  52. package/src/client-session/in-memory-adapter.ts +0 -170
  53. package/tmp/pack.tgz +0 -0
  54. package/tsconfig.json +0 -17
@@ -8,8 +8,13 @@ import type {
8
8
  ClientSessionLeaderThreadProxy,
9
9
  IntentionalShutdownCause,
10
10
  LockStatus,
11
+ MakeSqliteDb,
12
+ SyncOptions,
11
13
  } from '@livestore/common'
12
14
  import { Devtools, UnexpectedError } from '@livestore/common'
15
+ import { Eventlog, LeaderThreadCtx } from '@livestore/common/leader-thread'
16
+ import type { LiveStoreSchema } from '@livestore/common/schema'
17
+ import { LiveStoreEvent } from '@livestore/common/schema'
13
18
  import * as DevtoolsNode from '@livestore/devtools-node-common/web-channel'
14
19
  import { loadSqlite3Wasm } from '@livestore/sqlite-wasm/load-wasm'
15
20
  import { sqliteDbFactory } from '@livestore/sqlite-wasm/node'
@@ -29,49 +34,82 @@ import {
29
34
  } from '@livestore/utils/effect'
30
35
  import { PlatformNode } from '@livestore/utils/node'
31
36
 
37
+ import type { TestingOverrides } from '../leader-thread-shared.js'
38
+ import { makeLeaderThread } from '../leader-thread-shared.js'
32
39
  import { makeShutdownChannel } from '../shutdown-channel.js'
33
40
  import * as WorkerSchema from '../worker-schema.js'
34
41
 
35
42
  export interface NodeAdapterOptions {
36
- /**
37
- * Example: `new URL('./livestore.worker.js', import.meta.url)`
38
- */
39
- workerUrl: URL
40
- /** Needed for the worker and the devtools */
41
- schemaPath: string
42
- /** Where to store the database files */
43
- baseDirectory?: string
43
+ storage: WorkerSchema.StorageType
44
44
  /** The default is the hostname of the current machine */
45
45
  clientId?: string
46
- /** @default 'static' */
46
+ /**
47
+ * Warning: This adapter doesn't currently support multiple client sessions for the same client (i.e. same storeId + clientId)
48
+ * @default 'static'
49
+ */
47
50
  sessionId?: string
51
+
48
52
  devtools?: {
53
+ schemaPath: string
49
54
  /**
50
55
  * Where to run the devtools server (via Vite)
51
56
  *
52
57
  * @default 4242
53
58
  */
54
- port: number
59
+ port?: number
55
60
  /**
56
61
  * @default 'localhost'
57
62
  */
58
- host: string
63
+ host?: string
64
+ }
65
+
66
+ /** Only used internally for testing */
67
+ testing?: {
68
+ overrides?: TestingOverrides
59
69
  }
60
70
  }
61
71
 
72
+ /** Runs everything in the same thread. Use `makeWorkerAdapter` for multi-threaded implementation. */
73
+ export const makeAdapter = ({
74
+ sync,
75
+ ...options
76
+ }: NodeAdapterOptions & {
77
+ sync?: SyncOptions
78
+ }): Adapter => makeAdapterImpl({ ...options, leaderThread: { _tag: 'single-threaded', sync } })
79
+
62
80
  /**
63
- * Warning: This adapter doesn't currently support multiple client sessions for the same client (i.e. same storeId + clientId)
81
+ * Runs persistence and syncing in a worker thread.
64
82
  */
65
- export const makePersistedAdapter = ({
83
+ export const makeWorkerAdapter = ({
66
84
  workerUrl,
67
- schemaPath,
68
- baseDirectory,
69
- devtools: devtoolsOptions = { port: 4242, host: 'localhost' },
85
+ ...options
86
+ }: NodeAdapterOptions & {
87
+ /**
88
+ * Example: `new URL('./livestore.worker.js', import.meta.url)`
89
+ */
90
+ workerUrl: URL
91
+ }): Adapter => makeAdapterImpl({ ...options, leaderThread: { _tag: 'multi-threaded', workerUrl } })
92
+
93
+ const makeAdapterImpl = ({
94
+ storage,
95
+ devtools: devtoolsOptionsInput,
70
96
  clientId = hostname(),
71
97
  // TODO make this dynamic and actually support multiple sessions
72
98
  sessionId = 'static',
73
- }: NodeAdapterOptions): Adapter =>
74
- (({ storeId, devtoolsEnabled, shutdown, connectDevtoolsToStore, bootStatusQueue, syncPayload }) =>
99
+ testing,
100
+ leaderThread: leaderThreadInput,
101
+ }: NodeAdapterOptions & {
102
+ leaderThread:
103
+ | {
104
+ _tag: 'single-threaded'
105
+ sync: SyncOptions | undefined
106
+ }
107
+ | {
108
+ _tag: 'multi-threaded'
109
+ workerUrl: URL
110
+ }
111
+ }): Adapter =>
112
+ (({ storeId, devtoolsEnabled, shutdown, connectDevtoolsToStore, bootStatusQueue, syncPayload, schema }) =>
75
113
  Effect.gen(function* () {
76
114
  yield* Queue.offer(bootStatusQueue, { stage: 'loading' })
77
115
 
@@ -86,39 +124,65 @@ export const makePersistedAdapter = ({
86
124
  // yield* Effect.logWarning('Failed to load database file', fileData.left)
87
125
  // }
88
126
 
89
- const shutdownChannel = yield* makeShutdownChannel(storeId)
90
-
91
- yield* shutdownChannel.listen.pipe(
92
- Stream.flatten(),
93
- Stream.tap((error) => Effect.sync(() => shutdown(Cause.fail(error)))),
94
- Stream.runDrain,
95
- Effect.interruptible,
96
- Effect.tapCauseLogPretty,
97
- Effect.forkScoped,
98
- )
127
+ if (leaderThreadInput._tag === 'multi-threaded') {
128
+ // TODO make static import again once BroadcastChannel is stable in Deno
129
+ //
130
+ // const { makeShutdownChannel } = yield* Effect.promise(() => import('../shutdown-channel.js'))
131
+ const shutdownChannel = yield* makeShutdownChannel(storeId)
132
+
133
+ yield* shutdownChannel.listen.pipe(
134
+ Stream.flatten(),
135
+ Stream.tap((error) => Effect.sync(() => shutdown(Cause.fail(error)))),
136
+ Stream.runDrain,
137
+ Effect.interruptible,
138
+ Effect.tapCauseLogPretty,
139
+ Effect.forkScoped,
140
+ )
141
+ }
99
142
 
100
143
  const syncInMemoryDb = yield* makeSqliteDb({ _tag: 'in-memory' }).pipe(Effect.orDie)
101
144
 
102
145
  // TODO actually implement this multi-session support
103
146
  const lockStatus = yield* SubscriptionRef.make<LockStatus>('has-lock')
104
147
 
105
- const { leaderThread, initialSnapshot } = yield* makeLeaderThread({
106
- shutdown,
107
- storeId,
108
- clientId,
109
- sessionId,
110
- workerUrl,
111
- baseDirectory,
112
- devtoolsEnabled,
113
- devtoolsOptions,
114
- schemaPath,
115
- bootStatusQueue,
116
- syncPayload,
117
- })
148
+ const devtoolsOptions: WorkerSchema.LeaderWorkerInner.InitialMessage['devtools'] =
149
+ devtoolsEnabled && devtoolsOptionsInput !== undefined
150
+ ? {
151
+ enabled: true,
152
+ schemaPath: devtoolsOptionsInput.schemaPath,
153
+ port: devtoolsOptionsInput.port ?? 4242,
154
+ host: devtoolsOptionsInput.host ?? 'localhost',
155
+ }
156
+ : { enabled: false }
157
+
158
+ const { leaderThread, initialSnapshot } =
159
+ leaderThreadInput._tag === 'single-threaded'
160
+ ? yield* makeLocalLeaderThread({
161
+ storeId,
162
+ clientId,
163
+ schema,
164
+ makeSqliteDb,
165
+ syncOptions: leaderThreadInput.sync,
166
+ syncPayload,
167
+ devtools: devtoolsOptions,
168
+ storage,
169
+ testing,
170
+ }).pipe(UnexpectedError.mapToUnexpectedError)
171
+ : yield* makeWorkerLeaderThread({
172
+ shutdown,
173
+ storeId,
174
+ clientId,
175
+ sessionId,
176
+ workerUrl: leaderThreadInput.workerUrl,
177
+ storage,
178
+ devtools: devtoolsOptions,
179
+ bootStatusQueue,
180
+ syncPayload,
181
+ })
118
182
 
119
183
  syncInMemoryDb.import(initialSnapshot)
120
184
 
121
- if (devtoolsEnabled) {
185
+ if (devtoolsOptions.enabled) {
122
186
  yield* Effect.gen(function* () {
123
187
  const webmeshNode = yield* DevtoolsNode.makeNodeDevtoolsConnectedMeshNode({
124
188
  url: `ws://${devtoolsOptions.host}:${devtoolsOptions.port}`,
@@ -142,7 +206,11 @@ export const makePersistedAdapter = ({
142
206
  })
143
207
 
144
208
  yield* connectDevtoolsToStore(storeDevtoolsChannel)
145
- }).pipe(Effect.tapCauseLogPretty, Effect.forkScoped)
209
+ }).pipe(
210
+ Effect.tapCauseLogPretty,
211
+ Effect.withSpan('@livestore/adapter-node:client-session:devtools'),
212
+ Effect.forkScoped,
213
+ )
146
214
  }
147
215
 
148
216
  const devtools: ClientSession['devtools'] = devtoolsEnabled
@@ -167,30 +235,97 @@ export const makePersistedAdapter = ({
167
235
  Effect.provide(FetchHttpClient.layer),
168
236
  )) satisfies Adapter
169
237
 
170
- const makeLeaderThread = ({
238
+ const makeLocalLeaderThread = ({
239
+ storeId,
240
+ clientId,
241
+ schema,
242
+ makeSqliteDb,
243
+ syncOptions,
244
+ syncPayload,
245
+ storage,
246
+ devtools,
247
+ testing,
248
+ }: {
249
+ storeId: string
250
+ clientId: string
251
+ schema: LiveStoreSchema
252
+ makeSqliteDb: MakeSqliteDb
253
+ syncOptions: SyncOptions | undefined
254
+ storage: WorkerSchema.StorageType
255
+ syncPayload: Schema.JsonValue | undefined
256
+ devtools: WorkerSchema.LeaderWorkerInner.InitialMessage['devtools']
257
+ testing?: {
258
+ overrides?: TestingOverrides
259
+ }
260
+ }) =>
261
+ Effect.gen(function* () {
262
+ const layer = yield* Layer.build(
263
+ makeLeaderThread({
264
+ storeId,
265
+ clientId,
266
+ schema,
267
+ syncOptions,
268
+ storage,
269
+ syncPayload,
270
+ devtools,
271
+ makeSqliteDb,
272
+ testing: testing?.overrides,
273
+ }).pipe(Layer.unwrapScoped),
274
+ )
275
+
276
+ return yield* Effect.gen(function* () {
277
+ const { dbState, dbEventlog, syncProcessor, extraIncomingMessagesQueue, initialState } = yield* LeaderThreadCtx
278
+
279
+ const initialLeaderHead = Eventlog.getClientHeadFromDb(dbEventlog)
280
+
281
+ const leaderThread = {
282
+ events: {
283
+ pull:
284
+ testing?.overrides?.clientSession?.leaderThreadProxy?.events?.pull ??
285
+ (({ cursor }) => syncProcessor.pull({ cursor })),
286
+ push: (batch) =>
287
+ syncProcessor.push(
288
+ batch.map((item) => new LiveStoreEvent.EncodedWithMeta(item)),
289
+ { waitForProcessing: true },
290
+ ),
291
+ },
292
+ initialState: { leaderHead: initialLeaderHead, migrationsReport: initialState.migrationsReport },
293
+ export: Effect.sync(() => dbState.export()),
294
+ getEventlogData: Effect.sync(() => dbEventlog.export()),
295
+ getSyncState: syncProcessor.syncState,
296
+ sendDevtoolsMessage: (message) => extraIncomingMessagesQueue.offer(message),
297
+ } satisfies ClientSessionLeaderThreadProxy
298
+
299
+ const initialSnapshot = dbState.export()
300
+
301
+ return { leaderThread, initialSnapshot }
302
+ }).pipe(Effect.provide(layer))
303
+ })
304
+
305
+ const makeWorkerLeaderThread = ({
171
306
  shutdown,
172
307
  storeId,
173
308
  clientId,
174
309
  sessionId,
175
310
  workerUrl,
176
- baseDirectory,
177
- devtoolsEnabled,
178
- devtoolsOptions,
179
- schemaPath,
311
+ storage,
312
+ devtools,
180
313
  bootStatusQueue,
181
314
  syncPayload,
315
+ testing,
182
316
  }: {
183
317
  shutdown: (cause: Cause.Cause<UnexpectedError | IntentionalShutdownCause>) => void
184
318
  storeId: string
185
319
  clientId: string
186
320
  sessionId: string
187
321
  workerUrl: URL
188
- baseDirectory: string | undefined
189
- devtoolsEnabled: boolean
190
- devtoolsOptions: { port: number; host: string }
191
- schemaPath: string
322
+ storage: WorkerSchema.StorageType
323
+ devtools: WorkerSchema.LeaderWorkerInner.InitialMessage['devtools']
192
324
  bootStatusQueue: Queue.Queue<BootStatus>
193
325
  syncPayload: Schema.JsonValue | undefined
326
+ testing?: {
327
+ overrides?: TestingOverrides
328
+ }
194
329
  }) =>
195
330
  Effect.gen(function* () {
196
331
  const nodeWorker = new WT.Worker(workerUrl, {
@@ -206,9 +341,8 @@ const makeLeaderThread = ({
206
341
  new WorkerSchema.LeaderWorkerInner.InitialMessage({
207
342
  storeId,
208
343
  clientId,
209
- baseDirectory,
210
- devtools: { enabled: devtoolsEnabled, port: devtoolsOptions.port, host: devtoolsOptions.host },
211
- schemaPath,
344
+ storage,
345
+ devtools,
212
346
  syncPayload,
213
347
  }),
214
348
  }).pipe(
@@ -296,8 +430,10 @@ const makeLeaderThread = ({
296
430
 
297
431
  const leaderThread = {
298
432
  events: {
299
- pull: ({ cursor }) =>
300
- runInWorkerStream(new WorkerSchema.LeaderWorkerInner.PullStream({ cursor })).pipe(Stream.orDie),
433
+ pull:
434
+ testing?.overrides?.clientSession?.leaderThreadProxy?.events?.pull ??
435
+ (({ cursor }) =>
436
+ runInWorkerStream(new WorkerSchema.LeaderWorkerInner.PullStream({ cursor })).pipe(Stream.orDie)),
301
437
  push: (batch) =>
302
438
  runInWorker(new WorkerSchema.LeaderWorkerInner.PushToLeader({ batch })).pipe(
303
439
  Effect.withSpan('@livestore/adapter-node:client-session:pushToLeader', {
@@ -111,6 +111,9 @@ export const startDevtoolsServer = ({
111
111
 
112
112
  return HttpServer.serve(handler, HttpMiddleware.logger)
113
113
  }).pipe(
114
+ Effect.withSpan('@livestore/adapter-node:startDevtoolsServer', {
115
+ attributes: { storeId, clientId, sessionId, port, host, schemaPath },
116
+ }),
114
117
  Layer.unwrapScoped,
115
118
  // HttpServer.withLogAddress,
116
119
  Layer.provide(PlatformNode.NodeHttpServer.layer(() => http.createServer(), { port, host })),
package/src/index.ts CHANGED
@@ -1,2 +1 @@
1
- export { makeInMemoryAdapter } from './client-session/in-memory-adapter.js'
2
- export { makePersistedAdapter } from './client-session/persisted-adapter.js'
1
+ export { makeAdapter, makeWorkerAdapter } from './client-session/adapter.js'
@@ -1,3 +1,8 @@
1
+ // NOTE This file isn't currently used but was part of an experiment to see whether we can improve
2
+ // the Node startup time by lazy loading the leader thread bundle.
3
+ // This indeed provided a nice speedup but it takes quite a bit of tooling to set up and comes with
4
+ // other downsides (e.g. treeshaking).
5
+
1
6
  const run = async () => {
2
7
  const start = Date.now()
3
8
  // @ts-expect-error todo
@@ -0,0 +1,167 @@
1
+ import inspector from 'node:inspector'
2
+ import path from 'node:path'
3
+
4
+ if (process.execArgv.includes('--inspect')) {
5
+ inspector.open()
6
+ inspector.waitForDebugger()
7
+ }
8
+
9
+ import type { ClientSessionLeaderThreadProxy, MakeSqliteDb, SqliteDb, SyncOptions } from '@livestore/common'
10
+ import { Devtools, liveStoreStorageFormatVersion, UnexpectedError } from '@livestore/common'
11
+ import type { DevtoolsOptions, LeaderSqliteDb, LeaderThreadCtx } from '@livestore/common/leader-thread'
12
+ import { configureConnection, makeLeaderThreadLayer } from '@livestore/common/leader-thread'
13
+ import type { LiveStoreSchema } from '@livestore/common/schema'
14
+ import { makeNodeDevtoolsChannel } from '@livestore/devtools-node-common/web-channel'
15
+ import type { MakeNodeSqliteDb } from '@livestore/sqlite-wasm/node'
16
+ import type { FileSystem, HttpClient, Layer, Schema, Scope } from '@livestore/utils/effect'
17
+ import { Effect, FetchHttpClient } from '@livestore/utils/effect'
18
+
19
+ import { makeShutdownChannel } from './shutdown-channel.js'
20
+ import type * as WorkerSchema from './worker-schema.js'
21
+
22
+ export type TestingOverrides = {
23
+ clientSession?: {
24
+ leaderThreadProxy?: Partial<ClientSessionLeaderThreadProxy>
25
+ }
26
+ makeLeaderThread?: {
27
+ dbEventlog?: (makeSqliteDb: MakeSqliteDb) => Effect.Effect<SqliteDb, UnexpectedError>
28
+ }
29
+ }
30
+
31
+ export interface MakeLeaderThreadArgs {
32
+ storeId: string
33
+ clientId: string
34
+ syncOptions: SyncOptions | undefined
35
+ storage: WorkerSchema.StorageType
36
+ makeSqliteDb: MakeNodeSqliteDb
37
+ devtools: WorkerSchema.LeaderWorkerInner.InitialMessage['devtools']
38
+ schema: LiveStoreSchema
39
+ syncPayload: Schema.JsonValue | undefined
40
+ testing: TestingOverrides | undefined
41
+ }
42
+
43
+ export const makeLeaderThread = ({
44
+ storeId,
45
+ clientId,
46
+ syncOptions,
47
+ makeSqliteDb,
48
+ storage,
49
+ devtools,
50
+ schema,
51
+ syncPayload,
52
+ testing,
53
+ }: MakeLeaderThreadArgs): Effect.Effect<
54
+ Layer.Layer<LeaderThreadCtx, UnexpectedError, Scope.Scope | HttpClient.HttpClient | FileSystem.FileSystem>,
55
+ UnexpectedError,
56
+ Scope.Scope
57
+ > =>
58
+ Effect.gen(function* () {
59
+ const runtime = yield* Effect.runtime<never>()
60
+
61
+ const schemaHashSuffix =
62
+ schema.state.sqlite.migrations.strategy === 'manual' ? 'fixed' : schema.state.sqlite.hash.toString()
63
+
64
+ const makeDb = (kind: 'state' | 'eventlog') => {
65
+ if (testing?.makeLeaderThread?.dbEventlog && kind === 'eventlog') {
66
+ return testing.makeLeaderThread.dbEventlog(makeSqliteDb)
67
+ }
68
+
69
+ return storage.type === 'in-memory'
70
+ ? makeSqliteDb({
71
+ _tag: 'in-memory',
72
+ configureDb: (db) =>
73
+ configureConnection(db, { foreignKeys: true }).pipe(Effect.provide(runtime), Effect.runSync),
74
+ })
75
+ : makeSqliteDb({
76
+ _tag: 'fs',
77
+ directory: path.join(storage.baseDirectory ?? '', storeId),
78
+ fileName:
79
+ kind === 'state' ? getStateDbFileName(schemaHashSuffix) : `eventlog@${liveStoreStorageFormatVersion}.db`,
80
+ // TODO enable WAL for nodejs
81
+ configureDb: (db) =>
82
+ configureConnection(db, { foreignKeys: true }).pipe(Effect.provide(runtime), Effect.runSync),
83
+ }).pipe(Effect.acquireRelease((db) => Effect.sync(() => db.close())))
84
+ }
85
+
86
+ // Might involve some async work, so we're running them concurrently
87
+ const [dbState, dbEventlog] = yield* Effect.all([makeDb('state'), makeDb('eventlog')], { concurrency: 2 })
88
+
89
+ const devtoolsOptions = yield* makeDevtoolsOptions({ devtools, dbState, dbEventlog, storeId, clientId })
90
+
91
+ const shutdownChannel = yield* makeShutdownChannel(storeId)
92
+
93
+ return makeLeaderThreadLayer({
94
+ schema,
95
+ storeId,
96
+ clientId,
97
+ makeSqliteDb,
98
+ syncOptions,
99
+ dbState,
100
+ dbEventlog,
101
+ devtoolsOptions,
102
+ shutdownChannel,
103
+ syncPayload,
104
+ })
105
+ }).pipe(
106
+ Effect.tapCauseLogPretty,
107
+ UnexpectedError.mapToUnexpectedError,
108
+ Effect.withSpan('@livestore/adapter-node:makeLeaderThread', {
109
+ attributes: { storeId, clientId, storage, devtools, syncOptions },
110
+ }),
111
+ )
112
+
113
+ const getStateDbFileName = (suffix: string) => `state${suffix}@${liveStoreStorageFormatVersion}.db`
114
+
115
+ const makeDevtoolsOptions = ({
116
+ dbState,
117
+ dbEventlog,
118
+ storeId,
119
+ clientId,
120
+ devtools,
121
+ }: {
122
+ dbState: LeaderSqliteDb
123
+ dbEventlog: LeaderSqliteDb
124
+ storeId: string
125
+ clientId: string
126
+ devtools: WorkerSchema.LeaderWorkerInner.InitialMessage['devtools']
127
+ }): Effect.Effect<DevtoolsOptions, UnexpectedError, Scope.Scope> =>
128
+ Effect.gen(function* () {
129
+ if (devtools.enabled === false) {
130
+ return {
131
+ enabled: false,
132
+ }
133
+ }
134
+
135
+ return {
136
+ enabled: true,
137
+ makeBootContext: Effect.gen(function* () {
138
+ // Lazy import to improve startup time
139
+ const { startDevtoolsServer } = yield* Effect.promise(() => import('./devtools/devtools-server.js'))
140
+
141
+ // TODO instead of failing when the port is already in use, we should try to use that WS server instead of starting a new one
142
+ yield* startDevtoolsServer({
143
+ schemaPath: devtools.schemaPath,
144
+ storeId,
145
+ clientId,
146
+ sessionId: 'static', // TODO make this dynamic
147
+ port: devtools.port,
148
+ host: devtools.host,
149
+ }).pipe(Effect.tapCauseLogPretty, Effect.forkScoped)
150
+
151
+ const devtoolsWebChannel = yield* makeNodeDevtoolsChannel({
152
+ nodeName: `leader-${storeId}-${clientId}`,
153
+ target: `devtools-${storeId}-${clientId}-static`,
154
+ url: `ws://localhost:${devtools.port}`,
155
+ schema: { listen: Devtools.Leader.MessageToApp, send: Devtools.Leader.MessageFromApp },
156
+ })
157
+
158
+ return {
159
+ devtoolsWebChannel,
160
+ persistenceInfo: {
161
+ state: dbState.metadata.persistenceInfo,
162
+ eventlog: dbEventlog.metadata.persistenceInfo,
163
+ },
164
+ }
165
+ }).pipe(Effect.provide(FetchHttpClient.layer)),
166
+ }
167
+ })