@livestore/adapter-node 0.0.0-snapshot-5a6440f111a5a7c2441a649866587d5d51e22820.3 → 0.0.0-snapshot-412a36a7e6c9b0e9e237b553fd0522aed285228f

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.
Files changed (48) hide show
  1. package/dist/.tsbuildinfo +1 -1
  2. package/dist/client-session/adapter.d.ts +44 -0
  3. package/dist/client-session/adapter.d.ts.map +1 -0
  4. package/dist/client-session/adapter.js +199 -0
  5. package/dist/client-session/adapter.js.map +1 -0
  6. package/dist/client-session/in-memory-adapter.d.ts +145 -10
  7. package/dist/client-session/in-memory-adapter.d.ts.map +1 -1
  8. package/dist/client-session/in-memory-adapter.js +2 -3
  9. package/dist/client-session/in-memory-adapter.js.map +1 -1
  10. package/dist/client-session/persisted-adapter.d.ts +26 -14
  11. package/dist/client-session/persisted-adapter.d.ts.map +1 -1
  12. package/dist/client-session/persisted-adapter.js +81 -24
  13. package/dist/client-session/persisted-adapter.js.map +1 -1
  14. package/dist/devtools/devtools-server.d.ts.map +1 -1
  15. package/dist/devtools/devtools-server.js +3 -1
  16. package/dist/devtools/devtools-server.js.map +1 -1
  17. package/dist/index.d.ts +1 -2
  18. package/dist/index.d.ts.map +1 -1
  19. package/dist/index.js +1 -2
  20. package/dist/index.js.map +1 -1
  21. package/dist/leader-shared.d.ts +29 -0
  22. package/dist/leader-shared.d.ts.map +1 -0
  23. package/dist/leader-shared.js +87 -0
  24. package/dist/leader-shared.js.map +1 -0
  25. package/dist/leader-thread-lazy.js +4 -0
  26. package/dist/leader-thread-lazy.js.map +1 -1
  27. package/dist/leader-thread-shared.d.ts +29 -0
  28. package/dist/leader-thread-shared.d.ts.map +1 -0
  29. package/dist/leader-thread-shared.js +88 -0
  30. package/dist/leader-thread-shared.js.map +1 -0
  31. package/dist/make-leader-worker.d.ts +5 -1
  32. package/dist/make-leader-worker.d.ts.map +1 -1
  33. package/dist/make-leader-worker.js +14 -81
  34. package/dist/make-leader-worker.js.map +1 -1
  35. package/dist/shutdown-channel.d.ts +1 -1
  36. package/dist/worker-schema.d.ts +35 -19
  37. package/dist/worker-schema.d.ts.map +1 -1
  38. package/dist/worker-schema.js +14 -19
  39. package/dist/worker-schema.js.map +1 -1
  40. package/package.json +6 -10
  41. package/src/client-session/{persisted-adapter.ts → adapter.ts} +190 -57
  42. package/src/devtools/devtools-server.ts +3 -0
  43. package/src/index.ts +1 -2
  44. package/src/leader-thread-lazy.ts +5 -0
  45. package/src/leader-thread-shared.ts +167 -0
  46. package/src/make-leader-worker.ts +20 -138
  47. package/src/worker-schema.ts +23 -26
  48. package/src/client-session/in-memory-adapter.ts +0 -169
@@ -1,7 +1,6 @@
1
1
  import './thread-polyfill.js'
2
2
 
3
3
  import inspector from 'node:inspector'
4
- import path from 'node:path'
5
4
 
6
5
  if (process.execArgv.includes('--inspect')) {
7
6
  inspector.open()
@@ -9,15 +8,12 @@ if (process.execArgv.includes('--inspect')) {
9
8
  }
10
9
 
11
10
  import type { SyncOptions } from '@livestore/common'
12
- import { Devtools, liveStoreStorageFormatVersion, UnexpectedError } from '@livestore/common'
13
- import type { DevtoolsOptions, LeaderSqliteDb } from '@livestore/common/leader-thread'
14
- import { configureConnection, Eventlog, LeaderThreadCtx, makeLeaderThreadLayer } from '@livestore/common/leader-thread'
11
+ import { UnexpectedError } from '@livestore/common'
12
+ import { Eventlog, LeaderThreadCtx } from '@livestore/common/leader-thread'
15
13
  import type { LiveStoreSchema } from '@livestore/common/schema'
16
14
  import { LiveStoreEvent } from '@livestore/common/schema'
17
- import { makeNodeDevtoolsChannel } from '@livestore/devtools-node-common/web-channel'
18
15
  import { loadSqlite3Wasm } from '@livestore/sqlite-wasm/load-wasm'
19
16
  import { sqliteDbFactory } from '@livestore/sqlite-wasm/node'
20
- import type { FileSystem, HttpClient, Scope } from '@livestore/utils/effect'
21
17
  import {
22
18
  Effect,
23
19
  FetchHttpClient,
@@ -33,17 +29,19 @@ import {
33
29
  import { PlatformNode } from '@livestore/utils/node'
34
30
  import type * as otel from '@opentelemetry/api'
35
31
 
36
- import { startDevtoolsServer } from './devtools/devtools-server.js'
37
- import { makeShutdownChannel } from './shutdown-channel.js'
32
+ import type { TestingOverrides } from './leader-thread-shared.js'
33
+ import { makeLeaderThread } from './leader-thread-shared.js'
38
34
  import * as WorkerSchema from './worker-schema.js'
39
35
 
40
36
  export type WorkerOptions = {
37
+ schema: LiveStoreSchema
41
38
  sync?: SyncOptions
42
39
  otelOptions?: {
43
40
  tracer?: otel.Tracer
44
41
  /** @default 'livestore-node-leader-thread' */
45
42
  serviceName?: string
46
43
  }
44
+ testing?: TestingOverrides
47
45
  }
48
46
 
49
47
  export const getWorkerArgs = () => Schema.decodeSync(WorkerSchema.WorkerArgv)(process.argv[2]!)
@@ -60,7 +58,20 @@ export const makeWorkerEffect = (options: WorkerOptions) => {
60
58
  : undefined
61
59
 
62
60
  return WorkerRunner.layerSerialized(WorkerSchema.LeaderWorkerInner.Request, {
63
- InitialMessage: (args) => makeLeaderThread({ ...args, syncOptions: options.sync }),
61
+ InitialMessage: (args) =>
62
+ Effect.gen(function* () {
63
+ const sqlite3 = yield* Effect.promise(() => loadSqlite3Wasm()).pipe(
64
+ Effect.withSpan('@livestore/adapter-node:leader-thread:loadSqlite3Wasm'),
65
+ )
66
+ const makeSqliteDb = yield* sqliteDbFactory({ sqlite3 })
67
+ return yield* makeLeaderThread({
68
+ ...args,
69
+ syncOptions: options.sync,
70
+ schema: options.schema,
71
+ testing: options.testing,
72
+ makeSqliteDb,
73
+ })
74
+ }).pipe(Layer.unwrapScoped),
64
75
  PushToLeader: ({ batch }) =>
65
76
  Effect.andThen(LeaderThreadCtx, (_) =>
66
77
  _.syncProcessor.push(
@@ -149,132 +160,3 @@ export const makeWorkerEffect = (options: WorkerOptions) => {
149
160
  Logger.withMinimumLogLevel(LogLevel.Debug),
150
161
  )
151
162
  }
152
-
153
- const makeLeaderThread = ({
154
- storeId,
155
- clientId,
156
- syncOptions,
157
- baseDirectory,
158
- devtools,
159
- schemaPath,
160
- syncPayload,
161
- }: WorkerSchema.LeaderWorkerInner.InitialMessage & {
162
- syncOptions: SyncOptions | undefined
163
- schemaPath: string
164
- }): Layer.Layer<LeaderThreadCtx, UnexpectedError, Scope.Scope | HttpClient.HttpClient | FileSystem.FileSystem> =>
165
- Effect.gen(function* () {
166
- const schema = yield* Effect.promise(() => import(schemaPath).then((m) => m.schema as LiveStoreSchema))
167
-
168
- const sqlite3 = yield* Effect.promise(() => loadSqlite3Wasm()).pipe(
169
- Effect.withSpan('@livestore/adapter-node:leader-thread:loadSqlite3Wasm'),
170
- )
171
- const makeSqliteDb = yield* sqliteDbFactory({ sqlite3 })
172
- const runtime = yield* Effect.runtime<never>()
173
-
174
- const schemaHashSuffix =
175
- schema.state.sqlite.migrations.strategy === 'manual' ? 'fixed' : schema.state.sqlite.hash.toString()
176
-
177
- const makeDb = (kind: 'state' | 'eventlog') =>
178
- makeSqliteDb({
179
- _tag: 'fs',
180
- directory: path.join(baseDirectory ?? '', storeId),
181
- fileName:
182
- kind === 'state' ? getStateDbFileName(schemaHashSuffix) : `eventlog@${liveStoreStorageFormatVersion}.db`,
183
- // TODO enable WAL for nodejs
184
- configureDb: (db) =>
185
- configureConnection(db, { foreignKeys: true }).pipe(Effect.provide(runtime), Effect.runSync),
186
- }).pipe(Effect.acquireRelease((db) => Effect.sync(() => db.close())))
187
-
188
- // Might involve some async work, so we're running them concurrently
189
- const [dbState, dbEventlog] = yield* Effect.all([makeDb('state'), makeDb('eventlog')], { concurrency: 2 })
190
-
191
- const devtoolsOptions = yield* makeDevtoolsOptions({
192
- devtoolsEnabled: devtools.enabled,
193
- devtoolsPort: devtools.port,
194
- devtoolsHost: devtools.host,
195
- dbState,
196
- dbEventlog,
197
- storeId,
198
- clientId,
199
- schemaPath,
200
- })
201
-
202
- const shutdownChannel = yield* makeShutdownChannel(storeId)
203
-
204
- return makeLeaderThreadLayer({
205
- schema,
206
- storeId,
207
- clientId,
208
- makeSqliteDb,
209
- syncOptions,
210
- dbState,
211
- dbEventlog,
212
- devtoolsOptions,
213
- shutdownChannel,
214
- syncPayload,
215
- })
216
- }).pipe(
217
- Effect.tapCauseLogPretty,
218
- UnexpectedError.mapToUnexpectedError,
219
- Effect.withSpan('@livestore/adapter-node:worker:InitialMessage'),
220
- Layer.unwrapScoped,
221
- )
222
-
223
- const getStateDbFileName = (suffix: string) => `state${suffix}@${liveStoreStorageFormatVersion}.db`
224
-
225
- const makeDevtoolsOptions = ({
226
- devtoolsEnabled,
227
- dbState,
228
- dbEventlog,
229
- storeId,
230
- clientId,
231
- devtoolsPort,
232
- devtoolsHost,
233
- schemaPath,
234
- }: {
235
- devtoolsEnabled: boolean
236
- dbState: LeaderSqliteDb
237
- dbEventlog: LeaderSqliteDb
238
- storeId: string
239
- clientId: string
240
- devtoolsPort: number
241
- devtoolsHost: string
242
- schemaPath: string
243
- }): Effect.Effect<DevtoolsOptions, UnexpectedError, Scope.Scope> =>
244
- Effect.gen(function* () {
245
- if (devtoolsEnabled === false) {
246
- return {
247
- enabled: false,
248
- }
249
- }
250
-
251
- return {
252
- enabled: true,
253
- makeBootContext: Effect.gen(function* () {
254
- // 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
255
- yield* startDevtoolsServer({
256
- schemaPath,
257
- storeId,
258
- clientId,
259
- sessionId: 'static', // TODO make this dynamic
260
- port: devtoolsPort,
261
- host: devtoolsHost,
262
- }).pipe(Effect.tapCauseLogPretty, Effect.forkScoped)
263
-
264
- const devtoolsWebChannel = yield* makeNodeDevtoolsChannel({
265
- nodeName: `leader-${storeId}-${clientId}`,
266
- target: `devtools-${storeId}-${clientId}-static`,
267
- url: `ws://localhost:${devtoolsPort}`,
268
- schema: { listen: Devtools.Leader.MessageToApp, send: Devtools.Leader.MessageFromApp },
269
- })
270
-
271
- return {
272
- devtoolsWebChannel,
273
- persistenceInfo: {
274
- state: dbState.metadata.persistenceInfo,
275
- eventlog: dbEventlog.metadata.persistenceInfo,
276
- },
277
- }
278
- }).pipe(Effect.provide(FetchHttpClient.layer)),
279
- }
280
- })
@@ -18,31 +18,25 @@ export const WorkerArgv = Schema.parseJson(
18
18
  }),
19
19
  )
20
20
 
21
- export const StorageTypeOpfs = Schema.Struct({
22
- type: Schema.Literal('opfs'),
21
+ export const StorageTypeInMemory = Schema.Struct({
22
+ type: Schema.Literal('in-memory'),
23
+ })
24
+
25
+ export type StorageTypeInMemory = typeof StorageTypeInMemory.Type
26
+
27
+ export const StorageTypeFs = Schema.Struct({
28
+ type: Schema.Literal('fs'),
23
29
  /**
24
- * Default is `livestore-${storeId}`
30
+ * Where to store the database files
25
31
  *
26
- * When providing this option, make sure to include the `storeId` in the path to avoid
27
- * conflicts with other LiveStore apps.
32
+ * @default Current working directory
28
33
  */
29
- directory: Schema.optional(Schema.String),
34
+ baseDirectory: Schema.String,
30
35
  })
31
36
 
32
- export type StorageTypeOpfs = typeof StorageTypeOpfs.Type
37
+ export type StorageTypeFs = typeof StorageTypeFs.Type
33
38
 
34
- // export const StorageTypeIndexeddb = Schema.Struct({
35
- // type: Schema.Literal('indexeddb'),
36
- // /** @default "livestore" */
37
- // databaseName: Schema.optionalWith(Schema.String, { default: () => 'livestore' }),
38
- // /** @default "livestore-" */
39
- // storeNamePrefix: Schema.optionalWith(Schema.String, { default: () => 'livestore-' }),
40
- // })
41
-
42
- export const StorageType = Schema.Union(
43
- StorageTypeOpfs,
44
- // StorageTypeIndexeddb
45
- )
39
+ export const StorageType = Schema.Union(StorageTypeInMemory, StorageTypeFs)
46
40
  export type StorageType = typeof StorageType.Type
47
41
  export type StorageTypeEncoded = typeof StorageType.Encoded
48
42
 
@@ -71,14 +65,17 @@ export namespace LeaderWorkerInner {
71
65
  payload: {
72
66
  storeId: Schema.String,
73
67
  clientId: Schema.String,
74
- baseDirectory: Schema.optional(Schema.String),
75
- schemaPath: Schema.String,
68
+ storage: StorageType,
76
69
  syncPayload: Schema.UndefinedOr(Schema.JsonValue),
77
- devtools: Schema.Struct({
78
- port: Schema.Number,
79
- host: Schema.String,
80
- enabled: Schema.Boolean,
81
- }),
70
+ devtools: Schema.Union(
71
+ Schema.Struct({
72
+ enabled: Schema.Literal(true),
73
+ schemaPath: Schema.String,
74
+ port: Schema.Number,
75
+ host: Schema.String,
76
+ }),
77
+ Schema.Struct({ enabled: Schema.Literal(false) }),
78
+ ),
82
79
  },
83
80
  success: Schema.Void,
84
81
  failure: UnexpectedError,
@@ -1,169 +0,0 @@
1
- import type {
2
- Adapter,
3
- ClientSession,
4
- ClientSessionLeaderThreadProxy,
5
- LockStatus,
6
- MakeSqliteDb,
7
- SqliteDb,
8
- SyncOptions,
9
- } from '@livestore/common'
10
- import { UnexpectedError } from '@livestore/common'
11
- import { Eventlog, LeaderThreadCtx, makeLeaderThreadLayer } from '@livestore/common/leader-thread'
12
- import type { LiveStoreSchema } from '@livestore/common/schema'
13
- import { LiveStoreEvent } from '@livestore/common/schema'
14
- import { sqliteDbFactory } from '@livestore/sqlite-wasm/browser'
15
- import { loadSqlite3Wasm } from '@livestore/sqlite-wasm/load-wasm'
16
- import type { Schema } from '@livestore/utils/effect'
17
- import { Cause, Effect, FetchHttpClient, Layer, Stream, SubscriptionRef } from '@livestore/utils/effect'
18
- import { nanoid } from '@livestore/utils/nanoid'
19
-
20
- import { makeShutdownChannel } from '../shutdown-channel.js'
21
-
22
- // TODO unify in-memory adapter with other in-memory adapter implementations
23
-
24
- export interface InMemoryAdapterOptions {
25
- sync?: SyncOptions
26
- /**
27
- * @default 'in-memory'
28
- */
29
- clientId?: string
30
- /**
31
- * @default nanoid(6)
32
- */
33
- sessionId?: string
34
-
35
- /** Only used internally for testing */
36
- testing?: {
37
- overrides?: TestingOverrides
38
- }
39
- }
40
-
41
- export type TestingOverrides = {
42
- clientSession?: {
43
- leaderThreadProxy?: Partial<ClientSessionLeaderThreadProxy>
44
- }
45
- makeLeaderThread?: {
46
- dbEventlog?: (makeSqliteDb: MakeSqliteDb) => Effect.Effect<SqliteDb, UnexpectedError>
47
- }
48
- }
49
-
50
- /** NOTE: This adapter is currently only used for testing */
51
- export const makeInMemoryAdapter =
52
- ({ sync: syncOptions, clientId = 'in-memory', sessionId = nanoid(6), testing }: InMemoryAdapterOptions): Adapter =>
53
- ({
54
- schema,
55
- storeId,
56
- shutdown,
57
- syncPayload,
58
- // devtoolsEnabled, bootStatusQueue, shutdown, connectDevtoolsToStore
59
- }) =>
60
- Effect.gen(function* () {
61
- const sqlite3 = yield* Effect.promise(() => loadSqlite3Wasm())
62
-
63
- const makeSqliteDb = sqliteDbFactory({ sqlite3 })
64
- const sqliteDb = yield* makeSqliteDb({ _tag: 'in-memory' })
65
-
66
- const lockStatus = yield* SubscriptionRef.make<LockStatus>('has-lock')
67
-
68
- const shutdownChannel = yield* makeShutdownChannel(storeId)
69
-
70
- yield* shutdownChannel.listen.pipe(
71
- Stream.flatten(),
72
- Stream.tap((error) => Effect.sync(() => shutdown(Cause.fail(error)))),
73
- Stream.runDrain,
74
- Effect.interruptible,
75
- Effect.tapCauseLogPretty,
76
- Effect.forkScoped,
77
- )
78
-
79
- const { leaderThread, initialSnapshot } = yield* makeLeaderThread({
80
- storeId,
81
- clientId,
82
- schema,
83
- makeSqliteDb,
84
- syncOptions,
85
- syncPayload,
86
- testing,
87
- })
88
-
89
- sqliteDb.import(initialSnapshot)
90
-
91
- const clientSession = {
92
- sqliteDb,
93
- devtools: { enabled: false },
94
- clientId,
95
- sessionId,
96
- lockStatus,
97
- leaderThread,
98
- shutdown,
99
- } satisfies ClientSession
100
-
101
- return clientSession
102
- }).pipe(UnexpectedError.mapToUnexpectedError)
103
-
104
- const makeLeaderThread = ({
105
- storeId,
106
- clientId,
107
- schema,
108
- makeSqliteDb,
109
- syncOptions,
110
- syncPayload,
111
- testing,
112
- }: {
113
- storeId: string
114
- clientId: string
115
- schema: LiveStoreSchema
116
- makeSqliteDb: MakeSqliteDb
117
- syncOptions: SyncOptions | undefined
118
- syncPayload: Schema.JsonValue | undefined
119
- testing?: {
120
- overrides?: TestingOverrides
121
- }
122
- }) =>
123
- Effect.gen(function* () {
124
- const layer = yield* Layer.memoize(
125
- makeLeaderThreadLayer({
126
- clientId,
127
- dbState: yield* makeSqliteDb({ _tag: 'in-memory' }),
128
- dbEventlog: testing?.overrides?.makeLeaderThread?.dbEventlog
129
- ? yield* testing.overrides.makeLeaderThread.dbEventlog(makeSqliteDb)
130
- : yield* makeSqliteDb({ _tag: 'in-memory' }),
131
- devtoolsOptions: { enabled: false },
132
- makeSqliteDb,
133
- schema,
134
- // NOTE we're creating a separate channel here since you can't listen to your own channel messages
135
- shutdownChannel: yield* makeShutdownChannel(storeId),
136
- storeId,
137
- syncOptions,
138
- syncPayload,
139
- }).pipe(Layer.provideMerge(FetchHttpClient.layer)),
140
- )
141
-
142
- return yield* Effect.gen(function* () {
143
- const { dbState, dbEventlog, syncProcessor, extraIncomingMessagesQueue, initialState } = yield* LeaderThreadCtx
144
-
145
- const initialLeaderHead = Eventlog.getClientHeadFromDb(dbEventlog)
146
-
147
- const leaderThread = {
148
- events: {
149
- pull:
150
- testing?.overrides?.clientSession?.leaderThreadProxy?.events?.pull ??
151
- (({ cursor }) => syncProcessor.pull({ cursor })),
152
- push: (batch) =>
153
- syncProcessor.push(
154
- batch.map((item) => new LiveStoreEvent.EncodedWithMeta(item)),
155
- { waitForProcessing: true },
156
- ),
157
- },
158
- initialState: { leaderHead: initialLeaderHead, migrationsReport: initialState.migrationsReport },
159
- export: Effect.sync(() => dbState.export()),
160
- getEventlogData: Effect.sync(() => dbEventlog.export()),
161
- getSyncState: syncProcessor.syncState,
162
- sendDevtoolsMessage: (message) => extraIncomingMessagesQueue.offer(message),
163
- } satisfies ClientSessionLeaderThreadProxy
164
-
165
- const initialSnapshot = dbState.export()
166
-
167
- return { leaderThread, initialSnapshot }
168
- }).pipe(Effect.provide(layer))
169
- })