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

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
 
@@ -281,8 +341,9 @@ const makeLocalLeaderThread = ({
281
341
  getEventlogData: Effect.sync(() => dbEventlog.export()),
282
342
  getSyncState: 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
 
@@ -432,9 +495,13 @@ const makeWorkerLeaderThread = ({
432
495
  UnexpectedError.mapToUnexpectedError,
433
496
  Effect.withSpan('@livestore/adapter-node:client-session:devtoolsMessageForLeader'),
434
497
  ),
498
+ networkStatus: Subscribable.make({
499
+ get: runInWorker(new WorkerSchema.LeaderWorkerInnerGetNetworkStatus()).pipe(Effect.orDie),
500
+ changes: runInWorkerStream(new WorkerSchema.LeaderWorkerInnerNetworkStatusStream()).pipe(Stream.orDie),
501
+ }),
435
502
  },
436
503
  {
437
- overrides: testing?.overrides?.clientSession?.leaderThreadProxy,
504
+ ...omitUndefineds({ overrides: testing?.overrides?.clientSession?.leaderThreadProxy }),
438
505
  },
439
506
  )
440
507
 
@@ -113,6 +113,16 @@ export const makeWorkerEffect = (options: WorkerOptions) => {
113
113
  UnexpectedError.mapToUnexpectedError,
114
114
  Effect.withSpan('@livestore/adapter-node:worker:GetLeaderSyncState'),
115
115
  ),
116
+ GetNetworkStatus: () =>
117
+ Effect.gen(function* () {
118
+ const workerCtx = yield* LeaderThreadCtx
119
+ return yield* workerCtx.networkStatus
120
+ }).pipe(UnexpectedError.mapToUnexpectedError, Effect.withSpan('@livestore/adapter-node:worker:GetNetworkStatus')),
121
+ NetworkStatusStream: () =>
122
+ Effect.gen(function* () {
123
+ const workerCtx = yield* LeaderThreadCtx
124
+ return workerCtx.networkStatus.changes
125
+ }).pipe(Stream.unwrapScoped),
116
126
  GetRecreateSnapshot: () =>
117
127
  Effect.gen(function* () {
118
128
  const workerCtx = yield* LeaderThreadCtx
@@ -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,24 @@ export class LeaderWorkerInnerGetLeaderSyncState extends Schema.TaggedRequest<Le
158
167
  },
159
168
  ) {}
160
169
 
170
+ export class LeaderWorkerInnerGetNetworkStatus extends Schema.TaggedRequest<LeaderWorkerInnerGetNetworkStatus>()(
171
+ 'GetNetworkStatus',
172
+ {
173
+ payload: {},
174
+ success: SyncBackend.NetworkStatus,
175
+ failure: UnexpectedError,
176
+ },
177
+ ) {}
178
+
179
+ export class LeaderWorkerInnerNetworkStatusStream extends Schema.TaggedRequest<LeaderWorkerInnerNetworkStatusStream>()(
180
+ 'NetworkStatusStream',
181
+ {
182
+ payload: {},
183
+ success: SyncBackend.NetworkStatus,
184
+ failure: UnexpectedError,
185
+ },
186
+ ) {}
187
+
161
188
  export class LeaderWorkerInnerShutdown extends Schema.TaggedRequest<LeaderWorkerInnerShutdown>()('Shutdown', {
162
189
  payload: {},
163
190
  success: Schema.Void,
@@ -185,6 +212,8 @@ export const LeaderWorkerInnerRequest = Schema.Union(
185
212
  LeaderWorkerInnerExportEventlog,
186
213
  LeaderWorkerInnerGetLeaderHead,
187
214
  LeaderWorkerInnerGetLeaderSyncState,
215
+ LeaderWorkerInnerGetNetworkStatus,
216
+ LeaderWorkerInnerNetworkStatusStream,
188
217
  LeaderWorkerInnerShutdown,
189
218
  LeaderWorkerInnerExtraDevtoolsMessage,
190
219
  )