@livestore/adapter-node 0.4.0-dev.9 → 0.4.0

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 (39) hide show
  1. package/dist/.tsbuildinfo +1 -1
  2. package/dist/client-session/adapter.d.ts +68 -2
  3. package/dist/client-session/adapter.d.ts.map +1 -1
  4. package/dist/client-session/adapter.js +121 -42
  5. package/dist/client-session/adapter.js.map +1 -1
  6. package/dist/devtools/devtools-server.d.ts.map +1 -1
  7. package/dist/devtools/devtools-server.js +14 -6
  8. package/dist/devtools/devtools-server.js.map +1 -1
  9. package/dist/devtools/mod.d.ts +1 -1
  10. package/dist/devtools/mod.d.ts.map +1 -1
  11. package/dist/devtools/mod.js +1 -1
  12. package/dist/devtools/mod.js.map +1 -1
  13. package/dist/devtools/vite-dev-server.d.ts +26 -4
  14. package/dist/devtools/vite-dev-server.d.ts.map +1 -1
  15. package/dist/devtools/vite-dev-server.js +43 -8
  16. package/dist/devtools/vite-dev-server.js.map +1 -1
  17. package/dist/leader-thread-shared.d.ts +5 -4
  18. package/dist/leader-thread-shared.d.ts.map +1 -1
  19. package/dist/leader-thread-shared.js +8 -6
  20. package/dist/leader-thread-shared.js.map +1 -1
  21. package/dist/make-leader-worker.d.ts +4 -2
  22. package/dist/make-leader-worker.d.ts.map +1 -1
  23. package/dist/make-leader-worker.js +45 -20
  24. package/dist/make-leader-worker.js.map +1 -1
  25. package/dist/shutdown-channel.d.ts.map +1 -1
  26. package/dist/shutdown-channel.js.map +1 -1
  27. package/dist/worker-schema.d.ts +141 -41
  28. package/dist/worker-schema.d.ts.map +1 -1
  29. package/dist/worker-schema.js +43 -18
  30. package/dist/worker-schema.js.map +1 -1
  31. package/package.json +59 -19
  32. package/src/client-session/adapter.ts +149 -72
  33. package/src/devtools/devtools-server.ts +22 -10
  34. package/src/devtools/mod.ts +1 -1
  35. package/src/devtools/vite-dev-server.ts +63 -11
  36. package/src/leader-thread-shared.ts +17 -13
  37. package/src/make-leader-worker.ts +80 -67
  38. package/src/shutdown-channel.ts +1 -0
  39. package/src/worker-schema.ts +67 -18
@@ -1,19 +1,21 @@
1
1
  import { hostname } from 'node:os'
2
2
  import path from 'node:path'
3
+ import type { URL } from 'node:url'
3
4
  import * as WT from 'node:worker_threads'
5
+
4
6
  import {
5
7
  type Adapter,
6
8
  type BootStatus,
7
9
  ClientSessionLeaderThreadProxy,
8
10
  IntentionalShutdownCause,
11
+ isWorkerTransportError,
9
12
  type LockStatus,
10
13
  type MakeSqliteDb,
11
14
  makeClientSession,
12
- type SyncError,
13
15
  type SyncOptions,
14
- UnexpectedError,
16
+ UnknownError,
15
17
  } from '@livestore/common'
16
- import { Eventlog, LeaderThreadCtx } from '@livestore/common/leader-thread'
18
+ import { Eventlog, LeaderThreadCtx, streamEventsWithSyncState } from '@livestore/common/leader-thread'
17
19
  import type { LiveStoreSchema } from '@livestore/common/schema'
18
20
  import { LiveStoreEvent } from '@livestore/common/schema'
19
21
  import { loadSqlite3Wasm } from '@livestore/sqlite-wasm/load-wasm'
@@ -27,14 +29,14 @@ import {
27
29
  Fiber,
28
30
  FileSystem,
29
31
  Layer,
30
- ParseResult,
32
+ Option,
31
33
  Queue,
32
34
  Schedule,
33
35
  Schema,
34
36
  Stream,
37
+ Subscribable,
35
38
  SubscriptionRef,
36
39
  Worker,
37
- WorkerError,
38
40
  } from '@livestore/utils/effect'
39
41
  import { PlatformNode } from '@livestore/utils/node'
40
42
  import * as Webmesh from '@livestore/webmesh'
@@ -87,7 +89,43 @@ export interface NodeAdapterOptions {
87
89
  }
88
90
  }
89
91
 
90
- /** Runs everything in the same thread. Use `makeWorkerAdapter` for multi-threaded implementation. */
92
+ /**
93
+ * Creates a single-threaded LiveStore adapter for Node.js applications.
94
+ *
95
+ * This adapter runs the leader thread (persistence and sync) in the same thread as
96
+ * your application. Suitable for CLI tools, scripts, and applications where simplicity
97
+ * is preferred over maximum performance.
98
+ *
99
+ * For production servers or performance-critical applications, consider `makeWorkerAdapter`
100
+ * which runs persistence/sync in a separate worker thread.
101
+ *
102
+ * @example
103
+ * ```ts
104
+ * import { makeAdapter } from '@livestore/adapter-node'
105
+ * import { makeWsSync } from '@livestore/sync-cf/client'
106
+ *
107
+ * const adapter = makeAdapter({
108
+ * storage: { type: 'fs', baseDirectory: './data' },
109
+ * sync: {
110
+ * backend: makeWsSync({ url: 'wss://api.example.com/sync' }),
111
+ * },
112
+ * })
113
+ * ```
114
+ *
115
+ * @example
116
+ * ```ts
117
+ * // With DevTools support
118
+ * const adapter = makeAdapter({
119
+ * storage: { type: 'fs', baseDirectory: './data' },
120
+ * devtools: {
121
+ * schemaPath: new URL('./schema.ts', import.meta.url),
122
+ * port: 4242,
123
+ * },
124
+ * })
125
+ * ```
126
+ *
127
+ * @see https://livestore.dev/docs/reference/adapters/node for setup guide
128
+ */
91
129
  export const makeAdapter = ({
92
130
  sync,
93
131
  ...options
@@ -96,7 +134,36 @@ export const makeAdapter = ({
96
134
  }): Adapter => makeAdapterImpl({ ...options, leaderThread: { _tag: 'single-threaded', sync } })
97
135
 
98
136
  /**
99
- * Runs persistence and syncing in a worker thread.
137
+ * Creates a multi-threaded LiveStore adapter for Node.js applications.
138
+ *
139
+ * This adapter runs the leader thread (persistence, sync, and heavy SQLite operations)
140
+ * in a separate worker thread, keeping your main thread responsive. Recommended for
141
+ * production servers and performance-critical applications.
142
+ *
143
+ * You must create a worker file that calls `makeLeaderWorker()` and pass its URL
144
+ * to this function.
145
+ *
146
+ * @example
147
+ * ```ts
148
+ * // In your main file:
149
+ * import { makeWorkerAdapter } from '@livestore/adapter-node'
150
+ *
151
+ * const adapter = makeWorkerAdapter({
152
+ * storage: { type: 'fs', baseDirectory: './data' },
153
+ * workerUrl: new URL('./livestore.worker.ts', import.meta.url),
154
+ * })
155
+ * ```
156
+ *
157
+ * @example
158
+ * ```ts
159
+ * // In livestore.worker.ts:
160
+ * import { makeLeaderWorker } from '@livestore/adapter-node/worker'
161
+ * import { schema } from './schema'
162
+ *
163
+ * makeLeaderWorker({ schema })
164
+ * ```
165
+ *
166
+ * @see https://livestore.dev/docs/reference/adapters/node for setup guide
100
167
  */
101
168
  export const makeWorkerAdapter = ({
102
169
  workerUrl,
@@ -137,7 +204,8 @@ const makeAdapterImpl = ({
137
204
  }): Adapter =>
138
205
  ((adapterArgs) =>
139
206
  Effect.gen(function* () {
140
- const { storeId, devtoolsEnabled, shutdown, bootStatusQueue, syncPayload, schema } = adapterArgs
207
+ const { storeId, devtoolsEnabled, shutdown, bootStatusQueue, syncPayloadEncoded, syncPayloadSchema, schema } =
208
+ adapterArgs
141
209
 
142
210
  yield* Queue.offer(bootStatusQueue, { stage: 'loading' })
143
211
 
@@ -157,7 +225,7 @@ const makeAdapterImpl = ({
157
225
  if (resetPersistence === true) {
158
226
  yield* shutdownChannel
159
227
  .send(IntentionalShutdownCause.make({ reason: 'adapter-reset' }))
160
- .pipe(UnexpectedError.mapToUnexpectedError)
228
+ .pipe(UnknownError.mapToUnknownError)
161
229
 
162
230
  yield* resetNodePersistence({ storage, storeId })
163
231
  }
@@ -165,7 +233,7 @@ const makeAdapterImpl = ({
165
233
  yield* shutdownChannel.listen.pipe(
166
234
  Stream.flatten(),
167
235
  Stream.tap((cause) =>
168
- shutdown(cause._tag === 'LiveStore.IntentionalShutdownCause' ? Exit.succeed(cause) : Exit.fail(cause)),
236
+ shutdown(cause._tag === 'IntentionalShutdownCause' ? Exit.succeed(cause) : Exit.fail(cause)),
169
237
  ),
170
238
  Stream.runDrain,
171
239
  Effect.interruptible,
@@ -179,7 +247,7 @@ const makeAdapterImpl = ({
179
247
  const lockStatus = yield* SubscriptionRef.make<LockStatus>('has-lock')
180
248
 
181
249
  const devtoolsOptions: WorkerSchema.LeaderWorkerInnerInitialMessage['devtools'] =
182
- devtoolsEnabled && devtoolsOptionsInput !== undefined
250
+ devtoolsEnabled === true && devtoolsOptionsInput !== undefined
183
251
  ? {
184
252
  enabled: true,
185
253
  schemaPath:
@@ -204,10 +272,11 @@ const makeAdapterImpl = ({
204
272
  storage,
205
273
  ...omitUndefineds({
206
274
  syncOptions: leaderThreadInput.sync,
207
- syncPayload,
275
+ syncPayloadEncoded,
276
+ syncPayloadSchema,
208
277
  testing,
209
278
  }),
210
- }).pipe(UnexpectedError.mapToUnexpectedError)
279
+ }).pipe(UnknownError.mapToUnknownError)
211
280
  : yield* makeWorkerLeaderThread({
212
281
  shutdown,
213
282
  storeId,
@@ -218,7 +287,7 @@ const makeAdapterImpl = ({
218
287
  storage,
219
288
  devtools: devtoolsOptions,
220
289
  bootStatusQueue,
221
- syncPayload,
290
+ syncPayloadEncoded,
222
291
  })
223
292
 
224
293
  syncInMemoryDb.import(initialSnapshot)
@@ -229,7 +298,7 @@ const makeAdapterImpl = ({
229
298
  sqliteDb: syncInMemoryDb,
230
299
  webmeshMode: 'proxy',
231
300
  connectWebmeshNode: Effect.fnUntraced(function* ({ webmeshNode }) {
232
- if (devtoolsOptions.enabled) {
301
+ if (devtoolsOptions.enabled === true) {
233
302
  yield* Webmesh.connectViaWebSocket({
234
303
  node: webmeshNode,
235
304
  url: `ws://${devtoolsOptions.host}:${devtoolsOptions.port}`,
@@ -244,6 +313,7 @@ const makeAdapterImpl = ({
244
313
  isLeader: true,
245
314
  // Not really applicable for node as there is no "reload the app" concept
246
315
  registerBeforeUnload: (_onBeforeUnload) => () => {},
316
+ origin: undefined,
247
317
  })
248
318
 
249
319
  return clientSession
@@ -258,7 +328,7 @@ const resetNodePersistence = ({
258
328
  }: {
259
329
  storage: WorkerSchema.StorageType
260
330
  storeId: string
261
- }): Effect.Effect<void, UnexpectedError, FileSystem.FileSystem> => {
331
+ }): Effect.Effect<void, UnknownError, FileSystem.FileSystem> => {
262
332
  if (storage.type !== 'fs') {
263
333
  return Effect.void
264
334
  }
@@ -268,13 +338,13 @@ const resetNodePersistence = ({
268
338
  return Effect.gen(function* () {
269
339
  const fs = yield* FileSystem.FileSystem
270
340
 
271
- const directoryExists = yield* fs.exists(directory).pipe(UnexpectedError.mapToUnexpectedError)
341
+ const directoryExists = yield* fs.exists(directory).pipe(UnknownError.mapToUnknownError)
272
342
 
273
343
  if (directoryExists === false) {
274
344
  return
275
345
  }
276
346
 
277
- yield* fs.remove(directory, { recursive: true }).pipe(UnexpectedError.mapToUnexpectedError)
347
+ yield* fs.remove(directory, { recursive: true }).pipe(UnknownError.mapToUnknownError)
278
348
  }).pipe(
279
349
  Effect.retry({ schedule: Schedule.exponentialBackoff10Sec }),
280
350
  Effect.withSpan('@livestore/adapter-node:resetPersistence', { attributes: { directory } }),
@@ -287,7 +357,8 @@ const makeLocalLeaderThread = ({
287
357
  schema,
288
358
  makeSqliteDb,
289
359
  syncOptions,
290
- syncPayload,
360
+ syncPayloadEncoded,
361
+ syncPayloadSchema,
291
362
  storage,
292
363
  devtools,
293
364
  testing,
@@ -298,7 +369,8 @@ const makeLocalLeaderThread = ({
298
369
  makeSqliteDb: MakeSqliteDb
299
370
  syncOptions: SyncOptions | undefined
300
371
  storage: WorkerSchema.StorageType
301
- syncPayload: Schema.JsonValue | undefined
372
+ syncPayloadEncoded: Schema.JsonValue | undefined
373
+ syncPayloadSchema: Schema.Schema<any>
302
374
  devtools: WorkerSchema.LeaderWorkerInnerInitialMessage['devtools']
303
375
  testing?: {
304
376
  overrides?: TestingOverrides
@@ -312,7 +384,8 @@ const makeLocalLeaderThread = ({
312
384
  schema,
313
385
  syncOptions,
314
386
  storage,
315
- syncPayload,
387
+ syncPayloadEncoded,
388
+ syncPayloadSchema,
316
389
  devtools,
317
390
  makeSqliteDb,
318
391
  ...omitUndefineds({ testing: testing?.overrides }),
@@ -320,7 +393,8 @@ const makeLocalLeaderThread = ({
320
393
  )
321
394
 
322
395
  return yield* Effect.gen(function* () {
323
- const { dbState, dbEventlog, syncProcessor, extraIncomingMessagesQueue, initialState } = yield* LeaderThreadCtx
396
+ const { dbState, dbEventlog, syncProcessor, extraIncomingMessagesQueue, initialState, networkStatus } =
397
+ yield* LeaderThreadCtx
324
398
 
325
399
  const initialLeaderHead = Eventlog.getClientHeadFromDb(dbEventlog)
326
400
 
@@ -330,15 +404,26 @@ const makeLocalLeaderThread = ({
330
404
  pull: ({ cursor }) => syncProcessor.pull({ cursor }),
331
405
  push: (batch) =>
332
406
  syncProcessor.push(
333
- batch.map((item) => new LiveStoreEvent.EncodedWithMeta(item)),
407
+ batch.map((item) => new LiveStoreEvent.Client.EncodedWithMeta(item)),
334
408
  { waitForProcessing: true },
335
409
  ),
410
+ stream: (options) =>
411
+ streamEventsWithSyncState({
412
+ dbEventlog,
413
+ syncState: syncProcessor.syncState,
414
+ options,
415
+ }),
416
+ },
417
+ initialState: {
418
+ leaderHead: initialLeaderHead,
419
+ migrationsReport: initialState.migrationsReport,
420
+ storageMode: 'persisted',
336
421
  },
337
- initialState: { leaderHead: initialLeaderHead, migrationsReport: initialState.migrationsReport },
338
422
  export: Effect.sync(() => dbState.export()),
339
423
  getEventlogData: Effect.sync(() => dbEventlog.export()),
340
- getSyncState: syncProcessor.syncState,
424
+ syncState: syncProcessor.syncState,
341
425
  sendDevtoolsMessage: (message) => extraIncomingMessagesQueue.offer(message),
426
+ networkStatus,
342
427
  },
343
428
  { ...omitUndefineds({ overrides: testing?.overrides?.clientSession?.leaderThreadProxy }) },
344
429
  )
@@ -359,10 +444,10 @@ const makeWorkerLeaderThread = ({
359
444
  storage,
360
445
  devtools,
361
446
  bootStatusQueue,
362
- syncPayload,
447
+ syncPayloadEncoded,
363
448
  testing,
364
449
  }: {
365
- shutdown: (cause: Exit.Exit<IntentionalShutdownCause, UnexpectedError | SyncError>) => Effect.Effect<void>
450
+ shutdown: (cause: Exit.Exit<IntentionalShutdownCause, UnknownError>) => Effect.Effect<void>
366
451
  storeId: string
367
452
  clientId: string
368
453
  sessionId: string
@@ -371,15 +456,15 @@ const makeWorkerLeaderThread = ({
371
456
  storage: WorkerSchema.StorageType
372
457
  devtools: WorkerSchema.LeaderWorkerInnerInitialMessage['devtools']
373
458
  bootStatusQueue: Queue.Queue<BootStatus>
374
- syncPayload: Schema.JsonValue | undefined
459
+ syncPayloadEncoded: Schema.JsonValue | undefined
375
460
  testing?: {
376
461
  overrides?: TestingOverrides
377
462
  }
378
463
  }) =>
379
464
  Effect.gen(function* () {
380
465
  const nodeWorker = new WT.Worker(workerUrl, {
381
- execArgv: process.env.DEBUG_WORKER ? ['--inspect --enable-source-maps'] : ['--enable-source-maps'],
382
- argv: [Schema.encodeSync(WorkerSchema.WorkerArgv)({ storeId, clientId, sessionId, extraArgs: workerExtraArgs })],
466
+ execArgv: process.env.DEBUG_WORKER !== undefined ? ['--inspect --enable-source-maps'] : ['--enable-source-maps'],
467
+ argv: [yield* Schema.encode(WorkerSchema.WorkerArgv)({ storeId, clientId, sessionId, extraArgs: workerExtraArgs }).pipe(Effect.orDie)],
383
468
  })
384
469
  const nodeWorkerLayer = yield* Layer.build(PlatformNode.NodeWorker.layer(() => nodeWorker))
385
470
 
@@ -392,56 +477,39 @@ const makeWorkerLeaderThread = ({
392
477
  clientId,
393
478
  storage,
394
479
  devtools,
395
- syncPayload,
480
+ syncPayloadEncoded,
396
481
  }),
397
482
  }).pipe(
398
483
  Effect.provide(nodeWorkerLayer),
399
- UnexpectedError.mapToUnexpectedError,
484
+ UnknownError.mapToUnknownError,
400
485
  Effect.tapErrorCause((cause) => shutdown(Exit.failCause(cause))),
401
486
  Effect.withSpan('@livestore/adapter-node:adapter:setupLeaderThread'),
402
487
  )
403
488
 
404
- const runInWorker = <TReq extends typeof WorkerSchema.LeaderWorkerInnerRequest.Type>(
405
- req: TReq,
406
- ): TReq extends Schema.WithResult<infer A, infer _I, infer _E, infer _EI, infer R>
407
- ? Effect.Effect<A, UnexpectedError, R>
408
- : never =>
409
- (worker.executeEffect(req) as any).pipe(
489
+ const runInWorker = <A, I, E, EI, R>(
490
+ req: WorkerSchema.LeaderWorkerInnerRequest & Schema.WithResult<A, I, E, EI, R>,
491
+ ): Effect.Effect<A, E, R> =>
492
+ worker.executeEffect(req).pipe(
493
+ Effect.catchIf(isWorkerTransportError, (e) => Effect.die(e)),
410
494
  Effect.logWarnIfTakesLongerThan({
411
495
  label: `@livestore/adapter-node:client-session:runInWorker:${req._tag}`,
412
496
  duration: 2000,
413
497
  }),
414
498
  Effect.withSpan(`@livestore/adapter-node:client-session:runInWorker:${req._tag}`),
415
- Effect.mapError((cause) =>
416
- Schema.is(UnexpectedError)(cause)
417
- ? cause
418
- : ParseResult.isParseError(cause) || Schema.is(WorkerError.WorkerError)(cause)
419
- ? new UnexpectedError({ cause })
420
- : cause,
421
- ),
422
- Effect.catchAllDefect((cause) => new UnexpectedError({ cause })),
423
- ) as any
424
-
425
- const runInWorkerStream = <TReq extends typeof WorkerSchema.LeaderWorkerInnerRequest.Type>(
426
- req: TReq,
427
- ): TReq extends Schema.WithResult<infer A, infer _I, infer _E, infer _EI, infer R>
428
- ? Stream.Stream<A, UnexpectedError, R>
429
- : never =>
430
- worker.execute(req as any).pipe(
431
- Stream.mapError((cause) =>
432
- Schema.is(UnexpectedError)(cause)
433
- ? cause
434
- : ParseResult.isParseError(cause) || Schema.is(WorkerError.WorkerError)(cause)
435
- ? new UnexpectedError({ cause })
436
- : cause,
437
- ),
499
+ )
500
+
501
+ const runInWorkerStream = <A, I, E, EI, R>(
502
+ req: WorkerSchema.LeaderWorkerInnerRequest & Schema.WithResult<A, I, E, EI, R>,
503
+ ): Stream.Stream<A, E, R> =>
504
+ worker.execute(req).pipe(
505
+ Stream.refineOrDie((e) => isWorkerTransportError(e) === true ? Option.none() : Option.some(e)),
438
506
  Stream.withSpan(`@livestore/adapter-node:client-session:runInWorkerStream:${req._tag}`),
439
- ) as any
507
+ )
440
508
 
441
509
  const bootStatusFiber = yield* runInWorkerStream(new WorkerSchema.LeaderWorkerInnerBootStatusStream()).pipe(
442
510
  Stream.tap((bootStatus) => Queue.offer(bootStatusQueue, bootStatus)),
443
511
  Stream.runDrain,
444
- Effect.tapErrorCause((cause) => (Cause.isInterruptedOnly(cause) ? Effect.void : shutdown(Exit.failCause(cause)))),
512
+ Effect.tapErrorCause((cause) => (Cause.isInterruptedOnly(cause) === true ? Effect.void : shutdown(Exit.failCause(cause)))),
445
513
  Effect.interruptible,
446
514
  Effect.tapCauseLogPretty,
447
515
  Effect.forkScoped,
@@ -456,8 +524,7 @@ const makeWorkerLeaderThread = ({
456
524
  const initialLeaderHead = yield* runInWorker(new WorkerSchema.LeaderWorkerInnerGetLeaderHead())
457
525
 
458
526
  const bootResult = yield* runInWorker(new WorkerSchema.LeaderWorkerInnerGetRecreateSnapshot()).pipe(
459
- Effect.timeout(10_000),
460
- UnexpectedError.mapToUnexpectedError,
527
+ Effect.timeoutOrDie(10_000),
461
528
  Effect.withSpan('@livestore/adapter-node:client-session:export'),
462
529
  )
463
530
 
@@ -472,26 +539,36 @@ const makeWorkerLeaderThread = ({
472
539
  attributes: { batchSize: batch.length },
473
540
  }),
474
541
  ),
542
+ stream: (options) =>
543
+ runInWorkerStream(new WorkerSchema.LeaderWorkerInnerStreamEvents(options)).pipe(
544
+ Stream.withSpan('@livestore/adapter-node:client-session:streamEvents'),
545
+ Stream.orDie,
546
+ ),
475
547
  },
476
548
  initialState: {
477
549
  leaderHead: initialLeaderHead,
478
550
  migrationsReport: bootResult.migrationsReport,
551
+ storageMode: 'persisted',
479
552
  },
480
553
  export: runInWorker(new WorkerSchema.LeaderWorkerInnerExport()).pipe(
481
- Effect.timeout(10_000),
482
- UnexpectedError.mapToUnexpectedError,
554
+ Effect.timeoutOrDie(10_000),
483
555
  Effect.withSpan('@livestore/adapter-node:client-session:export'),
484
556
  ),
485
557
  getEventlogData: Effect.dieMessage('Not implemented'),
486
- getSyncState: runInWorker(new WorkerSchema.LeaderWorkerInnerGetLeaderSyncState()).pipe(
487
- UnexpectedError.mapToUnexpectedError,
488
- Effect.withSpan('@livestore/adapter-node:client-session:getLeaderSyncState'),
489
- ),
558
+ syncState: Subscribable.make({
559
+ get: runInWorker(new WorkerSchema.LeaderWorkerInnerGetLeaderSyncState()).pipe(
560
+ Effect.withSpan('@livestore/adapter-node:client-session:getLeaderSyncState'),
561
+ ),
562
+ changes: runInWorkerStream(new WorkerSchema.LeaderWorkerInnerSyncStateStream()).pipe(Stream.orDie),
563
+ }),
490
564
  sendDevtoolsMessage: (message) =>
491
565
  runInWorker(new WorkerSchema.LeaderWorkerInnerExtraDevtoolsMessage({ message })).pipe(
492
- UnexpectedError.mapToUnexpectedError,
493
566
  Effect.withSpan('@livestore/adapter-node:client-session:devtoolsMessageForLeader'),
494
567
  ),
568
+ networkStatus: Subscribable.make({
569
+ get: runInWorker(new WorkerSchema.LeaderWorkerInnerGetNetworkStatus()).pipe(Effect.orDie),
570
+ changes: runInWorkerStream(new WorkerSchema.LeaderWorkerInnerNetworkStatusStream()).pipe(Stream.orDie),
571
+ }),
495
572
  },
496
573
  {
497
574
  ...omitUndefineds({ overrides: testing?.overrides?.clientSession?.leaderThreadProxy }),
@@ -20,6 +20,16 @@ import { makeMeshNode, makeWebSocketEdge } from '@livestore/webmesh'
20
20
 
21
21
  import { makeViteMiddleware } from './vite-dev-server.ts'
22
22
 
23
+ /**
24
+ * Determines if a request URL should be routed to the Vite middleware.
25
+ * Includes LiveStore devtools paths and Vite internal paths like `/@fs/`, `/@vite/`, etc.
26
+ */
27
+ const shouldRouteToVite = (url: string): boolean =>
28
+ url.startsWith('/_livestore') ||
29
+ url.startsWith('/@fs') ||
30
+ url.startsWith('/@vite') ||
31
+ url.startsWith('/@react-refresh')
32
+
23
33
  /**
24
34
  * Starts a devtools HTTP/WS server which serves ...
25
35
  * - the Devtools UI via Vite
@@ -39,11 +49,12 @@ export const startDevtoolsServer = ({
39
49
  Effect.gen(function* () {
40
50
  const viteMiddleware = yield* makeViteMiddleware({
41
51
  mode: { _tag: 'node', url: `http://${host}:${port}` },
42
- schemaPath: isReadonlyArray(schemaPath)
43
- ? schemaPath.map((schemaPath) => path.resolve(process.cwd(), schemaPath))
44
- : path.resolve(process.cwd(), schemaPath),
52
+ schemaPath:
53
+ isReadonlyArray(schemaPath) === true
54
+ ? schemaPath.map((schemaPath) => path.resolve(process.cwd(), schemaPath))
55
+ : path.resolve(process.cwd(), schemaPath),
45
56
  viteConfig: (viteConfig) => {
46
- if (LS_DEV) {
57
+ if (LS_DEV === true) {
47
58
  viteConfig.server ??= {}
48
59
  viteConfig.server.fs ??= {}
49
60
  viteConfig.server.fs.strict = false
@@ -63,7 +74,7 @@ export const startDevtoolsServer = ({
63
74
  const handler = Effect.gen(function* () {
64
75
  const req = yield* HttpServerRequest.HttpServerRequest
65
76
 
66
- if (Headers.has(req.headers, 'upgrade')) {
77
+ if (Headers.has(req.headers, 'upgrade') === true) {
67
78
  // yield* Effect.logDebug(`WS Relay ${relayNodeName}: WS upgrade request ${req.url}`)
68
79
 
69
80
  const socket = yield* req.upgrade
@@ -81,7 +92,7 @@ export const startDevtoolsServer = ({
81
92
  .addEdge({ target: from, edgeChannel: webChannel, replaceIfExists: true })
82
93
  .pipe(Effect.acquireRelease(() => node.removeEdge(from).pipe(Effect.orDie)))
83
94
 
84
- if (LS_DEV) {
95
+ if (LS_DEV === true) {
85
96
  yield* Effect.log(`WS Relay ${relayNodeName}: added edge from '${from}'`)
86
97
  yield* Effect.addFinalizerLog(`WS Relay ${relayNodeName}: removed edge from '${from}'`)
87
98
  }
@@ -96,7 +107,7 @@ export const startDevtoolsServer = ({
96
107
  } else {
97
108
  if (req.url === '/' || req.url === '') {
98
109
  return HttpServerResponse.redirect('/_livestore/node')
99
- } else if (req.url.startsWith('/_livestore')) {
110
+ } else if (shouldRouteToVite(req.url) === true) {
100
111
  // Here we're delegating to the Vite middleware
101
112
 
102
113
  // TODO replace this once @effect/platform-node supports Node HTTP middlewares
@@ -114,9 +125,10 @@ export const startDevtoolsServer = ({
114
125
  return HttpServerResponse.text('Not found')
115
126
  }).pipe(Effect.tapCauseLogPretty, Effect.interruptible)
116
127
 
117
- const sessionSuffix = clientSessionInfo
118
- ? `/${clientSessionInfo.storeId}/${clientSessionInfo.clientId}/${clientSessionInfo.sessionId}/${clientSessionInfo.schemaAlias}`
119
- : '?autoconnect'
128
+ const sessionSuffix =
129
+ clientSessionInfo !== undefined
130
+ ? `/${clientSessionInfo.storeId}/${clientSessionInfo.clientId}/${clientSessionInfo.sessionId}/${clientSessionInfo.schemaAlias}`
131
+ : '?autoconnect'
120
132
 
121
133
  // Use `localhost` instead of `0.0.0.0` as it doesn't have the `navigator.locks` web adapter limitation (https://share.cleanshot.com/nHBnmk6S)
122
134
  const maybeLocalhost = host === '0.0.0.0' ? 'localhost' : host
@@ -1,3 +1,3 @@
1
1
  export { startDevtoolsServer } from './devtools-server.ts'
2
2
  export type { ViteDevtoolsOptions } from './vite-dev-server.ts'
3
- export { makeViteMiddleware } from './vite-dev-server.ts'
3
+ export { DevtoolsViteNotInstalledError, makeViteMiddleware } from './vite-dev-server.ts'
@@ -1,12 +1,44 @@
1
1
  import path from 'node:path'
2
2
 
3
+ import type * as Vite from 'vite'
4
+
3
5
  import type { Devtools } from '@livestore/common'
4
- import { UnexpectedError } from '@livestore/common'
5
- import { livestoreDevtoolsPlugin } from '@livestore/devtools-vite'
6
+ import { UnknownError } from '@livestore/common'
6
7
  import { isReadonlyArray } from '@livestore/utils'
7
- import { Effect } from '@livestore/utils/effect'
8
+ import { Effect, Schema } from '@livestore/utils/effect'
8
9
  import { getFreePort } from '@livestore/utils/node'
9
- import * as Vite from 'vite'
10
+
11
+ /**
12
+ * Error thrown when @livestore/devtools-vite is not installed.
13
+ * This is a peer dependency that must be installed separately.
14
+ */
15
+ export class DevtoolsViteNotInstalledError extends Schema.TaggedError<DevtoolsViteNotInstalledError>(
16
+ '~@livestore/adapter-node/DevtoolsViteNotInstalledError',
17
+ )('DevtoolsViteNotInstalledError', {
18
+ cause: Schema.Defect,
19
+ }) {
20
+ override get message(): string {
21
+ return (
22
+ `@livestore/devtools-vite is required for devtools but not installed. ` +
23
+ `Install it with: pnpm add @livestore/devtools-vite@<version>. ` +
24
+ `Make sure to use the same version as @livestore/adapter-node.`
25
+ )
26
+ }
27
+ }
28
+
29
+ /** Error thrown when vite is not installed for the devtools server path. */
30
+ export class ViteNotInstalledError extends Schema.TaggedError<ViteNotInstalledError>(
31
+ '~@livestore/adapter-node/ViteNotInstalledError',
32
+ )('ViteNotInstalledError', {
33
+ cause: Schema.Defect,
34
+ }) {
35
+ override get message(): string {
36
+ return (
37
+ `vite is required for @livestore/adapter-node/devtools but not installed. ` +
38
+ `Install it with: pnpm add -D vite@<version>.`
39
+ )
40
+ }
41
+ }
10
42
 
11
43
  export type ViteDevtoolsOptions = {
12
44
  viteConfig?: (config: Vite.UserConfig) => Vite.UserConfig
@@ -26,11 +58,16 @@ export type ViteDevtoolsOptions = {
26
58
  }
27
59
 
28
60
  // NOTE this is currently also used in @livestore/devtools-expo
29
- export const makeViteMiddleware = (options: ViteDevtoolsOptions): Effect.Effect<Vite.ViteDevServer, UnexpectedError> =>
61
+ export const makeViteMiddleware = (
62
+ options: ViteDevtoolsOptions,
63
+ ): Effect.Effect<Vite.ViteDevServer, DevtoolsViteNotInstalledError | ViteNotInstalledError | UnknownError> =>
30
64
  Effect.gen(function* () {
65
+ const { livestoreDevtoolsPlugin } = yield* importDevtoolsVite()
66
+ const Vite = yield* importVite()
67
+
31
68
  const cwd = process.cwd()
32
69
 
33
- const hmrPort = yield* getFreePort.pipe(UnexpectedError.mapToUnexpectedError)
70
+ const hmrPort = yield* getFreePort.pipe(UnknownError.mapToUnknownError)
34
71
 
35
72
  const defaultViteConfig = Vite.defineConfig({
36
73
  server: {
@@ -39,13 +76,13 @@ export const makeViteMiddleware = (options: ViteDevtoolsOptions): Effect.Effect<
39
76
  port: hmrPort,
40
77
  },
41
78
  // Relaxing fs access for monorepo setup
42
- fs: { strict: process.env.LS_DEV ? false : true },
79
+ fs: { strict: process.env.LS_DEV !== undefined ? false : true },
43
80
  },
44
81
  appType: 'spa',
45
82
  base: '/_livestore/',
46
83
  plugins: [
47
84
  livestoreDevtoolsPlugin({
48
- schemaPath: isReadonlyArray(options.schemaPath)
85
+ schemaPath: isReadonlyArray(options.schemaPath) === true
49
86
  ? options.schemaPath.map((schemaPath) => path.resolve(cwd, schemaPath))
50
87
  : path.resolve(cwd, options.schemaPath),
51
88
  mode: options.mode,
@@ -58,9 +95,24 @@ export const makeViteMiddleware = (options: ViteDevtoolsOptions): Effect.Effect<
58
95
 
59
96
  const viteConfig = options.viteConfig?.(defaultViteConfig) ?? defaultViteConfig
60
97
 
61
- const viteServer = yield* Effect.promise(() => Vite.createServer(viteConfig)).pipe(
62
- UnexpectedError.mapToUnexpectedError,
63
- )
98
+ const viteServer = yield* Effect.promise(() => Vite.createServer(viteConfig)).pipe(UnknownError.mapToUnknownError)
64
99
 
65
100
  return viteServer
66
101
  }).pipe(Effect.withSpan('@livestore/adapter-node:devtools:makeViteServer'))
102
+
103
+ /**
104
+ * Dynamically imports @livestore/devtools-vite.
105
+ * This package is a peer dependency and may not be installed.
106
+ */
107
+ const importDevtoolsVite = () =>
108
+ Effect.tryPromise({
109
+ try: () => import('@livestore/devtools-vite'),
110
+ catch: (cause) => new DevtoolsViteNotInstalledError({ cause }),
111
+ })
112
+
113
+ /** Dynamically imports vite for the devtools-only server path. */
114
+ const importVite = () =>
115
+ Effect.tryPromise({
116
+ try: () => import('vite'),
117
+ catch: (cause) => new ViteNotInstalledError({ cause }),
118
+ })