@livestore/adapter-node 0.3.0-dev.32 → 0.3.0-dev.34
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 +44 -0
- package/dist/client-session/adapter.d.ts.map +1 -0
- package/dist/client-session/adapter.js +202 -0
- package/dist/client-session/adapter.js.map +1 -0
- package/dist/client-session/in-memory-adapter.d.ts +145 -10
- package/dist/client-session/in-memory-adapter.d.ts.map +1 -1
- package/dist/client-session/in-memory-adapter.js +6 -7
- package/dist/client-session/in-memory-adapter.js.map +1 -1
- package/dist/client-session/persisted-adapter.d.ts +26 -14
- package/dist/client-session/persisted-adapter.d.ts.map +1 -1
- package/dist/client-session/persisted-adapter.js +81 -24
- package/dist/client-session/persisted-adapter.js.map +1 -1
- package/dist/devtools/devtools-server.d.ts.map +1 -1
- package/dist/devtools/devtools-server.js +3 -1
- package/dist/devtools/devtools-server.js.map +1 -1
- package/dist/index.d.ts +1 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -2
- package/dist/index.js.map +1 -1
- package/dist/leader-shared.d.ts +29 -0
- package/dist/leader-shared.d.ts.map +1 -0
- package/dist/leader-shared.js +87 -0
- package/dist/leader-shared.js.map +1 -0
- package/dist/leader-thread-lazy.js +4 -0
- package/dist/leader-thread-lazy.js.map +1 -1
- package/dist/leader-thread-shared.d.ts +29 -0
- package/dist/leader-thread-shared.d.ts.map +1 -0
- package/dist/leader-thread-shared.js +88 -0
- package/dist/leader-thread-shared.js.map +1 -0
- package/dist/make-leader-worker.d.ts +5 -1
- package/dist/make-leader-worker.d.ts.map +1 -1
- package/dist/make-leader-worker.js +16 -83
- package/dist/make-leader-worker.js.map +1 -1
- package/dist/webchannel.d.ts.map +1 -1
- package/dist/webchannel.js +13 -3
- package/dist/webchannel.js.map +1 -1
- package/dist/worker-schema.d.ts +35 -19
- package/dist/worker-schema.d.ts.map +1 -1
- package/dist/worker-schema.js +14 -19
- package/dist/worker-schema.js.map +1 -1
- package/package.json +12 -11
- package/src/client-session/{persisted-adapter.ts → adapter.ts} +193 -57
- package/src/devtools/devtools-server.ts +3 -0
- package/src/index.ts +1 -2
- package/src/leader-thread-lazy.ts +5 -0
- package/src/leader-thread-shared.ts +167 -0
- package/src/make-leader-worker.ts +22 -138
- package/src/webchannel.ts +16 -3
- package/src/worker-schema.ts +23 -26
- package/rollup.config.mjs +0 -24
- package/src/client-session/in-memory-adapter.ts +0 -170
- package/tmp/pack.tgz +0 -0
- package/tsconfig.json +0 -17
|
@@ -8,8 +8,13 @@ import type {
|
|
|
8
8
|
ClientSessionLeaderThreadProxy,
|
|
9
9
|
IntentionalShutdownCause,
|
|
10
10
|
LockStatus,
|
|
11
|
+
MakeSqliteDb,
|
|
12
|
+
SyncOptions,
|
|
11
13
|
} from '@livestore/common'
|
|
12
14
|
import { Devtools, UnexpectedError } from '@livestore/common'
|
|
15
|
+
import { Eventlog, LeaderThreadCtx } from '@livestore/common/leader-thread'
|
|
16
|
+
import type { LiveStoreSchema } from '@livestore/common/schema'
|
|
17
|
+
import { LiveStoreEvent } from '@livestore/common/schema'
|
|
13
18
|
import * as DevtoolsNode from '@livestore/devtools-node-common/web-channel'
|
|
14
19
|
import { loadSqlite3Wasm } from '@livestore/sqlite-wasm/load-wasm'
|
|
15
20
|
import { sqliteDbFactory } from '@livestore/sqlite-wasm/node'
|
|
@@ -29,49 +34,82 @@ import {
|
|
|
29
34
|
} from '@livestore/utils/effect'
|
|
30
35
|
import { PlatformNode } from '@livestore/utils/node'
|
|
31
36
|
|
|
37
|
+
import type { TestingOverrides } from '../leader-thread-shared.js'
|
|
38
|
+
import { makeLeaderThread } from '../leader-thread-shared.js'
|
|
32
39
|
import { makeShutdownChannel } from '../shutdown-channel.js'
|
|
33
40
|
import * as WorkerSchema from '../worker-schema.js'
|
|
34
41
|
|
|
35
42
|
export interface NodeAdapterOptions {
|
|
36
|
-
|
|
37
|
-
* Example: `new URL('./livestore.worker.js', import.meta.url)`
|
|
38
|
-
*/
|
|
39
|
-
workerUrl: URL
|
|
40
|
-
/** Needed for the worker and the devtools */
|
|
41
|
-
schemaPath: string
|
|
42
|
-
/** Where to store the database files */
|
|
43
|
-
baseDirectory?: string
|
|
43
|
+
storage: WorkerSchema.StorageType
|
|
44
44
|
/** The default is the hostname of the current machine */
|
|
45
45
|
clientId?: string
|
|
46
|
-
/**
|
|
46
|
+
/**
|
|
47
|
+
* Warning: This adapter doesn't currently support multiple client sessions for the same client (i.e. same storeId + clientId)
|
|
48
|
+
* @default 'static'
|
|
49
|
+
*/
|
|
47
50
|
sessionId?: string
|
|
51
|
+
|
|
48
52
|
devtools?: {
|
|
53
|
+
schemaPath: string
|
|
49
54
|
/**
|
|
50
55
|
* Where to run the devtools server (via Vite)
|
|
51
56
|
*
|
|
52
57
|
* @default 4242
|
|
53
58
|
*/
|
|
54
|
-
port
|
|
59
|
+
port?: number
|
|
55
60
|
/**
|
|
56
61
|
* @default 'localhost'
|
|
57
62
|
*/
|
|
58
|
-
host
|
|
63
|
+
host?: string
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/** Only used internally for testing */
|
|
67
|
+
testing?: {
|
|
68
|
+
overrides?: TestingOverrides
|
|
59
69
|
}
|
|
60
70
|
}
|
|
61
71
|
|
|
72
|
+
/** Runs everything in the same thread. Use `makeWorkerAdapter` for multi-threaded implementation. */
|
|
73
|
+
export const makeAdapter = ({
|
|
74
|
+
sync,
|
|
75
|
+
...options
|
|
76
|
+
}: NodeAdapterOptions & {
|
|
77
|
+
sync?: SyncOptions
|
|
78
|
+
}): Adapter => makeAdapterImpl({ ...options, leaderThread: { _tag: 'single-threaded', sync } })
|
|
79
|
+
|
|
62
80
|
/**
|
|
63
|
-
*
|
|
81
|
+
* Runs persistence and syncing in a worker thread.
|
|
64
82
|
*/
|
|
65
|
-
export const
|
|
83
|
+
export const makeWorkerAdapter = ({
|
|
66
84
|
workerUrl,
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
85
|
+
...options
|
|
86
|
+
}: NodeAdapterOptions & {
|
|
87
|
+
/**
|
|
88
|
+
* Example: `new URL('./livestore.worker.js', import.meta.url)`
|
|
89
|
+
*/
|
|
90
|
+
workerUrl: URL
|
|
91
|
+
}): Adapter => makeAdapterImpl({ ...options, leaderThread: { _tag: 'multi-threaded', workerUrl } })
|
|
92
|
+
|
|
93
|
+
const makeAdapterImpl = ({
|
|
94
|
+
storage,
|
|
95
|
+
devtools: devtoolsOptionsInput,
|
|
70
96
|
clientId = hostname(),
|
|
71
97
|
// TODO make this dynamic and actually support multiple sessions
|
|
72
98
|
sessionId = 'static',
|
|
73
|
-
|
|
74
|
-
|
|
99
|
+
testing,
|
|
100
|
+
leaderThread: leaderThreadInput,
|
|
101
|
+
}: NodeAdapterOptions & {
|
|
102
|
+
leaderThread:
|
|
103
|
+
| {
|
|
104
|
+
_tag: 'single-threaded'
|
|
105
|
+
sync: SyncOptions | undefined
|
|
106
|
+
}
|
|
107
|
+
| {
|
|
108
|
+
_tag: 'multi-threaded'
|
|
109
|
+
workerUrl: URL
|
|
110
|
+
}
|
|
111
|
+
}): Adapter =>
|
|
112
|
+
(({ storeId, devtoolsEnabled, shutdown, connectDevtoolsToStore, bootStatusQueue, syncPayload, schema }) =>
|
|
75
113
|
Effect.gen(function* () {
|
|
76
114
|
yield* Queue.offer(bootStatusQueue, { stage: 'loading' })
|
|
77
115
|
|
|
@@ -86,39 +124,65 @@ export const makePersistedAdapter = ({
|
|
|
86
124
|
// yield* Effect.logWarning('Failed to load database file', fileData.left)
|
|
87
125
|
// }
|
|
88
126
|
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
127
|
+
if (leaderThreadInput._tag === 'multi-threaded') {
|
|
128
|
+
// TODO make static import again once BroadcastChannel is stable in Deno
|
|
129
|
+
//
|
|
130
|
+
// const { makeShutdownChannel } = yield* Effect.promise(() => import('../shutdown-channel.js'))
|
|
131
|
+
const shutdownChannel = yield* makeShutdownChannel(storeId)
|
|
132
|
+
|
|
133
|
+
yield* shutdownChannel.listen.pipe(
|
|
134
|
+
Stream.flatten(),
|
|
135
|
+
Stream.tap((error) => Effect.sync(() => shutdown(Cause.fail(error)))),
|
|
136
|
+
Stream.runDrain,
|
|
137
|
+
Effect.interruptible,
|
|
138
|
+
Effect.tapCauseLogPretty,
|
|
139
|
+
Effect.forkScoped,
|
|
140
|
+
)
|
|
141
|
+
}
|
|
99
142
|
|
|
100
143
|
const syncInMemoryDb = yield* makeSqliteDb({ _tag: 'in-memory' }).pipe(Effect.orDie)
|
|
101
144
|
|
|
102
145
|
// TODO actually implement this multi-session support
|
|
103
146
|
const lockStatus = yield* SubscriptionRef.make<LockStatus>('has-lock')
|
|
104
147
|
|
|
105
|
-
const
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
148
|
+
const devtoolsOptions: WorkerSchema.LeaderWorkerInner.InitialMessage['devtools'] =
|
|
149
|
+
devtoolsEnabled && devtoolsOptionsInput !== undefined
|
|
150
|
+
? {
|
|
151
|
+
enabled: true,
|
|
152
|
+
schemaPath: devtoolsOptionsInput.schemaPath,
|
|
153
|
+
port: devtoolsOptionsInput.port ?? 4242,
|
|
154
|
+
host: devtoolsOptionsInput.host ?? 'localhost',
|
|
155
|
+
}
|
|
156
|
+
: { enabled: false }
|
|
157
|
+
|
|
158
|
+
const { leaderThread, initialSnapshot } =
|
|
159
|
+
leaderThreadInput._tag === 'single-threaded'
|
|
160
|
+
? yield* makeLocalLeaderThread({
|
|
161
|
+
storeId,
|
|
162
|
+
clientId,
|
|
163
|
+
schema,
|
|
164
|
+
makeSqliteDb,
|
|
165
|
+
syncOptions: leaderThreadInput.sync,
|
|
166
|
+
syncPayload,
|
|
167
|
+
devtools: devtoolsOptions,
|
|
168
|
+
storage,
|
|
169
|
+
testing,
|
|
170
|
+
}).pipe(UnexpectedError.mapToUnexpectedError)
|
|
171
|
+
: yield* makeWorkerLeaderThread({
|
|
172
|
+
shutdown,
|
|
173
|
+
storeId,
|
|
174
|
+
clientId,
|
|
175
|
+
sessionId,
|
|
176
|
+
workerUrl: leaderThreadInput.workerUrl,
|
|
177
|
+
storage,
|
|
178
|
+
devtools: devtoolsOptions,
|
|
179
|
+
bootStatusQueue,
|
|
180
|
+
syncPayload,
|
|
181
|
+
})
|
|
118
182
|
|
|
119
183
|
syncInMemoryDb.import(initialSnapshot)
|
|
120
184
|
|
|
121
|
-
if (
|
|
185
|
+
if (devtoolsOptions.enabled) {
|
|
122
186
|
yield* Effect.gen(function* () {
|
|
123
187
|
const webmeshNode = yield* DevtoolsNode.makeNodeDevtoolsConnectedMeshNode({
|
|
124
188
|
url: `ws://${devtoolsOptions.host}:${devtoolsOptions.port}`,
|
|
@@ -142,7 +206,11 @@ export const makePersistedAdapter = ({
|
|
|
142
206
|
})
|
|
143
207
|
|
|
144
208
|
yield* connectDevtoolsToStore(storeDevtoolsChannel)
|
|
145
|
-
}).pipe(
|
|
209
|
+
}).pipe(
|
|
210
|
+
Effect.tapCauseLogPretty,
|
|
211
|
+
Effect.withSpan('@livestore/adapter-node:client-session:devtools'),
|
|
212
|
+
Effect.forkScoped,
|
|
213
|
+
)
|
|
146
214
|
}
|
|
147
215
|
|
|
148
216
|
const devtools: ClientSession['devtools'] = devtoolsEnabled
|
|
@@ -167,30 +235,97 @@ export const makePersistedAdapter = ({
|
|
|
167
235
|
Effect.provide(FetchHttpClient.layer),
|
|
168
236
|
)) satisfies Adapter
|
|
169
237
|
|
|
170
|
-
const
|
|
238
|
+
const makeLocalLeaderThread = ({
|
|
239
|
+
storeId,
|
|
240
|
+
clientId,
|
|
241
|
+
schema,
|
|
242
|
+
makeSqliteDb,
|
|
243
|
+
syncOptions,
|
|
244
|
+
syncPayload,
|
|
245
|
+
storage,
|
|
246
|
+
devtools,
|
|
247
|
+
testing,
|
|
248
|
+
}: {
|
|
249
|
+
storeId: string
|
|
250
|
+
clientId: string
|
|
251
|
+
schema: LiveStoreSchema
|
|
252
|
+
makeSqliteDb: MakeSqliteDb
|
|
253
|
+
syncOptions: SyncOptions | undefined
|
|
254
|
+
storage: WorkerSchema.StorageType
|
|
255
|
+
syncPayload: Schema.JsonValue | undefined
|
|
256
|
+
devtools: WorkerSchema.LeaderWorkerInner.InitialMessage['devtools']
|
|
257
|
+
testing?: {
|
|
258
|
+
overrides?: TestingOverrides
|
|
259
|
+
}
|
|
260
|
+
}) =>
|
|
261
|
+
Effect.gen(function* () {
|
|
262
|
+
const layer = yield* Layer.build(
|
|
263
|
+
makeLeaderThread({
|
|
264
|
+
storeId,
|
|
265
|
+
clientId,
|
|
266
|
+
schema,
|
|
267
|
+
syncOptions,
|
|
268
|
+
storage,
|
|
269
|
+
syncPayload,
|
|
270
|
+
devtools,
|
|
271
|
+
makeSqliteDb,
|
|
272
|
+
testing: testing?.overrides,
|
|
273
|
+
}).pipe(Layer.unwrapScoped),
|
|
274
|
+
)
|
|
275
|
+
|
|
276
|
+
return yield* Effect.gen(function* () {
|
|
277
|
+
const { dbState, dbEventlog, syncProcessor, extraIncomingMessagesQueue, initialState } = yield* LeaderThreadCtx
|
|
278
|
+
|
|
279
|
+
const initialLeaderHead = Eventlog.getClientHeadFromDb(dbEventlog)
|
|
280
|
+
|
|
281
|
+
const leaderThread = {
|
|
282
|
+
events: {
|
|
283
|
+
pull:
|
|
284
|
+
testing?.overrides?.clientSession?.leaderThreadProxy?.events?.pull ??
|
|
285
|
+
(({ cursor }) => syncProcessor.pull({ cursor })),
|
|
286
|
+
push: (batch) =>
|
|
287
|
+
syncProcessor.push(
|
|
288
|
+
batch.map((item) => new LiveStoreEvent.EncodedWithMeta(item)),
|
|
289
|
+
{ waitForProcessing: true },
|
|
290
|
+
),
|
|
291
|
+
},
|
|
292
|
+
initialState: { leaderHead: initialLeaderHead, migrationsReport: initialState.migrationsReport },
|
|
293
|
+
export: Effect.sync(() => dbState.export()),
|
|
294
|
+
getEventlogData: Effect.sync(() => dbEventlog.export()),
|
|
295
|
+
getSyncState: syncProcessor.syncState,
|
|
296
|
+
sendDevtoolsMessage: (message) => extraIncomingMessagesQueue.offer(message),
|
|
297
|
+
} satisfies ClientSessionLeaderThreadProxy
|
|
298
|
+
|
|
299
|
+
const initialSnapshot = dbState.export()
|
|
300
|
+
|
|
301
|
+
return { leaderThread, initialSnapshot }
|
|
302
|
+
}).pipe(Effect.provide(layer))
|
|
303
|
+
})
|
|
304
|
+
|
|
305
|
+
const makeWorkerLeaderThread = ({
|
|
171
306
|
shutdown,
|
|
172
307
|
storeId,
|
|
173
308
|
clientId,
|
|
174
309
|
sessionId,
|
|
175
310
|
workerUrl,
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
devtoolsOptions,
|
|
179
|
-
schemaPath,
|
|
311
|
+
storage,
|
|
312
|
+
devtools,
|
|
180
313
|
bootStatusQueue,
|
|
181
314
|
syncPayload,
|
|
315
|
+
testing,
|
|
182
316
|
}: {
|
|
183
317
|
shutdown: (cause: Cause.Cause<UnexpectedError | IntentionalShutdownCause>) => void
|
|
184
318
|
storeId: string
|
|
185
319
|
clientId: string
|
|
186
320
|
sessionId: string
|
|
187
321
|
workerUrl: URL
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
devtoolsOptions: { port: number; host: string }
|
|
191
|
-
schemaPath: string
|
|
322
|
+
storage: WorkerSchema.StorageType
|
|
323
|
+
devtools: WorkerSchema.LeaderWorkerInner.InitialMessage['devtools']
|
|
192
324
|
bootStatusQueue: Queue.Queue<BootStatus>
|
|
193
325
|
syncPayload: Schema.JsonValue | undefined
|
|
326
|
+
testing?: {
|
|
327
|
+
overrides?: TestingOverrides
|
|
328
|
+
}
|
|
194
329
|
}) =>
|
|
195
330
|
Effect.gen(function* () {
|
|
196
331
|
const nodeWorker = new WT.Worker(workerUrl, {
|
|
@@ -206,9 +341,8 @@ const makeLeaderThread = ({
|
|
|
206
341
|
new WorkerSchema.LeaderWorkerInner.InitialMessage({
|
|
207
342
|
storeId,
|
|
208
343
|
clientId,
|
|
209
|
-
|
|
210
|
-
devtools
|
|
211
|
-
schemaPath,
|
|
344
|
+
storage,
|
|
345
|
+
devtools,
|
|
212
346
|
syncPayload,
|
|
213
347
|
}),
|
|
214
348
|
}).pipe(
|
|
@@ -296,8 +430,10 @@ const makeLeaderThread = ({
|
|
|
296
430
|
|
|
297
431
|
const leaderThread = {
|
|
298
432
|
events: {
|
|
299
|
-
pull:
|
|
300
|
-
|
|
433
|
+
pull:
|
|
434
|
+
testing?.overrides?.clientSession?.leaderThreadProxy?.events?.pull ??
|
|
435
|
+
(({ cursor }) =>
|
|
436
|
+
runInWorkerStream(new WorkerSchema.LeaderWorkerInner.PullStream({ cursor })).pipe(Stream.orDie)),
|
|
301
437
|
push: (batch) =>
|
|
302
438
|
runInWorker(new WorkerSchema.LeaderWorkerInner.PushToLeader({ batch })).pipe(
|
|
303
439
|
Effect.withSpan('@livestore/adapter-node:client-session:pushToLeader', {
|
|
@@ -111,6 +111,9 @@ export const startDevtoolsServer = ({
|
|
|
111
111
|
|
|
112
112
|
return HttpServer.serve(handler, HttpMiddleware.logger)
|
|
113
113
|
}).pipe(
|
|
114
|
+
Effect.withSpan('@livestore/adapter-node:startDevtoolsServer', {
|
|
115
|
+
attributes: { storeId, clientId, sessionId, port, host, schemaPath },
|
|
116
|
+
}),
|
|
114
117
|
Layer.unwrapScoped,
|
|
115
118
|
// HttpServer.withLogAddress,
|
|
116
119
|
Layer.provide(PlatformNode.NodeHttpServer.layer(() => http.createServer(), { port, host })),
|
package/src/index.ts
CHANGED
|
@@ -1,2 +1 @@
|
|
|
1
|
-
export {
|
|
2
|
-
export { makePersistedAdapter } from './client-session/persisted-adapter.js'
|
|
1
|
+
export { makeAdapter, makeWorkerAdapter } from './client-session/adapter.js'
|
|
@@ -1,3 +1,8 @@
|
|
|
1
|
+
// NOTE This file isn't currently used but was part of an experiment to see whether we can improve
|
|
2
|
+
// the Node startup time by lazy loading the leader thread bundle.
|
|
3
|
+
// This indeed provided a nice speedup but it takes quite a bit of tooling to set up and comes with
|
|
4
|
+
// other downsides (e.g. treeshaking).
|
|
5
|
+
|
|
1
6
|
const run = async () => {
|
|
2
7
|
const start = Date.now()
|
|
3
8
|
// @ts-expect-error todo
|
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
import inspector from 'node:inspector'
|
|
2
|
+
import path from 'node:path'
|
|
3
|
+
|
|
4
|
+
if (process.execArgv.includes('--inspect')) {
|
|
5
|
+
inspector.open()
|
|
6
|
+
inspector.waitForDebugger()
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
import type { ClientSessionLeaderThreadProxy, MakeSqliteDb, SqliteDb, SyncOptions } from '@livestore/common'
|
|
10
|
+
import { Devtools, liveStoreStorageFormatVersion, UnexpectedError } from '@livestore/common'
|
|
11
|
+
import type { DevtoolsOptions, LeaderSqliteDb, LeaderThreadCtx } from '@livestore/common/leader-thread'
|
|
12
|
+
import { configureConnection, makeLeaderThreadLayer } from '@livestore/common/leader-thread'
|
|
13
|
+
import type { LiveStoreSchema } from '@livestore/common/schema'
|
|
14
|
+
import { makeNodeDevtoolsChannel } from '@livestore/devtools-node-common/web-channel'
|
|
15
|
+
import type { MakeNodeSqliteDb } from '@livestore/sqlite-wasm/node'
|
|
16
|
+
import type { FileSystem, HttpClient, Layer, Schema, Scope } from '@livestore/utils/effect'
|
|
17
|
+
import { Effect, FetchHttpClient } from '@livestore/utils/effect'
|
|
18
|
+
|
|
19
|
+
import { makeShutdownChannel } from './shutdown-channel.js'
|
|
20
|
+
import type * as WorkerSchema from './worker-schema.js'
|
|
21
|
+
|
|
22
|
+
export type TestingOverrides = {
|
|
23
|
+
clientSession?: {
|
|
24
|
+
leaderThreadProxy?: Partial<ClientSessionLeaderThreadProxy>
|
|
25
|
+
}
|
|
26
|
+
makeLeaderThread?: {
|
|
27
|
+
dbEventlog?: (makeSqliteDb: MakeSqliteDb) => Effect.Effect<SqliteDb, UnexpectedError>
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export interface MakeLeaderThreadArgs {
|
|
32
|
+
storeId: string
|
|
33
|
+
clientId: string
|
|
34
|
+
syncOptions: SyncOptions | undefined
|
|
35
|
+
storage: WorkerSchema.StorageType
|
|
36
|
+
makeSqliteDb: MakeNodeSqliteDb
|
|
37
|
+
devtools: WorkerSchema.LeaderWorkerInner.InitialMessage['devtools']
|
|
38
|
+
schema: LiveStoreSchema
|
|
39
|
+
syncPayload: Schema.JsonValue | undefined
|
|
40
|
+
testing: TestingOverrides | undefined
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export const makeLeaderThread = ({
|
|
44
|
+
storeId,
|
|
45
|
+
clientId,
|
|
46
|
+
syncOptions,
|
|
47
|
+
makeSqliteDb,
|
|
48
|
+
storage,
|
|
49
|
+
devtools,
|
|
50
|
+
schema,
|
|
51
|
+
syncPayload,
|
|
52
|
+
testing,
|
|
53
|
+
}: MakeLeaderThreadArgs): Effect.Effect<
|
|
54
|
+
Layer.Layer<LeaderThreadCtx, UnexpectedError, Scope.Scope | HttpClient.HttpClient | FileSystem.FileSystem>,
|
|
55
|
+
UnexpectedError,
|
|
56
|
+
Scope.Scope
|
|
57
|
+
> =>
|
|
58
|
+
Effect.gen(function* () {
|
|
59
|
+
const runtime = yield* Effect.runtime<never>()
|
|
60
|
+
|
|
61
|
+
const schemaHashSuffix =
|
|
62
|
+
schema.state.sqlite.migrations.strategy === 'manual' ? 'fixed' : schema.state.sqlite.hash.toString()
|
|
63
|
+
|
|
64
|
+
const makeDb = (kind: 'state' | 'eventlog') => {
|
|
65
|
+
if (testing?.makeLeaderThread?.dbEventlog && kind === 'eventlog') {
|
|
66
|
+
return testing.makeLeaderThread.dbEventlog(makeSqliteDb)
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
return storage.type === 'in-memory'
|
|
70
|
+
? makeSqliteDb({
|
|
71
|
+
_tag: 'in-memory',
|
|
72
|
+
configureDb: (db) =>
|
|
73
|
+
configureConnection(db, { foreignKeys: true }).pipe(Effect.provide(runtime), Effect.runSync),
|
|
74
|
+
})
|
|
75
|
+
: makeSqliteDb({
|
|
76
|
+
_tag: 'fs',
|
|
77
|
+
directory: path.join(storage.baseDirectory ?? '', storeId),
|
|
78
|
+
fileName:
|
|
79
|
+
kind === 'state' ? getStateDbFileName(schemaHashSuffix) : `eventlog@${liveStoreStorageFormatVersion}.db`,
|
|
80
|
+
// TODO enable WAL for nodejs
|
|
81
|
+
configureDb: (db) =>
|
|
82
|
+
configureConnection(db, { foreignKeys: true }).pipe(Effect.provide(runtime), Effect.runSync),
|
|
83
|
+
}).pipe(Effect.acquireRelease((db) => Effect.sync(() => db.close())))
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
// Might involve some async work, so we're running them concurrently
|
|
87
|
+
const [dbState, dbEventlog] = yield* Effect.all([makeDb('state'), makeDb('eventlog')], { concurrency: 2 })
|
|
88
|
+
|
|
89
|
+
const devtoolsOptions = yield* makeDevtoolsOptions({ devtools, dbState, dbEventlog, storeId, clientId })
|
|
90
|
+
|
|
91
|
+
const shutdownChannel = yield* makeShutdownChannel(storeId)
|
|
92
|
+
|
|
93
|
+
return makeLeaderThreadLayer({
|
|
94
|
+
schema,
|
|
95
|
+
storeId,
|
|
96
|
+
clientId,
|
|
97
|
+
makeSqliteDb,
|
|
98
|
+
syncOptions,
|
|
99
|
+
dbState,
|
|
100
|
+
dbEventlog,
|
|
101
|
+
devtoolsOptions,
|
|
102
|
+
shutdownChannel,
|
|
103
|
+
syncPayload,
|
|
104
|
+
})
|
|
105
|
+
}).pipe(
|
|
106
|
+
Effect.tapCauseLogPretty,
|
|
107
|
+
UnexpectedError.mapToUnexpectedError,
|
|
108
|
+
Effect.withSpan('@livestore/adapter-node:makeLeaderThread', {
|
|
109
|
+
attributes: { storeId, clientId, storage, devtools, syncOptions },
|
|
110
|
+
}),
|
|
111
|
+
)
|
|
112
|
+
|
|
113
|
+
const getStateDbFileName = (suffix: string) => `state${suffix}@${liveStoreStorageFormatVersion}.db`
|
|
114
|
+
|
|
115
|
+
const makeDevtoolsOptions = ({
|
|
116
|
+
dbState,
|
|
117
|
+
dbEventlog,
|
|
118
|
+
storeId,
|
|
119
|
+
clientId,
|
|
120
|
+
devtools,
|
|
121
|
+
}: {
|
|
122
|
+
dbState: LeaderSqliteDb
|
|
123
|
+
dbEventlog: LeaderSqliteDb
|
|
124
|
+
storeId: string
|
|
125
|
+
clientId: string
|
|
126
|
+
devtools: WorkerSchema.LeaderWorkerInner.InitialMessage['devtools']
|
|
127
|
+
}): Effect.Effect<DevtoolsOptions, UnexpectedError, Scope.Scope> =>
|
|
128
|
+
Effect.gen(function* () {
|
|
129
|
+
if (devtools.enabled === false) {
|
|
130
|
+
return {
|
|
131
|
+
enabled: false,
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
return {
|
|
136
|
+
enabled: true,
|
|
137
|
+
makeBootContext: Effect.gen(function* () {
|
|
138
|
+
// Lazy import to improve startup time
|
|
139
|
+
const { startDevtoolsServer } = yield* Effect.promise(() => import('./devtools/devtools-server.js'))
|
|
140
|
+
|
|
141
|
+
// 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
|
|
142
|
+
yield* startDevtoolsServer({
|
|
143
|
+
schemaPath: devtools.schemaPath,
|
|
144
|
+
storeId,
|
|
145
|
+
clientId,
|
|
146
|
+
sessionId: 'static', // TODO make this dynamic
|
|
147
|
+
port: devtools.port,
|
|
148
|
+
host: devtools.host,
|
|
149
|
+
}).pipe(Effect.tapCauseLogPretty, Effect.forkScoped)
|
|
150
|
+
|
|
151
|
+
const devtoolsWebChannel = yield* makeNodeDevtoolsChannel({
|
|
152
|
+
nodeName: `leader-${storeId}-${clientId}`,
|
|
153
|
+
target: `devtools-${storeId}-${clientId}-static`,
|
|
154
|
+
url: `ws://localhost:${devtools.port}`,
|
|
155
|
+
schema: { listen: Devtools.Leader.MessageToApp, send: Devtools.Leader.MessageFromApp },
|
|
156
|
+
})
|
|
157
|
+
|
|
158
|
+
return {
|
|
159
|
+
devtoolsWebChannel,
|
|
160
|
+
persistenceInfo: {
|
|
161
|
+
state: dbState.metadata.persistenceInfo,
|
|
162
|
+
eventlog: dbEventlog.metadata.persistenceInfo,
|
|
163
|
+
},
|
|
164
|
+
}
|
|
165
|
+
}).pipe(Effect.provide(FetchHttpClient.layer)),
|
|
166
|
+
}
|
|
167
|
+
})
|