@livestore/adapter-node 0.4.0 → 0.5.0-dev.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 +1 -1
- package/dist/client-session/adapter.d.ts.map +1 -1
- package/dist/client-session/adapter.js +58 -39
- 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 +12 -14
- package/dist/devtools/devtools-server.js.map +1 -1
- package/dist/devtools/vite-dev-server.d.ts +7 -11
- package/dist/devtools/vite-dev-server.d.ts.map +1 -1
- package/dist/devtools/vite-dev-server.js +5 -6
- package/dist/devtools/vite-dev-server.js.map +1 -1
- package/dist/leader-thread-shared.d.ts +4 -4
- package/dist/leader-thread-shared.d.ts.map +1 -1
- package/dist/leader-thread-shared.js +13 -12
- package/dist/leader-thread-shared.js.map +1 -1
- package/dist/make-leader-worker.d.ts +4 -4
- package/dist/make-leader-worker.d.ts.map +1 -1
- package/dist/make-leader-worker.js +62 -71
- package/dist/make-leader-worker.js.map +1 -1
- package/dist/rpc-worker.d.ts +8 -0
- package/dist/rpc-worker.d.ts.map +1 -0
- package/dist/rpc-worker.js +5 -0
- package/dist/rpc-worker.js.map +1 -0
- package/dist/webchannel.d.ts +1 -1
- package/dist/webchannel.d.ts.map +1 -1
- package/dist/webchannel.js +17 -18
- package/dist/webchannel.js.map +1 -1
- package/dist/worker-schema.d.ts +248 -212
- package/dist/worker-schema.d.ts.map +1 -1
- package/dist/worker-schema.js +62 -53
- package/dist/worker-schema.js.map +1 -1
- package/package.json +22 -31
- package/src/client-session/adapter.ts +104 -77
- package/src/devtools/devtools-server.ts +33 -30
- package/src/devtools/vite-dev-server.ts +7 -9
- package/src/leader-thread-shared.ts +24 -25
- package/src/make-leader-worker.ts +163 -127
- package/src/rpc-worker.ts +28 -0
- package/src/webchannel.ts +21 -28
- package/src/worker-schema.ts +115 -149
|
@@ -8,7 +8,6 @@ import {
|
|
|
8
8
|
type BootStatus,
|
|
9
9
|
ClientSessionLeaderThreadProxy,
|
|
10
10
|
IntentionalShutdownCause,
|
|
11
|
-
isWorkerTransportError,
|
|
12
11
|
type LockStatus,
|
|
13
12
|
type MakeSqliteDb,
|
|
14
13
|
makeClientSession,
|
|
@@ -29,20 +28,22 @@ import {
|
|
|
29
28
|
Fiber,
|
|
30
29
|
FileSystem,
|
|
31
30
|
Layer,
|
|
32
|
-
Option,
|
|
33
31
|
Queue,
|
|
32
|
+
RpcClient,
|
|
33
|
+
RpcWorker,
|
|
34
34
|
Schedule,
|
|
35
35
|
Schema,
|
|
36
|
+
Scope,
|
|
36
37
|
Stream,
|
|
37
38
|
Subscribable,
|
|
38
39
|
SubscriptionRef,
|
|
39
|
-
Worker,
|
|
40
40
|
} from '@livestore/utils/effect'
|
|
41
41
|
import { PlatformNode } from '@livestore/utils/node'
|
|
42
42
|
import * as Webmesh from '@livestore/webmesh'
|
|
43
43
|
|
|
44
44
|
import type { TestingOverrides } from '../leader-thread-shared.ts'
|
|
45
45
|
import { makeLeaderThread } from '../leader-thread-shared.ts'
|
|
46
|
+
import { type WithoutRpcClientError, dieOnRpcClientError, dieOnRpcClientErrorStream } from '../rpc-worker.ts'
|
|
46
47
|
import { makeShutdownChannel } from '../shutdown-channel.ts'
|
|
47
48
|
import * as WorkerSchema from '../worker-schema.ts'
|
|
48
49
|
|
|
@@ -178,7 +179,7 @@ export const makeWorkerAdapter = ({
|
|
|
178
179
|
* Extra arguments to pass to the worker which can be accessed in the worker
|
|
179
180
|
* via `getWorkerArgs()`
|
|
180
181
|
*/
|
|
181
|
-
workerExtraArgs?: Schema.
|
|
182
|
+
workerExtraArgs?: Schema.Json
|
|
182
183
|
}): Adapter => makeAdapterImpl({ ...options, leaderThread: { _tag: 'multi-threaded', workerUrl, workerExtraArgs } })
|
|
183
184
|
|
|
184
185
|
const makeAdapterImpl = ({
|
|
@@ -199,7 +200,7 @@ const makeAdapterImpl = ({
|
|
|
199
200
|
| {
|
|
200
201
|
_tag: 'multi-threaded'
|
|
201
202
|
workerUrl: URL
|
|
202
|
-
workerExtraArgs: Schema.
|
|
203
|
+
workerExtraArgs: Schema.Json | undefined
|
|
203
204
|
}
|
|
204
205
|
}): Adapter =>
|
|
205
206
|
((adapterArgs) =>
|
|
@@ -231,7 +232,7 @@ const makeAdapterImpl = ({
|
|
|
231
232
|
}
|
|
232
233
|
|
|
233
234
|
yield* shutdownChannel.listen.pipe(
|
|
234
|
-
Stream.
|
|
235
|
+
Stream.mapEffect(Effect.fromResult),
|
|
235
236
|
Stream.tap((cause) =>
|
|
236
237
|
shutdown(cause._tag === 'IntentionalShutdownCause' ? Exit.succeed(cause) : Exit.fail(cause)),
|
|
237
238
|
),
|
|
@@ -246,7 +247,7 @@ const makeAdapterImpl = ({
|
|
|
246
247
|
// TODO actually implement this multi-session support
|
|
247
248
|
const lockStatus = yield* SubscriptionRef.make<LockStatus>('has-lock')
|
|
248
249
|
|
|
249
|
-
const devtoolsOptions: WorkerSchema.
|
|
250
|
+
const devtoolsOptions: WorkerSchema.LeaderWorkerInnerInitialMessagePayload['devtools'] =
|
|
250
251
|
devtoolsEnabled === true && devtoolsOptionsInput !== undefined
|
|
251
252
|
? {
|
|
252
253
|
enabled: true,
|
|
@@ -288,7 +289,7 @@ const makeAdapterImpl = ({
|
|
|
288
289
|
devtools: devtoolsOptions,
|
|
289
290
|
bootStatusQueue,
|
|
290
291
|
syncPayloadEncoded,
|
|
291
|
-
})
|
|
292
|
+
}).pipe(UnknownError.mapToUnknownError)
|
|
292
293
|
|
|
293
294
|
syncInMemoryDb.import(initialSnapshot)
|
|
294
295
|
syncInMemoryDb.debug.head = leaderThread.initialState.leaderHead
|
|
@@ -369,28 +370,42 @@ const makeLocalLeaderThread = ({
|
|
|
369
370
|
makeSqliteDb: MakeSqliteDb
|
|
370
371
|
syncOptions: SyncOptions | undefined
|
|
371
372
|
storage: WorkerSchema.StorageType
|
|
372
|
-
syncPayloadEncoded: Schema.
|
|
373
|
+
syncPayloadEncoded: Schema.Json | undefined
|
|
373
374
|
syncPayloadSchema: Schema.Schema<any>
|
|
374
|
-
devtools: WorkerSchema.
|
|
375
|
+
devtools: WorkerSchema.LeaderWorkerInnerInitialMessagePayload['devtools']
|
|
375
376
|
testing?: {
|
|
376
377
|
overrides?: TestingOverrides
|
|
377
378
|
}
|
|
378
379
|
}) =>
|
|
379
380
|
Effect.gen(function* () {
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
381
|
+
// The single-threaded node adapter runs the leader thread co-located in the same runtime as the
|
|
382
|
+
// client session. Unlike the worker adapters (where the leader lives on its own event loop and drains
|
|
383
|
+
// pushes independently), a co-located leader only makes progress when the surrounding fibers yield.
|
|
384
|
+
//
|
|
385
|
+
// We give the leader thread its own dedicated scope whose teardown finalizer is registered *before*
|
|
386
|
+
// `makeClientSession`. Finalizers run in reverse registration order, so on store shutdown the client
|
|
387
|
+
// session tears down first and only then is the leader thread scope closed — keeping the leader (and
|
|
388
|
+
// its SQLite connections) alive while the client-session pusher's in-flight batch is still being
|
|
389
|
+
// persisted. Building the leader into the ambient store scope instead (via `Layer.build`) interrupts
|
|
390
|
+
// the leader's push drainer concurrently with the client session, dropping the in-flight batch and
|
|
391
|
+
// causing silent data loss on commit-then-shutdown.
|
|
392
|
+
const leaderThreadScope = yield* Scope.make()
|
|
393
|
+
yield* Effect.addFinalizer((exit) => Scope.close(leaderThreadScope, exit))
|
|
394
|
+
|
|
395
|
+
const leaderThreadLayer = yield* makeLeaderThread({
|
|
396
|
+
storeId,
|
|
397
|
+
clientId,
|
|
398
|
+
schema,
|
|
399
|
+
syncOptions,
|
|
400
|
+
storage,
|
|
401
|
+
syncPayloadEncoded,
|
|
402
|
+
syncPayloadSchema,
|
|
403
|
+
devtools,
|
|
404
|
+
makeSqliteDb,
|
|
405
|
+
...omitUndefineds({ testing: testing?.overrides }),
|
|
406
|
+
}).pipe(Scope.provide(leaderThreadScope))
|
|
407
|
+
|
|
408
|
+
const layer = yield* Layer.buildWithScope(leaderThreadLayer, leaderThreadScope)
|
|
394
409
|
|
|
395
410
|
return yield* Effect.gen(function* () {
|
|
396
411
|
const { dbState, dbEventlog, syncProcessor, extraIncomingMessagesQueue, initialState, networkStatus } =
|
|
@@ -403,10 +418,13 @@ const makeLocalLeaderThread = ({
|
|
|
403
418
|
events: {
|
|
404
419
|
pull: ({ cursor }) => syncProcessor.pull({ cursor }),
|
|
405
420
|
push: (batch) =>
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
421
|
+
// Mirror the worker `PushToLeader` handler which runs the leader push uninterruptibly.
|
|
422
|
+
// Together with the dedicated `leaderThreadScope` above, this guarantees an in-flight batch is
|
|
423
|
+
// fully persisted by the (still-live) leader before the co-located pusher fiber is interrupted
|
|
424
|
+
// on store shutdown.
|
|
425
|
+
syncProcessor
|
|
426
|
+
.push(batch.map((item) => new LiveStoreEvent.Client.EncodedWithMeta(item)))
|
|
427
|
+
.pipe(Effect.uninterruptible),
|
|
410
428
|
stream: (options) =>
|
|
411
429
|
streamEventsWithSyncState({
|
|
412
430
|
dbEventlog,
|
|
@@ -422,7 +440,7 @@ const makeLocalLeaderThread = ({
|
|
|
422
440
|
export: Effect.sync(() => dbState.export()),
|
|
423
441
|
getEventlogData: Effect.sync(() => dbEventlog.export()),
|
|
424
442
|
syncState: syncProcessor.syncState,
|
|
425
|
-
sendDevtoolsMessage: (message) =>
|
|
443
|
+
sendDevtoolsMessage: (message) => Queue.offer(extraIncomingMessagesQueue, message),
|
|
426
444
|
networkStatus,
|
|
427
445
|
},
|
|
428
446
|
{ ...omitUndefineds({ overrides: testing?.overrides?.clientSession?.leaderThreadProxy }) },
|
|
@@ -452,11 +470,11 @@ const makeWorkerLeaderThread = ({
|
|
|
452
470
|
clientId: string
|
|
453
471
|
sessionId: string
|
|
454
472
|
workerUrl: URL
|
|
455
|
-
workerExtraArgs: Schema.
|
|
473
|
+
workerExtraArgs: Schema.Json | undefined
|
|
456
474
|
storage: WorkerSchema.StorageType
|
|
457
|
-
devtools: WorkerSchema.
|
|
475
|
+
devtools: WorkerSchema.LeaderWorkerInnerInitialMessagePayload['devtools']
|
|
458
476
|
bootStatusQueue: Queue.Queue<BootStatus>
|
|
459
|
-
syncPayloadEncoded: Schema.
|
|
477
|
+
syncPayloadEncoded: Schema.Json | undefined
|
|
460
478
|
testing?: {
|
|
461
479
|
overrides?: TestingOverrides
|
|
462
480
|
}
|
|
@@ -464,66 +482,76 @@ const makeWorkerLeaderThread = ({
|
|
|
464
482
|
Effect.gen(function* () {
|
|
465
483
|
const nodeWorker = new WT.Worker(workerUrl, {
|
|
466
484
|
execArgv: process.env.DEBUG_WORKER !== undefined ? ['--inspect --enable-source-maps'] : ['--enable-source-maps'],
|
|
467
|
-
argv: [
|
|
468
|
-
|
|
469
|
-
const nodeWorkerLayer = yield* Layer.build(PlatformNode.NodeWorker.layer(() => nodeWorker))
|
|
470
|
-
|
|
471
|
-
const worker = yield* Worker.makePoolSerialized<typeof WorkerSchema.LeaderWorkerInnerRequest.Type>({
|
|
472
|
-
size: 1,
|
|
473
|
-
concurrency: 100,
|
|
474
|
-
initialMessage: () =>
|
|
475
|
-
new WorkerSchema.LeaderWorkerInnerInitialMessage({
|
|
485
|
+
argv: [
|
|
486
|
+
yield* Schema.encodeEffect(WorkerSchema.WorkerArgv)({
|
|
476
487
|
storeId,
|
|
477
488
|
clientId,
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
})
|
|
483
|
-
|
|
489
|
+
sessionId,
|
|
490
|
+
extraArgs: workerExtraArgs,
|
|
491
|
+
}).pipe(Effect.orDie),
|
|
492
|
+
],
|
|
493
|
+
})
|
|
494
|
+
const innerWorkerContext = yield* Layer.build(
|
|
495
|
+
RpcClient.layerProtocolWorker({ size: 1, concurrency: 100 }).pipe(
|
|
496
|
+
Layer.provide(PlatformNode.NodeWorker.layer(() => nodeWorker)),
|
|
497
|
+
Layer.provide(
|
|
498
|
+
RpcWorker.layerInitialMessage(
|
|
499
|
+
WorkerSchema.LeaderWorkerInnerInitialMessage.payloadSchema,
|
|
500
|
+
Effect.succeed({ storeId, clientId, storage, devtools, syncPayloadEncoded }),
|
|
501
|
+
),
|
|
502
|
+
),
|
|
503
|
+
),
|
|
504
|
+
)
|
|
505
|
+
|
|
506
|
+
const worker = yield* RpcClient.make(WorkerSchema.LeaderWorkerInnerRpcs).pipe(
|
|
507
|
+
Effect.provide(innerWorkerContext),
|
|
484
508
|
UnknownError.mapToUnknownError,
|
|
485
|
-
Effect.
|
|
509
|
+
Effect.tapCause((cause) => shutdown(Exit.failCause(cause))),
|
|
486
510
|
Effect.withSpan('@livestore/adapter-node:adapter:setupLeaderThread'),
|
|
487
511
|
)
|
|
488
512
|
|
|
489
|
-
const runInWorker = <A,
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
513
|
+
const runInWorker = <A, E, R>(
|
|
514
|
+
tag: string,
|
|
515
|
+
effect: Effect.Effect<A, E, R>,
|
|
516
|
+
): Effect.Effect<A, WithoutRpcClientError<E>, R> =>
|
|
517
|
+
effect.pipe(
|
|
518
|
+
dieOnRpcClientError,
|
|
494
519
|
Effect.logWarnIfTakesLongerThan({
|
|
495
|
-
label: `@livestore/adapter-node:client-session:runInWorker:${
|
|
520
|
+
label: `@livestore/adapter-node:client-session:runInWorker:${tag}`,
|
|
496
521
|
duration: 2000,
|
|
497
522
|
}),
|
|
498
|
-
Effect.withSpan(`@livestore/adapter-node:client-session:runInWorker:${
|
|
523
|
+
Effect.withSpan(`@livestore/adapter-node:client-session:runInWorker:${tag}`),
|
|
499
524
|
)
|
|
500
525
|
|
|
501
|
-
const runInWorkerStream = <A,
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
526
|
+
const runInWorkerStream = <A, E, R>(
|
|
527
|
+
tag: string,
|
|
528
|
+
stream: Stream.Stream<A, E, R>,
|
|
529
|
+
): Stream.Stream<A, WithoutRpcClientError<E>, R> =>
|
|
530
|
+
stream.pipe(
|
|
531
|
+
dieOnRpcClientErrorStream,
|
|
532
|
+
Stream.withSpan(`@livestore/adapter-node:client-session:runInWorkerStream:${tag}`),
|
|
507
533
|
)
|
|
508
534
|
|
|
509
|
-
const bootStatusFiber = yield* runInWorkerStream(
|
|
535
|
+
const bootStatusFiber = yield* runInWorkerStream('BootStatusStream', worker.BootStatusStream({})).pipe(
|
|
510
536
|
Stream.tap((bootStatus) => Queue.offer(bootStatusQueue, bootStatus)),
|
|
511
537
|
Stream.runDrain,
|
|
512
|
-
Effect.
|
|
538
|
+
Effect.tapCause((cause) =>
|
|
539
|
+
Cause.hasInterruptsOnly(cause) === true ? Effect.void : shutdown(Exit.failCause(cause)),
|
|
540
|
+
),
|
|
513
541
|
Effect.interruptible,
|
|
514
542
|
Effect.tapCauseLogPretty,
|
|
515
543
|
Effect.forkScoped,
|
|
516
544
|
)
|
|
517
545
|
|
|
518
|
-
yield* Queue.
|
|
546
|
+
yield* Queue.await(bootStatusQueue).pipe(
|
|
519
547
|
Effect.andThen(Fiber.interrupt(bootStatusFiber)),
|
|
520
548
|
Effect.tapCauseLogPretty,
|
|
521
549
|
Effect.forkScoped,
|
|
522
550
|
)
|
|
523
551
|
|
|
524
|
-
const initialLeaderHead = yield* runInWorker(
|
|
552
|
+
const initialLeaderHead = yield* runInWorker('GetLeaderHead', worker.GetLeaderHead({}))
|
|
525
553
|
|
|
526
|
-
const bootResult = yield* runInWorker(
|
|
554
|
+
const bootResult = yield* runInWorker('GetRecreateSnapshot', worker.GetRecreateSnapshot({})).pipe(
|
|
527
555
|
Effect.timeoutOrDie(10_000),
|
|
528
556
|
Effect.withSpan('@livestore/adapter-node:client-session:export'),
|
|
529
557
|
)
|
|
@@ -531,16 +559,15 @@ const makeWorkerLeaderThread = ({
|
|
|
531
559
|
const leaderThread = ClientSessionLeaderThreadProxy.of(
|
|
532
560
|
{
|
|
533
561
|
events: {
|
|
534
|
-
pull: ({ cursor }) =>
|
|
535
|
-
runInWorkerStream(new WorkerSchema.LeaderWorkerInnerPullStream({ cursor })).pipe(Stream.orDie),
|
|
562
|
+
pull: ({ cursor }) => runInWorkerStream('PullStream', worker.PullStream({ cursor })).pipe(Stream.orDie),
|
|
536
563
|
push: (batch) =>
|
|
537
|
-
runInWorker(
|
|
564
|
+
runInWorker('PushToLeader', worker.PushToLeader({ batch })).pipe(
|
|
538
565
|
Effect.withSpan('@livestore/adapter-node:client-session:pushToLeader', {
|
|
539
566
|
attributes: { batchSize: batch.length },
|
|
540
567
|
}),
|
|
541
568
|
),
|
|
542
569
|
stream: (options) =>
|
|
543
|
-
runInWorkerStream(
|
|
570
|
+
runInWorkerStream('StreamEvents', worker.StreamEvents(options)).pipe(
|
|
544
571
|
Stream.withSpan('@livestore/adapter-node:client-session:streamEvents'),
|
|
545
572
|
Stream.orDie,
|
|
546
573
|
),
|
|
@@ -550,24 +577,24 @@ const makeWorkerLeaderThread = ({
|
|
|
550
577
|
migrationsReport: bootResult.migrationsReport,
|
|
551
578
|
storageMode: 'persisted',
|
|
552
579
|
},
|
|
553
|
-
export: runInWorker(
|
|
580
|
+
export: runInWorker('Export', worker.Export({})).pipe(
|
|
554
581
|
Effect.timeoutOrDie(10_000),
|
|
555
582
|
Effect.withSpan('@livestore/adapter-node:client-session:export'),
|
|
556
583
|
),
|
|
557
|
-
getEventlogData: Effect.
|
|
584
|
+
getEventlogData: Effect.die('Not implemented'),
|
|
558
585
|
syncState: Subscribable.make({
|
|
559
|
-
get: runInWorker(
|
|
586
|
+
get: runInWorker('GetLeaderSyncState', worker.GetLeaderSyncState({})).pipe(
|
|
560
587
|
Effect.withSpan('@livestore/adapter-node:client-session:getLeaderSyncState'),
|
|
561
588
|
),
|
|
562
|
-
changes: runInWorkerStream(
|
|
589
|
+
changes: runInWorkerStream('SyncStateStream', worker.SyncStateStream({})).pipe(Stream.orDie),
|
|
563
590
|
}),
|
|
564
591
|
sendDevtoolsMessage: (message) =>
|
|
565
|
-
runInWorker(
|
|
592
|
+
runInWorker('ExtraDevtoolsMessage', worker.ExtraDevtoolsMessage({ message })).pipe(
|
|
566
593
|
Effect.withSpan('@livestore/adapter-node:client-session:devtoolsMessageForLeader'),
|
|
567
594
|
),
|
|
568
595
|
networkStatus: Subscribable.make({
|
|
569
|
-
get: runInWorker(
|
|
570
|
-
changes: runInWorkerStream(
|
|
596
|
+
get: runInWorker('GetNetworkStatus', worker.GetNetworkStatus({})).pipe(Effect.orDie),
|
|
597
|
+
changes: runInWorkerStream('NetworkStatusStream', worker.NetworkStatusStream({})).pipe(Stream.orDie),
|
|
571
598
|
}),
|
|
572
599
|
},
|
|
573
600
|
{
|
|
@@ -2,7 +2,7 @@ import http from 'node:http'
|
|
|
2
2
|
import path from 'node:path'
|
|
3
3
|
|
|
4
4
|
import type { Devtools } from '@livestore/common'
|
|
5
|
-
import {
|
|
5
|
+
import { LS_DEV } from '@livestore/utils'
|
|
6
6
|
import type { HttpClient } from '@livestore/utils/effect'
|
|
7
7
|
import {
|
|
8
8
|
Deferred,
|
|
@@ -47,25 +47,28 @@ export const startDevtoolsServer = ({
|
|
|
47
47
|
port: number
|
|
48
48
|
}): Effect.Effect<never, never, HttpClient.HttpClient> =>
|
|
49
49
|
Effect.gen(function* () {
|
|
50
|
-
const viteMiddleware = yield*
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
50
|
+
const viteMiddleware = yield* Effect.acquireRelease(
|
|
51
|
+
makeViteMiddleware({
|
|
52
|
+
mode: { _tag: 'node', url: `http://${host}:${port}` },
|
|
53
|
+
schemaPath:
|
|
54
|
+
typeof schemaPath === 'string'
|
|
55
|
+
? path.resolve(process.cwd(), schemaPath)
|
|
56
|
+
: schemaPath.map((schemaPath) => path.resolve(process.cwd(), schemaPath)),
|
|
57
|
+
viteConfig: (viteConfig) => {
|
|
58
|
+
if (LS_DEV === true) {
|
|
59
|
+
viteConfig.server ??= {}
|
|
60
|
+
viteConfig.server.fs ??= {}
|
|
61
|
+
viteConfig.server.fs.strict = false
|
|
62
|
+
|
|
63
|
+
viteConfig.optimizeDeps ??= {}
|
|
64
|
+
viteConfig.optimizeDeps.force = true
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
return viteConfig
|
|
68
|
+
},
|
|
69
|
+
}),
|
|
70
|
+
(viteMiddleware) => Effect.promise(() => viteMiddleware.close()),
|
|
71
|
+
)
|
|
69
72
|
|
|
70
73
|
const relayNodeName = 'ws'
|
|
71
74
|
|
|
@@ -88,9 +91,10 @@ export const startDevtoolsServer = ({
|
|
|
88
91
|
// To handle websocket closing, we need to race the `webChannel.closedDeferred` to properly interrupt the handler
|
|
89
92
|
yield* Effect.raceFirst(
|
|
90
93
|
Effect.gen(function* () {
|
|
91
|
-
yield*
|
|
92
|
-
.addEdge({ target: from, edgeChannel: webChannel, replaceIfExists: true })
|
|
93
|
-
|
|
94
|
+
yield* Effect.acquireRelease(
|
|
95
|
+
node.addEdge({ target: from, edgeChannel: webChannel, replaceIfExists: true }),
|
|
96
|
+
() => node.removeEdge(from).pipe(Effect.orDie),
|
|
97
|
+
)
|
|
94
98
|
|
|
95
99
|
if (LS_DEV === true) {
|
|
96
100
|
yield* Effect.log(`WS Relay ${relayNodeName}: added edge from '${from}'`)
|
|
@@ -100,7 +104,7 @@ export const startDevtoolsServer = ({
|
|
|
100
104
|
// We want to keep the websocket open until the client disconnects or the server shuts down
|
|
101
105
|
return yield* Effect.never
|
|
102
106
|
}),
|
|
103
|
-
webChannel.closedDeferred,
|
|
107
|
+
Deferred.await(webChannel.closedDeferred),
|
|
104
108
|
)
|
|
105
109
|
|
|
106
110
|
return HttpServerResponse.empty({ status: 101 })
|
|
@@ -113,9 +117,9 @@ export const startDevtoolsServer = ({
|
|
|
113
117
|
// TODO replace this once @effect/platform-node supports Node HTTP middlewares
|
|
114
118
|
const nodeReq = PlatformNode.NodeHttpServerRequest.toIncomingMessage(req)
|
|
115
119
|
const nodeRes = PlatformNode.NodeHttpServerRequest.toServerResponse(req)
|
|
116
|
-
const deferred = yield* Deferred.make()
|
|
117
|
-
viteMiddleware.middlewares(nodeReq, nodeRes, () => Deferred.
|
|
118
|
-
yield* deferred
|
|
120
|
+
const deferred = yield* Deferred.make<void>()
|
|
121
|
+
viteMiddleware.middlewares(nodeReq, nodeRes, () => Deferred.doneUnsafe(deferred, Exit.void))
|
|
122
|
+
yield* Deferred.await(deferred)
|
|
119
123
|
|
|
120
124
|
// The response is already sent, so we need to return an empty response (which won't be sent)
|
|
121
125
|
return HttpServerResponse.empty()
|
|
@@ -123,7 +127,7 @@ export const startDevtoolsServer = ({
|
|
|
123
127
|
}
|
|
124
128
|
|
|
125
129
|
return HttpServerResponse.text('Not found')
|
|
126
|
-
}).pipe(Effect.tapCauseLogPretty, Effect.interruptible)
|
|
130
|
+
}).pipe(Effect.tapCauseLogPretty, Effect.interruptible, HttpMiddleware.withLoggerDisabled)
|
|
127
131
|
|
|
128
132
|
const sessionSuffix =
|
|
129
133
|
clientSessionInfo !== undefined
|
|
@@ -141,8 +145,7 @@ export const startDevtoolsServer = ({
|
|
|
141
145
|
Effect.withSpan('@livestore/adapter-node:startDevtoolsServer', {
|
|
142
146
|
attributes: { clientSessionInfo, port, host, schemaPath },
|
|
143
147
|
}),
|
|
144
|
-
|
|
145
|
-
Layer.unwrapScoped,
|
|
148
|
+
Layer.unwrap,
|
|
146
149
|
Layer.provide(PlatformNode.NodeHttpServer.layer(() => http.createServer(), { port, host })),
|
|
147
150
|
Layer.launch,
|
|
148
151
|
Effect.orDie,
|
|
@@ -1,12 +1,9 @@
|
|
|
1
1
|
import path from 'node:path'
|
|
2
|
-
|
|
3
|
-
import type * as Vite from 'vite'
|
|
4
|
-
|
|
5
2
|
import type { Devtools } from '@livestore/common'
|
|
6
3
|
import { UnknownError } from '@livestore/common'
|
|
7
|
-
import { isReadonlyArray } from '@livestore/utils'
|
|
8
4
|
import { Effect, Schema } from '@livestore/utils/effect'
|
|
9
5
|
import { getFreePort } from '@livestore/utils/node'
|
|
6
|
+
import type * as Vite from 'vite'
|
|
10
7
|
|
|
11
8
|
/**
|
|
12
9
|
* Error thrown when @livestore/devtools-vite is not installed.
|
|
@@ -15,7 +12,7 @@ import { getFreePort } from '@livestore/utils/node'
|
|
|
15
12
|
export class DevtoolsViteNotInstalledError extends Schema.TaggedError<DevtoolsViteNotInstalledError>(
|
|
16
13
|
'~@livestore/adapter-node/DevtoolsViteNotInstalledError',
|
|
17
14
|
)('DevtoolsViteNotInstalledError', {
|
|
18
|
-
cause: Schema.Defect,
|
|
15
|
+
cause: Schema.Defect(),
|
|
19
16
|
}) {
|
|
20
17
|
override get message(): string {
|
|
21
18
|
return (
|
|
@@ -30,7 +27,7 @@ export class DevtoolsViteNotInstalledError extends Schema.TaggedError<DevtoolsVi
|
|
|
30
27
|
export class ViteNotInstalledError extends Schema.TaggedError<ViteNotInstalledError>(
|
|
31
28
|
'~@livestore/adapter-node/ViteNotInstalledError',
|
|
32
29
|
)('ViteNotInstalledError', {
|
|
33
|
-
cause: Schema.Defect,
|
|
30
|
+
cause: Schema.Defect(),
|
|
34
31
|
}) {
|
|
35
32
|
override get message(): string {
|
|
36
33
|
return (
|
|
@@ -82,9 +79,10 @@ export const makeViteMiddleware = (
|
|
|
82
79
|
base: '/_livestore/',
|
|
83
80
|
plugins: [
|
|
84
81
|
livestoreDevtoolsPlugin({
|
|
85
|
-
schemaPath:
|
|
86
|
-
|
|
87
|
-
|
|
82
|
+
schemaPath:
|
|
83
|
+
typeof options.schemaPath === 'string'
|
|
84
|
+
? path.resolve(cwd, options.schemaPath)
|
|
85
|
+
: options.schemaPath.map((schemaPath) => path.resolve(cwd, schemaPath)),
|
|
88
86
|
mode: options.mode,
|
|
89
87
|
path: '/',
|
|
90
88
|
}),
|
|
@@ -7,13 +7,14 @@ if (process.execArgv.includes('--inspect') === true) {
|
|
|
7
7
|
}
|
|
8
8
|
|
|
9
9
|
import type { ClientSessionLeaderThreadProxy, MakeSqliteDb, SqliteDb, SyncOptions } from '@livestore/common'
|
|
10
|
-
import { Devtools, liveStoreStorageFormatVersion, migrateDb, UnknownError } from '@livestore/common'
|
|
10
|
+
import { Devtools, liveStoreStorageFormatVersion, migrateDb, StateHead, 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'
|
|
14
|
+
import { getStateDbBaseName } from '@livestore/common/schema'
|
|
14
15
|
import type { MakeNodeSqliteDb } from '@livestore/sqlite-wasm/node'
|
|
15
|
-
import type { FileSystem, HttpClient,
|
|
16
|
-
import { Effect } from '@livestore/utils/effect'
|
|
16
|
+
import type { FileSystem, HttpClient, Schema, Scope } from '@livestore/utils/effect'
|
|
17
|
+
import { Effect, Layer } from '@livestore/utils/effect'
|
|
17
18
|
import * as Webmesh from '@livestore/webmesh'
|
|
18
19
|
|
|
19
20
|
import { makeShutdownChannel } from './shutdown-channel.ts'
|
|
@@ -40,9 +41,9 @@ export interface MakeLeaderThreadArgs {
|
|
|
40
41
|
syncOptions: SyncOptions | undefined
|
|
41
42
|
storage: WorkerSchema.StorageType
|
|
42
43
|
makeSqliteDb: MakeNodeSqliteDb
|
|
43
|
-
devtools: WorkerSchema.
|
|
44
|
+
devtools: WorkerSchema.LeaderWorkerInnerInitialMessagePayload['devtools']
|
|
44
45
|
schema: LiveStoreSchema
|
|
45
|
-
syncPayloadEncoded: Schema.
|
|
46
|
+
syncPayloadEncoded: Schema.Json | undefined
|
|
46
47
|
syncPayloadSchema: Schema.Schema<any> | undefined
|
|
47
48
|
testing: TestingOverrides | undefined
|
|
48
49
|
}
|
|
@@ -64,10 +65,7 @@ export const makeLeaderThread = ({
|
|
|
64
65
|
Scope.Scope
|
|
65
66
|
> =>
|
|
66
67
|
Effect.gen(function* () {
|
|
67
|
-
const
|
|
68
|
-
|
|
69
|
-
const schemaHashSuffix =
|
|
70
|
-
schema.state.sqlite.migrations.strategy === 'manual' ? 'fixed' : schema.state.sqlite.hash.toString()
|
|
68
|
+
const services = yield* Effect.context()
|
|
71
69
|
|
|
72
70
|
const makeDb = (kind: 'state' | 'eventlog') => {
|
|
73
71
|
if (testing?.makeLeaderThread !== undefined) {
|
|
@@ -79,18 +77,21 @@ export const makeLeaderThread = ({
|
|
|
79
77
|
return storage.type === 'in-memory'
|
|
80
78
|
? makeSqliteDb({
|
|
81
79
|
_tag: 'in-memory',
|
|
82
|
-
configureDb: (db) =>
|
|
83
|
-
configureConnection(db, { foreignKeys: true }).pipe(Effect.provide(runtime), Effect.runSync),
|
|
80
|
+
configureDb: (db) => configureConnection(db, { foreignKeys: true }).pipe(Effect.runSyncWith(services)),
|
|
84
81
|
})
|
|
85
|
-
:
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
82
|
+
: Effect.acquireRelease(
|
|
83
|
+
makeSqliteDb({
|
|
84
|
+
_tag: 'fs',
|
|
85
|
+
directory: path.join(storage.baseDirectory ?? '', storeId),
|
|
86
|
+
fileName:
|
|
87
|
+
kind === 'state'
|
|
88
|
+
? `${getStateDbBaseName(schema)}@${liveStoreStorageFormatVersion}.db`
|
|
89
|
+
: `eventlog@${liveStoreStorageFormatVersion}.db`,
|
|
90
|
+
// TODO enable WAL for nodejs
|
|
91
|
+
configureDb: (db) => configureConnection(db, { foreignKeys: true }).pipe(Effect.runSyncWith(services)),
|
|
92
|
+
}),
|
|
93
|
+
(db) => Effect.sync(() => db.close()),
|
|
94
|
+
)
|
|
94
95
|
}
|
|
95
96
|
|
|
96
97
|
// Might involve some async work, so we're running them concurrently
|
|
@@ -116,8 +117,8 @@ export const makeLeaderThread = ({
|
|
|
116
117
|
devtoolsOptions,
|
|
117
118
|
shutdownChannel,
|
|
118
119
|
syncPayloadEncoded,
|
|
119
|
-
syncPayloadSchema,
|
|
120
|
-
})
|
|
120
|
+
syncPayloadSchema: syncPayloadSchema as Schema.Decoder<Schema.Json, never> | undefined,
|
|
121
|
+
}).pipe(Layer.provide(StateHead.layer({ dbState })))
|
|
121
122
|
}).pipe(
|
|
122
123
|
Effect.tapCauseLogPretty,
|
|
123
124
|
UnknownError.mapToUnknownError,
|
|
@@ -126,8 +127,6 @@ export const makeLeaderThread = ({
|
|
|
126
127
|
}),
|
|
127
128
|
)
|
|
128
129
|
|
|
129
|
-
const getStateDbFileName = (suffix: string) => `state${suffix}@${liveStoreStorageFormatVersion}.db`
|
|
130
|
-
|
|
131
130
|
const makeDevtoolsOptions = ({
|
|
132
131
|
dbState,
|
|
133
132
|
dbEventlog,
|
|
@@ -139,7 +138,7 @@ const makeDevtoolsOptions = ({
|
|
|
139
138
|
dbEventlog: LeaderSqliteDb
|
|
140
139
|
storeId: string
|
|
141
140
|
clientId: string
|
|
142
|
-
devtools: WorkerSchema.
|
|
141
|
+
devtools: WorkerSchema.LeaderWorkerInnerInitialMessagePayload['devtools']
|
|
143
142
|
}): Effect.Effect<DevtoolsOptions, UnknownError, Scope.Scope> =>
|
|
144
143
|
Effect.gen(function* () {
|
|
145
144
|
if (devtools.enabled === false) {
|