@livestore/adapter-node 0.4.0-dev.2 → 0.4.0-dev.20

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,33 +1,38 @@
1
1
  import { hostname } from 'node:os'
2
+ import path from 'node:path'
2
3
  import * as WT from 'node:worker_threads'
3
4
  import {
4
5
  type Adapter,
5
6
  type BootStatus,
6
7
  ClientSessionLeaderThreadProxy,
7
- type IntentionalShutdownCause,
8
+ IntentionalShutdownCause,
8
9
  type LockStatus,
9
10
  type MakeSqliteDb,
10
11
  makeClientSession,
11
12
  type SyncError,
12
13
  type SyncOptions,
13
- UnexpectedError,
14
+ UnknownError,
14
15
  } from '@livestore/common'
15
16
  import { Eventlog, LeaderThreadCtx } from '@livestore/common/leader-thread'
16
17
  import type { LiveStoreSchema } from '@livestore/common/schema'
17
18
  import { LiveStoreEvent } from '@livestore/common/schema'
18
19
  import { loadSqlite3Wasm } from '@livestore/sqlite-wasm/load-wasm'
19
20
  import { sqliteDbFactory } from '@livestore/sqlite-wasm/node'
21
+ import { omitUndefineds } from '@livestore/utils'
20
22
  import {
21
23
  Cause,
22
24
  Effect,
23
25
  Exit,
24
26
  FetchHttpClient,
25
27
  Fiber,
28
+ FileSystem,
26
29
  Layer,
27
30
  ParseResult,
28
31
  Queue,
32
+ Schedule,
29
33
  Schema,
30
34
  Stream,
35
+ Subscribable,
31
36
  SubscriptionRef,
32
37
  Worker,
33
38
  WorkerError,
@@ -50,6 +55,13 @@ export interface NodeAdapterOptions {
50
55
  */
51
56
  sessionId?: string
52
57
 
58
+ /**
59
+ * Warning: This will reset both the app and eventlog database. This should only be used during development.
60
+ *
61
+ * @default false
62
+ */
63
+ resetPersistence?: boolean
64
+
53
65
  devtools?: {
54
66
  schemaPath: string | URL
55
67
  /**
@@ -89,13 +101,19 @@ export const makeAdapter = ({
89
101
  */
90
102
  export const makeWorkerAdapter = ({
91
103
  workerUrl,
104
+ workerExtraArgs,
92
105
  ...options
93
106
  }: NodeAdapterOptions & {
94
107
  /**
95
108
  * Example: `new URL('./livestore.worker.ts', import.meta.url)`
96
109
  */
97
110
  workerUrl: URL
98
- }): Adapter => makeAdapterImpl({ ...options, leaderThread: { _tag: 'multi-threaded', workerUrl } })
111
+ /**
112
+ * Extra arguments to pass to the worker which can be accessed in the worker
113
+ * via `getWorkerArgs()`
114
+ */
115
+ workerExtraArgs?: Schema.JsonValue
116
+ }): Adapter => makeAdapterImpl({ ...options, leaderThread: { _tag: 'multi-threaded', workerUrl, workerExtraArgs } })
99
117
 
100
118
  const makeAdapterImpl = ({
101
119
  storage,
@@ -104,6 +122,7 @@ const makeAdapterImpl = ({
104
122
  // TODO make this dynamic and actually support multiple sessions
105
123
  sessionId = 'static',
106
124
  testing,
125
+ resetPersistence = false,
107
126
  leaderThread: leaderThreadInput,
108
127
  }: NodeAdapterOptions & {
109
128
  leaderThread:
@@ -114,11 +133,13 @@ const makeAdapterImpl = ({
114
133
  | {
115
134
  _tag: 'multi-threaded'
116
135
  workerUrl: URL
136
+ workerExtraArgs: Schema.JsonValue | undefined
117
137
  }
118
138
  }): Adapter =>
119
139
  ((adapterArgs) =>
120
140
  Effect.gen(function* () {
121
- const { storeId, devtoolsEnabled, shutdown, bootStatusQueue, syncPayload, schema } = adapterArgs
141
+ const { storeId, devtoolsEnabled, shutdown, bootStatusQueue, syncPayloadEncoded, syncPayloadSchema, schema } =
142
+ adapterArgs
122
143
 
123
144
  yield* Queue.offer(bootStatusQueue, { stage: 'loading' })
124
145
 
@@ -135,6 +156,14 @@ const makeAdapterImpl = ({
135
156
 
136
157
  const shutdownChannel = yield* makeShutdownChannel(storeId)
137
158
 
159
+ if (resetPersistence === true) {
160
+ yield* shutdownChannel
161
+ .send(IntentionalShutdownCause.make({ reason: 'adapter-reset' }))
162
+ .pipe(UnknownError.mapToUnknownError)
163
+
164
+ yield* resetNodePersistence({ storage, storeId })
165
+ }
166
+
138
167
  yield* shutdownChannel.listen.pipe(
139
168
  Stream.flatten(),
140
169
  Stream.tap((cause) =>
@@ -173,22 +202,26 @@ const makeAdapterImpl = ({
173
202
  clientId,
174
203
  schema,
175
204
  makeSqliteDb,
176
- syncOptions: leaderThreadInput.sync,
177
- syncPayload,
178
205
  devtools: devtoolsOptions,
179
206
  storage,
180
- testing,
181
- }).pipe(UnexpectedError.mapToUnexpectedError)
207
+ ...omitUndefineds({
208
+ syncOptions: leaderThreadInput.sync,
209
+ syncPayloadEncoded,
210
+ syncPayloadSchema,
211
+ testing,
212
+ }),
213
+ }).pipe(UnknownError.mapToUnknownError)
182
214
  : yield* makeWorkerLeaderThread({
183
215
  shutdown,
184
216
  storeId,
185
217
  clientId,
186
218
  sessionId,
187
219
  workerUrl: leaderThreadInput.workerUrl,
220
+ workerExtraArgs: leaderThreadInput.workerExtraArgs,
188
221
  storage,
189
222
  devtools: devtoolsOptions,
190
223
  bootStatusQueue,
191
- syncPayload,
224
+ syncPayloadEncoded,
192
225
  })
193
226
 
194
227
  syncInMemoryDb.import(initialSnapshot)
@@ -214,22 +247,52 @@ const makeAdapterImpl = ({
214
247
  isLeader: true,
215
248
  // Not really applicable for node as there is no "reload the app" concept
216
249
  registerBeforeUnload: (_onBeforeUnload) => () => {},
250
+ origin: undefined,
217
251
  })
218
252
 
219
253
  return clientSession
220
254
  }).pipe(
221
255
  Effect.withSpan('@livestore/adapter-node:adapter'),
222
- Effect.provide(PlatformNode.NodeFileSystem.layer),
223
- Effect.provide(FetchHttpClient.layer),
256
+ Effect.provide(Layer.mergeAll(PlatformNode.NodeFileSystem.layer, FetchHttpClient.layer)),
224
257
  )) satisfies Adapter
225
258
 
259
+ const resetNodePersistence = ({
260
+ storage,
261
+ storeId,
262
+ }: {
263
+ storage: WorkerSchema.StorageType
264
+ storeId: string
265
+ }): Effect.Effect<void, UnknownError, FileSystem.FileSystem> => {
266
+ if (storage.type !== 'fs') {
267
+ return Effect.void
268
+ }
269
+
270
+ const directory = path.join(storage.baseDirectory ?? '', storeId)
271
+
272
+ return Effect.gen(function* () {
273
+ const fs = yield* FileSystem.FileSystem
274
+
275
+ const directoryExists = yield* fs.exists(directory).pipe(UnknownError.mapToUnknownError)
276
+
277
+ if (directoryExists === false) {
278
+ return
279
+ }
280
+
281
+ yield* fs.remove(directory, { recursive: true }).pipe(UnknownError.mapToUnknownError)
282
+ }).pipe(
283
+ Effect.retry({ schedule: Schedule.exponentialBackoff10Sec }),
284
+ Effect.withSpan('@livestore/adapter-node:resetPersistence', { attributes: { directory } }),
285
+ )
286
+ }
287
+
226
288
  const makeLocalLeaderThread = ({
227
289
  storeId,
228
290
  clientId,
229
291
  schema,
230
292
  makeSqliteDb,
231
293
  syncOptions,
232
- syncPayload,
294
+ syncPayloadEncoded,
295
+ syncPayloadSchema,
233
296
  storage,
234
297
  devtools,
235
298
  testing,
@@ -240,7 +303,8 @@ const makeLocalLeaderThread = ({
240
303
  makeSqliteDb: MakeSqliteDb
241
304
  syncOptions: SyncOptions | undefined
242
305
  storage: WorkerSchema.StorageType
243
- syncPayload: Schema.JsonValue | undefined
306
+ syncPayloadEncoded: Schema.JsonValue | undefined
307
+ syncPayloadSchema: Schema.Schema<any>
244
308
  devtools: WorkerSchema.LeaderWorkerInnerInitialMessage['devtools']
245
309
  testing?: {
246
310
  overrides?: TestingOverrides
@@ -254,15 +318,17 @@ const makeLocalLeaderThread = ({
254
318
  schema,
255
319
  syncOptions,
256
320
  storage,
257
- syncPayload,
321
+ syncPayloadEncoded,
322
+ syncPayloadSchema,
258
323
  devtools,
259
324
  makeSqliteDb,
260
- testing: testing?.overrides,
325
+ ...omitUndefineds({ testing: testing?.overrides }),
261
326
  }).pipe(Layer.unwrapScoped),
262
327
  )
263
328
 
264
329
  return yield* Effect.gen(function* () {
265
- const { dbState, dbEventlog, syncProcessor, extraIncomingMessagesQueue, initialState } = yield* LeaderThreadCtx
330
+ const { dbState, dbEventlog, syncProcessor, extraIncomingMessagesQueue, initialState, networkStatus } =
331
+ yield* LeaderThreadCtx
266
332
 
267
333
  const initialLeaderHead = Eventlog.getClientHeadFromDb(dbEventlog)
268
334
 
@@ -272,17 +338,18 @@ const makeLocalLeaderThread = ({
272
338
  pull: ({ cursor }) => syncProcessor.pull({ cursor }),
273
339
  push: (batch) =>
274
340
  syncProcessor.push(
275
- batch.map((item) => new LiveStoreEvent.EncodedWithMeta(item)),
341
+ batch.map((item) => new LiveStoreEvent.Client.EncodedWithMeta(item)),
276
342
  { waitForProcessing: true },
277
343
  ),
278
344
  },
279
345
  initialState: { leaderHead: initialLeaderHead, migrationsReport: initialState.migrationsReport },
280
346
  export: Effect.sync(() => dbState.export()),
281
347
  getEventlogData: Effect.sync(() => dbEventlog.export()),
282
- getSyncState: syncProcessor.syncState,
348
+ syncState: syncProcessor.syncState,
283
349
  sendDevtoolsMessage: (message) => extraIncomingMessagesQueue.offer(message),
350
+ networkStatus,
284
351
  },
285
- { overrides: testing?.overrides?.clientSession?.leaderThreadProxy },
352
+ { ...omitUndefineds({ overrides: testing?.overrides?.clientSession?.leaderThreadProxy }) },
286
353
  )
287
354
 
288
355
  const initialSnapshot = dbState.export()
@@ -297,21 +364,23 @@ const makeWorkerLeaderThread = ({
297
364
  clientId,
298
365
  sessionId,
299
366
  workerUrl,
367
+ workerExtraArgs,
300
368
  storage,
301
369
  devtools,
302
370
  bootStatusQueue,
303
- syncPayload,
371
+ syncPayloadEncoded,
304
372
  testing,
305
373
  }: {
306
- shutdown: (cause: Exit.Exit<IntentionalShutdownCause, UnexpectedError | SyncError>) => Effect.Effect<void>
374
+ shutdown: (cause: Exit.Exit<IntentionalShutdownCause, UnknownError | SyncError>) => Effect.Effect<void>
307
375
  storeId: string
308
376
  clientId: string
309
377
  sessionId: string
310
378
  workerUrl: URL
379
+ workerExtraArgs: Schema.JsonValue | undefined
311
380
  storage: WorkerSchema.StorageType
312
381
  devtools: WorkerSchema.LeaderWorkerInnerInitialMessage['devtools']
313
382
  bootStatusQueue: Queue.Queue<BootStatus>
314
- syncPayload: Schema.JsonValue | undefined
383
+ syncPayloadEncoded: Schema.JsonValue | undefined
315
384
  testing?: {
316
385
  overrides?: TestingOverrides
317
386
  }
@@ -319,7 +388,7 @@ const makeWorkerLeaderThread = ({
319
388
  Effect.gen(function* () {
320
389
  const nodeWorker = new WT.Worker(workerUrl, {
321
390
  execArgv: process.env.DEBUG_WORKER ? ['--inspect --enable-source-maps'] : ['--enable-source-maps'],
322
- argv: [Schema.encodeSync(WorkerSchema.WorkerArgv)({ storeId, clientId, sessionId })],
391
+ argv: [Schema.encodeSync(WorkerSchema.WorkerArgv)({ storeId, clientId, sessionId, extraArgs: workerExtraArgs })],
323
392
  })
324
393
  const nodeWorkerLayer = yield* Layer.build(PlatformNode.NodeWorker.layer(() => nodeWorker))
325
394
 
@@ -332,11 +401,11 @@ const makeWorkerLeaderThread = ({
332
401
  clientId,
333
402
  storage,
334
403
  devtools,
335
- syncPayload,
404
+ syncPayloadEncoded,
336
405
  }),
337
406
  }).pipe(
338
407
  Effect.provide(nodeWorkerLayer),
339
- UnexpectedError.mapToUnexpectedError,
408
+ UnknownError.mapToUnknownError,
340
409
  Effect.tapErrorCause((cause) => shutdown(Exit.failCause(cause))),
341
410
  Effect.withSpan('@livestore/adapter-node:adapter:setupLeaderThread'),
342
411
  )
@@ -344,7 +413,7 @@ const makeWorkerLeaderThread = ({
344
413
  const runInWorker = <TReq extends typeof WorkerSchema.LeaderWorkerInnerRequest.Type>(
345
414
  req: TReq,
346
415
  ): TReq extends Schema.WithResult<infer A, infer _I, infer _E, infer _EI, infer R>
347
- ? Effect.Effect<A, UnexpectedError, R>
416
+ ? Effect.Effect<A, UnknownError, R>
348
417
  : never =>
349
418
  (worker.executeEffect(req) as any).pipe(
350
419
  Effect.logWarnIfTakesLongerThan({
@@ -353,26 +422,26 @@ const makeWorkerLeaderThread = ({
353
422
  }),
354
423
  Effect.withSpan(`@livestore/adapter-node:client-session:runInWorker:${req._tag}`),
355
424
  Effect.mapError((cause) =>
356
- Schema.is(UnexpectedError)(cause)
425
+ Schema.is(UnknownError)(cause)
357
426
  ? cause
358
427
  : ParseResult.isParseError(cause) || Schema.is(WorkerError.WorkerError)(cause)
359
- ? new UnexpectedError({ cause })
428
+ ? new UnknownError({ cause })
360
429
  : cause,
361
430
  ),
362
- Effect.catchAllDefect((cause) => new UnexpectedError({ cause })),
431
+ Effect.catchAllDefect((cause) => new UnknownError({ cause })),
363
432
  ) as any
364
433
 
365
434
  const runInWorkerStream = <TReq extends typeof WorkerSchema.LeaderWorkerInnerRequest.Type>(
366
435
  req: TReq,
367
436
  ): TReq extends Schema.WithResult<infer A, infer _I, infer _E, infer _EI, infer R>
368
- ? Stream.Stream<A, UnexpectedError, R>
437
+ ? Stream.Stream<A, UnknownError, R>
369
438
  : never =>
370
439
  worker.execute(req as any).pipe(
371
440
  Stream.mapError((cause) =>
372
- Schema.is(UnexpectedError)(cause)
441
+ Schema.is(UnknownError)(cause)
373
442
  ? cause
374
443
  : ParseResult.isParseError(cause) || Schema.is(WorkerError.WorkerError)(cause)
375
- ? new UnexpectedError({ cause })
444
+ ? new UnknownError({ cause })
376
445
  : cause,
377
446
  ),
378
447
  Stream.withSpan(`@livestore/adapter-node:client-session:runInWorkerStream:${req._tag}`),
@@ -397,7 +466,7 @@ const makeWorkerLeaderThread = ({
397
466
 
398
467
  const bootResult = yield* runInWorker(new WorkerSchema.LeaderWorkerInnerGetRecreateSnapshot()).pipe(
399
468
  Effect.timeout(10_000),
400
- UnexpectedError.mapToUnexpectedError,
469
+ UnknownError.mapToUnknownError,
401
470
  Effect.withSpan('@livestore/adapter-node:client-session:export'),
402
471
  )
403
472
 
@@ -419,22 +488,29 @@ const makeWorkerLeaderThread = ({
419
488
  },
420
489
  export: runInWorker(new WorkerSchema.LeaderWorkerInnerExport()).pipe(
421
490
  Effect.timeout(10_000),
422
- UnexpectedError.mapToUnexpectedError,
491
+ UnknownError.mapToUnknownError,
423
492
  Effect.withSpan('@livestore/adapter-node:client-session:export'),
424
493
  ),
425
494
  getEventlogData: Effect.dieMessage('Not implemented'),
426
- getSyncState: runInWorker(new WorkerSchema.LeaderWorkerInnerGetLeaderSyncState()).pipe(
427
- UnexpectedError.mapToUnexpectedError,
428
- Effect.withSpan('@livestore/adapter-node:client-session:getLeaderSyncState'),
429
- ),
495
+ syncState: Subscribable.make({
496
+ get: runInWorker(new WorkerSchema.LeaderWorkerInnerGetLeaderSyncState()).pipe(
497
+ UnknownError.mapToUnknownError,
498
+ Effect.withSpan('@livestore/adapter-node:client-session:getLeaderSyncState'),
499
+ ),
500
+ changes: runInWorkerStream(new WorkerSchema.LeaderWorkerInnerSyncStateStream()).pipe(Stream.orDie),
501
+ }),
430
502
  sendDevtoolsMessage: (message) =>
431
503
  runInWorker(new WorkerSchema.LeaderWorkerInnerExtraDevtoolsMessage({ message })).pipe(
432
- UnexpectedError.mapToUnexpectedError,
504
+ UnknownError.mapToUnknownError,
433
505
  Effect.withSpan('@livestore/adapter-node:client-session:devtoolsMessageForLeader'),
434
506
  ),
507
+ networkStatus: Subscribable.make({
508
+ get: runInWorker(new WorkerSchema.LeaderWorkerInnerGetNetworkStatus()).pipe(Effect.orDie),
509
+ changes: runInWorkerStream(new WorkerSchema.LeaderWorkerInnerNetworkStatusStream()).pipe(Stream.orDie),
510
+ }),
435
511
  },
436
512
  {
437
- overrides: testing?.overrides?.clientSession?.leaderThreadProxy,
513
+ ...omitUndefineds({ overrides: testing?.overrides?.clientSession?.leaderThreadProxy }),
438
514
  },
439
515
  )
440
516
 
@@ -1,7 +1,7 @@
1
1
  import path from 'node:path'
2
2
 
3
3
  import type { Devtools } from '@livestore/common'
4
- import { UnexpectedError } from '@livestore/common'
4
+ import { UnknownError } from '@livestore/common'
5
5
  import { livestoreDevtoolsPlugin } from '@livestore/devtools-vite'
6
6
  import { isReadonlyArray } from '@livestore/utils'
7
7
  import { Effect } from '@livestore/utils/effect'
@@ -26,11 +26,11 @@ export type ViteDevtoolsOptions = {
26
26
  }
27
27
 
28
28
  // NOTE this is currently also used in @livestore/devtools-expo
29
- export const makeViteMiddleware = (options: ViteDevtoolsOptions): Effect.Effect<Vite.ViteDevServer, UnexpectedError> =>
29
+ export const makeViteMiddleware = (options: ViteDevtoolsOptions): Effect.Effect<Vite.ViteDevServer, UnknownError> =>
30
30
  Effect.gen(function* () {
31
31
  const cwd = process.cwd()
32
32
 
33
- const hmrPort = yield* getFreePort.pipe(UnexpectedError.mapToUnexpectedError)
33
+ const hmrPort = yield* getFreePort.pipe(UnknownError.mapToUnknownError)
34
34
 
35
35
  const defaultViteConfig = Vite.defineConfig({
36
36
  server: {
@@ -58,9 +58,7 @@ export const makeViteMiddleware = (options: ViteDevtoolsOptions): Effect.Effect<
58
58
 
59
59
  const viteConfig = options.viteConfig?.(defaultViteConfig) ?? defaultViteConfig
60
60
 
61
- const viteServer = yield* Effect.promise(() => Vite.createServer(viteConfig)).pipe(
62
- UnexpectedError.mapToUnexpectedError,
63
- )
61
+ const viteServer = yield* Effect.promise(() => Vite.createServer(viteConfig)).pipe(UnknownError.mapToUnknownError)
64
62
 
65
63
  return viteServer
66
64
  }).pipe(Effect.withSpan('@livestore/adapter-node:devtools:makeViteServer'))
@@ -7,7 +7,7 @@ if (process.execArgv.includes('--inspect')) {
7
7
  }
8
8
 
9
9
  import type { ClientSessionLeaderThreadProxy, MakeSqliteDb, SqliteDb, SyncOptions } from '@livestore/common'
10
- import { Devtools, liveStoreStorageFormatVersion, migrateDb, UnexpectedError } from '@livestore/common'
10
+ import { Devtools, liveStoreStorageFormatVersion, migrateDb, UnknownError } from '@livestore/common'
11
11
  import type { DevtoolsOptions, LeaderSqliteDb, LeaderThreadCtx } from '@livestore/common/leader-thread'
12
12
  import { configureConnection, makeLeaderThreadLayer } from '@livestore/common/leader-thread'
13
13
  import type { LiveStoreSchema } from '@livestore/common/schema'
@@ -30,7 +30,7 @@ export type TestingOverrides = {
30
30
  dbEventlog: SqliteDb
31
31
  dbState: SqliteDb
32
32
  },
33
- UnexpectedError
33
+ UnknownError
34
34
  >
35
35
  }
36
36
 
@@ -42,7 +42,8 @@ export interface MakeLeaderThreadArgs {
42
42
  makeSqliteDb: MakeNodeSqliteDb
43
43
  devtools: WorkerSchema.LeaderWorkerInnerInitialMessage['devtools']
44
44
  schema: LiveStoreSchema
45
- syncPayload: Schema.JsonValue | undefined
45
+ syncPayloadEncoded: Schema.JsonValue | undefined
46
+ syncPayloadSchema: Schema.Schema<any> | undefined
46
47
  testing: TestingOverrides | undefined
47
48
  }
48
49
 
@@ -54,11 +55,12 @@ export const makeLeaderThread = ({
54
55
  storage,
55
56
  devtools,
56
57
  schema,
57
- syncPayload,
58
+ syncPayloadEncoded,
59
+ syncPayloadSchema,
58
60
  testing,
59
61
  }: MakeLeaderThreadArgs): Effect.Effect<
60
- Layer.Layer<LeaderThreadCtx, UnexpectedError, Scope.Scope | HttpClient.HttpClient | FileSystem.FileSystem>,
61
- UnexpectedError,
62
+ Layer.Layer<LeaderThreadCtx, UnknownError, Scope.Scope | HttpClient.HttpClient | FileSystem.FileSystem>,
63
+ UnknownError,
62
64
  Scope.Scope
63
65
  > =>
64
66
  Effect.gen(function* () {
@@ -113,11 +115,12 @@ export const makeLeaderThread = ({
113
115
  dbEventlog,
114
116
  devtoolsOptions,
115
117
  shutdownChannel,
116
- syncPayload,
118
+ syncPayloadEncoded,
119
+ syncPayloadSchema,
117
120
  })
118
121
  }).pipe(
119
122
  Effect.tapCauseLogPretty,
120
- UnexpectedError.mapToUnexpectedError,
123
+ UnknownError.mapToUnknownError,
121
124
  Effect.withSpan('@livestore/adapter-node:makeLeaderThread', {
122
125
  attributes: { storeId, clientId, storage, devtools, syncOptions },
123
126
  }),
@@ -137,7 +140,7 @@ const makeDevtoolsOptions = ({
137
140
  storeId: string
138
141
  clientId: string
139
142
  devtools: WorkerSchema.LeaderWorkerInnerInitialMessage['devtools']
140
- }): Effect.Effect<DevtoolsOptions, UnexpectedError, Scope.Scope> =>
143
+ }): Effect.Effect<DevtoolsOptions, UnknownError, Scope.Scope> =>
141
144
  Effect.gen(function* () {
142
145
  if (devtools.enabled === false) {
143
146
  return {
@@ -161,6 +164,7 @@ const makeDevtoolsOptions = ({
161
164
  sessionId: 'static', // TODO make this dynamic
162
165
  schemaAlias: devtools.schemaAlias,
163
166
  isLeader: true,
167
+ origin: undefined,
164
168
  }),
165
169
  port: devtools.port,
166
170
  host: devtools.host,
@@ -8,24 +8,13 @@ if (process.execArgv.includes('--inspect')) {
8
8
  }
9
9
 
10
10
  import type { SyncOptions } from '@livestore/common'
11
- import { UnexpectedError } from '@livestore/common'
11
+ import { LogConfig, UnknownError } from '@livestore/common'
12
12
  import { Eventlog, LeaderThreadCtx } from '@livestore/common/leader-thread'
13
13
  import type { LiveStoreSchema } from '@livestore/common/schema'
14
14
  import { LiveStoreEvent } from '@livestore/common/schema'
15
15
  import { loadSqlite3Wasm } from '@livestore/sqlite-wasm/load-wasm'
16
16
  import { sqliteDbFactory } from '@livestore/sqlite-wasm/node'
17
- import {
18
- Effect,
19
- FetchHttpClient,
20
- identity,
21
- Layer,
22
- Logger,
23
- LogLevel,
24
- OtelTracer,
25
- Schema,
26
- Stream,
27
- WorkerRunner,
28
- } from '@livestore/utils/effect'
17
+ import { Effect, FetchHttpClient, Layer, OtelTracer, Schema, Stream, WorkerRunner } from '@livestore/utils/effect'
29
18
  import { PlatformNode } from '@livestore/utils/node'
30
19
  import type * as otel from '@opentelemetry/api'
31
20
 
@@ -36,21 +25,19 @@ import * as WorkerSchema from './worker-schema.ts'
36
25
  export type WorkerOptions = {
37
26
  schema: LiveStoreSchema
38
27
  sync?: SyncOptions
28
+ syncPayloadSchema?: Schema.Schema<any>
39
29
  otelOptions?: {
40
30
  tracer?: otel.Tracer
41
31
  /** @default 'livestore-node-leader-thread' */
42
32
  serviceName?: string
43
33
  }
44
34
  testing?: TestingOverrides
45
- }
35
+ } & LogConfig.WithLoggerOptions
46
36
 
47
37
  export const getWorkerArgs = () => Schema.decodeSync(WorkerSchema.WorkerArgv)(process.argv[2]!)
48
38
 
49
39
  export const makeWorker = (options: WorkerOptions) => {
50
- makeWorkerEffect(options).pipe(
51
- Effect.provide(Logger.prettyWithThread(options.otelOptions?.serviceName ?? 'livestore-node-leader-thread')),
52
- PlatformNode.NodeRuntime.runMain,
53
- )
40
+ makeWorkerEffect(options).pipe(PlatformNode.NodeRuntime.runMain)
54
41
  }
55
42
 
56
43
  export const makeWorkerEffect = (options: WorkerOptions) => {
@@ -60,6 +47,13 @@ export const makeWorkerEffect = (options: WorkerOptions) => {
60
47
  )
61
48
  : undefined
62
49
 
50
+ // Merge the runtime dependencies once so we can provide them together without chaining Effect.provide.
51
+ const runtimeLayer = Layer.mergeAll(
52
+ FetchHttpClient.layer,
53
+ PlatformNode.NodeFileSystem.layer,
54
+ TracingLive ?? Layer.empty,
55
+ )
56
+
63
57
  return WorkerRunner.layerSerialized(WorkerSchema.LeaderWorkerInnerRequest, {
64
58
  InitialMessage: (args) =>
65
59
  Effect.gen(function* () {
@@ -73,12 +67,14 @@ export const makeWorkerEffect = (options: WorkerOptions) => {
73
67
  schema: options.schema,
74
68
  testing: options.testing,
75
69
  makeSqliteDb,
70
+ syncPayloadEncoded: args.syncPayloadEncoded,
71
+ syncPayloadSchema: options.syncPayloadSchema,
76
72
  })
77
73
  }).pipe(Layer.unwrapScoped),
78
74
  PushToLeader: ({ batch }) =>
79
75
  Effect.andThen(LeaderThreadCtx, (_) =>
80
76
  _.syncProcessor.push(
81
- batch.map((item) => new LiveStoreEvent.EncodedWithMeta(item)),
77
+ batch.map((item) => new LiveStoreEvent.Client.EncodedWithMeta(item)),
82
78
  // We'll wait in order to keep back pressure on the client session
83
79
  { waitForProcessing: true },
84
80
  ),
@@ -92,27 +88,39 @@ export const makeWorkerEffect = (options: WorkerOptions) => {
92
88
  }).pipe(Stream.unwrapScoped),
93
89
  Export: () =>
94
90
  Effect.andThen(LeaderThreadCtx, (_) => _.dbState.export()).pipe(
95
- UnexpectedError.mapToUnexpectedError,
91
+ UnknownError.mapToUnknownError,
96
92
  Effect.withSpan('@livestore/adapter-node:worker:Export'),
97
93
  ),
98
94
  ExportEventlog: () =>
99
95
  Effect.andThen(LeaderThreadCtx, (_) => _.dbEventlog.export()).pipe(
100
- UnexpectedError.mapToUnexpectedError,
96
+ UnknownError.mapToUnknownError,
101
97
  Effect.withSpan('@livestore/adapter-node:worker:ExportEventlog'),
102
98
  ),
103
99
  GetLeaderHead: () =>
104
100
  Effect.gen(function* () {
105
101
  const workerCtx = yield* LeaderThreadCtx
106
102
  return Eventlog.getClientHeadFromDb(workerCtx.dbEventlog)
107
- }).pipe(UnexpectedError.mapToUnexpectedError, Effect.withSpan('@livestore/adapter-node:worker:GetLeaderHead')),
103
+ }).pipe(UnknownError.mapToUnknownError, Effect.withSpan('@livestore/adapter-node:worker:GetLeaderHead')),
108
104
  GetLeaderSyncState: () =>
109
105
  Effect.gen(function* () {
110
106
  const workerCtx = yield* LeaderThreadCtx
111
107
  return yield* workerCtx.syncProcessor.syncState
112
- }).pipe(
113
- UnexpectedError.mapToUnexpectedError,
114
- Effect.withSpan('@livestore/adapter-node:worker:GetLeaderSyncState'),
115
- ),
108
+ }).pipe(UnknownError.mapToUnknownError, Effect.withSpan('@livestore/adapter-node:worker:GetLeaderSyncState')),
109
+ SyncStateStream: () =>
110
+ Effect.gen(function* () {
111
+ const workerCtx = yield* LeaderThreadCtx
112
+ return workerCtx.syncProcessor.syncState.changes
113
+ }).pipe(Stream.unwrapScoped),
114
+ GetNetworkStatus: () =>
115
+ Effect.gen(function* () {
116
+ const workerCtx = yield* LeaderThreadCtx
117
+ return yield* workerCtx.networkStatus
118
+ }).pipe(UnknownError.mapToUnknownError, Effect.withSpan('@livestore/adapter-node:worker:GetNetworkStatus')),
119
+ NetworkStatusStream: () =>
120
+ Effect.gen(function* () {
121
+ const workerCtx = yield* LeaderThreadCtx
122
+ return workerCtx.networkStatus.changes
123
+ }).pipe(Stream.unwrapScoped),
116
124
  GetRecreateSnapshot: () =>
117
125
  Effect.gen(function* () {
118
126
  const workerCtx = yield* LeaderThreadCtx
@@ -123,11 +131,9 @@ export const makeWorkerEffect = (options: WorkerOptions) => {
123
131
  // return cachedSnapshot ?? workerCtx.db.export()
124
132
  const snapshot = workerCtx.dbState.export()
125
133
  return { snapshot, migrationsReport: workerCtx.initialState.migrationsReport }
126
- }).pipe(
127
- UnexpectedError.mapToUnexpectedError,
128
- Effect.withSpan('@livestore/adapter-node:worker:GetRecreateSnapshot'),
129
- ),
134
+ }).pipe(UnknownError.mapToUnknownError, Effect.withSpan('@livestore/adapter-node:worker:GetRecreateSnapshot')),
130
135
  Shutdown: () =>
136
+ // @effect-diagnostics-next-line unnecessaryEffectGen:off
131
137
  Effect.gen(function* () {
132
138
  // const { db, dbEventlog } = yield* LeaderThreadCtx
133
139
  yield* Effect.logDebug('[@livestore/adapter-node:worker] Shutdown')
@@ -141,10 +147,10 @@ export const makeWorkerEffect = (options: WorkerOptions) => {
141
147
  // Buy some time for Otel to flush
142
148
  // TODO find a cleaner way to do this
143
149
  // yield* Effect.sleep(1000)
144
- }).pipe(UnexpectedError.mapToUnexpectedError, Effect.withSpan('@livestore/adapter-node:worker:Shutdown')),
150
+ }).pipe(UnknownError.mapToUnknownError, Effect.withSpan('@livestore/adapter-node:worker:Shutdown')),
145
151
  ExtraDevtoolsMessage: ({ message }) =>
146
152
  Effect.andThen(LeaderThreadCtx, (_) => _.extraIncomingMessagesQueue.offer(message)).pipe(
147
- UnexpectedError.mapToUnexpectedError,
153
+ UnknownError.mapToUnknownError,
148
154
  Effect.withSpan('@livestore/adapter-node:worker:ExtraDevtoolsMessage'),
149
155
  ),
150
156
  }).pipe(
@@ -156,12 +162,13 @@ export const makeWorkerEffect = (options: WorkerOptions) => {
156
162
  thread: options.otelOptions?.serviceName ?? 'livestore-node-leader-thread',
157
163
  processId: process.pid,
158
164
  }),
165
+ LogConfig.withLoggerConfig(
166
+ { logger: options.logger, logLevel: options.logLevel },
167
+ { threadName: options.otelOptions?.serviceName ?? 'livestore-node-leader-thread' },
168
+ ),
159
169
  // TODO bring back with Effect 4 once it's easier to work with replacing loggers.
160
170
  // We basically only want to provide this logger if it's replacing the default logger, not if there's a custom logger already provided.
161
171
  // Effect.provide(Logger.prettyWithThread(options.otelOptions?.serviceName ?? 'livestore-node-leader-thread')),
162
- Effect.provide(FetchHttpClient.layer),
163
- Effect.provide(PlatformNode.NodeFileSystem.layer),
164
- TracingLive ? Effect.provide(TracingLive) : identity,
165
- Logger.withMinimumLogLevel(LogLevel.Debug),
172
+ Effect.provide(runtimeLayer),
166
173
  )
167
174
  }