@livestore/webmesh 0.0.0-snapshot-1d99fea7d2ce2c7a5d9ed0a3752f8a7bda6bc3db → 0.0.0-snapshot-aed277ba0960f72b8d464508961ab4aec1881230

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 (43) hide show
  1. package/README.md +19 -1
  2. package/dist/.tsbuildinfo +1 -1
  3. package/dist/channel/message-channel-internal.d.ts +26 -0
  4. package/dist/channel/message-channel-internal.d.ts.map +1 -0
  5. package/dist/channel/message-channel-internal.js +202 -0
  6. package/dist/channel/message-channel-internal.js.map +1 -0
  7. package/dist/channel/message-channel.d.ts +21 -19
  8. package/dist/channel/message-channel.d.ts.map +1 -1
  9. package/dist/channel/message-channel.js +125 -162
  10. package/dist/channel/message-channel.js.map +1 -1
  11. package/dist/channel/proxy-channel.d.ts +2 -2
  12. package/dist/channel/proxy-channel.d.ts.map +1 -1
  13. package/dist/channel/proxy-channel.js +7 -5
  14. package/dist/channel/proxy-channel.js.map +1 -1
  15. package/dist/common.d.ts +8 -4
  16. package/dist/common.d.ts.map +1 -1
  17. package/dist/common.js +2 -1
  18. package/dist/common.js.map +1 -1
  19. package/dist/mesh-schema.d.ts +23 -1
  20. package/dist/mesh-schema.d.ts.map +1 -1
  21. package/dist/mesh-schema.js +21 -2
  22. package/dist/mesh-schema.js.map +1 -1
  23. package/dist/node.d.ts +12 -1
  24. package/dist/node.d.ts.map +1 -1
  25. package/dist/node.js +39 -9
  26. package/dist/node.js.map +1 -1
  27. package/dist/node.test.d.ts +1 -1
  28. package/dist/node.test.d.ts.map +1 -1
  29. package/dist/node.test.js +256 -124
  30. package/dist/node.test.js.map +1 -1
  31. package/dist/websocket-connection.d.ts +1 -2
  32. package/dist/websocket-connection.d.ts.map +1 -1
  33. package/dist/websocket-connection.js +5 -4
  34. package/dist/websocket-connection.js.map +1 -1
  35. package/package.json +3 -3
  36. package/src/channel/message-channel-internal.ts +337 -0
  37. package/src/channel/message-channel.ts +177 -308
  38. package/src/channel/proxy-channel.ts +238 -230
  39. package/src/common.ts +3 -1
  40. package/src/mesh-schema.ts +20 -2
  41. package/src/node.test.ts +367 -150
  42. package/src/node.ts +68 -12
  43. package/src/websocket-connection.ts +83 -79
package/src/node.ts CHANGED
@@ -1,4 +1,4 @@
1
- import { LS_DEV, shouldNeverHappen } from '@livestore/utils'
1
+ import { indent, LS_DEV, shouldNeverHappen } from '@livestore/utils'
2
2
  import type { Scope } from '@livestore/utils/effect'
3
3
  import {
4
4
  Cause,
@@ -29,6 +29,8 @@ export interface MeshNode {
29
29
 
30
30
  debug: {
31
31
  print: () => void
32
+ /** Sends a ping message to all connected nodes and channels */
33
+ ping: (payload?: string) => void
32
34
  }
33
35
 
34
36
  /**
@@ -58,7 +60,16 @@ export interface MeshNode {
58
60
  /**
59
61
  * Tries to broker a MessageChannel connection between the nodes, otherwise will proxy messages via hop-nodes
60
62
  *
61
- * For a channel to successfully open, both sides need to have a connection and call `makeChannel`
63
+ * For a channel to successfully open, both sides need to have a connection and call `makeChannel`.
64
+ *
65
+ * Example:
66
+ * ```ts
67
+ * // Code on node A
68
+ * const channel = nodeA.makeChannel({ target: 'B', channelName: 'my-channel', schema: ... })
69
+ *
70
+ * // Code on node B
71
+ * const channel = nodeB.makeChannel({ target: 'A', channelName: 'my-channel', schema: ... })
72
+ * ```
62
73
  */
63
74
  makeChannel: <MsgListen, MsgSend>(args: {
64
75
  target: MeshNodeName
@@ -330,7 +341,12 @@ export const makeMeshNode = (nodeName: MeshNodeName): Effect.Effect<MeshNode, ne
330
341
  const schema = WebChannel.mapSchema(inputSchema)
331
342
  const channelKey = `${target}-${channelName}` satisfies ChannelKey
332
343
 
333
- if (!channelMap.has(channelKey)) {
344
+ if (channelMap.has(channelKey)) {
345
+ const existingChannel = channelMap.get(channelKey)!.debugInfo?.channel
346
+ if (existingChannel) {
347
+ shouldNeverHappen(`Channel ${channelKey} already exists`, existingChannel)
348
+ }
349
+ } else {
334
350
  const queue = yield* Queue.unbounded<MessageQueueItem | ProxyQueueItem>().pipe(
335
351
  Effect.acquireRelease(Queue.shutdown),
336
352
  )
@@ -342,12 +358,23 @@ export const makeMeshNode = (nodeName: MeshNodeName): Effect.Effect<MeshNode, ne
342
358
  yield* Effect.addFinalizer(() => Effect.sync(() => channelMap.delete(channelKey)))
343
359
 
344
360
  if (mode === 'messagechannel') {
345
- // console.debug(nodeName, 'message mode', modeRef.current)
361
+ const incomingPacketsQueue = yield* Queue.unbounded<any>()
362
+
363
+ // We're we're draining the queue into another new queue.
364
+ // It's a bit of a mystery why this is needed, since the unit tests also work without it.
365
+ // But for the LiveStore devtools to actually work, we need to do this.
366
+ // We should figure out some day why this is needed and further simplify if possible.
367
+ yield* Queue.takeBetween(queue, 1, 10).pipe(
368
+ Effect.tap((_) => Queue.offerAll(incomingPacketsQueue, _)),
369
+ Effect.forever,
370
+ Effect.tapCauseLogPretty,
371
+ Effect.forkScoped,
372
+ )
346
373
 
347
374
  // NOTE already retries internally when transferables are required
348
- const channel = yield* makeMessageChannel({
375
+ const { webChannel, initialConnectionDeferred } = yield* makeMessageChannel({
349
376
  nodeName,
350
- queue,
377
+ incomingPacketsQueue,
351
378
  newConnectionAvailablePubSub,
352
379
  target,
353
380
  channelName,
@@ -356,9 +383,11 @@ export const makeMeshNode = (nodeName: MeshNodeName): Effect.Effect<MeshNode, ne
356
383
  checkTransferableConnections,
357
384
  })
358
385
 
359
- channelMap.set(channelKey, { queue, debugInfo: { channel, target } })
386
+ channelMap.set(channelKey, { queue, debugInfo: { channel: webChannel, target } })
360
387
 
361
- return channel
388
+ yield* initialConnectionDeferred
389
+
390
+ return webChannel
362
391
  } else {
363
392
  const channel = yield* makeProxyChannel({
364
393
  nodeName,
@@ -375,6 +404,7 @@ export const makeMeshNode = (nodeName: MeshNodeName): Effect.Effect<MeshNode, ne
375
404
  return channel
376
405
  }
377
406
  }).pipe(
407
+ // Effect.timeout(timeout),
378
408
  Effect.withSpanScoped(`makeChannel:${nodeName}→${target}(${channelName})`, {
379
409
  attributes: { target, channelName, mode, timeout },
380
410
  }),
@@ -393,15 +423,41 @@ export const makeMeshNode = (nodeName: MeshNodeName): Effect.Effect<MeshNode, ne
393
423
  console.log('Channels:', channelMap.size)
394
424
  for (const [key, value] of channelMap) {
395
425
  console.log(
396
- ` ${key}: \n`,
397
- ` Queue: ${value.queue.unsafeSize().pipe(Option.getOrUndefined)}`,
398
- value.queue,
426
+ indent(key, 2),
399
427
  '\n',
400
- ` Channel: target=${value.debugInfo?.target} supportsTransferables=${value.debugInfo?.channel.supportsTransferables}`,
428
+ Object.entries({
429
+ target: value.debugInfo?.target,
430
+ supportsTransferables: value.debugInfo?.channel.supportsTransferables,
431
+ ...value.debugInfo?.channel.debugInfo,
432
+ })
433
+ .map(([key, value]) => indent(`${key}=${value}`, 4))
434
+ .join('\n'),
435
+ ' ',
401
436
  value.debugInfo?.channel,
437
+ '\n',
438
+ indent(`Queue: ${value.queue.unsafeSize().pipe(Option.getOrUndefined)}`, 4),
439
+ value.queue,
402
440
  )
403
441
  }
404
442
  },
443
+ ping: (payload) => {
444
+ Effect.gen(function* () {
445
+ const msg = (via: string) => {
446
+ console.log(`sending message to ${via}`)
447
+ return WebChannel.DebugPingMessage.make({ message: `ping from ${nodeName} via ${via}`, payload })
448
+ }
449
+
450
+ yield* Effect.forEach(connectionChannels, ([channelName, con]) =>
451
+ con.channel.send(msg(`connection ${channelName}`) as any),
452
+ )
453
+
454
+ yield* Effect.forEach(
455
+ channelMap,
456
+ ([channelKey, channel]) =>
457
+ channel.debugInfo?.channel.send(msg(`channel ${channelKey}`) as any) ?? Effect.void,
458
+ )
459
+ }).pipe(Effect.runFork)
460
+ },
405
461
  }
406
462
 
407
463
  return { nodeName, addConnection, removeConnection, makeChannel, connectionKeys, debug } satisfies MeshNode
@@ -1,12 +1,13 @@
1
- import type { Scope } from '@livestore/utils/effect'
2
1
  import {
3
2
  Deferred,
4
3
  Effect,
5
4
  Either,
5
+ Exit,
6
6
  FiberHandle,
7
7
  Queue,
8
8
  Schedule,
9
9
  Schema,
10
+ Scope,
10
11
  Stream,
11
12
  WebChannel,
12
13
  WebSocket,
@@ -77,82 +78,85 @@ export const makeWebSocketConnection = (
77
78
  never,
78
79
  Scope.Scope
79
80
  > =>
80
- Effect.gen(function* () {
81
- socket.binaryType = 'arraybuffer'
82
-
83
- const fromDeferred = yield* Deferred.make<string>()
84
-
85
- yield* Stream.fromEventListener<MessageEvent>(socket as any, 'message').pipe(
86
- Stream.map((msg) => Schema.decodeUnknownEither(MessageMsgPack)(new Uint8Array(msg.data))),
87
- Stream.flatten(),
88
- Stream.tap((msg) =>
81
+ Effect.scopeWithCloseable((scope) =>
82
+ Effect.gen(function* () {
83
+ socket.binaryType = 'arraybuffer'
84
+
85
+ const fromDeferred = yield* Deferred.make<string>()
86
+
87
+ yield* Stream.fromEventListener<MessageEvent>(socket as any, 'message').pipe(
88
+ Stream.map((msg) => Schema.decodeUnknownEither(MessageMsgPack)(new Uint8Array(msg.data))),
89
+ Stream.flatten(),
90
+ Stream.tap((msg) =>
91
+ Effect.gen(function* () {
92
+ if (msg._tag === 'WSConnectionInit') {
93
+ yield* Deferred.succeed(fromDeferred, msg.from)
94
+ } else {
95
+ const decodedPayload = yield* Schema.decode(MeshSchema.Packet)(msg.payload)
96
+ yield* Queue.offer(listenQueue, decodedPayload)
97
+ }
98
+ }),
99
+ ),
100
+ Stream.runDrain,
101
+ Effect.tapCauseLogPretty,
102
+ Effect.forkScoped,
103
+ )
104
+
105
+ const listenQueue = yield* Queue.unbounded<typeof MeshSchema.Packet.Type>().pipe(
106
+ Effect.acquireRelease(Queue.shutdown),
107
+ )
108
+
109
+ const initHandshake = (from: string) =>
110
+ socket.send(Schema.encodeSync(MessageMsgPack)({ _tag: 'WSConnectionInit', from }))
111
+
112
+ if (socketType._tag === 'leaf') {
113
+ initHandshake(socketType.from)
114
+ }
115
+
116
+ const deferredResult = yield* fromDeferred
117
+ const from = socketType._tag === 'leaf' ? socketType.from : deferredResult
118
+
119
+ if (socketType._tag === 'relay') {
120
+ initHandshake(from)
121
+ }
122
+
123
+ const isConnectedLatch = yield* Effect.makeLatch(true)
124
+
125
+ const closedDeferred = yield* Deferred.make<void>().pipe(Effect.acquireRelease(Deferred.done(Exit.void)))
126
+
127
+ yield* Effect.eventListener<any>(
128
+ socket,
129
+ 'close',
130
+ () =>
131
+ Effect.gen(function* () {
132
+ yield* isConnectedLatch.close
133
+ yield* Deferred.succeed(closedDeferred, undefined)
134
+ }),
135
+ { once: true },
136
+ )
137
+
138
+ const send = (message: typeof MeshSchema.Packet.Type) =>
89
139
  Effect.gen(function* () {
90
- if (msg._tag === 'WSConnectionInit') {
91
- yield* Deferred.succeed(fromDeferred, msg.from)
92
- } else {
93
- const decodedPayload = yield* Schema.decode(MeshSchema.Packet)(msg.payload)
94
- yield* Queue.offer(listenQueue, decodedPayload)
95
- }
96
- }),
97
- ),
98
- Stream.runDrain,
99
- Effect.tapCauseLogPretty,
100
- Effect.forkScoped,
101
- )
102
-
103
- const listenQueue = yield* Queue.unbounded<typeof MeshSchema.Packet.Type>().pipe(
104
- Effect.acquireRelease(Queue.shutdown),
105
- )
106
-
107
- const initHandshake = (from: string) =>
108
- socket.send(Schema.encodeSync(MessageMsgPack)({ _tag: 'WSConnectionInit', from }))
109
-
110
- if (socketType._tag === 'leaf') {
111
- initHandshake(socketType.from)
112
- }
113
-
114
- const deferredResult = yield* fromDeferred
115
- const from = socketType._tag === 'leaf' ? socketType.from : deferredResult
116
-
117
- if (socketType._tag === 'relay') {
118
- initHandshake(from)
119
- }
120
-
121
- const isConnectedLatch = yield* Effect.makeLatch(true)
122
-
123
- const closedDeferred = yield* Deferred.make<void>()
124
-
125
- yield* Effect.eventListener<any>(
126
- socket,
127
- 'close',
128
- () =>
129
- Effect.gen(function* () {
130
- yield* isConnectedLatch.close
131
- yield* Deferred.succeed(closedDeferred, undefined)
132
- }),
133
- { once: true },
134
- )
135
-
136
- const send = (message: typeof MeshSchema.Packet.Type) =>
137
- Effect.gen(function* () {
138
- yield* isConnectedLatch.await
139
- const payload = yield* Schema.encode(MeshSchema.Packet)(message)
140
- socket.send(Schema.encodeSync(MessageMsgPack)({ _tag: 'WSConnectionPayload', payload, from }))
141
- })
142
-
143
- const listen = Stream.fromQueue(listenQueue).pipe(Stream.map(Either.right))
144
-
145
- const webChannel = {
146
- [WebChannel.WebChannelSymbol]: WebChannel.WebChannelSymbol,
147
- send,
148
- listen,
149
- closedDeferred,
150
- schema: { listen: MeshSchema.Packet, send: MeshSchema.Packet },
151
- supportsTransferables: false,
152
- } satisfies WebChannel.WebChannel<typeof MeshSchema.Packet.Type, typeof MeshSchema.Packet.Type>
153
-
154
- return {
155
- webChannel: webChannel as WebChannel.WebChannel<typeof MeshSchema.Packet.Type, typeof MeshSchema.Packet.Type>,
156
- from,
157
- }
158
- })
140
+ yield* isConnectedLatch.await
141
+ const payload = yield* Schema.encode(MeshSchema.Packet)(message)
142
+ socket.send(Schema.encodeSync(MessageMsgPack)({ _tag: 'WSConnectionPayload', payload, from }))
143
+ })
144
+
145
+ const listen = Stream.fromQueue(listenQueue).pipe(Stream.map(Either.right))
146
+
147
+ const webChannel = {
148
+ [WebChannel.WebChannelSymbol]: WebChannel.WebChannelSymbol,
149
+ send,
150
+ listen,
151
+ closedDeferred,
152
+ schema: { listen: MeshSchema.Packet, send: MeshSchema.Packet },
153
+ supportsTransferables: false,
154
+ shutdown: Scope.close(scope, Exit.void),
155
+ } satisfies WebChannel.WebChannel<typeof MeshSchema.Packet.Type, typeof MeshSchema.Packet.Type>
156
+
157
+ return {
158
+ webChannel: webChannel as WebChannel.WebChannel<typeof MeshSchema.Packet.Type, typeof MeshSchema.Packet.Type>,
159
+ from,
160
+ }
161
+ }).pipe(Effect.withSpanScoped('makeWebSocketConnection')),
162
+ )