@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.
Files changed (65) hide show
  1. package/dist/.tsbuildinfo +1 -0
  2. package/dist/client-session/index.d.ts +23 -0
  3. package/dist/client-session/index.d.ts.map +1 -0
  4. package/dist/client-session/index.js +140 -0
  5. package/dist/client-session/index.js.map +1 -0
  6. package/dist/devtools/devtools-server.d.ts +16 -0
  7. package/dist/devtools/devtools-server.d.ts.map +1 -0
  8. package/dist/devtools/devtools-server.js +58 -0
  9. package/dist/devtools/devtools-server.js.map +1 -0
  10. package/dist/devtools/mod.d.ts +3 -0
  11. package/dist/devtools/mod.d.ts.map +1 -0
  12. package/dist/devtools/mod.js +2 -0
  13. package/dist/devtools/mod.js.map +1 -0
  14. package/dist/devtools/vite-dev-server.d.ts +7 -0
  15. package/dist/devtools/vite-dev-server.d.ts.map +1 -0
  16. package/dist/devtools/vite-dev-server.js +107 -0
  17. package/dist/devtools/vite-dev-server.js.map +1 -0
  18. package/dist/in-memory/index.d.ts +11 -0
  19. package/dist/in-memory/index.d.ts.map +1 -0
  20. package/dist/in-memory/index.js +71 -0
  21. package/dist/in-memory/index.js.map +1 -0
  22. package/dist/index.d.ts +3 -0
  23. package/dist/index.d.ts.map +1 -0
  24. package/dist/index.js +3 -0
  25. package/dist/index.js.map +1 -0
  26. package/dist/leader-thread-lazy.d.ts +2 -0
  27. package/dist/leader-thread-lazy.d.ts.map +1 -0
  28. package/dist/leader-thread-lazy.js +10 -0
  29. package/dist/leader-thread-lazy.js.map +1 -0
  30. package/dist/make-leader-worker.d.ts +20 -0
  31. package/dist/make-leader-worker.d.ts.map +1 -0
  32. package/dist/make-leader-worker.js +151 -0
  33. package/dist/make-leader-worker.js.map +1 -0
  34. package/dist/shutdown-channel.d.ts +6 -0
  35. package/dist/shutdown-channel.d.ts.map +1 -0
  36. package/dist/shutdown-channel.js +7 -0
  37. package/dist/shutdown-channel.js.map +1 -0
  38. package/dist/thread-polyfill.d.ts +2 -0
  39. package/dist/thread-polyfill.d.ts.map +1 -0
  40. package/dist/thread-polyfill.js +3 -0
  41. package/dist/thread-polyfill.js.map +1 -0
  42. package/dist/webchannel.d.ts +6 -0
  43. package/dist/webchannel.d.ts.map +1 -0
  44. package/dist/webchannel.js +33 -0
  45. package/dist/webchannel.js.map +1 -0
  46. package/dist/worker-schema.d.ts +196 -0
  47. package/dist/worker-schema.d.ts.map +1 -0
  48. package/dist/worker-schema.js +161 -0
  49. package/dist/worker-schema.js.map +1 -0
  50. package/package.json +54 -0
  51. package/rollup.config.mjs +24 -0
  52. package/src/client-session/index.ts +295 -0
  53. package/src/devtools/devtools-server.ts +88 -0
  54. package/src/devtools/mod.ts +2 -0
  55. package/src/devtools/types.d.ts +33 -0
  56. package/src/devtools/vite-dev-server.ts +122 -0
  57. package/src/in-memory/index.ts +133 -0
  58. package/src/index.ts +2 -0
  59. package/src/leader-thread-lazy.ts +9 -0
  60. package/src/make-leader-worker.ts +285 -0
  61. package/src/shutdown-channel.ts +9 -0
  62. package/src/thread-polyfill.ts +1 -0
  63. package/src/webchannel.ts +54 -0
  64. package/src/worker-schema.ts +175 -0
  65. 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,2 @@
1
+ export { makeInMemoryAdapter } from './in-memory/index.js'
2
+ export { makeNodeAdapter } from './client-session/index.js'
@@ -0,0 +1,9 @@
1
+ const run = async () => {
2
+ const start = Date.now()
3
+ // @ts-expect-error todo
4
+ const _module = await import('./leader-thread.bundle.js')
5
+ const end = Date.now()
6
+ console.log(`[@livestore/adapter-node:leader] Loaded in ${end - start}ms`)
7
+ }
8
+
9
+ run()
@@ -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
+ }