@livestore/webmesh 0.0.0-snapshot-ee8e0fc3b894cf3159269c9c8969a8fc4b398dca → 0.0.0-snapshot-fec375f0f61a7bc75278adc60d1a55f96a9c292a

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 (50) hide show
  1. package/dist/.tsbuildinfo +1 -1
  2. package/dist/channel/message-channel-internal.d.ts +3 -3
  3. package/dist/channel/message-channel-internal.d.ts.map +1 -1
  4. package/dist/channel/message-channel-internal.js +8 -8
  5. package/dist/channel/message-channel-internal.js.map +1 -1
  6. package/dist/channel/message-channel.d.ts +5 -5
  7. package/dist/channel/message-channel.d.ts.map +1 -1
  8. package/dist/channel/message-channel.js +22 -22
  9. package/dist/channel/message-channel.js.map +1 -1
  10. package/dist/channel/proxy-channel.d.ts +2 -2
  11. package/dist/channel/proxy-channel.d.ts.map +1 -1
  12. package/dist/channel/proxy-channel.js +18 -14
  13. package/dist/channel/proxy-channel.js.map +1 -1
  14. package/dist/common.d.ts +15 -12
  15. package/dist/common.d.ts.map +1 -1
  16. package/dist/common.js +5 -3
  17. package/dist/common.js.map +1 -1
  18. package/dist/mesh-schema.d.ts +33 -10
  19. package/dist/mesh-schema.d.ts.map +1 -1
  20. package/dist/mesh-schema.js +19 -7
  21. package/dist/mesh-schema.js.map +1 -1
  22. package/dist/mod.d.ts +2 -2
  23. package/dist/mod.d.ts.map +1 -1
  24. package/dist/mod.js +2 -2
  25. package/dist/mod.js.map +1 -1
  26. package/dist/node.d.ts +26 -19
  27. package/dist/node.d.ts.map +1 -1
  28. package/dist/node.js +147 -83
  29. package/dist/node.js.map +1 -1
  30. package/dist/node.test.js +42 -25
  31. package/dist/node.test.js.map +1 -1
  32. package/dist/{websocket-connection.d.ts → websocket-edge.d.ts} +12 -12
  33. package/dist/websocket-edge.d.ts.map +1 -0
  34. package/dist/{websocket-connection.js → websocket-edge.js} +17 -16
  35. package/dist/websocket-edge.js.map +1 -0
  36. package/dist/websocket-server.js +6 -6
  37. package/dist/websocket-server.js.map +1 -1
  38. package/package.json +3 -3
  39. package/src/channel/message-channel-internal.ts +10 -10
  40. package/src/channel/message-channel.ts +25 -25
  41. package/src/channel/proxy-channel.ts +20 -16
  42. package/src/common.ts +8 -11
  43. package/src/mesh-schema.ts +23 -9
  44. package/src/mod.ts +2 -2
  45. package/src/node.test.ts +60 -25
  46. package/src/node.ts +206 -113
  47. package/src/{websocket-connection.ts → websocket-edge.ts} +20 -15
  48. package/src/websocket-server.ts +6 -6
  49. package/dist/websocket-connection.d.ts.map +0 -1
  50. package/dist/websocket-connection.js.map +0 -1
package/src/node.ts CHANGED
@@ -1,14 +1,16 @@
1
1
  import { indent, LS_DEV, shouldNeverHappen } from '@livestore/utils'
2
- import type { Scope } from '@livestore/utils/effect'
3
2
  import {
4
3
  Cause,
4
+ Deferred,
5
5
  Duration,
6
6
  Effect,
7
+ Exit,
7
8
  Fiber,
8
9
  Option,
9
10
  PubSub,
10
11
  Queue,
11
12
  Schema,
13
+ Scope,
12
14
  Stream,
13
15
  WebChannel,
14
16
  } from '@livestore/utils/effect'
@@ -16,16 +18,16 @@ import {
16
18
  import { makeMessageChannel } from './channel/message-channel.js'
17
19
  import { makeProxyChannel } from './channel/proxy-channel.js'
18
20
  import type { ChannelKey, MeshNodeName, MessageQueueItem, ProxyQueueItem } from './common.js'
19
- import { ConnectionAlreadyExistsError, packetAsOtelAttributes } from './common.js'
21
+ import { EdgeAlreadyExistsError, packetAsOtelAttributes } from './common.js'
20
22
  import * as WebmeshSchema from './mesh-schema.js'
21
23
  import { TimeoutSet } from './utils.js'
22
24
 
23
- type ConnectionChannel = WebChannel.WebChannel<typeof WebmeshSchema.Packet.Type, typeof WebmeshSchema.Packet.Type>
25
+ type EdgeChannel = WebChannel.WebChannel<typeof WebmeshSchema.Packet.Type, typeof WebmeshSchema.Packet.Type>
24
26
 
25
- export interface MeshNode {
26
- nodeName: MeshNodeName
27
+ export interface MeshNode<TName extends MeshNodeName = MeshNodeName> {
28
+ nodeName: TName
27
29
 
28
- connectionKeys: Effect.Effect<Set<MeshNodeName>>
30
+ edgeKeys: Effect.Effect<Set<MeshNodeName>>
29
31
 
30
32
  debug: {
31
33
  print: () => void
@@ -38,33 +40,33 @@ export interface MeshNode {
38
40
  }
39
41
 
40
42
  /**
41
- * Manually adds a connection to get connected to the network of nodes with an existing WebChannel.
43
+ * Manually adds a edge to get connected to the network of nodes with an existing WebChannel.
42
44
  *
43
- * Assumptions about the WebChannel connection:
44
- * - 1:1 connection
45
+ * Assumptions about the WebChannel edge:
46
+ * - 1:1 edge
45
47
  * - Queues messages internally to never drop messages
46
48
  * - Automatically reconnects
47
49
  * - Ideally supports transferables
48
50
  */
49
- addConnection: {
51
+ addEdge: {
50
52
  (options: {
51
53
  target: MeshNodeName
52
- connectionChannel: ConnectionChannel
54
+ edgeChannel: EdgeChannel
53
55
  replaceIfExists: true
54
56
  }): Effect.Effect<void, never, Scope.Scope>
55
57
  (options: {
56
58
  target: MeshNodeName
57
- connectionChannel: ConnectionChannel
59
+ edgeChannel: EdgeChannel
58
60
  replaceIfExists?: boolean
59
- }): Effect.Effect<void, ConnectionAlreadyExistsError, Scope.Scope>
61
+ }): Effect.Effect<void, EdgeAlreadyExistsError, Scope.Scope>
60
62
  }
61
63
 
62
- removeConnection: (targetNodeName: MeshNodeName) => Effect.Effect<void, Cause.NoSuchElementException>
64
+ removeEdge: (targetNodeName: MeshNodeName) => Effect.Effect<void, Cause.NoSuchElementException>
63
65
 
64
66
  /**
65
- * Tries to broker a MessageChannel connection between the nodes, otherwise will proxy messages via hop-nodes
67
+ * Tries to broker a MessageChannel edge between the nodes, otherwise will proxy messages via hop-nodes
66
68
  *
67
- * For a channel to successfully open, both sides need to have a connection and call `makeChannel`.
69
+ * For a channel to successfully open, both sides need to have a edge and call `makeChannel`.
68
70
  *
69
71
  * Example:
70
72
  * ```ts
@@ -93,27 +95,33 @@ export interface MeshNode {
93
95
  */
94
96
  mode: 'messagechannel' | 'proxy'
95
97
  /**
96
- * Amount of time before we consider a channel creation failed and retry when a new connection is available
98
+ * Amount of time before we consider a channel creation failed and retry when a new edge is available
97
99
  *
98
100
  * @default 1 second
99
101
  */
100
102
  timeout?: Duration.DurationInput
101
103
  }) => Effect.Effect<WebChannel.WebChannel<MsgListen, MsgSend>, never, Scope.Scope>
104
+
105
+ /**
106
+ * Creates a WebChannel that is broadcasted to all connected nodes.
107
+ * Messages won't be buffered for nodes that join the network after the broadcast channel has been created.
108
+ */
109
+ makeBroadcastChannel: <Msg>(args: {
110
+ channelName: string
111
+ schema: Schema.Schema<Msg, any>
112
+ }) => Effect.Effect<WebChannel.WebChannel<Msg, Msg>, never, Scope.Scope>
102
113
  }
103
114
 
104
- export const makeMeshNode = (nodeName: MeshNodeName): Effect.Effect<MeshNode, never, Scope.Scope> =>
115
+ export const makeMeshNode = <TName extends MeshNodeName>(
116
+ nodeName: TName,
117
+ ): Effect.Effect<MeshNode<TName>, never, Scope.Scope> =>
105
118
  Effect.gen(function* () {
106
- const connectionChannels = new Map<
107
- MeshNodeName,
108
- { channel: ConnectionChannel; listenFiber: Fiber.RuntimeFiber<void> }
109
- >()
119
+ const edgeChannels = new Map<MeshNodeName, { channel: EdgeChannel; listenFiber: Fiber.RuntimeFiber<void> }>()
110
120
 
111
121
  // To avoid unbounded memory growth, we automatically forget about packet ids after a while
112
122
  const handledPacketIds = new TimeoutSet<string>({ timeout: Duration.minutes(1) })
113
123
 
114
- const newConnectionAvailablePubSub = yield* PubSub.unbounded<MeshNodeName>().pipe(
115
- Effect.acquireRelease(PubSub.shutdown),
116
- )
124
+ const newEdgeAvailablePubSub = yield* PubSub.unbounded<MeshNodeName>().pipe(Effect.acquireRelease(PubSub.shutdown))
117
125
 
118
126
  // const proxyPacketsToProcess = yield* Queue.unbounded<ProxyQueueItem>().pipe(Effect.acquireRelease(Queue.shutdown))
119
127
  // const messagePacketsToProcess = yield* Queue.unbounded<MessageQueueItem>().pipe(
@@ -137,14 +145,17 @@ export const makeMeshNode = (nodeName: MeshNodeName): Effect.Effect<MeshNode, ne
137
145
  type RequestId = string
138
146
  const topologyRequestsMap = new Map<RequestId, Map<MeshNodeName, Set<MeshNodeName>>>()
139
147
 
140
- const checkTransferableConnections = (packet: typeof WebmeshSchema.MessageChannelPacket.Type) => {
148
+ type BroadcastChannelName = string
149
+ const broadcastChannelListenQueueMap = new Map<BroadcastChannelName, Queue.Queue<any>>()
150
+
151
+ const checkTransferableEdges = (packet: typeof WebmeshSchema.MessageChannelPacket.Type) => {
141
152
  if (
142
153
  (packet._tag === 'MessageChannelRequest' &&
143
- (connectionChannels.size === 0 ||
144
- // Either if direct connection does not support transferables ...
145
- connectionChannels.get(packet.target)?.channel.supportsTransferables === false)) ||
146
- // ... or if no forward-connections support transferables
147
- ![...connectionChannels.values()].some((c) => c.channel.supportsTransferables === true)
154
+ (edgeChannels.size === 0 ||
155
+ // Either if direct edge does not support transferables ...
156
+ edgeChannels.get(packet.target)?.channel.supportsTransferables === false)) ||
157
+ // ... or if no forward-edges support transferables
158
+ ![...edgeChannels.values()].some((c) => c.channel.supportsTransferables === true)
148
159
  ) {
149
160
  return WebmeshSchema.MessageChannelResponseNoTransferables.make({
150
161
  reqId: packet.id,
@@ -165,39 +176,65 @@ export const makeMeshNode = (nodeName: MeshNodeName): Effect.Effect<MeshNode, ne
165
176
  Effect.gen(function* () {
166
177
  // yield* Effect.log(`${nodeName}: sendPacket:${packet._tag} [${packet.id}]`)
167
178
 
168
- if (Schema.is(WebmeshSchema.NetworkConnectionAdded)(packet)) {
169
- yield* Effect.spanEvent('NetworkConnectionAdded', { packet, nodeName })
170
- yield* PubSub.publish(newConnectionAvailablePubSub, packet.target)
179
+ if (Schema.is(WebmeshSchema.NetworkEdgeAdded)(packet)) {
180
+ yield* Effect.spanEvent('NetworkEdgeAdded', { packet, nodeName })
181
+ yield* PubSub.publish(newEdgeAvailablePubSub, packet.target)
171
182
 
172
- const connectionsToForwardTo = Array.from(connectionChannels)
183
+ const edgesToForwardTo = Array.from(edgeChannels)
173
184
  .filter(([name]) => name !== packet.source)
174
185
  .map(([_, con]) => con.channel)
175
186
 
176
- yield* Effect.forEach(connectionsToForwardTo, (con) => con.send(packet), { concurrency: 'unbounded' })
187
+ yield* Effect.forEach(edgesToForwardTo, (con) => con.send(packet), { concurrency: 'unbounded' })
188
+ return
189
+ }
190
+
191
+ if (Schema.is(WebmeshSchema.BroadcastChannelPacket)(packet)) {
192
+ const edgesToForwardTo = Array.from(edgeChannels)
193
+ .filter(([name]) => !packet.hops.includes(name))
194
+ .map(([_, con]) => con.channel)
195
+
196
+ const adjustedPacket = {
197
+ ...packet,
198
+ hops: [...packet.hops, nodeName],
199
+ }
200
+
201
+ yield* Effect.forEach(edgesToForwardTo, (con) => con.send(adjustedPacket), { concurrency: 'unbounded' })
202
+
203
+ // Don't emit the packet to the own node listen queue
204
+ if (packet.source === nodeName) {
205
+ return
206
+ }
207
+
208
+ const queue = broadcastChannelListenQueueMap.get(packet.channelName)
209
+ // In case this node is listening to this channel, add the packet to the listen queue
210
+ if (queue !== undefined) {
211
+ yield* Queue.offer(queue, packet)
212
+ }
213
+
177
214
  return
178
215
  }
179
216
 
180
- if (Schema.is(WebmeshSchema.NetworkConnectionTopologyRequest)(packet)) {
217
+ if (Schema.is(WebmeshSchema.NetworkTopologyRequest)(packet)) {
181
218
  if (packet.source !== nodeName) {
182
- const backConnectionName =
219
+ const backEdgeName =
183
220
  packet.hops.at(-1) ?? shouldNeverHappen(`${nodeName}: Expected hops for packet`, packet)
184
- const backConnectionChannel = connectionChannels.get(backConnectionName)!.channel
221
+ const backEdgeChannel = edgeChannels.get(backEdgeName)!.channel
185
222
 
186
- // Respond with own connection info
187
- const response = WebmeshSchema.NetworkConnectionTopologyResponse.make({
223
+ // Respond with own edge info
224
+ const response = WebmeshSchema.NetworkTopologyResponse.make({
188
225
  reqId: packet.id,
189
226
  source: packet.source,
190
227
  target: packet.target,
191
228
  remainingHops: packet.hops.slice(0, -1),
192
229
  nodeName,
193
- connections: Array.from(connectionChannels.keys()),
230
+ edges: Array.from(edgeChannels.keys()),
194
231
  })
195
232
 
196
- yield* backConnectionChannel.send(response)
233
+ yield* backEdgeChannel.send(response)
197
234
  }
198
235
 
199
- // Forward the packet to all connections except the already visited ones
200
- const connectionsToForwardTo = Array.from(connectionChannels)
236
+ // Forward the packet to all edges except the already visited ones
237
+ const edgesToForwardTo = Array.from(edgeChannels)
201
238
  .filter(([name]) => !packet.hops.includes(name))
202
239
  .map(([_, con]) => con.channel)
203
240
 
@@ -206,72 +243,72 @@ export const makeMeshNode = (nodeName: MeshNodeName): Effect.Effect<MeshNode, ne
206
243
  hops: [...packet.hops, nodeName],
207
244
  }
208
245
 
209
- yield* Effect.forEach(connectionsToForwardTo, (con) => con.send(adjustedPacket), { concurrency: 'unbounded' })
246
+ yield* Effect.forEach(edgesToForwardTo, (con) => con.send(adjustedPacket), { concurrency: 'unbounded' })
210
247
 
211
248
  return
212
249
  }
213
250
 
214
- if (Schema.is(WebmeshSchema.NetworkConnectionTopologyResponse)(packet)) {
251
+ if (Schema.is(WebmeshSchema.NetworkTopologyResponse)(packet)) {
215
252
  if (packet.source === nodeName) {
216
253
  const topologyRequestItem = topologyRequestsMap.get(packet.reqId)!
217
- topologyRequestItem.set(packet.nodeName, new Set(packet.connections))
254
+ topologyRequestItem.set(packet.nodeName, new Set(packet.edges))
218
255
  } else {
219
256
  const remainingHops = packet.remainingHops
220
257
  // Forwarding the response to the original sender via the route back
221
258
  const routeBack =
222
259
  remainingHops.at(-1) ?? shouldNeverHappen(`${nodeName}: Expected remaining hops for packet`, packet)
223
- const connectionChannel =
224
- connectionChannels.get(routeBack)?.channel ??
260
+ const edgeChannel =
261
+ edgeChannels.get(routeBack)?.channel ??
225
262
  shouldNeverHappen(
226
- `${nodeName}: Expected connection channel (${routeBack}) for packet`,
263
+ `${nodeName}: Expected edge channel (${routeBack}) for packet`,
227
264
  packet,
228
- 'Available connections:',
229
- Array.from(connectionChannels.keys()),
265
+ 'Available edges:',
266
+ Array.from(edgeChannels.keys()),
230
267
  )
231
268
 
232
- yield* connectionChannel.send({ ...packet, remainingHops: packet.remainingHops.slice(0, -1) })
269
+ yield* edgeChannel.send({ ...packet, remainingHops: packet.remainingHops.slice(0, -1) })
233
270
  }
234
271
  return
235
272
  }
236
273
 
237
- // We have a direct connection to the target node
238
- if (connectionChannels.has(packet.target)) {
239
- const connectionChannel = connectionChannels.get(packet.target)!.channel
274
+ // We have a direct edge to the target node
275
+ if (edgeChannels.has(packet.target)) {
276
+ const edgeChannel = edgeChannels.get(packet.target)!.channel
240
277
  const hops = packet.source === nodeName ? [] : [...packet.hops, nodeName]
241
- yield* connectionChannel.send({ ...packet, hops })
278
+ yield* edgeChannel.send({ ...packet, hops })
242
279
  }
243
280
  // In this case we have an expected route back we should follow
244
281
  // eslint-disable-next-line unicorn/no-negated-condition
245
282
  else if (packet.remainingHops !== undefined) {
246
283
  const hopTarget =
247
284
  packet.remainingHops.at(-1) ?? shouldNeverHappen(`${nodeName}: Expected remaining hops for packet`, packet)
248
- const connectionChannel = connectionChannels.get(hopTarget)?.channel
285
+ const edgeChannel = edgeChannels.get(hopTarget)?.channel
249
286
 
250
- if (connectionChannel === undefined) {
287
+ if (edgeChannel === undefined) {
251
288
  yield* Effect.logWarning(
252
- `${nodeName}: Expected to find hop target ${hopTarget} in connections. Dropping packet.`,
289
+ `${nodeName}: Expected to find hop target ${hopTarget} in edges. Dropping packet.`,
253
290
  packet,
254
291
  )
255
292
  return
256
293
  }
257
294
 
258
- yield* connectionChannel.send({
295
+ yield* edgeChannel.send({
259
296
  ...packet,
260
297
  remainingHops: packet.remainingHops.slice(0, -1),
261
298
  hops: [...packet.hops, nodeName],
262
299
  })
263
300
  }
264
- // No route found, forward to all connections
301
+ // No route found, forward to all edges
265
302
  else {
266
303
  const hops = packet.source === nodeName ? [] : [...packet.hops, nodeName]
267
304
 
268
- // Optimization: filter out connection where packet just came from
269
- const connectionsToForwardTo = Array.from(connectionChannels)
305
+ // Optimization: filter out edge where packet just came from
306
+ const edgesToForwardTo = Array.from(edgeChannels)
270
307
  .filter(([name]) => name !== packet.source)
271
308
  .map(([_, con]) => con.channel)
272
309
 
273
310
  // TODO if hops-depth=0, we should fail right away with no route found
274
- if (hops.length === 0 && connectionsToForwardTo.length === 0 && LS_DEV) {
311
+ if (hops.length === 0 && edgesToForwardTo.length === 0 && LS_DEV) {
275
312
  console.log(nodeName, 'no route found', packet._tag, 'TODO handle better')
276
313
  // TODO return a expected failure
277
314
  }
@@ -279,7 +316,7 @@ export const makeMeshNode = (nodeName: MeshNodeName): Effect.Effect<MeshNode, ne
279
316
  const packetToSend = { ...packet, hops }
280
317
  // console.debug(nodeName, 'sendPacket:forwarding', packetToSend)
281
318
 
282
- yield* Effect.forEach(connectionsToForwardTo, (con) => con.send(packetToSend), { concurrency: 'unbounded' })
319
+ yield* Effect.forEach(edgesToForwardTo, (con) => con.send(packetToSend), { concurrency: 'unbounded' })
283
320
  }
284
321
  }).pipe(
285
322
  Effect.withSpan(`sendPacket:${packet._tag}:${packet.source}→${packet.target}`, {
@@ -288,24 +325,20 @@ export const makeMeshNode = (nodeName: MeshNodeName): Effect.Effect<MeshNode, ne
288
325
  Effect.orDie,
289
326
  )
290
327
 
291
- const addConnection: MeshNode['addConnection'] = ({
292
- target: targetNodeName,
293
- connectionChannel,
294
- replaceIfExists = false,
295
- }) =>
328
+ const addEdge: MeshNode['addEdge'] = ({ target: targetNodeName, edgeChannel, replaceIfExists = false }) =>
296
329
  Effect.gen(function* () {
297
- if (connectionChannels.has(targetNodeName)) {
330
+ if (edgeChannels.has(targetNodeName)) {
298
331
  if (replaceIfExists) {
299
- yield* removeConnection(targetNodeName).pipe(Effect.orDie)
332
+ yield* removeEdge(targetNodeName).pipe(Effect.orDie)
300
333
  // console.log('interrupting', targetNodeName)
301
- // yield* Fiber.interrupt(connectionChannels.get(targetNodeName)!.listenFiber)
334
+ // yield* Fiber.interrupt(edgeChannels.get(targetNodeName)!.listenFiber)
302
335
  } else {
303
- return yield* new ConnectionAlreadyExistsError({ target: targetNodeName })
336
+ return yield* new EdgeAlreadyExistsError({ target: targetNodeName })
304
337
  }
305
338
  }
306
339
 
307
- // TODO use a priority queue instead to prioritize network-changes/connection-requests over payloads
308
- const listenFiber = yield* connectionChannel.listen.pipe(
340
+ // TODO use a priority queue instead to prioritize network-changes/edge-requests over payloads
341
+ const listenFiber = yield* edgeChannel.listen.pipe(
309
342
  Stream.flatten(),
310
343
  Stream.tap((message) =>
311
344
  Effect.gen(function* () {
@@ -317,9 +350,9 @@ export const makeMeshNode = (nodeName: MeshNodeName): Effect.Effect<MeshNode, ne
317
350
  handledPacketIds.add(packet.id)
318
351
 
319
352
  switch (packet._tag) {
320
- case 'NetworkConnectionAdded':
321
- case 'NetworkConnectionTopologyRequest':
322
- case 'NetworkConnectionTopologyResponse': {
353
+ case 'NetworkEdgeAdded':
354
+ case 'NetworkTopologyRequest':
355
+ case 'NetworkTopologyResponse': {
323
356
  yield* sendPacket(packet)
324
357
 
325
358
  break
@@ -338,7 +371,7 @@ export const makeMeshNode = (nodeName: MeshNodeName): Effect.Effect<MeshNode, ne
338
371
  const queue = channelMap.get(channelKey)!.queue
339
372
 
340
373
  const respondToSender = (outgoingPacket: typeof WebmeshSchema.Packet.Type) =>
341
- connectionChannel
374
+ edgeChannel
342
375
  .send(outgoingPacket)
343
376
  .pipe(
344
377
  Effect.withSpan(
@@ -355,12 +388,10 @@ export const makeMeshNode = (nodeName: MeshNodeName): Effect.Effect<MeshNode, ne
355
388
  }
356
389
  } else {
357
390
  if (Schema.is(WebmeshSchema.MessageChannelPacket)(packet)) {
358
- const noTransferableResponse = checkTransferableConnections(packet)
391
+ const noTransferableResponse = checkTransferableEdges(packet)
359
392
  if (noTransferableResponse !== undefined) {
360
- yield* Effect.spanEvent(
361
- `No transferable connections found for ${packet.source}→${packet.target}`,
362
- )
363
- return yield* connectionChannel.send(noTransferableResponse).pipe(
393
+ yield* Effect.spanEvent(`No transferable edges found for ${packet.source}→${packet.target}`)
394
+ return yield* edgeChannel.send(noTransferableResponse).pipe(
364
395
  Effect.withSpan(`sendNoTransferableResponse:${packet.source}→${packet.target}`, {
365
396
  attributes: packetAsOtelAttributes(noTransferableResponse),
366
397
  }),
@@ -380,31 +411,31 @@ export const makeMeshNode = (nodeName: MeshNodeName): Effect.Effect<MeshNode, ne
380
411
  Effect.forkScoped,
381
412
  )
382
413
 
383
- connectionChannels.set(targetNodeName, { channel: connectionChannel, listenFiber })
414
+ edgeChannels.set(targetNodeName, { channel: edgeChannel, listenFiber })
384
415
 
385
- const connectionAddedPacket = WebmeshSchema.NetworkConnectionAdded.make({
416
+ const edgeAddedPacket = WebmeshSchema.NetworkEdgeAdded.make({
386
417
  source: nodeName,
387
418
  target: targetNodeName,
388
419
  })
389
- yield* sendPacket(connectionAddedPacket).pipe(Effect.orDie)
420
+ yield* sendPacket(edgeAddedPacket).pipe(Effect.orDie)
390
421
  }).pipe(
391
- Effect.withSpan(`addConnection:${nodeName}→${targetNodeName}`, {
392
- attributes: { supportsTransferables: connectionChannel.supportsTransferables },
422
+ Effect.withSpan(`addEdge:${nodeName}→${targetNodeName}`, {
423
+ attributes: { supportsTransferables: edgeChannel.supportsTransferables },
393
424
  }),
394
425
  ) as any // any-cast needed for error/never overload
395
426
 
396
- const removeConnection: MeshNode['removeConnection'] = (targetNodeName) =>
427
+ const removeEdge: MeshNode['removeEdge'] = (targetNodeName) =>
397
428
  Effect.gen(function* () {
398
- if (!connectionChannels.has(targetNodeName)) {
399
- yield* new Cause.NoSuchElementException(`No connection found for ${targetNodeName}`)
429
+ if (!edgeChannels.has(targetNodeName)) {
430
+ yield* new Cause.NoSuchElementException(`No edge found for ${targetNodeName}`)
400
431
  }
401
432
 
402
- yield* Fiber.interrupt(connectionChannels.get(targetNodeName)!.listenFiber)
433
+ yield* Fiber.interrupt(edgeChannels.get(targetNodeName)!.listenFiber)
403
434
 
404
- connectionChannels.delete(targetNodeName)
435
+ edgeChannels.delete(targetNodeName)
405
436
  })
406
437
 
407
- // TODO add heartbeat to detect dead connections (for both e2e and proxying)
438
+ // TODO add heartbeat to detect dead edges (for both e2e and proxying)
408
439
  // TODO when a channel is established in the same origin, we can use a weblock to detect disconnects
409
440
  const makeChannel: MeshNode['makeChannel'] = ({
410
441
  target,
@@ -449,26 +480,26 @@ export const makeMeshNode = (nodeName: MeshNodeName): Effect.Effect<MeshNode, ne
449
480
  )
450
481
 
451
482
  // NOTE already retries internally when transferables are required
452
- const { webChannel, initialConnectionDeferred } = yield* makeMessageChannel({
483
+ const { webChannel, initialEdgeDeferred } = yield* makeMessageChannel({
453
484
  nodeName,
454
485
  incomingPacketsQueue,
455
- newConnectionAvailablePubSub,
486
+ newEdgeAvailablePubSub,
456
487
  target,
457
488
  channelName,
458
489
  schema,
459
490
  sendPacket,
460
- checkTransferableConnections,
491
+ checkTransferableEdges,
461
492
  })
462
493
 
463
494
  channelMap.set(channelKey, { queue, debugInfo: { channel: webChannel, target } })
464
495
 
465
- yield* initialConnectionDeferred
496
+ yield* initialEdgeDeferred
466
497
 
467
498
  return webChannel
468
499
  } else {
469
500
  const channel = yield* makeProxyChannel({
470
501
  nodeName,
471
- newConnectionAvailablePubSub,
502
+ newEdgeAvailablePubSub,
472
503
  target,
473
504
  channelName,
474
505
  schema,
@@ -488,7 +519,56 @@ export const makeMeshNode = (nodeName: MeshNodeName): Effect.Effect<MeshNode, ne
488
519
  Effect.annotateLogs({ nodeName }),
489
520
  )
490
521
 
491
- const connectionKeys: MeshNode['connectionKeys'] = Effect.sync(() => new Set(connectionChannels.keys()))
522
+ const makeBroadcastChannel: MeshNode['makeBroadcastChannel'] = ({ channelName, schema }) =>
523
+ Effect.scopeWithCloseable((scope) =>
524
+ Effect.gen(function* () {
525
+ if (broadcastChannelListenQueueMap.has(channelName)) {
526
+ return shouldNeverHappen(
527
+ `Broadcast channel ${channelName} already exists`,
528
+ broadcastChannelListenQueueMap.get(channelName),
529
+ )
530
+ }
531
+
532
+ const debugInfo = {}
533
+
534
+ const queue = yield* Queue.unbounded<any>().pipe(Effect.acquireRelease(Queue.shutdown))
535
+ broadcastChannelListenQueueMap.set(channelName, queue)
536
+
537
+ const send = (message: any) =>
538
+ Effect.gen(function* () {
539
+ const payload = yield* Schema.encode(schema)(message)
540
+ const packet = WebmeshSchema.BroadcastChannelPacket.make({
541
+ channelName,
542
+ payload,
543
+ source: nodeName,
544
+ target: '-',
545
+ hops: [],
546
+ })
547
+
548
+ yield* sendPacket(packet)
549
+ })
550
+
551
+ const listen = Stream.fromQueue(queue).pipe(
552
+ Stream.filter(Schema.is(WebmeshSchema.BroadcastChannelPacket)),
553
+ Stream.map((_) => Schema.decodeEither(schema)(_.payload)),
554
+ )
555
+
556
+ const closedDeferred = yield* Deferred.make<void>().pipe(Effect.acquireRelease(Deferred.done(Exit.void)))
557
+
558
+ return {
559
+ [WebChannel.WebChannelSymbol]: WebChannel.WebChannelSymbol,
560
+ send,
561
+ listen,
562
+ closedDeferred,
563
+ supportsTransferables: true,
564
+ schema: { listen: schema, send: schema },
565
+ shutdown: Scope.close(scope, Exit.void),
566
+ debugInfo,
567
+ } satisfies WebChannel.WebChannel<any, any>
568
+ }),
569
+ )
570
+
571
+ const edgeKeys: MeshNode['edgeKeys'] = Effect.sync(() => new Set(edgeChannels.keys()))
492
572
 
493
573
  const runtime = yield* Effect.runtime()
494
574
 
@@ -496,8 +576,8 @@ export const makeMeshNode = (nodeName: MeshNodeName): Effect.Effect<MeshNode, ne
496
576
  print: () => {
497
577
  console.log('Webmesh debug info for node:', nodeName)
498
578
 
499
- console.log('Connections:', connectionChannels.size)
500
- for (const [key, value] of connectionChannels) {
579
+ console.log('Edges:', edgeChannels.size)
580
+ for (const [key, value] of edgeChannels) {
501
581
  console.log(` ${key}: supportsTransferables=${value.channel.supportsTransferables}`)
502
582
  }
503
583
 
@@ -520,15 +600,20 @@ export const makeMeshNode = (nodeName: MeshNodeName): Effect.Effect<MeshNode, ne
520
600
  value.queue,
521
601
  )
522
602
  }
603
+
604
+ console.log('Broadcast channels:', broadcastChannelListenQueueMap.size)
605
+ for (const [key, _value] of broadcastChannelListenQueueMap) {
606
+ console.log(indent(key, 2))
607
+ }
523
608
  },
524
609
  ping: (payload) => {
525
610
  Effect.gen(function* () {
526
611
  const msg = (via: string) =>
527
- WebChannel.DebugPingMessage.make({ message: `ping from ${nodeName} via connection ${via}`, payload })
612
+ WebChannel.DebugPingMessage.make({ message: `ping from ${nodeName} via edge ${via}`, payload })
528
613
 
529
- for (const [channelName, con] of connectionChannels) {
530
- yield* Effect.logDebug(`sending ping via connection ${channelName}`)
531
- yield* con.channel.send(msg(`connection ${channelName}`) as any)
614
+ for (const [channelName, con] of edgeChannels) {
615
+ yield* Effect.logDebug(`sending ping via edge ${channelName}`)
616
+ yield* con.channel.send(msg(`edge ${channelName}`) as any)
532
617
  }
533
618
 
534
619
  for (const [channelKey, channel] of channelMap) {
@@ -540,14 +625,14 @@ export const makeMeshNode = (nodeName: MeshNodeName): Effect.Effect<MeshNode, ne
540
625
  },
541
626
  requestTopology: (timeoutMs = 1000) =>
542
627
  Effect.gen(function* () {
543
- const packet = WebmeshSchema.NetworkConnectionTopologyRequest.make({
628
+ const packet = WebmeshSchema.NetworkTopologyRequest.make({
544
629
  source: nodeName,
545
630
  target: '-',
546
631
  hops: [],
547
632
  })
548
633
 
549
634
  const item = new Map<MeshNodeName, Set<MeshNodeName>>()
550
- item.set(nodeName, new Set(connectionChannels.keys()))
635
+ item.set(nodeName, new Set(edgeChannels.keys()))
551
636
  topologyRequestsMap.set(packet.id, item)
552
637
 
553
638
  yield* sendPacket(packet)
@@ -561,5 +646,13 @@ export const makeMeshNode = (nodeName: MeshNodeName): Effect.Effect<MeshNode, ne
561
646
  }).pipe(Effect.provide(runtime), Effect.tapCauseLogPretty, Effect.runPromise),
562
647
  }
563
648
 
564
- return { nodeName, addConnection, removeConnection, makeChannel, connectionKeys, debug } satisfies MeshNode
649
+ return {
650
+ nodeName,
651
+ addEdge,
652
+ removeEdge,
653
+ makeChannel,
654
+ makeBroadcastChannel,
655
+ edgeKeys,
656
+ debug,
657
+ } satisfies MeshNode
565
658
  }).pipe(Effect.withSpan(`makeMeshNode:${nodeName}`))