@livestore/adapter-node 0.0.0-snapshot-54e706b7e73bd685653cd43e1e34a02c1d8054a2 → 0.0.0-snapshot-412a36a7e6c9b0e9e237b553fd0522aed285228f.1

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 (48) 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 +199 -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 +2 -3
  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 +14 -81
  34. package/dist/make-leader-worker.js.map +1 -1
  35. package/dist/shutdown-channel.d.ts +1 -1
  36. package/dist/worker-schema.d.ts +35 -19
  37. package/dist/worker-schema.d.ts.map +1 -1
  38. package/dist/worker-schema.js +14 -19
  39. package/dist/worker-schema.js.map +1 -1
  40. package/package.json +6 -10
  41. package/src/client-session/{persisted-adapter.ts → adapter.ts} +190 -57
  42. package/src/devtools/devtools-server.ts +3 -0
  43. package/src/index.ts +1 -2
  44. package/src/leader-thread-lazy.ts +5 -0
  45. package/src/leader-thread-shared.ts +167 -0
  46. package/src/make-leader-worker.ts +20 -138
  47. package/src/worker-schema.ts +23 -26
  48. package/src/client-session/in-memory-adapter.ts +0 -169
@@ -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,62 @@ 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
+ const shutdownChannel = yield* makeShutdownChannel(storeId)
129
+
130
+ yield* shutdownChannel.listen.pipe(
131
+ Stream.flatten(),
132
+ Stream.tap((error) => Effect.sync(() => shutdown(Cause.fail(error)))),
133
+ Stream.runDrain,
134
+ Effect.interruptible,
135
+ Effect.tapCauseLogPretty,
136
+ Effect.forkScoped,
137
+ )
138
+ }
99
139
 
100
140
  const syncInMemoryDb = yield* makeSqliteDb({ _tag: 'in-memory' }).pipe(Effect.orDie)
101
141
 
102
142
  // TODO actually implement this multi-session support
103
143
  const lockStatus = yield* SubscriptionRef.make<LockStatus>('has-lock')
104
144
 
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
- })
145
+ const devtoolsOptions: WorkerSchema.LeaderWorkerInner.InitialMessage['devtools'] =
146
+ devtoolsEnabled && devtoolsOptionsInput !== undefined
147
+ ? {
148
+ enabled: true,
149
+ schemaPath: devtoolsOptionsInput.schemaPath,
150
+ port: devtoolsOptionsInput.port ?? 4242,
151
+ host: devtoolsOptionsInput.host ?? 'localhost',
152
+ }
153
+ : { enabled: false }
154
+
155
+ const { leaderThread, initialSnapshot } =
156
+ leaderThreadInput._tag === 'single-threaded'
157
+ ? yield* makeLocalLeaderThread({
158
+ storeId,
159
+ clientId,
160
+ schema,
161
+ makeSqliteDb,
162
+ syncOptions: leaderThreadInput.sync,
163
+ syncPayload,
164
+ devtools: devtoolsOptions,
165
+ storage,
166
+ testing,
167
+ }).pipe(UnexpectedError.mapToUnexpectedError)
168
+ : yield* makeWorkerLeaderThread({
169
+ shutdown,
170
+ storeId,
171
+ clientId,
172
+ sessionId,
173
+ workerUrl: leaderThreadInput.workerUrl,
174
+ storage,
175
+ devtools: devtoolsOptions,
176
+ bootStatusQueue,
177
+ syncPayload,
178
+ })
118
179
 
119
180
  syncInMemoryDb.import(initialSnapshot)
120
181
 
121
- if (devtoolsEnabled) {
182
+ if (devtoolsOptions.enabled) {
122
183
  yield* Effect.gen(function* () {
123
184
  const webmeshNode = yield* DevtoolsNode.makeNodeDevtoolsConnectedMeshNode({
124
185
  url: `ws://${devtoolsOptions.host}:${devtoolsOptions.port}`,
@@ -142,7 +203,11 @@ export const makePersistedAdapter = ({
142
203
  })
143
204
 
144
205
  yield* connectDevtoolsToStore(storeDevtoolsChannel)
145
- }).pipe(Effect.tapCauseLogPretty, Effect.forkScoped)
206
+ }).pipe(
207
+ Effect.tapCauseLogPretty,
208
+ Effect.withSpan('@livestore/adapter-node:client-session:devtools'),
209
+ Effect.forkScoped,
210
+ )
146
211
  }
147
212
 
148
213
  const devtools: ClientSession['devtools'] = devtoolsEnabled
@@ -167,30 +232,97 @@ export const makePersistedAdapter = ({
167
232
  Effect.provide(FetchHttpClient.layer),
168
233
  )) satisfies Adapter
169
234
 
170
- const makeLeaderThread = ({
235
+ const makeLocalLeaderThread = ({
236
+ storeId,
237
+ clientId,
238
+ schema,
239
+ makeSqliteDb,
240
+ syncOptions,
241
+ syncPayload,
242
+ storage,
243
+ devtools,
244
+ testing,
245
+ }: {
246
+ storeId: string
247
+ clientId: string
248
+ schema: LiveStoreSchema
249
+ makeSqliteDb: MakeSqliteDb
250
+ syncOptions: SyncOptions | undefined
251
+ storage: WorkerSchema.StorageType
252
+ syncPayload: Schema.JsonValue | undefined
253
+ devtools: WorkerSchema.LeaderWorkerInner.InitialMessage['devtools']
254
+ testing?: {
255
+ overrides?: TestingOverrides
256
+ }
257
+ }) =>
258
+ Effect.gen(function* () {
259
+ const layer = yield* Layer.build(
260
+ makeLeaderThread({
261
+ storeId,
262
+ clientId,
263
+ schema,
264
+ syncOptions,
265
+ storage,
266
+ syncPayload,
267
+ devtools,
268
+ makeSqliteDb,
269
+ testing: testing?.overrides,
270
+ }).pipe(Layer.unwrapScoped),
271
+ )
272
+
273
+ return yield* Effect.gen(function* () {
274
+ const { dbState, dbEventlog, syncProcessor, extraIncomingMessagesQueue, initialState } = yield* LeaderThreadCtx
275
+
276
+ const initialLeaderHead = Eventlog.getClientHeadFromDb(dbEventlog)
277
+
278
+ const leaderThread = {
279
+ events: {
280
+ pull:
281
+ testing?.overrides?.clientSession?.leaderThreadProxy?.events?.pull ??
282
+ (({ cursor }) => syncProcessor.pull({ cursor })),
283
+ push: (batch) =>
284
+ syncProcessor.push(
285
+ batch.map((item) => new LiveStoreEvent.EncodedWithMeta(item)),
286
+ { waitForProcessing: true },
287
+ ),
288
+ },
289
+ initialState: { leaderHead: initialLeaderHead, migrationsReport: initialState.migrationsReport },
290
+ export: Effect.sync(() => dbState.export()),
291
+ getEventlogData: Effect.sync(() => dbEventlog.export()),
292
+ getSyncState: syncProcessor.syncState,
293
+ sendDevtoolsMessage: (message) => extraIncomingMessagesQueue.offer(message),
294
+ } satisfies ClientSessionLeaderThreadProxy
295
+
296
+ const initialSnapshot = dbState.export()
297
+
298
+ return { leaderThread, initialSnapshot }
299
+ }).pipe(Effect.provide(layer))
300
+ })
301
+
302
+ const makeWorkerLeaderThread = ({
171
303
  shutdown,
172
304
  storeId,
173
305
  clientId,
174
306
  sessionId,
175
307
  workerUrl,
176
- baseDirectory,
177
- devtoolsEnabled,
178
- devtoolsOptions,
179
- schemaPath,
308
+ storage,
309
+ devtools,
180
310
  bootStatusQueue,
181
311
  syncPayload,
312
+ testing,
182
313
  }: {
183
314
  shutdown: (cause: Cause.Cause<UnexpectedError | IntentionalShutdownCause>) => void
184
315
  storeId: string
185
316
  clientId: string
186
317
  sessionId: string
187
318
  workerUrl: URL
188
- baseDirectory: string | undefined
189
- devtoolsEnabled: boolean
190
- devtoolsOptions: { port: number; host: string }
191
- schemaPath: string
319
+ storage: WorkerSchema.StorageType
320
+ devtools: WorkerSchema.LeaderWorkerInner.InitialMessage['devtools']
192
321
  bootStatusQueue: Queue.Queue<BootStatus>
193
322
  syncPayload: Schema.JsonValue | undefined
323
+ testing?: {
324
+ overrides?: TestingOverrides
325
+ }
194
326
  }) =>
195
327
  Effect.gen(function* () {
196
328
  const nodeWorker = new WT.Worker(workerUrl, {
@@ -206,9 +338,8 @@ const makeLeaderThread = ({
206
338
  new WorkerSchema.LeaderWorkerInner.InitialMessage({
207
339
  storeId,
208
340
  clientId,
209
- baseDirectory,
210
- devtools: { enabled: devtoolsEnabled, port: devtoolsOptions.port, host: devtoolsOptions.host },
211
- schemaPath,
341
+ storage,
342
+ devtools,
212
343
  syncPayload,
213
344
  }),
214
345
  }).pipe(
@@ -296,8 +427,10 @@ const makeLeaderThread = ({
296
427
 
297
428
  const leaderThread = {
298
429
  events: {
299
- pull: ({ cursor }) =>
300
- runInWorkerStream(new WorkerSchema.LeaderWorkerInner.PullStream({ cursor })).pipe(Stream.orDie),
430
+ pull:
431
+ testing?.overrides?.clientSession?.leaderThreadProxy?.events?.pull ??
432
+ (({ cursor }) =>
433
+ runInWorkerStream(new WorkerSchema.LeaderWorkerInner.PullStream({ cursor })).pipe(Stream.orDie)),
301
434
  push: (batch) =>
302
435
  runInWorker(new WorkerSchema.LeaderWorkerInner.PushToLeader({ batch })).pipe(
303
436
  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
+ })