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

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 (55) 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 +204 -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 +2 -1
  15. package/dist/devtools/devtools-server.d.ts.map +1 -1
  16. package/dist/devtools/devtools-server.js +5 -3
  17. package/dist/devtools/devtools-server.js.map +1 -1
  18. package/dist/index.d.ts +1 -2
  19. package/dist/index.d.ts.map +1 -1
  20. package/dist/index.js +1 -2
  21. package/dist/index.js.map +1 -1
  22. package/dist/leader-shared.d.ts +29 -0
  23. package/dist/leader-shared.d.ts.map +1 -0
  24. package/dist/leader-shared.js +87 -0
  25. package/dist/leader-shared.js.map +1 -0
  26. package/dist/leader-thread-lazy.js +4 -0
  27. package/dist/leader-thread-lazy.js.map +1 -1
  28. package/dist/leader-thread-shared.d.ts +29 -0
  29. package/dist/leader-thread-shared.d.ts.map +1 -0
  30. package/dist/leader-thread-shared.js +89 -0
  31. package/dist/leader-thread-shared.js.map +1 -0
  32. package/dist/make-leader-worker.d.ts +5 -1
  33. package/dist/make-leader-worker.d.ts.map +1 -1
  34. package/dist/make-leader-worker.js +16 -83
  35. package/dist/make-leader-worker.js.map +1 -1
  36. package/dist/webchannel.d.ts.map +1 -1
  37. package/dist/webchannel.js +13 -3
  38. package/dist/webchannel.js.map +1 -1
  39. package/dist/worker-schema.d.ts +36 -19
  40. package/dist/worker-schema.d.ts.map +1 -1
  41. package/dist/worker-schema.js +15 -19
  42. package/dist/worker-schema.js.map +1 -1
  43. package/package.json +12 -11
  44. package/src/client-session/{persisted-adapter.ts → adapter.ts} +196 -58
  45. package/src/devtools/devtools-server.ts +6 -1
  46. package/src/index.ts +1 -2
  47. package/src/leader-thread-lazy.ts +5 -0
  48. package/src/leader-thread-shared.ts +168 -0
  49. package/src/make-leader-worker.ts +22 -138
  50. package/src/webchannel.ts +16 -3
  51. package/src/worker-schema.ts +24 -26
  52. package/rollup.config.mjs +0 -24
  53. package/src/client-session/in-memory-adapter.ts +0 -170
  54. package/tmp/pack.tgz +0 -0
  55. 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,66 @@ 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
+ schemaAlias: schema.devtools.alias,
154
+ port: devtoolsOptionsInput.port ?? 4242,
155
+ host: devtoolsOptionsInput.host ?? 'localhost',
156
+ }
157
+ : { enabled: false }
158
+
159
+ const { leaderThread, initialSnapshot } =
160
+ leaderThreadInput._tag === 'single-threaded'
161
+ ? yield* makeLocalLeaderThread({
162
+ storeId,
163
+ clientId,
164
+ schema,
165
+ makeSqliteDb,
166
+ syncOptions: leaderThreadInput.sync,
167
+ syncPayload,
168
+ devtools: devtoolsOptions,
169
+ storage,
170
+ testing,
171
+ }).pipe(UnexpectedError.mapToUnexpectedError)
172
+ : yield* makeWorkerLeaderThread({
173
+ shutdown,
174
+ storeId,
175
+ clientId,
176
+ sessionId,
177
+ workerUrl: leaderThreadInput.workerUrl,
178
+ storage,
179
+ devtools: devtoolsOptions,
180
+ bootStatusQueue,
181
+ syncPayload,
182
+ })
118
183
 
119
184
  syncInMemoryDb.import(initialSnapshot)
120
185
 
121
- if (devtoolsEnabled) {
186
+ if (devtoolsOptions.enabled) {
122
187
  yield* Effect.gen(function* () {
123
188
  const webmeshNode = yield* DevtoolsNode.makeNodeDevtoolsConnectedMeshNode({
124
189
  url: `ws://${devtoolsOptions.host}:${devtoolsOptions.port}`,
@@ -130,9 +195,10 @@ export const makePersistedAdapter = ({
130
195
  schema: Devtools.SessionInfo.Message,
131
196
  })
132
197
 
198
+ const schemaAlias = schema.devtools.alias
133
199
  yield* Devtools.SessionInfo.provideSessionInfo({
134
200
  webChannel: sessionsChannel,
135
- sessionInfo: Devtools.SessionInfo.SessionInfo.make({ storeId, clientId, sessionId }),
201
+ sessionInfo: Devtools.SessionInfo.SessionInfo.make({ storeId, clientId, sessionId, schemaAlias }),
136
202
  }).pipe(Effect.tapCauseLogPretty, Effect.forkScoped)
137
203
 
138
204
  const storeDevtoolsChannel = yield* DevtoolsNode.makeChannelForConnectedMeshNode({
@@ -142,7 +208,11 @@ export const makePersistedAdapter = ({
142
208
  })
143
209
 
144
210
  yield* connectDevtoolsToStore(storeDevtoolsChannel)
145
- }).pipe(Effect.tapCauseLogPretty, Effect.forkScoped)
211
+ }).pipe(
212
+ Effect.tapCauseLogPretty,
213
+ Effect.withSpan('@livestore/adapter-node:client-session:devtools'),
214
+ Effect.forkScoped,
215
+ )
146
216
  }
147
217
 
148
218
  const devtools: ClientSession['devtools'] = devtoolsEnabled
@@ -167,30 +237,97 @@ export const makePersistedAdapter = ({
167
237
  Effect.provide(FetchHttpClient.layer),
168
238
  )) satisfies Adapter
169
239
 
170
- const makeLeaderThread = ({
240
+ const makeLocalLeaderThread = ({
241
+ storeId,
242
+ clientId,
243
+ schema,
244
+ makeSqliteDb,
245
+ syncOptions,
246
+ syncPayload,
247
+ storage,
248
+ devtools,
249
+ testing,
250
+ }: {
251
+ storeId: string
252
+ clientId: string
253
+ schema: LiveStoreSchema
254
+ makeSqliteDb: MakeSqliteDb
255
+ syncOptions: SyncOptions | undefined
256
+ storage: WorkerSchema.StorageType
257
+ syncPayload: Schema.JsonValue | undefined
258
+ devtools: WorkerSchema.LeaderWorkerInner.InitialMessage['devtools']
259
+ testing?: {
260
+ overrides?: TestingOverrides
261
+ }
262
+ }) =>
263
+ Effect.gen(function* () {
264
+ const layer = yield* Layer.build(
265
+ makeLeaderThread({
266
+ storeId,
267
+ clientId,
268
+ schema,
269
+ syncOptions,
270
+ storage,
271
+ syncPayload,
272
+ devtools,
273
+ makeSqliteDb,
274
+ testing: testing?.overrides,
275
+ }).pipe(Layer.unwrapScoped),
276
+ )
277
+
278
+ return yield* Effect.gen(function* () {
279
+ const { dbState, dbEventlog, syncProcessor, extraIncomingMessagesQueue, initialState } = yield* LeaderThreadCtx
280
+
281
+ const initialLeaderHead = Eventlog.getClientHeadFromDb(dbEventlog)
282
+
283
+ const leaderThread = {
284
+ events: {
285
+ pull:
286
+ testing?.overrides?.clientSession?.leaderThreadProxy?.events?.pull ??
287
+ (({ cursor }) => syncProcessor.pull({ cursor })),
288
+ push: (batch) =>
289
+ syncProcessor.push(
290
+ batch.map((item) => new LiveStoreEvent.EncodedWithMeta(item)),
291
+ { waitForProcessing: true },
292
+ ),
293
+ },
294
+ initialState: { leaderHead: initialLeaderHead, migrationsReport: initialState.migrationsReport },
295
+ export: Effect.sync(() => dbState.export()),
296
+ getEventlogData: Effect.sync(() => dbEventlog.export()),
297
+ getSyncState: syncProcessor.syncState,
298
+ sendDevtoolsMessage: (message) => extraIncomingMessagesQueue.offer(message),
299
+ } satisfies ClientSessionLeaderThreadProxy
300
+
301
+ const initialSnapshot = dbState.export()
302
+
303
+ return { leaderThread, initialSnapshot }
304
+ }).pipe(Effect.provide(layer))
305
+ })
306
+
307
+ const makeWorkerLeaderThread = ({
171
308
  shutdown,
172
309
  storeId,
173
310
  clientId,
174
311
  sessionId,
175
312
  workerUrl,
176
- baseDirectory,
177
- devtoolsEnabled,
178
- devtoolsOptions,
179
- schemaPath,
313
+ storage,
314
+ devtools,
180
315
  bootStatusQueue,
181
316
  syncPayload,
317
+ testing,
182
318
  }: {
183
319
  shutdown: (cause: Cause.Cause<UnexpectedError | IntentionalShutdownCause>) => void
184
320
  storeId: string
185
321
  clientId: string
186
322
  sessionId: string
187
323
  workerUrl: URL
188
- baseDirectory: string | undefined
189
- devtoolsEnabled: boolean
190
- devtoolsOptions: { port: number; host: string }
191
- schemaPath: string
324
+ storage: WorkerSchema.StorageType
325
+ devtools: WorkerSchema.LeaderWorkerInner.InitialMessage['devtools']
192
326
  bootStatusQueue: Queue.Queue<BootStatus>
193
327
  syncPayload: Schema.JsonValue | undefined
328
+ testing?: {
329
+ overrides?: TestingOverrides
330
+ }
194
331
  }) =>
195
332
  Effect.gen(function* () {
196
333
  const nodeWorker = new WT.Worker(workerUrl, {
@@ -206,9 +343,8 @@ const makeLeaderThread = ({
206
343
  new WorkerSchema.LeaderWorkerInner.InitialMessage({
207
344
  storeId,
208
345
  clientId,
209
- baseDirectory,
210
- devtools: { enabled: devtoolsEnabled, port: devtoolsOptions.port, host: devtoolsOptions.host },
211
- schemaPath,
346
+ storage,
347
+ devtools,
212
348
  syncPayload,
213
349
  }),
214
350
  }).pipe(
@@ -296,8 +432,10 @@ const makeLeaderThread = ({
296
432
 
297
433
  const leaderThread = {
298
434
  events: {
299
- pull: ({ cursor }) =>
300
- runInWorkerStream(new WorkerSchema.LeaderWorkerInner.PullStream({ cursor })).pipe(Stream.orDie),
435
+ pull:
436
+ testing?.overrides?.clientSession?.leaderThreadProxy?.events?.pull ??
437
+ (({ cursor }) =>
438
+ runInWorkerStream(new WorkerSchema.LeaderWorkerInner.PullStream({ cursor })).pipe(Stream.orDie)),
301
439
  push: (batch) =>
302
440
  runInWorker(new WorkerSchema.LeaderWorkerInner.PushToLeader({ batch })).pipe(
303
441
  Effect.withSpan('@livestore/adapter-node:client-session:pushToLeader', {
@@ -25,6 +25,7 @@ import { makeViteMiddleware } from './vite-dev-server.js'
25
25
  */
26
26
  export const startDevtoolsServer = ({
27
27
  schemaPath,
28
+ schemaAlias,
28
29
  storeId,
29
30
  clientId,
30
31
  sessionId,
@@ -32,6 +33,7 @@ export const startDevtoolsServer = ({
32
33
  host,
33
34
  }: {
34
35
  schemaPath: string
36
+ schemaAlias: string
35
37
  storeId: string
36
38
  clientId: string
37
39
  sessionId: string
@@ -106,11 +108,14 @@ export const startDevtoolsServer = ({
106
108
  }).pipe(Effect.interruptible)
107
109
 
108
110
  yield* Effect.logDebug(
109
- `[@livestore/adapter-node:devtools] LiveStore devtools are available at http://${host}:${port}/_livestore/node/${storeId}/${clientId}/${sessionId}`,
111
+ `[@livestore/adapter-node:devtools] LiveStore devtools are available at http://${host}:${port}/_livestore/node/${storeId}/${clientId}/${sessionId}/${schemaAlias}`,
110
112
  )
111
113
 
112
114
  return HttpServer.serve(handler, HttpMiddleware.logger)
113
115
  }).pipe(
116
+ Effect.withSpan('@livestore/adapter-node:startDevtoolsServer', {
117
+ attributes: { storeId, clientId, sessionId, port, host, schemaPath },
118
+ }),
114
119
  Layer.unwrapScoped,
115
120
  // HttpServer.withLogAddress,
116
121
  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,168 @@
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
+ schemaAlias: devtools.schemaAlias,
145
+ storeId,
146
+ clientId,
147
+ sessionId: 'static', // TODO make this dynamic
148
+ port: devtools.port,
149
+ host: devtools.host,
150
+ }).pipe(Effect.tapCauseLogPretty, Effect.forkScoped)
151
+
152
+ const devtoolsWebChannel = yield* makeNodeDevtoolsChannel({
153
+ nodeName: `leader-${storeId}-${clientId}`,
154
+ target: `devtools-${storeId}-${clientId}-static`,
155
+ url: `ws://localhost:${devtools.port}`,
156
+ schema: { listen: Devtools.Leader.MessageToApp, send: Devtools.Leader.MessageFromApp },
157
+ })
158
+
159
+ return {
160
+ devtoolsWebChannel,
161
+ persistenceInfo: {
162
+ state: dbState.metadata.persistenceInfo,
163
+ eventlog: dbEventlog.metadata.persistenceInfo,
164
+ },
165
+ }
166
+ }).pipe(Effect.provide(FetchHttpClient.layer)),
167
+ }
168
+ })