@livestore/adapter-node 0.4.0-dev.1 → 0.4.0-dev.11

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,10 +1,11 @@
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,
@@ -17,17 +18,21 @@ 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,6 +133,7 @@ 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) =>
@@ -135,6 +155,14 @@ const makeAdapterImpl = ({
135
155
 
136
156
  const shutdownChannel = yield* makeShutdownChannel(storeId)
137
157
 
158
+ if (resetPersistence === true) {
159
+ yield* shutdownChannel
160
+ .send(IntentionalShutdownCause.make({ reason: 'adapter-reset' }))
161
+ .pipe(UnexpectedError.mapToUnexpectedError)
162
+
163
+ yield* resetNodePersistence({ storage, storeId })
164
+ }
165
+
138
166
  yield* shutdownChannel.listen.pipe(
139
167
  Stream.flatten(),
140
168
  Stream.tap((cause) =>
@@ -173,11 +201,13 @@ const makeAdapterImpl = ({
173
201
  clientId,
174
202
  schema,
175
203
  makeSqliteDb,
176
- syncOptions: leaderThreadInput.sync,
177
- syncPayload,
178
204
  devtools: devtoolsOptions,
179
205
  storage,
180
- testing,
206
+ ...omitUndefineds({
207
+ syncOptions: leaderThreadInput.sync,
208
+ syncPayload,
209
+ testing,
210
+ }),
181
211
  }).pipe(UnexpectedError.mapToUnexpectedError)
182
212
  : yield* makeWorkerLeaderThread({
183
213
  shutdown,
@@ -185,6 +215,7 @@ const makeAdapterImpl = ({
185
215
  clientId,
186
216
  sessionId,
187
217
  workerUrl: leaderThreadInput.workerUrl,
218
+ workerExtraArgs: leaderThreadInput.workerExtraArgs,
188
219
  storage,
189
220
  devtools: devtoolsOptions,
190
221
  bootStatusQueue,
@@ -219,10 +250,38 @@ const makeAdapterImpl = ({
219
250
  return clientSession
220
251
  }).pipe(
221
252
  Effect.withSpan('@livestore/adapter-node:adapter'),
222
- Effect.provide(PlatformNode.NodeFileSystem.layer),
223
- Effect.provide(FetchHttpClient.layer),
253
+ Effect.provide(Layer.mergeAll(PlatformNode.NodeFileSystem.layer, FetchHttpClient.layer)),
224
254
  )) satisfies Adapter
225
255
 
256
+ const resetNodePersistence = ({
257
+ storage,
258
+ storeId,
259
+ }: {
260
+ storage: WorkerSchema.StorageType
261
+ storeId: string
262
+ }): Effect.Effect<void, UnexpectedError, FileSystem.FileSystem> => {
263
+ if (storage.type !== 'fs') {
264
+ return Effect.void
265
+ }
266
+
267
+ const directory = path.join(storage.baseDirectory ?? '', storeId)
268
+
269
+ return Effect.gen(function* () {
270
+ const fs = yield* FileSystem.FileSystem
271
+
272
+ const directoryExists = yield* fs.exists(directory).pipe(UnexpectedError.mapToUnexpectedError)
273
+
274
+ if (directoryExists === false) {
275
+ return
276
+ }
277
+
278
+ yield* fs.remove(directory, { recursive: true }).pipe(UnexpectedError.mapToUnexpectedError)
279
+ }).pipe(
280
+ Effect.retry({ schedule: Schedule.exponentialBackoff10Sec }),
281
+ Effect.withSpan('@livestore/adapter-node:resetPersistence', { attributes: { directory } }),
282
+ )
283
+ }
284
+
226
285
  const makeLocalLeaderThread = ({
227
286
  storeId,
228
287
  clientId,
@@ -257,12 +316,13 @@ const makeLocalLeaderThread = ({
257
316
  syncPayload,
258
317
  devtools,
259
318
  makeSqliteDb,
260
- testing: testing?.overrides,
319
+ ...omitUndefineds({ testing: testing?.overrides }),
261
320
  }).pipe(Layer.unwrapScoped),
262
321
  )
263
322
 
264
323
  return yield* Effect.gen(function* () {
265
- const { dbState, dbEventlog, syncProcessor, extraIncomingMessagesQueue, initialState } = yield* LeaderThreadCtx
324
+ const { dbState, dbEventlog, syncProcessor, extraIncomingMessagesQueue, initialState, networkStatus } =
325
+ yield* LeaderThreadCtx
266
326
 
267
327
  const initialLeaderHead = Eventlog.getClientHeadFromDb(dbEventlog)
268
328
 
@@ -279,10 +339,11 @@ const makeLocalLeaderThread = ({
279
339
  initialState: { leaderHead: initialLeaderHead, migrationsReport: initialState.migrationsReport },
280
340
  export: Effect.sync(() => dbState.export()),
281
341
  getEventlogData: Effect.sync(() => dbEventlog.export()),
282
- getSyncState: syncProcessor.syncState,
342
+ syncState: syncProcessor.syncState,
283
343
  sendDevtoolsMessage: (message) => extraIncomingMessagesQueue.offer(message),
344
+ networkStatus,
284
345
  },
285
- { overrides: testing?.overrides?.clientSession?.leaderThreadProxy },
346
+ { ...omitUndefineds({ overrides: testing?.overrides?.clientSession?.leaderThreadProxy }) },
286
347
  )
287
348
 
288
349
  const initialSnapshot = dbState.export()
@@ -297,6 +358,7 @@ const makeWorkerLeaderThread = ({
297
358
  clientId,
298
359
  sessionId,
299
360
  workerUrl,
361
+ workerExtraArgs,
300
362
  storage,
301
363
  devtools,
302
364
  bootStatusQueue,
@@ -308,6 +370,7 @@ const makeWorkerLeaderThread = ({
308
370
  clientId: string
309
371
  sessionId: string
310
372
  workerUrl: URL
373
+ workerExtraArgs: Schema.JsonValue | undefined
311
374
  storage: WorkerSchema.StorageType
312
375
  devtools: WorkerSchema.LeaderWorkerInnerInitialMessage['devtools']
313
376
  bootStatusQueue: Queue.Queue<BootStatus>
@@ -319,7 +382,7 @@ const makeWorkerLeaderThread = ({
319
382
  Effect.gen(function* () {
320
383
  const nodeWorker = new WT.Worker(workerUrl, {
321
384
  execArgv: process.env.DEBUG_WORKER ? ['--inspect --enable-source-maps'] : ['--enable-source-maps'],
322
- argv: [Schema.encodeSync(WorkerSchema.WorkerArgv)({ storeId, clientId, sessionId })],
385
+ argv: [Schema.encodeSync(WorkerSchema.WorkerArgv)({ storeId, clientId, sessionId, extraArgs: workerExtraArgs })],
323
386
  })
324
387
  const nodeWorkerLayer = yield* Layer.build(PlatformNode.NodeWorker.layer(() => nodeWorker))
325
388
 
@@ -423,18 +486,25 @@ const makeWorkerLeaderThread = ({
423
486
  Effect.withSpan('@livestore/adapter-node:client-session:export'),
424
487
  ),
425
488
  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
- ),
489
+ syncState: Subscribable.make({
490
+ get: runInWorker(new WorkerSchema.LeaderWorkerInnerGetLeaderSyncState()).pipe(
491
+ UnexpectedError.mapToUnexpectedError,
492
+ Effect.withSpan('@livestore/adapter-node:client-session:getLeaderSyncState'),
493
+ ),
494
+ changes: runInWorkerStream(new WorkerSchema.LeaderWorkerInnerSyncStateStream()).pipe(Stream.orDie),
495
+ }),
430
496
  sendDevtoolsMessage: (message) =>
431
497
  runInWorker(new WorkerSchema.LeaderWorkerInnerExtraDevtoolsMessage({ message })).pipe(
432
498
  UnexpectedError.mapToUnexpectedError,
433
499
  Effect.withSpan('@livestore/adapter-node:client-session:devtoolsMessageForLeader'),
434
500
  ),
501
+ networkStatus: Subscribable.make({
502
+ get: runInWorker(new WorkerSchema.LeaderWorkerInnerGetNetworkStatus()).pipe(Effect.orDie),
503
+ changes: runInWorkerStream(new WorkerSchema.LeaderWorkerInnerNetworkStatusStream()).pipe(Stream.orDie),
504
+ }),
435
505
  },
436
506
  {
437
- overrides: testing?.overrides?.clientSession?.leaderThreadProxy,
507
+ ...omitUndefineds({ overrides: testing?.overrides?.clientSession?.leaderThreadProxy }),
438
508
  },
439
509
  )
440
510
 
@@ -17,7 +17,6 @@ import { sqliteDbFactory } from '@livestore/sqlite-wasm/node'
17
17
  import {
18
18
  Effect,
19
19
  FetchHttpClient,
20
- identity,
21
20
  Layer,
22
21
  Logger,
23
22
  LogLevel,
@@ -60,6 +59,13 @@ export const makeWorkerEffect = (options: WorkerOptions) => {
60
59
  )
61
60
  : undefined
62
61
 
62
+ // Merge the runtime dependencies once so we can provide them together without chaining Effect.provide.
63
+ const runtimeLayer = Layer.mergeAll(
64
+ FetchHttpClient.layer,
65
+ PlatformNode.NodeFileSystem.layer,
66
+ TracingLive ?? Layer.empty,
67
+ )
68
+
63
69
  return WorkerRunner.layerSerialized(WorkerSchema.LeaderWorkerInnerRequest, {
64
70
  InitialMessage: (args) =>
65
71
  Effect.gen(function* () {
@@ -113,6 +119,21 @@ export const makeWorkerEffect = (options: WorkerOptions) => {
113
119
  UnexpectedError.mapToUnexpectedError,
114
120
  Effect.withSpan('@livestore/adapter-node:worker:GetLeaderSyncState'),
115
121
  ),
122
+ SyncStateStream: () =>
123
+ Effect.gen(function* () {
124
+ const workerCtx = yield* LeaderThreadCtx
125
+ return workerCtx.syncProcessor.syncState.changes
126
+ }).pipe(Stream.unwrapScoped),
127
+ GetNetworkStatus: () =>
128
+ Effect.gen(function* () {
129
+ const workerCtx = yield* LeaderThreadCtx
130
+ return yield* workerCtx.networkStatus
131
+ }).pipe(UnexpectedError.mapToUnexpectedError, Effect.withSpan('@livestore/adapter-node:worker:GetNetworkStatus')),
132
+ NetworkStatusStream: () =>
133
+ Effect.gen(function* () {
134
+ const workerCtx = yield* LeaderThreadCtx
135
+ return workerCtx.networkStatus.changes
136
+ }).pipe(Stream.unwrapScoped),
116
137
  GetRecreateSnapshot: () =>
117
138
  Effect.gen(function* () {
118
139
  const workerCtx = yield* LeaderThreadCtx
@@ -159,9 +180,7 @@ export const makeWorkerEffect = (options: WorkerOptions) => {
159
180
  // TODO bring back with Effect 4 once it's easier to work with replacing loggers.
160
181
  // 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
182
  // 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,
183
+ Effect.provide(runtimeLayer),
165
184
  Logger.withMinimumLogLevel(LogLevel.Debug),
166
185
  )
167
186
  }
@@ -1,4 +1,12 @@
1
- import { BootStatus, Devtools, LeaderAheadError, MigrationsReport, SyncState, UnexpectedError } from '@livestore/common'
1
+ import {
2
+ BootStatus,
3
+ Devtools,
4
+ LeaderAheadError,
5
+ MigrationsReport,
6
+ SyncBackend,
7
+ SyncState,
8
+ UnexpectedError,
9
+ } from '@livestore/common'
2
10
  import { EventSequenceNumber, LiveStoreEvent } from '@livestore/common/schema'
3
11
  import { Schema, Transferable } from '@livestore/utils/effect'
4
12
 
@@ -7,6 +15,7 @@ export const WorkerArgv = Schema.parseJson(
7
15
  clientId: Schema.String,
8
16
  storeId: Schema.String,
9
17
  sessionId: Schema.String,
18
+ extraArgs: Schema.UndefinedOr(Schema.JsonValue),
10
19
  }),
11
20
  )
12
21
 
@@ -158,6 +167,33 @@ export class LeaderWorkerInnerGetLeaderSyncState extends Schema.TaggedRequest<Le
158
167
  },
159
168
  ) {}
160
169
 
170
+ export class LeaderWorkerInnerSyncStateStream extends Schema.TaggedRequest<LeaderWorkerInnerSyncStateStream>()(
171
+ 'SyncStateStream',
172
+ {
173
+ payload: {},
174
+ success: SyncState.SyncState,
175
+ failure: UnexpectedError,
176
+ },
177
+ ) {}
178
+
179
+ export class LeaderWorkerInnerGetNetworkStatus extends Schema.TaggedRequest<LeaderWorkerInnerGetNetworkStatus>()(
180
+ 'GetNetworkStatus',
181
+ {
182
+ payload: {},
183
+ success: SyncBackend.NetworkStatus,
184
+ failure: UnexpectedError,
185
+ },
186
+ ) {}
187
+
188
+ export class LeaderWorkerInnerNetworkStatusStream extends Schema.TaggedRequest<LeaderWorkerInnerNetworkStatusStream>()(
189
+ 'NetworkStatusStream',
190
+ {
191
+ payload: {},
192
+ success: SyncBackend.NetworkStatus,
193
+ failure: UnexpectedError,
194
+ },
195
+ ) {}
196
+
161
197
  export class LeaderWorkerInnerShutdown extends Schema.TaggedRequest<LeaderWorkerInnerShutdown>()('Shutdown', {
162
198
  payload: {},
163
199
  success: Schema.Void,
@@ -185,6 +221,9 @@ export const LeaderWorkerInnerRequest = Schema.Union(
185
221
  LeaderWorkerInnerExportEventlog,
186
222
  LeaderWorkerInnerGetLeaderHead,
187
223
  LeaderWorkerInnerGetLeaderSyncState,
224
+ LeaderWorkerInnerSyncStateStream,
225
+ LeaderWorkerInnerGetNetworkStatus,
226
+ LeaderWorkerInnerNetworkStatusStream,
188
227
  LeaderWorkerInnerShutdown,
189
228
  LeaderWorkerInnerExtraDevtoolsMessage,
190
229
  )