@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.
- package/dist/.tsbuildinfo +1 -1
- package/dist/client-session/adapter.d.ts +68 -2
- package/dist/client-session/adapter.d.ts.map +1 -1
- package/dist/client-session/adapter.js +121 -42
- package/dist/client-session/adapter.js.map +1 -1
- package/dist/devtools/devtools-server.d.ts.map +1 -1
- package/dist/devtools/devtools-server.js +14 -6
- package/dist/devtools/devtools-server.js.map +1 -1
- package/dist/devtools/mod.d.ts +1 -1
- package/dist/devtools/mod.d.ts.map +1 -1
- package/dist/devtools/mod.js +1 -1
- package/dist/devtools/mod.js.map +1 -1
- package/dist/devtools/vite-dev-server.d.ts +26 -4
- package/dist/devtools/vite-dev-server.d.ts.map +1 -1
- package/dist/devtools/vite-dev-server.js +43 -8
- package/dist/devtools/vite-dev-server.js.map +1 -1
- package/dist/leader-thread-shared.d.ts +5 -4
- package/dist/leader-thread-shared.d.ts.map +1 -1
- package/dist/leader-thread-shared.js +8 -6
- package/dist/leader-thread-shared.js.map +1 -1
- package/dist/make-leader-worker.d.ts +4 -2
- package/dist/make-leader-worker.d.ts.map +1 -1
- package/dist/make-leader-worker.js +45 -20
- package/dist/make-leader-worker.js.map +1 -1
- package/dist/shutdown-channel.d.ts.map +1 -1
- package/dist/shutdown-channel.js.map +1 -1
- package/dist/worker-schema.d.ts +141 -41
- package/dist/worker-schema.d.ts.map +1 -1
- package/dist/worker-schema.js +43 -18
- package/dist/worker-schema.js.map +1 -1
- package/package.json +59 -19
- package/src/client-session/adapter.ts +149 -72
- package/src/devtools/devtools-server.ts +22 -10
- package/src/devtools/mod.ts +1 -1
- package/src/devtools/vite-dev-server.ts +63 -11
- package/src/leader-thread-shared.ts +17 -13
- package/src/make-leader-worker.ts +80 -67
- package/src/shutdown-channel.ts +1 -0
- 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
|
-
|
|
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
|
-
|
|
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
|
-
/**
|
|
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
|
-
*
|
|
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,
|
|
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(
|
|
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 === '
|
|
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
|
-
|
|
275
|
+
syncPayloadEncoded,
|
|
276
|
+
syncPayloadSchema,
|
|
208
277
|
testing,
|
|
209
278
|
}),
|
|
210
|
-
}).pipe(
|
|
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
|
-
|
|
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,
|
|
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(
|
|
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(
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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 } =
|
|
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
|
-
|
|
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
|
-
|
|
447
|
+
syncPayloadEncoded,
|
|
363
448
|
testing,
|
|
364
449
|
}: {
|
|
365
|
-
shutdown: (cause: Exit.Exit<IntentionalShutdownCause,
|
|
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
|
-
|
|
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.
|
|
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
|
-
|
|
480
|
+
syncPayloadEncoded,
|
|
396
481
|
}),
|
|
397
482
|
}).pipe(
|
|
398
483
|
Effect.provide(nodeWorkerLayer),
|
|
399
|
-
|
|
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 = <
|
|
405
|
-
req:
|
|
406
|
-
):
|
|
407
|
-
|
|
408
|
-
|
|
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
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
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
|
-
)
|
|
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.
|
|
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.
|
|
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
|
-
|
|
487
|
-
|
|
488
|
-
|
|
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:
|
|
43
|
-
|
|
44
|
-
|
|
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
|
|
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 =
|
|
118
|
-
|
|
119
|
-
|
|
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
|
package/src/devtools/mod.ts
CHANGED
|
@@ -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 {
|
|
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
|
-
|
|
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 = (
|
|
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(
|
|
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
|
+
})
|