@livestore/adapter-node 0.0.0-snapshot-a953343ad2d7468c6573bcb5e26f0eab4302078f
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 -0
- package/dist/client-session/index.d.ts +23 -0
- package/dist/client-session/index.d.ts.map +1 -0
- package/dist/client-session/index.js +140 -0
- package/dist/client-session/index.js.map +1 -0
- package/dist/devtools/devtools-server.d.ts +16 -0
- package/dist/devtools/devtools-server.d.ts.map +1 -0
- package/dist/devtools/devtools-server.js +58 -0
- package/dist/devtools/devtools-server.js.map +1 -0
- package/dist/devtools/mod.d.ts +3 -0
- package/dist/devtools/mod.d.ts.map +1 -0
- package/dist/devtools/mod.js +2 -0
- package/dist/devtools/mod.js.map +1 -0
- package/dist/devtools/vite-dev-server.d.ts +7 -0
- package/dist/devtools/vite-dev-server.d.ts.map +1 -0
- package/dist/devtools/vite-dev-server.js +107 -0
- package/dist/devtools/vite-dev-server.js.map +1 -0
- package/dist/in-memory/index.d.ts +11 -0
- package/dist/in-memory/index.d.ts.map +1 -0
- package/dist/in-memory/index.js +71 -0
- package/dist/in-memory/index.js.map +1 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +3 -0
- package/dist/index.js.map +1 -0
- package/dist/leader-thread-lazy.d.ts +2 -0
- package/dist/leader-thread-lazy.d.ts.map +1 -0
- package/dist/leader-thread-lazy.js +10 -0
- package/dist/leader-thread-lazy.js.map +1 -0
- package/dist/make-leader-worker.d.ts +20 -0
- package/dist/make-leader-worker.d.ts.map +1 -0
- package/dist/make-leader-worker.js +151 -0
- package/dist/make-leader-worker.js.map +1 -0
- package/dist/shutdown-channel.d.ts +6 -0
- package/dist/shutdown-channel.d.ts.map +1 -0
- package/dist/shutdown-channel.js +7 -0
- package/dist/shutdown-channel.js.map +1 -0
- package/dist/thread-polyfill.d.ts +2 -0
- package/dist/thread-polyfill.d.ts.map +1 -0
- package/dist/thread-polyfill.js +3 -0
- package/dist/thread-polyfill.js.map +1 -0
- package/dist/webchannel.d.ts +6 -0
- package/dist/webchannel.d.ts.map +1 -0
- package/dist/webchannel.js +33 -0
- package/dist/webchannel.js.map +1 -0
- package/dist/worker-schema.d.ts +196 -0
- package/dist/worker-schema.d.ts.map +1 -0
- package/dist/worker-schema.js +161 -0
- package/dist/worker-schema.js.map +1 -0
- package/package.json +54 -0
- package/rollup.config.mjs +24 -0
- package/src/client-session/index.ts +295 -0
- package/src/devtools/devtools-server.ts +88 -0
- package/src/devtools/mod.ts +2 -0
- package/src/devtools/types.d.ts +33 -0
- package/src/devtools/vite-dev-server.ts +122 -0
- package/src/in-memory/index.ts +133 -0
- package/src/index.ts +2 -0
- package/src/leader-thread-lazy.ts +9 -0
- package/src/make-leader-worker.ts +285 -0
- package/src/shutdown-channel.ts +9 -0
- package/src/thread-polyfill.ts +1 -0
- package/src/webchannel.ts +54 -0
- package/src/worker-schema.ts +175 -0
- package/tsconfig.json +17 -0
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
Adapter,
|
|
3
|
+
ClientSession,
|
|
4
|
+
ClientSessionLeaderThreadProxy,
|
|
5
|
+
LockStatus,
|
|
6
|
+
MakeSqliteDb,
|
|
7
|
+
SyncOptions,
|
|
8
|
+
} from '@livestore/common'
|
|
9
|
+
import { UnexpectedError } from '@livestore/common'
|
|
10
|
+
import { getClientHeadFromDb, LeaderThreadCtx, makeLeaderThreadLayer } from '@livestore/common/leader-thread'
|
|
11
|
+
import type { LiveStoreSchema } from '@livestore/common/schema'
|
|
12
|
+
import { MutationEvent } from '@livestore/common/schema'
|
|
13
|
+
import { sqliteDbFactory } from '@livestore/sqlite-wasm/browser'
|
|
14
|
+
import { loadSqlite3Wasm } from '@livestore/sqlite-wasm/load-wasm'
|
|
15
|
+
import { Effect, FetchHttpClient, Layer, Stream, SubscriptionRef, WebChannel } from '@livestore/utils/effect'
|
|
16
|
+
import { nanoid } from '@livestore/utils/nanoid'
|
|
17
|
+
|
|
18
|
+
// TODO unify in-memory adapter with other in-memory adapter implementations
|
|
19
|
+
|
|
20
|
+
export interface InMemoryAdapterOptions {
|
|
21
|
+
sync?: SyncOptions
|
|
22
|
+
/**
|
|
23
|
+
* @default 'in-memory'
|
|
24
|
+
*/
|
|
25
|
+
clientId?: string
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/** NOTE: This adapter is currently only used for testing */
|
|
29
|
+
export const makeInMemoryAdapter =
|
|
30
|
+
({ sync: syncOptions, clientId = 'in-memory' }: InMemoryAdapterOptions): Adapter =>
|
|
31
|
+
({
|
|
32
|
+
schema,
|
|
33
|
+
storeId,
|
|
34
|
+
// devtoolsEnabled, bootStatusQueue, shutdown, connectDevtoolsToStore
|
|
35
|
+
}) =>
|
|
36
|
+
Effect.gen(function* () {
|
|
37
|
+
const sqlite3 = yield* Effect.promise(() => loadSqlite3Wasm())
|
|
38
|
+
|
|
39
|
+
const makeSqliteDb = sqliteDbFactory({ sqlite3 })
|
|
40
|
+
const sqliteDb = yield* makeSqliteDb({ _tag: 'in-memory' })
|
|
41
|
+
|
|
42
|
+
const lockStatus = SubscriptionRef.make<LockStatus>('has-lock').pipe(Effect.runSync)
|
|
43
|
+
|
|
44
|
+
const sessionId = nanoid(6)
|
|
45
|
+
|
|
46
|
+
const { leaderThread, initialSnapshot } = yield* makeLeaderThread({
|
|
47
|
+
storeId,
|
|
48
|
+
clientId,
|
|
49
|
+
schema,
|
|
50
|
+
makeSqliteDb,
|
|
51
|
+
syncOptions,
|
|
52
|
+
})
|
|
53
|
+
|
|
54
|
+
sqliteDb.import(initialSnapshot)
|
|
55
|
+
|
|
56
|
+
const clientSession = {
|
|
57
|
+
sqliteDb,
|
|
58
|
+
devtools: { enabled: false },
|
|
59
|
+
clientId,
|
|
60
|
+
sessionId,
|
|
61
|
+
lockStatus,
|
|
62
|
+
leaderThread,
|
|
63
|
+
shutdown: () => Effect.dieMessage('TODO implement shutdown'),
|
|
64
|
+
} satisfies ClientSession
|
|
65
|
+
|
|
66
|
+
return clientSession
|
|
67
|
+
}).pipe(UnexpectedError.mapToUnexpectedError)
|
|
68
|
+
|
|
69
|
+
const makeLeaderThread = ({
|
|
70
|
+
storeId,
|
|
71
|
+
clientId,
|
|
72
|
+
schema,
|
|
73
|
+
makeSqliteDb,
|
|
74
|
+
syncOptions,
|
|
75
|
+
}: {
|
|
76
|
+
storeId: string
|
|
77
|
+
clientId: string
|
|
78
|
+
schema: LiveStoreSchema
|
|
79
|
+
makeSqliteDb: MakeSqliteDb
|
|
80
|
+
syncOptions: SyncOptions | undefined
|
|
81
|
+
}) =>
|
|
82
|
+
Effect.gen(function* () {
|
|
83
|
+
const layer = yield* Layer.memoize(
|
|
84
|
+
makeLeaderThreadLayer({
|
|
85
|
+
clientId,
|
|
86
|
+
dbReadModel: yield* makeSqliteDb({ _tag: 'in-memory' }),
|
|
87
|
+
dbMutationLog: yield* makeSqliteDb({ _tag: 'in-memory' }),
|
|
88
|
+
devtoolsOptions: { enabled: false },
|
|
89
|
+
makeSqliteDb,
|
|
90
|
+
schema,
|
|
91
|
+
shutdownChannel: yield* WebChannel.noopChannel<any, any>(),
|
|
92
|
+
storeId,
|
|
93
|
+
syncOptions,
|
|
94
|
+
}).pipe(Layer.provideMerge(FetchHttpClient.layer)),
|
|
95
|
+
)
|
|
96
|
+
|
|
97
|
+
return yield* Effect.gen(function* () {
|
|
98
|
+
const {
|
|
99
|
+
dbReadModel: db,
|
|
100
|
+
dbMutationLog,
|
|
101
|
+
syncProcessor,
|
|
102
|
+
connectedClientSessionPullQueues,
|
|
103
|
+
extraIncomingMessagesQueue,
|
|
104
|
+
initialState,
|
|
105
|
+
} = yield* LeaderThreadCtx
|
|
106
|
+
|
|
107
|
+
const initialLeaderHead = getClientHeadFromDb(dbMutationLog)
|
|
108
|
+
const pullQueue = yield* connectedClientSessionPullQueues.makeQueue(initialLeaderHead)
|
|
109
|
+
|
|
110
|
+
const leaderThread = {
|
|
111
|
+
mutations: {
|
|
112
|
+
pull: Stream.fromQueue(pullQueue),
|
|
113
|
+
push: (batch) =>
|
|
114
|
+
syncProcessor
|
|
115
|
+
.push(batch.map((item) => new MutationEvent.EncodedWithMeta(item)))
|
|
116
|
+
.pipe(Effect.provide(layer), Effect.scoped),
|
|
117
|
+
},
|
|
118
|
+
initialState: { leaderHead: initialLeaderHead, migrationsReport: initialState.migrationsReport },
|
|
119
|
+
export: Effect.sync(() => db.export()),
|
|
120
|
+
getMutationLogData: Effect.sync(() => dbMutationLog.export()),
|
|
121
|
+
// TODO
|
|
122
|
+
networkStatus: SubscriptionRef.make({ isConnected: false, timestampMs: Date.now(), latchClosed: false }).pipe(
|
|
123
|
+
Effect.runSync,
|
|
124
|
+
),
|
|
125
|
+
getSyncState: syncProcessor.syncState,
|
|
126
|
+
sendDevtoolsMessage: (message) => extraIncomingMessagesQueue.offer(message),
|
|
127
|
+
} satisfies ClientSessionLeaderThreadProxy
|
|
128
|
+
|
|
129
|
+
const initialSnapshot = db.export()
|
|
130
|
+
|
|
131
|
+
return { leaderThread, initialSnapshot }
|
|
132
|
+
}).pipe(Effect.provide(layer))
|
|
133
|
+
})
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,285 @@
|
|
|
1
|
+
import './thread-polyfill.js'
|
|
2
|
+
|
|
3
|
+
import inspector from 'node:inspector'
|
|
4
|
+
import path from 'node:path'
|
|
5
|
+
|
|
6
|
+
if (process.execArgv.includes('--inspect')) {
|
|
7
|
+
inspector.open()
|
|
8
|
+
inspector.waitForDebugger()
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
import type { NetworkStatus, SyncOptions } from '@livestore/common'
|
|
12
|
+
import { Devtools, liveStoreStorageFormatVersion, UnexpectedError } from '@livestore/common'
|
|
13
|
+
import type { DevtoolsOptions, LeaderSqliteDb } from '@livestore/common/leader-thread'
|
|
14
|
+
import {
|
|
15
|
+
configureConnection,
|
|
16
|
+
getClientHeadFromDb,
|
|
17
|
+
LeaderThreadCtx,
|
|
18
|
+
makeLeaderThreadLayer,
|
|
19
|
+
} from '@livestore/common/leader-thread'
|
|
20
|
+
import type { LiveStoreSchema } from '@livestore/common/schema'
|
|
21
|
+
import { MutationEvent } from '@livestore/common/schema'
|
|
22
|
+
import { makeNodeDevtoolsChannel } from '@livestore/devtools-node-common/web-channel'
|
|
23
|
+
import { loadSqlite3Wasm } from '@livestore/sqlite-wasm/load-wasm'
|
|
24
|
+
import { sqliteDbFactory } from '@livestore/sqlite-wasm/node'
|
|
25
|
+
import type { FileSystem, HttpClient, Scope } from '@livestore/utils/effect'
|
|
26
|
+
import {
|
|
27
|
+
Effect,
|
|
28
|
+
FetchHttpClient,
|
|
29
|
+
identity,
|
|
30
|
+
Layer,
|
|
31
|
+
Logger,
|
|
32
|
+
LogLevel,
|
|
33
|
+
OtelTracer,
|
|
34
|
+
Schema,
|
|
35
|
+
Stream,
|
|
36
|
+
WorkerRunner,
|
|
37
|
+
} from '@livestore/utils/effect'
|
|
38
|
+
import { PlatformNode } from '@livestore/utils/node'
|
|
39
|
+
import type * as otel from '@opentelemetry/api'
|
|
40
|
+
|
|
41
|
+
import { startDevtoolsServer } from './devtools/devtools-server.js'
|
|
42
|
+
import { makeShutdownChannel } from './shutdown-channel.js'
|
|
43
|
+
import * as WorkerSchema from './worker-schema.js'
|
|
44
|
+
|
|
45
|
+
export type WorkerOptions = {
|
|
46
|
+
sync?: SyncOptions
|
|
47
|
+
otelOptions?: {
|
|
48
|
+
tracer?: otel.Tracer
|
|
49
|
+
/** @default 'livestore-node-leader-thread' */
|
|
50
|
+
serviceName?: string
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export const getWorkerArgs = () => Schema.decodeSync(WorkerSchema.WorkerArgv)(process.argv[2]!)
|
|
55
|
+
|
|
56
|
+
export const makeWorker = (options: WorkerOptions) => {
|
|
57
|
+
makeWorkerEffect(options).pipe(Effect.runFork)
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
export const makeWorkerEffect = (options: WorkerOptions) => {
|
|
61
|
+
const TracingLive = options.otelOptions?.tracer
|
|
62
|
+
? Layer.unwrapEffect(Effect.map(OtelTracer.make, Layer.setTracer)).pipe(
|
|
63
|
+
Layer.provideMerge(Layer.succeed(OtelTracer.OtelTracer, options.otelOptions.tracer)),
|
|
64
|
+
)
|
|
65
|
+
: undefined
|
|
66
|
+
|
|
67
|
+
return WorkerRunner.layerSerialized(WorkerSchema.LeaderWorkerInner.Request, {
|
|
68
|
+
InitialMessage: (args) => makeLeaderThread({ ...args, syncOptions: options.sync }),
|
|
69
|
+
PushToLeader: ({ batch }) =>
|
|
70
|
+
Effect.andThen(LeaderThreadCtx, (_) =>
|
|
71
|
+
_.syncProcessor.push(batch.map((item) => new MutationEvent.EncodedWithMeta(item))),
|
|
72
|
+
).pipe(Effect.uninterruptible, Effect.withSpan('@livestore/adapter-node:worker:PushToLeader')),
|
|
73
|
+
BootStatusStream: () =>
|
|
74
|
+
Effect.andThen(LeaderThreadCtx, (_) => Stream.fromQueue(_.bootStatusQueue)).pipe(Stream.unwrap),
|
|
75
|
+
PullStream: ({ cursor }) =>
|
|
76
|
+
Effect.gen(function* () {
|
|
77
|
+
const { connectedClientSessionPullQueues } = yield* LeaderThreadCtx
|
|
78
|
+
const pullQueue = yield* connectedClientSessionPullQueues.makeQueue(cursor)
|
|
79
|
+
return Stream.fromQueue(pullQueue)
|
|
80
|
+
}).pipe(Stream.unwrapScoped),
|
|
81
|
+
Export: () =>
|
|
82
|
+
Effect.andThen(LeaderThreadCtx, (_) => _.dbReadModel.export()).pipe(
|
|
83
|
+
UnexpectedError.mapToUnexpectedError,
|
|
84
|
+
Effect.withSpan('@livestore/adapter-node:worker:Export'),
|
|
85
|
+
),
|
|
86
|
+
ExportMutationlog: () =>
|
|
87
|
+
Effect.andThen(LeaderThreadCtx, (_) => _.dbMutationLog.export()).pipe(
|
|
88
|
+
UnexpectedError.mapToUnexpectedError,
|
|
89
|
+
Effect.withSpan('@livestore/adapter-node:worker:ExportMutationlog'),
|
|
90
|
+
),
|
|
91
|
+
GetLeaderHead: () =>
|
|
92
|
+
Effect.gen(function* () {
|
|
93
|
+
const workerCtx = yield* LeaderThreadCtx
|
|
94
|
+
return getClientHeadFromDb(workerCtx.dbMutationLog)
|
|
95
|
+
}).pipe(UnexpectedError.mapToUnexpectedError, Effect.withSpan('@livestore/adapter-node:worker:GetLeaderHead')),
|
|
96
|
+
NetworkStatusStream: () =>
|
|
97
|
+
Effect.gen(function* (_) {
|
|
98
|
+
const ctx = yield* LeaderThreadCtx
|
|
99
|
+
|
|
100
|
+
if (ctx.syncBackend === undefined) {
|
|
101
|
+
return Stream.make<[NetworkStatus]>({ isConnected: false, timestampMs: Date.now(), latchClosed: false })
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
return ctx.syncBackend.isConnected.changes.pipe(
|
|
105
|
+
Stream.map((isConnected) => ({ isConnected, timestampMs: Date.now(), latchClosed: false })),
|
|
106
|
+
)
|
|
107
|
+
}).pipe(Stream.unwrap),
|
|
108
|
+
GetLeaderSyncState: () =>
|
|
109
|
+
Effect.gen(function* () {
|
|
110
|
+
const workerCtx = yield* LeaderThreadCtx
|
|
111
|
+
return yield* workerCtx.syncProcessor.syncState
|
|
112
|
+
}).pipe(
|
|
113
|
+
UnexpectedError.mapToUnexpectedError,
|
|
114
|
+
Effect.withSpan('@livestore/adapter-node:worker:GetLeaderSyncState'),
|
|
115
|
+
),
|
|
116
|
+
GetRecreateSnapshot: () =>
|
|
117
|
+
Effect.gen(function* () {
|
|
118
|
+
const workerCtx = yield* LeaderThreadCtx
|
|
119
|
+
// const result = yield* Deferred.await(workerCtx.initialSetupDeferred)
|
|
120
|
+
// NOTE we can only return the cached snapshot once as it's transferred (i.e. disposed), so we need to set it to undefined
|
|
121
|
+
// const cachedSnapshot =
|
|
122
|
+
// result._tag === 'Recreate' ? yield* Ref.getAndSet(result.snapshotRef, undefined) : undefined
|
|
123
|
+
// return cachedSnapshot ?? workerCtx.db.export()
|
|
124
|
+
const snapshot = workerCtx.dbReadModel.export()
|
|
125
|
+
return { snapshot, migrationsReport: workerCtx.initialState.migrationsReport }
|
|
126
|
+
}).pipe(
|
|
127
|
+
UnexpectedError.mapToUnexpectedError,
|
|
128
|
+
Effect.withSpan('@livestore/adapter-node:worker:GetRecreateSnapshot'),
|
|
129
|
+
),
|
|
130
|
+
Shutdown: () =>
|
|
131
|
+
Effect.gen(function* () {
|
|
132
|
+
// const { db, dbMutationLog } = yield* LeaderThreadCtx
|
|
133
|
+
yield* Effect.logDebug('[@livestore/adapter-node:worker] Shutdown')
|
|
134
|
+
|
|
135
|
+
// if (devtools.enabled) {
|
|
136
|
+
// yield* FiberSet.clear(devtools.connections)
|
|
137
|
+
// }
|
|
138
|
+
// db.close()
|
|
139
|
+
// dbMutationLog.close()
|
|
140
|
+
|
|
141
|
+
// Buy some time for Otel to flush
|
|
142
|
+
// TODO find a cleaner way to do this
|
|
143
|
+
// yield* Effect.sleep(1000)
|
|
144
|
+
}).pipe(UnexpectedError.mapToUnexpectedError, Effect.withSpan('@livestore/adapter-node:worker:Shutdown')),
|
|
145
|
+
ExtraDevtoolsMessage: ({ message }) =>
|
|
146
|
+
Effect.andThen(LeaderThreadCtx, (_) => _.extraIncomingMessagesQueue.offer(message)).pipe(
|
|
147
|
+
UnexpectedError.mapToUnexpectedError,
|
|
148
|
+
Effect.withSpan('@livestore/adapter-node:worker:ExtraDevtoolsMessage'),
|
|
149
|
+
),
|
|
150
|
+
}).pipe(
|
|
151
|
+
Layer.provide(PlatformNode.NodeWorkerRunner.layer),
|
|
152
|
+
Layer.launch,
|
|
153
|
+
Effect.scoped,
|
|
154
|
+
Effect.tapCauseLogPretty,
|
|
155
|
+
Effect.annotateLogs({ thread: options.otelOptions?.serviceName ?? 'livestore-node-leader-thread' }),
|
|
156
|
+
Effect.provide(Logger.prettyWithThread(options.otelOptions?.serviceName ?? 'livestore-node-leader-thread')),
|
|
157
|
+
Effect.provide(FetchHttpClient.layer),
|
|
158
|
+
Effect.provide(PlatformNode.NodeFileSystem.layer),
|
|
159
|
+
TracingLive ? Effect.provide(TracingLive) : identity,
|
|
160
|
+
Logger.withMinimumLogLevel(LogLevel.Debug),
|
|
161
|
+
)
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
const makeLeaderThread = ({
|
|
165
|
+
storeId,
|
|
166
|
+
clientId,
|
|
167
|
+
syncOptions,
|
|
168
|
+
baseDirectory,
|
|
169
|
+
devtools,
|
|
170
|
+
schemaPath,
|
|
171
|
+
}: WorkerSchema.LeaderWorkerInner.InitialMessage & {
|
|
172
|
+
syncOptions: SyncOptions | undefined
|
|
173
|
+
schemaPath: string
|
|
174
|
+
}): Layer.Layer<LeaderThreadCtx, UnexpectedError, Scope.Scope | HttpClient.HttpClient | FileSystem.FileSystem> =>
|
|
175
|
+
Effect.gen(function* () {
|
|
176
|
+
const schema = yield* Effect.promise(() => import(schemaPath).then((m) => m.schema as LiveStoreSchema))
|
|
177
|
+
|
|
178
|
+
const sqlite3 = yield* Effect.promise(() => loadSqlite3Wasm()).pipe(
|
|
179
|
+
Effect.withSpan('@livestore/adapter-node:leader-thread:loadSqlite3Wasm'),
|
|
180
|
+
)
|
|
181
|
+
const makeSqliteDb = yield* sqliteDbFactory({ sqlite3 })
|
|
182
|
+
const runtime = yield* Effect.runtime<never>()
|
|
183
|
+
|
|
184
|
+
const schemaHashSuffix = schema.migrationOptions.strategy === 'manual' ? 'fixed' : schema.hash.toString()
|
|
185
|
+
|
|
186
|
+
const makeDb = (kind: 'app' | 'mutationlog') =>
|
|
187
|
+
makeSqliteDb({
|
|
188
|
+
_tag: 'fs',
|
|
189
|
+
directory: path.join(baseDirectory ?? '', storeId),
|
|
190
|
+
fileName:
|
|
191
|
+
kind === 'app' ? getAppDbFileName(schemaHashSuffix) : `mutationlog@${liveStoreStorageFormatVersion}.db`,
|
|
192
|
+
// TODO enable WAL for nodejs
|
|
193
|
+
configureDb: (db) =>
|
|
194
|
+
configureConnection(db, { foreignKeys: true }).pipe(Effect.provide(runtime), Effect.runSync),
|
|
195
|
+
}).pipe(Effect.acquireRelease((db) => Effect.sync(() => db.close())))
|
|
196
|
+
|
|
197
|
+
// Might involve some async work, so we're running them concurrently
|
|
198
|
+
const [dbReadModel, dbMutationLog] = yield* Effect.all([makeDb('app'), makeDb('mutationlog')], { concurrency: 2 })
|
|
199
|
+
|
|
200
|
+
const devtoolsOptions = yield* makeDevtoolsOptions({
|
|
201
|
+
devtoolsEnabled: devtools.enabled,
|
|
202
|
+
devtoolsPort: devtools.port,
|
|
203
|
+
dbReadModel,
|
|
204
|
+
dbMutationLog,
|
|
205
|
+
storeId,
|
|
206
|
+
clientId,
|
|
207
|
+
schemaPath,
|
|
208
|
+
})
|
|
209
|
+
|
|
210
|
+
const shutdownChannel = yield* makeShutdownChannel(storeId)
|
|
211
|
+
|
|
212
|
+
return makeLeaderThreadLayer({
|
|
213
|
+
schema,
|
|
214
|
+
storeId,
|
|
215
|
+
clientId,
|
|
216
|
+
makeSqliteDb,
|
|
217
|
+
syncOptions,
|
|
218
|
+
dbReadModel,
|
|
219
|
+
dbMutationLog,
|
|
220
|
+
devtoolsOptions,
|
|
221
|
+
shutdownChannel,
|
|
222
|
+
})
|
|
223
|
+
}).pipe(
|
|
224
|
+
Effect.tapCauseLogPretty,
|
|
225
|
+
UnexpectedError.mapToUnexpectedError,
|
|
226
|
+
Effect.withSpan('@livestore/adapter-node:worker:InitialMessage'),
|
|
227
|
+
Layer.unwrapScoped,
|
|
228
|
+
)
|
|
229
|
+
|
|
230
|
+
const getAppDbFileName = (suffix: string) => `app${suffix}@${liveStoreStorageFormatVersion}.db`
|
|
231
|
+
|
|
232
|
+
const makeDevtoolsOptions = ({
|
|
233
|
+
devtoolsEnabled,
|
|
234
|
+
dbReadModel,
|
|
235
|
+
dbMutationLog,
|
|
236
|
+
storeId,
|
|
237
|
+
clientId,
|
|
238
|
+
devtoolsPort,
|
|
239
|
+
schemaPath,
|
|
240
|
+
}: {
|
|
241
|
+
devtoolsEnabled: boolean
|
|
242
|
+
dbReadModel: LeaderSqliteDb
|
|
243
|
+
dbMutationLog: LeaderSqliteDb
|
|
244
|
+
storeId: string
|
|
245
|
+
clientId: string
|
|
246
|
+
devtoolsPort: number
|
|
247
|
+
schemaPath: string
|
|
248
|
+
}): Effect.Effect<DevtoolsOptions, UnexpectedError, Scope.Scope> =>
|
|
249
|
+
Effect.gen(function* () {
|
|
250
|
+
if (devtoolsEnabled === false) {
|
|
251
|
+
return {
|
|
252
|
+
enabled: false,
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
return {
|
|
257
|
+
enabled: true,
|
|
258
|
+
makeBootContext: Effect.gen(function* () {
|
|
259
|
+
// TODO instead of failing when the port is already in use, we should try to use that WS server instead of starting a new one
|
|
260
|
+
yield* startDevtoolsServer({
|
|
261
|
+
schemaPath,
|
|
262
|
+
storeId,
|
|
263
|
+
clientId,
|
|
264
|
+
sessionId: 'static', // TODO make this dynamic
|
|
265
|
+
port: devtoolsPort,
|
|
266
|
+
})
|
|
267
|
+
|
|
268
|
+
return {
|
|
269
|
+
devtoolsWebChannel: yield* makeNodeDevtoolsChannel({
|
|
270
|
+
nodeName: `leader-${storeId}-${clientId}`,
|
|
271
|
+
target: `devtools`,
|
|
272
|
+
url: `ws://localhost:${devtoolsPort}`,
|
|
273
|
+
schema: {
|
|
274
|
+
listen: Devtools.Leader.MessageToApp,
|
|
275
|
+
send: Devtools.Leader.MessageFromApp,
|
|
276
|
+
},
|
|
277
|
+
}),
|
|
278
|
+
persistenceInfo: {
|
|
279
|
+
readModel: dbReadModel.metadata.persistenceInfo,
|
|
280
|
+
mutationLog: dbMutationLog.metadata.persistenceInfo,
|
|
281
|
+
},
|
|
282
|
+
}
|
|
283
|
+
}),
|
|
284
|
+
}
|
|
285
|
+
})
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import { ShutdownChannel } from '@livestore/common/leader-thread'
|
|
2
|
+
|
|
3
|
+
import { makeBroadcastChannel } from './webchannel.js'
|
|
4
|
+
|
|
5
|
+
export const makeShutdownChannel = (storeId: string) =>
|
|
6
|
+
makeBroadcastChannel({
|
|
7
|
+
channelName: `livestore.shutdown.${storeId}`,
|
|
8
|
+
schema: ShutdownChannel.All,
|
|
9
|
+
})
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
process.stdout.isTTY = true
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import { BroadcastChannel } from 'node:worker_threads'
|
|
2
|
+
|
|
3
|
+
import type { Either, ParseResult } from '@livestore/utils/effect'
|
|
4
|
+
import { Deferred, Effect, Exit, Schema, Scope, Stream, WebChannel } from '@livestore/utils/effect'
|
|
5
|
+
|
|
6
|
+
export const makeBroadcastChannel = <Msg, MsgEncoded>({
|
|
7
|
+
channelName,
|
|
8
|
+
schema,
|
|
9
|
+
}: {
|
|
10
|
+
channelName: string
|
|
11
|
+
schema: Schema.Schema<Msg, MsgEncoded>
|
|
12
|
+
}): Effect.Effect<WebChannel.WebChannel<Msg, Msg>, never, Scope.Scope> =>
|
|
13
|
+
Effect.scopeWithCloseable((scope) =>
|
|
14
|
+
Effect.gen(function* () {
|
|
15
|
+
const channel = new BroadcastChannel(channelName)
|
|
16
|
+
|
|
17
|
+
yield* Effect.addFinalizer(() => Effect.try(() => channel.close()).pipe(Effect.ignoreLogged))
|
|
18
|
+
|
|
19
|
+
const send = (message: Msg) =>
|
|
20
|
+
Effect.gen(function* () {
|
|
21
|
+
const messageEncoded = yield* Schema.encode(schema)(message)
|
|
22
|
+
channel.postMessage(messageEncoded)
|
|
23
|
+
})
|
|
24
|
+
|
|
25
|
+
// TODO also listen to `messageerror` in parallel
|
|
26
|
+
// const listen = Stream.fromEventListener<MessageEvent>(channel, 'message').pipe(
|
|
27
|
+
// Stream.map((_) => Schema.decodeEither(listenSchema)(_.data)),
|
|
28
|
+
// )
|
|
29
|
+
|
|
30
|
+
const listen = Stream.asyncPush<Either.Either<Msg, ParseResult.ParseError>>((emit) =>
|
|
31
|
+
Effect.gen(function* () {
|
|
32
|
+
// eslint-disable-next-line unicorn/prefer-add-event-listener
|
|
33
|
+
channel.onmessage = (event: any) => {
|
|
34
|
+
return emit.single(Schema.decodeEither(schema)(event.data))
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
return () => channel.unref()
|
|
38
|
+
}),
|
|
39
|
+
)
|
|
40
|
+
|
|
41
|
+
const closedDeferred = yield* Deferred.make<void>().pipe(Effect.acquireRelease(Deferred.done(Exit.void)))
|
|
42
|
+
const supportsTransferables = false
|
|
43
|
+
|
|
44
|
+
return {
|
|
45
|
+
[WebChannel.WebChannelSymbol]: WebChannel.WebChannelSymbol,
|
|
46
|
+
send,
|
|
47
|
+
listen,
|
|
48
|
+
closedDeferred,
|
|
49
|
+
schema: { listen: schema, send: schema },
|
|
50
|
+
supportsTransferables,
|
|
51
|
+
shutdown: Scope.close(scope, Exit.void),
|
|
52
|
+
}
|
|
53
|
+
}).pipe(Effect.withSpan(`WebChannel:broadcastChannel(${channelName})`)),
|
|
54
|
+
)
|
|
@@ -0,0 +1,175 @@
|
|
|
1
|
+
import { BootStatus, Devtools, InvalidPushError, MigrationsReport, SyncState, UnexpectedError } from '@livestore/common'
|
|
2
|
+
import { EventId, MutationEvent } from '@livestore/common/schema'
|
|
3
|
+
import { Schema, Transferable } from '@livestore/utils/effect'
|
|
4
|
+
|
|
5
|
+
export const WorkerArgv = Schema.parseJson(
|
|
6
|
+
Schema.Struct({
|
|
7
|
+
clientId: Schema.String,
|
|
8
|
+
storeId: Schema.String,
|
|
9
|
+
sessionId: Schema.String,
|
|
10
|
+
}),
|
|
11
|
+
)
|
|
12
|
+
|
|
13
|
+
export const StorageTypeOpfs = Schema.Struct({
|
|
14
|
+
type: Schema.Literal('opfs'),
|
|
15
|
+
/**
|
|
16
|
+
* Default is `livestore-${storeId}`
|
|
17
|
+
*
|
|
18
|
+
* When providing this option, make sure to include the `storeId` in the path to avoid
|
|
19
|
+
* conflicts with other LiveStore apps.
|
|
20
|
+
*/
|
|
21
|
+
directory: Schema.optional(Schema.String),
|
|
22
|
+
})
|
|
23
|
+
|
|
24
|
+
export type StorageTypeOpfs = typeof StorageTypeOpfs.Type
|
|
25
|
+
|
|
26
|
+
// export const StorageTypeIndexeddb = Schema.Struct({
|
|
27
|
+
// type: Schema.Literal('indexeddb'),
|
|
28
|
+
// /** @default "livestore" */
|
|
29
|
+
// databaseName: Schema.optionalWith(Schema.String, { default: () => 'livestore' }),
|
|
30
|
+
// /** @default "livestore-" */
|
|
31
|
+
// storeNamePrefix: Schema.optionalWith(Schema.String, { default: () => 'livestore-' }),
|
|
32
|
+
// })
|
|
33
|
+
|
|
34
|
+
export const StorageType = Schema.Union(
|
|
35
|
+
StorageTypeOpfs,
|
|
36
|
+
// StorageTypeIndexeddb
|
|
37
|
+
)
|
|
38
|
+
export type StorageType = typeof StorageType.Type
|
|
39
|
+
export type StorageTypeEncoded = typeof StorageType.Encoded
|
|
40
|
+
|
|
41
|
+
// export const SyncBackendOptionsWebsocket = Schema.Struct({
|
|
42
|
+
// type: Schema.Literal('websocket'),
|
|
43
|
+
// url: Schema.String,
|
|
44
|
+
// storeId: Schema.String,
|
|
45
|
+
// })
|
|
46
|
+
|
|
47
|
+
// export const SyncBackendOptions = Schema.Union(SyncBackendOptionsWebsocket)
|
|
48
|
+
export const SyncBackendOptions = Schema.Record({ key: Schema.String, value: Schema.JsonValue })
|
|
49
|
+
export type SyncBackendOptions = Record<string, Schema.JsonValue>
|
|
50
|
+
|
|
51
|
+
export namespace LeaderWorkerOuter {
|
|
52
|
+
export class InitialMessage extends Schema.TaggedRequest<InitialMessage>()('InitialMessage', {
|
|
53
|
+
payload: { port: Transferable.MessagePort },
|
|
54
|
+
success: Schema.Void,
|
|
55
|
+
failure: UnexpectedError,
|
|
56
|
+
}) {}
|
|
57
|
+
|
|
58
|
+
export class Request extends Schema.Union(InitialMessage) {}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
export namespace LeaderWorkerInner {
|
|
62
|
+
export class InitialMessage extends Schema.TaggedRequest<InitialMessage>()('InitialMessage', {
|
|
63
|
+
payload: {
|
|
64
|
+
storeId: Schema.String,
|
|
65
|
+
clientId: Schema.String,
|
|
66
|
+
baseDirectory: Schema.optional(Schema.String),
|
|
67
|
+
schemaPath: Schema.String,
|
|
68
|
+
devtools: Schema.Struct({
|
|
69
|
+
port: Schema.Number,
|
|
70
|
+
enabled: Schema.Boolean,
|
|
71
|
+
}),
|
|
72
|
+
},
|
|
73
|
+
success: Schema.Void,
|
|
74
|
+
failure: UnexpectedError,
|
|
75
|
+
}) {}
|
|
76
|
+
|
|
77
|
+
export class BootStatusStream extends Schema.TaggedRequest<BootStatusStream>()('BootStatusStream', {
|
|
78
|
+
payload: {},
|
|
79
|
+
success: BootStatus,
|
|
80
|
+
failure: UnexpectedError,
|
|
81
|
+
}) {}
|
|
82
|
+
|
|
83
|
+
export class PullStream extends Schema.TaggedRequest<PullStream>()('PullStream', {
|
|
84
|
+
payload: {
|
|
85
|
+
cursor: EventId.EventId,
|
|
86
|
+
},
|
|
87
|
+
success: Schema.Struct({
|
|
88
|
+
// mutationEvents: Schema.Array(EncodedAny),
|
|
89
|
+
// backendHead: Schema.Number,
|
|
90
|
+
payload: SyncState.PayloadUpstream,
|
|
91
|
+
remaining: Schema.Number,
|
|
92
|
+
}),
|
|
93
|
+
failure: UnexpectedError,
|
|
94
|
+
}) {}
|
|
95
|
+
|
|
96
|
+
export class PushToLeader extends Schema.TaggedRequest<PushToLeader>()('PushToLeader', {
|
|
97
|
+
payload: {
|
|
98
|
+
batch: Schema.Array(MutationEvent.AnyEncoded),
|
|
99
|
+
},
|
|
100
|
+
success: Schema.Void,
|
|
101
|
+
failure: Schema.Union(UnexpectedError, InvalidPushError),
|
|
102
|
+
}) {}
|
|
103
|
+
|
|
104
|
+
export class Export extends Schema.TaggedRequest<Export>()('Export', {
|
|
105
|
+
payload: {},
|
|
106
|
+
success: Transferable.Uint8Array,
|
|
107
|
+
failure: UnexpectedError,
|
|
108
|
+
}) {}
|
|
109
|
+
|
|
110
|
+
export class GetRecreateSnapshot extends Schema.TaggedRequest<GetRecreateSnapshot>()('GetRecreateSnapshot', {
|
|
111
|
+
payload: {},
|
|
112
|
+
success: Schema.Struct({
|
|
113
|
+
snapshot: Transferable.Uint8Array,
|
|
114
|
+
migrationsReport: MigrationsReport,
|
|
115
|
+
}),
|
|
116
|
+
failure: UnexpectedError,
|
|
117
|
+
}) {}
|
|
118
|
+
|
|
119
|
+
export class ExportMutationlog extends Schema.TaggedRequest<ExportMutationlog>()('ExportMutationlog', {
|
|
120
|
+
payload: {},
|
|
121
|
+
success: Transferable.Uint8Array,
|
|
122
|
+
failure: UnexpectedError,
|
|
123
|
+
}) {}
|
|
124
|
+
|
|
125
|
+
export class GetLeaderHead extends Schema.TaggedRequest<GetLeaderHead>()('GetLeaderHead', {
|
|
126
|
+
payload: {},
|
|
127
|
+
success: EventId.EventId,
|
|
128
|
+
failure: UnexpectedError,
|
|
129
|
+
}) {}
|
|
130
|
+
|
|
131
|
+
export class GetLeaderSyncState extends Schema.TaggedRequest<GetLeaderSyncState>()('GetLeaderSyncState', {
|
|
132
|
+
payload: {},
|
|
133
|
+
success: SyncState.SyncState,
|
|
134
|
+
failure: UnexpectedError,
|
|
135
|
+
}) {}
|
|
136
|
+
|
|
137
|
+
export class NetworkStatusStream extends Schema.TaggedRequest<NetworkStatusStream>()('NetworkStatusStream', {
|
|
138
|
+
payload: {},
|
|
139
|
+
success: Schema.Struct({
|
|
140
|
+
isConnected: Schema.Boolean,
|
|
141
|
+
timestampMs: Schema.Number,
|
|
142
|
+
}),
|
|
143
|
+
failure: UnexpectedError,
|
|
144
|
+
}) {}
|
|
145
|
+
|
|
146
|
+
export class Shutdown extends Schema.TaggedRequest<Shutdown>()('Shutdown', {
|
|
147
|
+
payload: {},
|
|
148
|
+
success: Schema.Void,
|
|
149
|
+
failure: UnexpectedError,
|
|
150
|
+
}) {}
|
|
151
|
+
|
|
152
|
+
export class ExtraDevtoolsMessage extends Schema.TaggedRequest<ExtraDevtoolsMessage>()('ExtraDevtoolsMessage', {
|
|
153
|
+
payload: {
|
|
154
|
+
message: Devtools.Leader.MessageToApp,
|
|
155
|
+
},
|
|
156
|
+
success: Schema.Void,
|
|
157
|
+
failure: UnexpectedError,
|
|
158
|
+
}) {}
|
|
159
|
+
|
|
160
|
+
export const Request = Schema.Union(
|
|
161
|
+
InitialMessage,
|
|
162
|
+
BootStatusStream,
|
|
163
|
+
PullStream,
|
|
164
|
+
PushToLeader,
|
|
165
|
+
Export,
|
|
166
|
+
GetRecreateSnapshot,
|
|
167
|
+
ExportMutationlog,
|
|
168
|
+
GetLeaderHead,
|
|
169
|
+
GetLeaderSyncState,
|
|
170
|
+
NetworkStatusStream,
|
|
171
|
+
Shutdown,
|
|
172
|
+
ExtraDevtoolsMessage,
|
|
173
|
+
)
|
|
174
|
+
export type Request = typeof Request.Type
|
|
175
|
+
}
|
package/tsconfig.json
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
{
|
|
2
|
+
"extends": "../../../tsconfig.base.json",
|
|
3
|
+
"compilerOptions": {
|
|
4
|
+
"outDir": "./dist",
|
|
5
|
+
"rootDir": "./src",
|
|
6
|
+
"resolveJsonModule": true,
|
|
7
|
+
"tsBuildInfoFile": "./dist/.tsbuildinfo"
|
|
8
|
+
},
|
|
9
|
+
"include": ["./src"],
|
|
10
|
+
"references": [
|
|
11
|
+
{ "path": "../common" },
|
|
12
|
+
{ "path": "../utils" },
|
|
13
|
+
{ "path": "../devtools-node-common" },
|
|
14
|
+
{ "path": "../webmesh" },
|
|
15
|
+
{ "path": "../sqlite-wasm" }
|
|
16
|
+
]
|
|
17
|
+
}
|