@livestore/adapter-node 0.3.0-dev.33 → 0.3.0-dev.36
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 +204 -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 +2 -1
- package/dist/devtools/devtools-server.d.ts.map +1 -1
- package/dist/devtools/devtools-server.js +5 -3
- 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 +89 -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 +36 -19
- package/dist/worker-schema.d.ts.map +1 -1
- package/dist/worker-schema.js +15 -19
- package/dist/worker-schema.js.map +1 -1
- package/package.json +12 -11
- package/src/client-session/{persisted-adapter.ts → adapter.ts} +196 -58
- package/src/devtools/devtools-server.ts +6 -1
- package/src/index.ts +1 -2
- package/src/leader-thread-lazy.ts +5 -0
- package/src/leader-thread-shared.ts +168 -0
- package/src/make-leader-worker.ts +22 -138
- package/src/webchannel.ts +16 -3
- package/src/worker-schema.ts +24 -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,66 @@ 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
|
+
schemaAlias: schema.devtools.alias,
|
|
154
|
+
port: devtoolsOptionsInput.port ?? 4242,
|
|
155
|
+
host: devtoolsOptionsInput.host ?? 'localhost',
|
|
156
|
+
}
|
|
157
|
+
: { enabled: false }
|
|
158
|
+
|
|
159
|
+
const { leaderThread, initialSnapshot } =
|
|
160
|
+
leaderThreadInput._tag === 'single-threaded'
|
|
161
|
+
? yield* makeLocalLeaderThread({
|
|
162
|
+
storeId,
|
|
163
|
+
clientId,
|
|
164
|
+
schema,
|
|
165
|
+
makeSqliteDb,
|
|
166
|
+
syncOptions: leaderThreadInput.sync,
|
|
167
|
+
syncPayload,
|
|
168
|
+
devtools: devtoolsOptions,
|
|
169
|
+
storage,
|
|
170
|
+
testing,
|
|
171
|
+
}).pipe(UnexpectedError.mapToUnexpectedError)
|
|
172
|
+
: yield* makeWorkerLeaderThread({
|
|
173
|
+
shutdown,
|
|
174
|
+
storeId,
|
|
175
|
+
clientId,
|
|
176
|
+
sessionId,
|
|
177
|
+
workerUrl: leaderThreadInput.workerUrl,
|
|
178
|
+
storage,
|
|
179
|
+
devtools: devtoolsOptions,
|
|
180
|
+
bootStatusQueue,
|
|
181
|
+
syncPayload,
|
|
182
|
+
})
|
|
118
183
|
|
|
119
184
|
syncInMemoryDb.import(initialSnapshot)
|
|
120
185
|
|
|
121
|
-
if (
|
|
186
|
+
if (devtoolsOptions.enabled) {
|
|
122
187
|
yield* Effect.gen(function* () {
|
|
123
188
|
const webmeshNode = yield* DevtoolsNode.makeNodeDevtoolsConnectedMeshNode({
|
|
124
189
|
url: `ws://${devtoolsOptions.host}:${devtoolsOptions.port}`,
|
|
@@ -130,9 +195,10 @@ export const makePersistedAdapter = ({
|
|
|
130
195
|
schema: Devtools.SessionInfo.Message,
|
|
131
196
|
})
|
|
132
197
|
|
|
198
|
+
const schemaAlias = schema.devtools.alias
|
|
133
199
|
yield* Devtools.SessionInfo.provideSessionInfo({
|
|
134
200
|
webChannel: sessionsChannel,
|
|
135
|
-
sessionInfo: Devtools.SessionInfo.SessionInfo.make({ storeId, clientId, sessionId }),
|
|
201
|
+
sessionInfo: Devtools.SessionInfo.SessionInfo.make({ storeId, clientId, sessionId, schemaAlias }),
|
|
136
202
|
}).pipe(Effect.tapCauseLogPretty, Effect.forkScoped)
|
|
137
203
|
|
|
138
204
|
const storeDevtoolsChannel = yield* DevtoolsNode.makeChannelForConnectedMeshNode({
|
|
@@ -142,7 +208,11 @@ export const makePersistedAdapter = ({
|
|
|
142
208
|
})
|
|
143
209
|
|
|
144
210
|
yield* connectDevtoolsToStore(storeDevtoolsChannel)
|
|
145
|
-
}).pipe(
|
|
211
|
+
}).pipe(
|
|
212
|
+
Effect.tapCauseLogPretty,
|
|
213
|
+
Effect.withSpan('@livestore/adapter-node:client-session:devtools'),
|
|
214
|
+
Effect.forkScoped,
|
|
215
|
+
)
|
|
146
216
|
}
|
|
147
217
|
|
|
148
218
|
const devtools: ClientSession['devtools'] = devtoolsEnabled
|
|
@@ -167,30 +237,97 @@ export const makePersistedAdapter = ({
|
|
|
167
237
|
Effect.provide(FetchHttpClient.layer),
|
|
168
238
|
)) satisfies Adapter
|
|
169
239
|
|
|
170
|
-
const
|
|
240
|
+
const makeLocalLeaderThread = ({
|
|
241
|
+
storeId,
|
|
242
|
+
clientId,
|
|
243
|
+
schema,
|
|
244
|
+
makeSqliteDb,
|
|
245
|
+
syncOptions,
|
|
246
|
+
syncPayload,
|
|
247
|
+
storage,
|
|
248
|
+
devtools,
|
|
249
|
+
testing,
|
|
250
|
+
}: {
|
|
251
|
+
storeId: string
|
|
252
|
+
clientId: string
|
|
253
|
+
schema: LiveStoreSchema
|
|
254
|
+
makeSqliteDb: MakeSqliteDb
|
|
255
|
+
syncOptions: SyncOptions | undefined
|
|
256
|
+
storage: WorkerSchema.StorageType
|
|
257
|
+
syncPayload: Schema.JsonValue | undefined
|
|
258
|
+
devtools: WorkerSchema.LeaderWorkerInner.InitialMessage['devtools']
|
|
259
|
+
testing?: {
|
|
260
|
+
overrides?: TestingOverrides
|
|
261
|
+
}
|
|
262
|
+
}) =>
|
|
263
|
+
Effect.gen(function* () {
|
|
264
|
+
const layer = yield* Layer.build(
|
|
265
|
+
makeLeaderThread({
|
|
266
|
+
storeId,
|
|
267
|
+
clientId,
|
|
268
|
+
schema,
|
|
269
|
+
syncOptions,
|
|
270
|
+
storage,
|
|
271
|
+
syncPayload,
|
|
272
|
+
devtools,
|
|
273
|
+
makeSqliteDb,
|
|
274
|
+
testing: testing?.overrides,
|
|
275
|
+
}).pipe(Layer.unwrapScoped),
|
|
276
|
+
)
|
|
277
|
+
|
|
278
|
+
return yield* Effect.gen(function* () {
|
|
279
|
+
const { dbState, dbEventlog, syncProcessor, extraIncomingMessagesQueue, initialState } = yield* LeaderThreadCtx
|
|
280
|
+
|
|
281
|
+
const initialLeaderHead = Eventlog.getClientHeadFromDb(dbEventlog)
|
|
282
|
+
|
|
283
|
+
const leaderThread = {
|
|
284
|
+
events: {
|
|
285
|
+
pull:
|
|
286
|
+
testing?.overrides?.clientSession?.leaderThreadProxy?.events?.pull ??
|
|
287
|
+
(({ cursor }) => syncProcessor.pull({ cursor })),
|
|
288
|
+
push: (batch) =>
|
|
289
|
+
syncProcessor.push(
|
|
290
|
+
batch.map((item) => new LiveStoreEvent.EncodedWithMeta(item)),
|
|
291
|
+
{ waitForProcessing: true },
|
|
292
|
+
),
|
|
293
|
+
},
|
|
294
|
+
initialState: { leaderHead: initialLeaderHead, migrationsReport: initialState.migrationsReport },
|
|
295
|
+
export: Effect.sync(() => dbState.export()),
|
|
296
|
+
getEventlogData: Effect.sync(() => dbEventlog.export()),
|
|
297
|
+
getSyncState: syncProcessor.syncState,
|
|
298
|
+
sendDevtoolsMessage: (message) => extraIncomingMessagesQueue.offer(message),
|
|
299
|
+
} satisfies ClientSessionLeaderThreadProxy
|
|
300
|
+
|
|
301
|
+
const initialSnapshot = dbState.export()
|
|
302
|
+
|
|
303
|
+
return { leaderThread, initialSnapshot }
|
|
304
|
+
}).pipe(Effect.provide(layer))
|
|
305
|
+
})
|
|
306
|
+
|
|
307
|
+
const makeWorkerLeaderThread = ({
|
|
171
308
|
shutdown,
|
|
172
309
|
storeId,
|
|
173
310
|
clientId,
|
|
174
311
|
sessionId,
|
|
175
312
|
workerUrl,
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
devtoolsOptions,
|
|
179
|
-
schemaPath,
|
|
313
|
+
storage,
|
|
314
|
+
devtools,
|
|
180
315
|
bootStatusQueue,
|
|
181
316
|
syncPayload,
|
|
317
|
+
testing,
|
|
182
318
|
}: {
|
|
183
319
|
shutdown: (cause: Cause.Cause<UnexpectedError | IntentionalShutdownCause>) => void
|
|
184
320
|
storeId: string
|
|
185
321
|
clientId: string
|
|
186
322
|
sessionId: string
|
|
187
323
|
workerUrl: URL
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
devtoolsOptions: { port: number; host: string }
|
|
191
|
-
schemaPath: string
|
|
324
|
+
storage: WorkerSchema.StorageType
|
|
325
|
+
devtools: WorkerSchema.LeaderWorkerInner.InitialMessage['devtools']
|
|
192
326
|
bootStatusQueue: Queue.Queue<BootStatus>
|
|
193
327
|
syncPayload: Schema.JsonValue | undefined
|
|
328
|
+
testing?: {
|
|
329
|
+
overrides?: TestingOverrides
|
|
330
|
+
}
|
|
194
331
|
}) =>
|
|
195
332
|
Effect.gen(function* () {
|
|
196
333
|
const nodeWorker = new WT.Worker(workerUrl, {
|
|
@@ -206,9 +343,8 @@ const makeLeaderThread = ({
|
|
|
206
343
|
new WorkerSchema.LeaderWorkerInner.InitialMessage({
|
|
207
344
|
storeId,
|
|
208
345
|
clientId,
|
|
209
|
-
|
|
210
|
-
devtools
|
|
211
|
-
schemaPath,
|
|
346
|
+
storage,
|
|
347
|
+
devtools,
|
|
212
348
|
syncPayload,
|
|
213
349
|
}),
|
|
214
350
|
}).pipe(
|
|
@@ -296,8 +432,10 @@ const makeLeaderThread = ({
|
|
|
296
432
|
|
|
297
433
|
const leaderThread = {
|
|
298
434
|
events: {
|
|
299
|
-
pull:
|
|
300
|
-
|
|
435
|
+
pull:
|
|
436
|
+
testing?.overrides?.clientSession?.leaderThreadProxy?.events?.pull ??
|
|
437
|
+
(({ cursor }) =>
|
|
438
|
+
runInWorkerStream(new WorkerSchema.LeaderWorkerInner.PullStream({ cursor })).pipe(Stream.orDie)),
|
|
301
439
|
push: (batch) =>
|
|
302
440
|
runInWorker(new WorkerSchema.LeaderWorkerInner.PushToLeader({ batch })).pipe(
|
|
303
441
|
Effect.withSpan('@livestore/adapter-node:client-session:pushToLeader', {
|
|
@@ -25,6 +25,7 @@ import { makeViteMiddleware } from './vite-dev-server.js'
|
|
|
25
25
|
*/
|
|
26
26
|
export const startDevtoolsServer = ({
|
|
27
27
|
schemaPath,
|
|
28
|
+
schemaAlias,
|
|
28
29
|
storeId,
|
|
29
30
|
clientId,
|
|
30
31
|
sessionId,
|
|
@@ -32,6 +33,7 @@ export const startDevtoolsServer = ({
|
|
|
32
33
|
host,
|
|
33
34
|
}: {
|
|
34
35
|
schemaPath: string
|
|
36
|
+
schemaAlias: string
|
|
35
37
|
storeId: string
|
|
36
38
|
clientId: string
|
|
37
39
|
sessionId: string
|
|
@@ -106,11 +108,14 @@ export const startDevtoolsServer = ({
|
|
|
106
108
|
}).pipe(Effect.interruptible)
|
|
107
109
|
|
|
108
110
|
yield* Effect.logDebug(
|
|
109
|
-
`[@livestore/adapter-node:devtools] LiveStore devtools are available at http://${host}:${port}/_livestore/node/${storeId}/${clientId}/${sessionId}`,
|
|
111
|
+
`[@livestore/adapter-node:devtools] LiveStore devtools are available at http://${host}:${port}/_livestore/node/${storeId}/${clientId}/${sessionId}/${schemaAlias}`,
|
|
110
112
|
)
|
|
111
113
|
|
|
112
114
|
return HttpServer.serve(handler, HttpMiddleware.logger)
|
|
113
115
|
}).pipe(
|
|
116
|
+
Effect.withSpan('@livestore/adapter-node:startDevtoolsServer', {
|
|
117
|
+
attributes: { storeId, clientId, sessionId, port, host, schemaPath },
|
|
118
|
+
}),
|
|
114
119
|
Layer.unwrapScoped,
|
|
115
120
|
// HttpServer.withLogAddress,
|
|
116
121
|
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,168 @@
|
|
|
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
|
+
schemaAlias: devtools.schemaAlias,
|
|
145
|
+
storeId,
|
|
146
|
+
clientId,
|
|
147
|
+
sessionId: 'static', // TODO make this dynamic
|
|
148
|
+
port: devtools.port,
|
|
149
|
+
host: devtools.host,
|
|
150
|
+
}).pipe(Effect.tapCauseLogPretty, Effect.forkScoped)
|
|
151
|
+
|
|
152
|
+
const devtoolsWebChannel = yield* makeNodeDevtoolsChannel({
|
|
153
|
+
nodeName: `leader-${storeId}-${clientId}`,
|
|
154
|
+
target: `devtools-${storeId}-${clientId}-static`,
|
|
155
|
+
url: `ws://localhost:${devtools.port}`,
|
|
156
|
+
schema: { listen: Devtools.Leader.MessageToApp, send: Devtools.Leader.MessageFromApp },
|
|
157
|
+
})
|
|
158
|
+
|
|
159
|
+
return {
|
|
160
|
+
devtoolsWebChannel,
|
|
161
|
+
persistenceInfo: {
|
|
162
|
+
state: dbState.metadata.persistenceInfo,
|
|
163
|
+
eventlog: dbEventlog.metadata.persistenceInfo,
|
|
164
|
+
},
|
|
165
|
+
}
|
|
166
|
+
}).pipe(Effect.provide(FetchHttpClient.layer)),
|
|
167
|
+
}
|
|
168
|
+
})
|