@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.
- package/dist/.tsbuildinfo +1 -1
- package/dist/channel/message-channel-internal.d.ts +3 -3
- package/dist/channel/message-channel-internal.d.ts.map +1 -1
- package/dist/channel/message-channel-internal.js +8 -8
- package/dist/channel/message-channel-internal.js.map +1 -1
- package/dist/channel/message-channel.d.ts +5 -5
- package/dist/channel/message-channel.d.ts.map +1 -1
- package/dist/channel/message-channel.js +22 -22
- package/dist/channel/message-channel.js.map +1 -1
- package/dist/channel/proxy-channel.d.ts +2 -2
- package/dist/channel/proxy-channel.d.ts.map +1 -1
- package/dist/channel/proxy-channel.js +18 -14
- package/dist/channel/proxy-channel.js.map +1 -1
- package/dist/common.d.ts +15 -12
- package/dist/common.d.ts.map +1 -1
- package/dist/common.js +5 -3
- package/dist/common.js.map +1 -1
- package/dist/mesh-schema.d.ts +33 -10
- package/dist/mesh-schema.d.ts.map +1 -1
- package/dist/mesh-schema.js +19 -7
- package/dist/mesh-schema.js.map +1 -1
- package/dist/mod.d.ts +2 -2
- package/dist/mod.d.ts.map +1 -1
- package/dist/mod.js +2 -2
- package/dist/mod.js.map +1 -1
- package/dist/node.d.ts +26 -19
- package/dist/node.d.ts.map +1 -1
- package/dist/node.js +147 -83
- package/dist/node.js.map +1 -1
- package/dist/node.test.js +42 -25
- package/dist/node.test.js.map +1 -1
- package/dist/{websocket-connection.d.ts → websocket-edge.d.ts} +12 -12
- package/dist/websocket-edge.d.ts.map +1 -0
- package/dist/{websocket-connection.js → websocket-edge.js} +17 -16
- package/dist/websocket-edge.js.map +1 -0
- package/dist/websocket-server.js +6 -6
- package/dist/websocket-server.js.map +1 -1
- package/package.json +3 -3
- package/src/channel/message-channel-internal.ts +10 -10
- package/src/channel/message-channel.ts +25 -25
- package/src/channel/proxy-channel.ts +20 -16
- package/src/common.ts +8 -11
- package/src/mesh-schema.ts +23 -9
- package/src/mod.ts +2 -2
- package/src/node.test.ts +60 -25
- package/src/node.ts +206 -113
- package/src/{websocket-connection.ts → websocket-edge.ts} +20 -15
- package/src/websocket-server.ts +6 -6
- package/dist/websocket-connection.d.ts.map +0 -1
- 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 {
|
|
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
|
|
25
|
+
type EdgeChannel = WebChannel.WebChannel<typeof WebmeshSchema.Packet.Type, typeof WebmeshSchema.Packet.Type>
|
|
24
26
|
|
|
25
|
-
export interface MeshNode {
|
|
26
|
-
nodeName:
|
|
27
|
+
export interface MeshNode<TName extends MeshNodeName = MeshNodeName> {
|
|
28
|
+
nodeName: TName
|
|
27
29
|
|
|
28
|
-
|
|
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
|
|
43
|
+
* Manually adds a edge to get connected to the network of nodes with an existing WebChannel.
|
|
42
44
|
*
|
|
43
|
-
* Assumptions about the WebChannel
|
|
44
|
-
* - 1:1
|
|
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
|
-
|
|
51
|
+
addEdge: {
|
|
50
52
|
(options: {
|
|
51
53
|
target: MeshNodeName
|
|
52
|
-
|
|
54
|
+
edgeChannel: EdgeChannel
|
|
53
55
|
replaceIfExists: true
|
|
54
56
|
}): Effect.Effect<void, never, Scope.Scope>
|
|
55
57
|
(options: {
|
|
56
58
|
target: MeshNodeName
|
|
57
|
-
|
|
59
|
+
edgeChannel: EdgeChannel
|
|
58
60
|
replaceIfExists?: boolean
|
|
59
|
-
}): Effect.Effect<void,
|
|
61
|
+
}): Effect.Effect<void, EdgeAlreadyExistsError, Scope.Scope>
|
|
60
62
|
}
|
|
61
63
|
|
|
62
|
-
|
|
64
|
+
removeEdge: (targetNodeName: MeshNodeName) => Effect.Effect<void, Cause.NoSuchElementException>
|
|
63
65
|
|
|
64
66
|
/**
|
|
65
|
-
* Tries to broker a MessageChannel
|
|
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
|
|
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
|
|
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 =
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
-
(
|
|
144
|
-
// Either if direct
|
|
145
|
-
|
|
146
|
-
// ... or if no forward-
|
|
147
|
-
![...
|
|
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.
|
|
169
|
-
yield* Effect.spanEvent('
|
|
170
|
-
yield* PubSub.publish(
|
|
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
|
|
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(
|
|
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.
|
|
217
|
+
if (Schema.is(WebmeshSchema.NetworkTopologyRequest)(packet)) {
|
|
181
218
|
if (packet.source !== nodeName) {
|
|
182
|
-
const
|
|
219
|
+
const backEdgeName =
|
|
183
220
|
packet.hops.at(-1) ?? shouldNeverHappen(`${nodeName}: Expected hops for packet`, packet)
|
|
184
|
-
const
|
|
221
|
+
const backEdgeChannel = edgeChannels.get(backEdgeName)!.channel
|
|
185
222
|
|
|
186
|
-
// Respond with own
|
|
187
|
-
const response = WebmeshSchema.
|
|
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
|
-
|
|
230
|
+
edges: Array.from(edgeChannels.keys()),
|
|
194
231
|
})
|
|
195
232
|
|
|
196
|
-
yield*
|
|
233
|
+
yield* backEdgeChannel.send(response)
|
|
197
234
|
}
|
|
198
235
|
|
|
199
|
-
// Forward the packet to all
|
|
200
|
-
const
|
|
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(
|
|
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.
|
|
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.
|
|
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
|
|
224
|
-
|
|
260
|
+
const edgeChannel =
|
|
261
|
+
edgeChannels.get(routeBack)?.channel ??
|
|
225
262
|
shouldNeverHappen(
|
|
226
|
-
`${nodeName}: Expected
|
|
263
|
+
`${nodeName}: Expected edge channel (${routeBack}) for packet`,
|
|
227
264
|
packet,
|
|
228
|
-
'Available
|
|
229
|
-
Array.from(
|
|
265
|
+
'Available edges:',
|
|
266
|
+
Array.from(edgeChannels.keys()),
|
|
230
267
|
)
|
|
231
268
|
|
|
232
|
-
yield*
|
|
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
|
|
238
|
-
if (
|
|
239
|
-
const
|
|
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*
|
|
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
|
|
285
|
+
const edgeChannel = edgeChannels.get(hopTarget)?.channel
|
|
249
286
|
|
|
250
|
-
if (
|
|
287
|
+
if (edgeChannel === undefined) {
|
|
251
288
|
yield* Effect.logWarning(
|
|
252
|
-
`${nodeName}: Expected to find hop target ${hopTarget} in
|
|
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*
|
|
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
|
|
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
|
|
269
|
-
const
|
|
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 &&
|
|
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(
|
|
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
|
|
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 (
|
|
330
|
+
if (edgeChannels.has(targetNodeName)) {
|
|
298
331
|
if (replaceIfExists) {
|
|
299
|
-
yield*
|
|
332
|
+
yield* removeEdge(targetNodeName).pipe(Effect.orDie)
|
|
300
333
|
// console.log('interrupting', targetNodeName)
|
|
301
|
-
// yield* Fiber.interrupt(
|
|
334
|
+
// yield* Fiber.interrupt(edgeChannels.get(targetNodeName)!.listenFiber)
|
|
302
335
|
} else {
|
|
303
|
-
return yield* new
|
|
336
|
+
return yield* new EdgeAlreadyExistsError({ target: targetNodeName })
|
|
304
337
|
}
|
|
305
338
|
}
|
|
306
339
|
|
|
307
|
-
// TODO use a priority queue instead to prioritize network-changes/
|
|
308
|
-
const listenFiber = yield*
|
|
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 '
|
|
321
|
-
case '
|
|
322
|
-
case '
|
|
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
|
-
|
|
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 =
|
|
391
|
+
const noTransferableResponse = checkTransferableEdges(packet)
|
|
359
392
|
if (noTransferableResponse !== undefined) {
|
|
360
|
-
yield* Effect.spanEvent(
|
|
361
|
-
|
|
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
|
-
|
|
414
|
+
edgeChannels.set(targetNodeName, { channel: edgeChannel, listenFiber })
|
|
384
415
|
|
|
385
|
-
const
|
|
416
|
+
const edgeAddedPacket = WebmeshSchema.NetworkEdgeAdded.make({
|
|
386
417
|
source: nodeName,
|
|
387
418
|
target: targetNodeName,
|
|
388
419
|
})
|
|
389
|
-
yield* sendPacket(
|
|
420
|
+
yield* sendPacket(edgeAddedPacket).pipe(Effect.orDie)
|
|
390
421
|
}).pipe(
|
|
391
|
-
Effect.withSpan(`
|
|
392
|
-
attributes: { 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
|
|
427
|
+
const removeEdge: MeshNode['removeEdge'] = (targetNodeName) =>
|
|
397
428
|
Effect.gen(function* () {
|
|
398
|
-
if (!
|
|
399
|
-
yield* new Cause.NoSuchElementException(`No
|
|
429
|
+
if (!edgeChannels.has(targetNodeName)) {
|
|
430
|
+
yield* new Cause.NoSuchElementException(`No edge found for ${targetNodeName}`)
|
|
400
431
|
}
|
|
401
432
|
|
|
402
|
-
yield* Fiber.interrupt(
|
|
433
|
+
yield* Fiber.interrupt(edgeChannels.get(targetNodeName)!.listenFiber)
|
|
403
434
|
|
|
404
|
-
|
|
435
|
+
edgeChannels.delete(targetNodeName)
|
|
405
436
|
})
|
|
406
437
|
|
|
407
|
-
// TODO add heartbeat to detect dead
|
|
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,
|
|
483
|
+
const { webChannel, initialEdgeDeferred } = yield* makeMessageChannel({
|
|
453
484
|
nodeName,
|
|
454
485
|
incomingPacketsQueue,
|
|
455
|
-
|
|
486
|
+
newEdgeAvailablePubSub,
|
|
456
487
|
target,
|
|
457
488
|
channelName,
|
|
458
489
|
schema,
|
|
459
490
|
sendPacket,
|
|
460
|
-
|
|
491
|
+
checkTransferableEdges,
|
|
461
492
|
})
|
|
462
493
|
|
|
463
494
|
channelMap.set(channelKey, { queue, debugInfo: { channel: webChannel, target } })
|
|
464
495
|
|
|
465
|
-
yield*
|
|
496
|
+
yield* initialEdgeDeferred
|
|
466
497
|
|
|
467
498
|
return webChannel
|
|
468
499
|
} else {
|
|
469
500
|
const channel = yield* makeProxyChannel({
|
|
470
501
|
nodeName,
|
|
471
|
-
|
|
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
|
|
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('
|
|
500
|
-
for (const [key, value] of
|
|
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
|
|
612
|
+
WebChannel.DebugPingMessage.make({ message: `ping from ${nodeName} via edge ${via}`, payload })
|
|
528
613
|
|
|
529
|
-
for (const [channelName, con] of
|
|
530
|
-
yield* Effect.logDebug(`sending ping via
|
|
531
|
-
yield* con.channel.send(msg(`
|
|
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.
|
|
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(
|
|
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 {
|
|
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}`))
|