@livestore/adapter-node 0.3.0-dev.18 → 0.3.0-dev.21

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.
@@ -3,6 +3,7 @@ import * as WT from 'node:worker_threads'
3
3
 
4
4
  import type {
5
5
  Adapter,
6
+ BootStatus,
6
7
  ClientSession,
7
8
  ClientSessionLeaderThreadProxy,
8
9
  IntentionalShutdownCause,
@@ -10,7 +11,7 @@ import type {
10
11
  NetworkStatus,
11
12
  } from '@livestore/common'
12
13
  import { Devtools, UnexpectedError } from '@livestore/common'
13
- import { makeNodeDevtoolsChannel } from '@livestore/devtools-node-common/web-channel'
14
+ import * as DevtoolsNode from '@livestore/devtools-node-common/web-channel'
14
15
  import { loadSqlite3Wasm } from '@livestore/sqlite-wasm/load-wasm'
15
16
  import { sqliteDbFactory } from '@livestore/sqlite-wasm/node'
16
17
  import {
@@ -18,6 +19,7 @@ import {
18
19
  Effect,
19
20
  Fiber,
20
21
  ParseResult,
22
+ Queue,
21
23
  Schema,
22
24
  Stream,
23
25
  SubscriptionRef,
@@ -40,6 +42,8 @@ export interface NodeAdapterOptions {
40
42
  baseDirectory?: string
41
43
  /** The default is the hostname of the current machine */
42
44
  clientId?: string
45
+ /** @default 'static' */
46
+ sessionId?: string
43
47
  devtools?: {
44
48
  /**
45
49
  * Where to run the devtools server (via Vite)
@@ -47,20 +51,28 @@ export interface NodeAdapterOptions {
47
51
  * @default 4242
48
52
  */
49
53
  port: number
54
+ /**
55
+ * @default 'localhost'
56
+ */
57
+ host: string
50
58
  }
51
59
  }
52
60
 
61
+ /**
62
+ * Warning: This adapter doesn't currently support multiple client sessions for the same client (i.e. same storeId + clientId)
63
+ */
53
64
  export const makeNodeAdapter = ({
54
65
  workerUrl,
55
66
  schemaPath,
56
67
  baseDirectory,
57
- devtools: devtoolsOptions = { port: 4242 },
68
+ devtools: devtoolsOptions = { port: 4242, host: 'localhost' },
58
69
  clientId = hostname(),
70
+ // TODO make this dynamic and actually support multiple sessions
71
+ sessionId = 'static',
59
72
  }: NodeAdapterOptions): Adapter =>
60
- (({ storeId, devtoolsEnabled, shutdown, connectDevtoolsToStore }) =>
73
+ (({ storeId, devtoolsEnabled, shutdown, connectDevtoolsToStore, bootStatusQueue }) =>
61
74
  Effect.gen(function* () {
62
- // TODO make this dynamic and actually support multiple sessions
63
- const sessionId = 'static'
75
+ yield* Queue.offer(bootStatusQueue, { stage: 'loading' })
64
76
 
65
77
  const sqlite3 = yield* Effect.promise(() => loadSqlite3Wasm())
66
78
  const makeSqliteDb = yield* sqliteDbFactory({ sqlite3 })
@@ -99,20 +111,34 @@ export const makeNodeAdapter = ({
99
111
  devtoolsEnabled,
100
112
  devtoolsOptions,
101
113
  schemaPath,
114
+ bootStatusQueue,
102
115
  })
103
116
 
104
117
  syncInMemoryDb.import(initialSnapshot)
105
118
 
106
119
  if (devtoolsEnabled) {
107
120
  yield* Effect.gen(function* () {
108
- const storeDevtoolsChannel = yield* makeNodeDevtoolsChannel({
121
+ const webmeshNode = yield* DevtoolsNode.makeNodeDevtoolsConnectedMeshNode({
122
+ url: `ws://${devtoolsOptions.host}:${devtoolsOptions.port}`,
109
123
  nodeName: `client-session-${storeId}-${clientId}-${sessionId}`,
110
- target: `devtools`,
111
- url: `ws://localhost:${devtoolsOptions.port}`,
112
- schema: {
113
- listen: Devtools.ClientSession.MessageToApp,
114
- send: Devtools.ClientSession.MessageFromApp,
115
- },
124
+ })
125
+
126
+ const sessionsChannel = yield* webmeshNode.makeBroadcastChannel({
127
+ channelName: 'session-info',
128
+ schema: Devtools.SessionInfo.Message,
129
+ })
130
+
131
+ yield* Devtools.SessionInfo.provideSessionInfo({
132
+ webChannel: sessionsChannel,
133
+ sessionInfo: Devtools.SessionInfo.SessionInfo.make({ storeId, clientId, sessionId }),
134
+ }).pipe(Effect.tapCauseLogPretty, Effect.forkScoped)
135
+
136
+ webmeshNode.debug.print()
137
+
138
+ const storeDevtoolsChannel = yield* DevtoolsNode.makeChannelForConnectedMeshNode({
139
+ node: webmeshNode,
140
+ target: `devtools-${storeId}-${clientId}-${sessionId}`,
141
+ schema: { listen: Devtools.ClientSession.MessageToApp, send: Devtools.ClientSession.MessageFromApp },
116
142
  })
117
143
 
118
144
  yield* connectDevtoolsToStore(storeDevtoolsChannel)
@@ -150,6 +176,7 @@ const makeLeaderThread = ({
150
176
  devtoolsEnabled,
151
177
  devtoolsOptions,
152
178
  schemaPath,
179
+ // bootStatusQueue,
153
180
  }: {
154
181
  shutdown: (cause: Cause.Cause<UnexpectedError | IntentionalShutdownCause>) => void
155
182
  storeId: string
@@ -158,8 +185,9 @@ const makeLeaderThread = ({
158
185
  workerUrl: URL
159
186
  baseDirectory: string | undefined
160
187
  devtoolsEnabled: boolean
161
- devtoolsOptions: { port: number }
188
+ devtoolsOptions: { port: number; host: string }
162
189
  schemaPath: string
190
+ bootStatusQueue: Queue.Queue<BootStatus>
163
191
  }) =>
164
192
  Effect.gen(function* () {
165
193
  const nodeWorker = new WT.Worker(workerUrl, {
@@ -175,7 +203,7 @@ const makeLeaderThread = ({
175
203
  storeId,
176
204
  clientId,
177
205
  baseDirectory,
178
- devtools: { enabled: devtoolsEnabled, port: devtoolsOptions.port },
206
+ devtools: { enabled: devtoolsEnabled, port: devtoolsOptions.port, host: devtoolsOptions.host },
179
207
  schemaPath,
180
208
  }),
181
209
  }).pipe(
@@ -242,6 +270,39 @@ const makeLeaderThread = ({
242
270
  )
243
271
  }).pipe(Stream.unwrap) as any
244
272
 
273
+ const _bootStatusFiber = yield* runInWorkerStream(new WorkerSchema.LeaderWorkerInner.BootStatusStream()).pipe(
274
+ // TODO bring back when fixed https://github.com/Effect-TS/effect/issues/4576
275
+ // Stream.tap((bootStatus) => Queue.offer(bootStatusQueue, bootStatus)),
276
+ // TODO remove when fixed https://github.com/Effect-TS/effect/issues/4576
277
+ // Stream.tap(
278
+ // Effect.fn(function* (_) {
279
+ // if (yield* Queue.isShutdown(bootStatusQueue)) {
280
+ // return
281
+ // } else {
282
+ // console.log('offering boot status', _)
283
+ // // yield* Queue.offer(bootStatusQueue, _).pipe(
284
+ // // Effect.onInterrupt(() => Effect.log('boot status stream interrupted')),
285
+ // // // Effect.tapErrorCause((cause) => Effect.logError('error offering boot status', cause)),
286
+ // // )
287
+ // }
288
+ // }),
289
+ // ),
290
+ Stream.runDrain,
291
+ Effect.tapErrorCause((cause) =>
292
+ Cause.isInterruptedOnly(cause) ? Effect.void : Effect.sync(() => shutdown(cause)),
293
+ ),
294
+ Effect.interruptible,
295
+ Effect.tapCauseLogPretty,
296
+ Effect.forkScoped,
297
+ )
298
+
299
+ // TODO bring back when fixed https://github.com/Effect-TS/effect/issues/4576
300
+ // yield* Queue.awaitShutdown(bootStatusQueue).pipe(
301
+ // Effect.andThen(Fiber.interrupt(bootStatusFiber)),
302
+ // Effect.tapCauseLogPretty,
303
+ // Effect.forkScoped,
304
+ // )
305
+
245
306
  const initialLeaderHead = yield* runInWorker(new WorkerSchema.LeaderWorkerInner.GetLeaderHead())
246
307
 
247
308
  const networkStatus = yield* SubscriptionRef.make<NetworkStatus>({
@@ -20,11 +20,13 @@ export const startDevtoolsServer = ({
20
20
  clientId,
21
21
  sessionId,
22
22
  port,
23
+ host,
23
24
  }: {
24
25
  schemaPath: string
25
26
  storeId: string
26
27
  clientId: string
27
28
  sessionId: string
29
+ host: string
28
30
  port: number
29
31
  }): Effect.Effect<void, UnexpectedError, Scope.Scope> =>
30
32
  Effect.gen(function* () {
@@ -59,7 +61,7 @@ export const startDevtoolsServer = ({
59
61
  cb(UnexpectedError.make({ cause: err }))
60
62
  })
61
63
 
62
- httpServer.listen(port, '0.0.0.0', () => {
64
+ httpServer.listen(port, host, () => {
63
65
  cb(Effect.succeed(undefined))
64
66
  })
65
67
  })
@@ -67,11 +69,12 @@ export const startDevtoolsServer = ({
67
69
  yield* startServer(port)
68
70
 
69
71
  yield* Effect.logDebug(
70
- `[@livestore/adapter-node:devtools] LiveStore devtools are available at http://localhost:${port}/_livestore`,
72
+ `[@livestore/adapter-node:devtools] LiveStore devtools are available at http://${host}:${port}/_livestore/node/${storeId}/${clientId}/${sessionId}`,
71
73
  )
72
74
 
75
+ const clientSessionInfo = { storeId, clientId, sessionId }
73
76
  const viteServer = yield* makeViteServer({
74
- mode: { _tag: 'node', storeId, clientId, sessionId, url: `ws://localhost:${port}` },
77
+ mode: { _tag: 'node', clientSessionInfo, url: `ws://localhost:${port}` },
75
78
  schemaPath: path.resolve(process.cwd(), schemaPath),
76
79
  viteConfig: (viteConfig) => {
77
80
  if (LS_DEV) {
@@ -91,7 +94,7 @@ export const startDevtoolsServer = ({
91
94
 
92
95
  httpServer.on('request', (req, res) => {
93
96
  if (req.url === '/' || req.url === '') {
94
- res.writeHead(302, { Location: '/_livestore' })
97
+ res.writeHead(302, { Location: '/_livestore/node' })
95
98
  res.end()
96
99
  } else if (req.url?.startsWith('/_livestore')) {
97
100
  return viteServer.middlewares(req, res as any)
@@ -25,11 +25,15 @@ export interface InMemoryAdapterOptions {
25
25
  * @default 'in-memory'
26
26
  */
27
27
  clientId?: string
28
+ /**
29
+ * @default nanoid(6)
30
+ */
31
+ sessionId?: string
28
32
  }
29
33
 
30
34
  /** NOTE: This adapter is currently only used for testing */
31
35
  export const makeInMemoryAdapter =
32
- ({ sync: syncOptions, clientId = 'in-memory' }: InMemoryAdapterOptions): Adapter =>
36
+ ({ sync: syncOptions, clientId = 'in-memory', sessionId = nanoid(6) }: InMemoryAdapterOptions): Adapter =>
33
37
  ({
34
38
  schema,
35
39
  storeId,
@@ -42,9 +46,7 @@ export const makeInMemoryAdapter =
42
46
  const makeSqliteDb = sqliteDbFactory({ sqlite3 })
43
47
  const sqliteDb = yield* makeSqliteDb({ _tag: 'in-memory' })
44
48
 
45
- const lockStatus = SubscriptionRef.make<LockStatus>('has-lock').pipe(Effect.runSync)
46
-
47
- const sessionId = nanoid(6)
49
+ const lockStatus = yield* SubscriptionRef.make<LockStatus>('has-lock')
48
50
 
49
51
  const shutdownChannel = yield* makeShutdownChannel(storeId)
50
52
 
@@ -207,6 +207,7 @@ const makeLeaderThread = ({
207
207
  const devtoolsOptions = yield* makeDevtoolsOptions({
208
208
  devtoolsEnabled: devtools.enabled,
209
209
  devtoolsPort: devtools.port,
210
+ devtoolsHost: devtools.host,
210
211
  dbReadModel,
211
212
  dbMutationLog,
212
213
  storeId,
@@ -243,6 +244,7 @@ const makeDevtoolsOptions = ({
243
244
  storeId,
244
245
  clientId,
245
246
  devtoolsPort,
247
+ devtoolsHost,
246
248
  schemaPath,
247
249
  }: {
248
250
  devtoolsEnabled: boolean
@@ -251,6 +253,7 @@ const makeDevtoolsOptions = ({
251
253
  storeId: string
252
254
  clientId: string
253
255
  devtoolsPort: number
256
+ devtoolsHost: string
254
257
  schemaPath: string
255
258
  }): Effect.Effect<DevtoolsOptions, UnexpectedError, Scope.Scope> =>
256
259
  Effect.gen(function* () {
@@ -270,18 +273,18 @@ const makeDevtoolsOptions = ({
270
273
  clientId,
271
274
  sessionId: 'static', // TODO make this dynamic
272
275
  port: devtoolsPort,
276
+ host: devtoolsHost,
277
+ })
278
+
279
+ const devtoolsWebChannel = yield* makeNodeDevtoolsChannel({
280
+ nodeName: `leader-${storeId}-${clientId}`,
281
+ target: `devtools-${storeId}-${clientId}-static`,
282
+ url: `ws://localhost:${devtoolsPort}`,
283
+ schema: { listen: Devtools.Leader.MessageToApp, send: Devtools.Leader.MessageFromApp },
273
284
  })
274
285
 
275
286
  return {
276
- devtoolsWebChannel: yield* makeNodeDevtoolsChannel({
277
- nodeName: `leader-${storeId}-${clientId}`,
278
- target: `devtools`,
279
- url: `ws://localhost:${devtoolsPort}`,
280
- schema: {
281
- listen: Devtools.Leader.MessageToApp,
282
- send: Devtools.Leader.MessageFromApp,
283
- },
284
- }),
287
+ devtoolsWebChannel,
285
288
  persistenceInfo: {
286
289
  readModel: dbReadModel.metadata.persistenceInfo,
287
290
  mutationLog: dbMutationLog.metadata.persistenceInfo,
@@ -67,6 +67,7 @@ export namespace LeaderWorkerInner {
67
67
  schemaPath: Schema.String,
68
68
  devtools: Schema.Struct({
69
69
  port: Schema.Number,
70
+ host: Schema.String,
70
71
  enabled: Schema.Boolean,
71
72
  }),
72
73
  },
@@ -0,0 +1,30 @@
1
+ import { Effect, Queue, Stream } from '@livestore/utils/effect'
2
+ import { PlatformNode } from '@livestore/utils/node'
3
+
4
+ const main = Effect.gen(function* () {
5
+ const queue = yield* Queue.unbounded<number>()
6
+
7
+ yield* Queue.shutdown(queue)
8
+
9
+ // yield* Effect.gen(function* () {
10
+ // yield* Queue.offer(queue, 1)
11
+ // yield* Queue.shutdown(queue)
12
+ // }).pipe(Effect.delay(200), Effect.forkScoped)
13
+
14
+ yield* Effect.addFinalizer((exit) => Effect.log('finalizer', exit))
15
+
16
+ // const exit = yield* Stream.fromQueue(queue).pipe(
17
+ // Stream.tap((n) => Effect.log(n)),
18
+ // Stream.runDrain,
19
+ // Effect.exit,
20
+ // )
21
+
22
+ const exit = yield* Effect.andThen(Effect.void, () => Stream.fromQueue(queue)).pipe(
23
+ Stream.unwrap,
24
+ Stream.tap((n) => Effect.log(n)),
25
+ Stream.runDrain,
26
+ Effect.exit,
27
+ )
28
+
29
+ console.log('exit', exit)
30
+ }).pipe(Effect.scoped, PlatformNode.NodeRuntime.runMain)