@livestore/webmesh 0.3.0-dev.4 → 0.3.0-dev.41
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/README.md +42 -0
- package/dist/.tsbuildinfo +1 -1
- package/dist/channel/direct-channel-internal.d.ts +26 -0
- package/dist/channel/direct-channel-internal.d.ts.map +1 -0
- package/dist/channel/direct-channel-internal.js +217 -0
- package/dist/channel/direct-channel-internal.js.map +1 -0
- package/dist/channel/direct-channel.d.ts +22 -0
- package/dist/channel/direct-channel.d.ts.map +1 -0
- package/dist/channel/direct-channel.js +153 -0
- package/dist/channel/direct-channel.js.map +1 -0
- package/dist/channel/proxy-channel.d.ts +3 -3
- package/dist/channel/proxy-channel.d.ts.map +1 -1
- package/dist/channel/proxy-channel.js +119 -37
- package/dist/channel/proxy-channel.js.map +1 -1
- package/dist/common.d.ts +47 -19
- package/dist/common.d.ts.map +1 -1
- package/dist/common.js +13 -5
- package/dist/common.js.map +1 -1
- package/dist/mesh-schema.d.ts +79 -13
- package/dist/mesh-schema.d.ts.map +1 -1
- package/dist/mesh-schema.js +59 -10
- 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 +51 -24
- package/dist/node.d.ts.map +1 -1
- package/dist/node.js +322 -111
- package/dist/node.js.map +1 -1
- package/dist/node.test.d.ts +1 -1
- package/dist/node.test.d.ts.map +1 -1
- package/dist/node.test.js +489 -157
- package/dist/node.test.js.map +1 -1
- package/dist/utils.d.ts +4 -4
- package/dist/utils.d.ts.map +1 -1
- package/dist/utils.js +7 -1
- package/dist/utils.js.map +1 -1
- package/dist/websocket-edge.d.ts +53 -0
- package/dist/websocket-edge.d.ts.map +1 -0
- package/dist/websocket-edge.js +89 -0
- package/dist/websocket-edge.js.map +1 -0
- package/package.json +10 -6
- package/src/channel/direct-channel-internal.ts +356 -0
- package/src/channel/direct-channel.ts +234 -0
- package/src/channel/proxy-channel.ts +344 -234
- package/src/common.ts +24 -17
- package/src/mesh-schema.ts +73 -20
- package/src/mod.ts +2 -2
- package/src/node.test.ts +723 -190
- package/src/node.ts +497 -152
- package/src/utils.ts +13 -2
- package/src/websocket-edge.ts +183 -0
- package/dist/channel/message-channel.d.ts +0 -20
- package/dist/channel/message-channel.d.ts.map +0 -1
- package/dist/channel/message-channel.js +0 -183
- package/dist/channel/message-channel.js.map +0 -1
- package/dist/websocket-connection.d.ts +0 -51
- package/dist/websocket-connection.d.ts.map +0 -1
- package/dist/websocket-connection.js +0 -74
- package/dist/websocket-connection.js.map +0 -1
- package/dist/websocket-server.d.ts +0 -7
- package/dist/websocket-server.d.ts.map +0 -1
- package/dist/websocket-server.js +0 -24
- package/dist/websocket-server.js.map +0 -1
- package/src/channel/message-channel.ts +0 -354
- package/src/websocket-connection.ts +0 -158
- package/src/websocket-server.ts +0 -40
- package/tsconfig.json +0 -11
package/src/node.ts
CHANGED
|
@@ -1,64 +1,89 @@
|
|
|
1
|
-
import { LS_DEV, shouldNeverHappen } from '@livestore/utils'
|
|
2
|
-
import type { Scope } from '@livestore/utils/effect'
|
|
1
|
+
import { indent, LS_DEV, shouldNeverHappen } from '@livestore/utils'
|
|
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'
|
|
15
17
|
|
|
16
|
-
import {
|
|
18
|
+
import { makeDirectChannel } from './channel/direct-channel.js'
|
|
17
19
|
import { makeProxyChannel } from './channel/proxy-channel.js'
|
|
18
|
-
import type { ChannelKey, MeshNodeName, MessageQueueItem, ProxyQueueItem } from './common.js'
|
|
19
|
-
import {
|
|
20
|
-
import * as
|
|
20
|
+
import type { ChannelKey, ListenForChannelResult, MeshNodeName, MessageQueueItem, ProxyQueueItem } from './common.js'
|
|
21
|
+
import { EdgeAlreadyExistsError, packetAsOtelAttributes } from './common.js'
|
|
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
|
|
34
|
+
/** Sends a ping message to all connected nodes and channels */
|
|
35
|
+
ping: (payload?: string) => void
|
|
36
|
+
/**
|
|
37
|
+
* Requests the topology of the network from all connected nodes
|
|
38
|
+
*/
|
|
39
|
+
requestTopology: (timeoutMs?: number) => Promise<void>
|
|
32
40
|
}
|
|
33
41
|
|
|
34
42
|
/**
|
|
35
|
-
* Manually adds a
|
|
43
|
+
* Manually adds a edge to get connected to the network of nodes with an existing WebChannel.
|
|
36
44
|
*
|
|
37
|
-
* Assumptions about the WebChannel
|
|
38
|
-
* - 1:1
|
|
45
|
+
* Assumptions about the WebChannel edge:
|
|
46
|
+
* - 1:1 edge
|
|
39
47
|
* - Queues messages internally to never drop messages
|
|
40
48
|
* - Automatically reconnects
|
|
41
49
|
* - Ideally supports transferables
|
|
42
50
|
*/
|
|
43
|
-
|
|
51
|
+
addEdge: {
|
|
44
52
|
(options: {
|
|
45
53
|
target: MeshNodeName
|
|
46
|
-
|
|
54
|
+
edgeChannel: EdgeChannel
|
|
47
55
|
replaceIfExists: true
|
|
48
56
|
}): Effect.Effect<void, never, Scope.Scope>
|
|
49
57
|
(options: {
|
|
50
58
|
target: MeshNodeName
|
|
51
|
-
|
|
59
|
+
edgeChannel: EdgeChannel
|
|
52
60
|
replaceIfExists?: boolean
|
|
53
|
-
}): Effect.Effect<void,
|
|
61
|
+
}): Effect.Effect<void, EdgeAlreadyExistsError, Scope.Scope>
|
|
54
62
|
}
|
|
55
63
|
|
|
56
|
-
|
|
64
|
+
removeEdge: (targetNodeName: MeshNodeName) => Effect.Effect<void, Cause.NoSuchElementException>
|
|
65
|
+
|
|
66
|
+
hasChannel: ({
|
|
67
|
+
target,
|
|
68
|
+
channelName,
|
|
69
|
+
}: {
|
|
70
|
+
target: MeshNodeName
|
|
71
|
+
channelName: string
|
|
72
|
+
}) => Effect.Effect<boolean, never, Scope.Scope>
|
|
57
73
|
|
|
58
74
|
/**
|
|
59
|
-
* Tries to broker a
|
|
75
|
+
* Tries to broker a DirectChannel edge between the nodes, otherwise will proxy messages via hop-nodes
|
|
76
|
+
*
|
|
77
|
+
* For a channel to successfully open, both sides need to have a edge and call `makeChannel`.
|
|
78
|
+
*
|
|
79
|
+
* Example:
|
|
80
|
+
* ```ts
|
|
81
|
+
* // Code on node A
|
|
82
|
+
* const channel = nodeA.makeChannel({ target: 'B', channelName: 'my-channel', schema: ... })
|
|
60
83
|
*
|
|
61
|
-
*
|
|
84
|
+
* // Code on node B
|
|
85
|
+
* const channel = nodeB.makeChannel({ target: 'A', channelName: 'my-channel', schema: ... })
|
|
86
|
+
* ```
|
|
62
87
|
*/
|
|
63
88
|
makeChannel: <MsgListen, MsgSend>(args: {
|
|
64
89
|
target: MeshNodeName
|
|
@@ -74,56 +99,85 @@ export interface MeshNode {
|
|
|
74
99
|
send: Schema.Schema<MsgSend, any>
|
|
75
100
|
}
|
|
76
101
|
/**
|
|
77
|
-
* If possible, prefer using a
|
|
102
|
+
* If possible, prefer using a DirectChannel with transferables (i.e. transferring memory instead of copying it).
|
|
78
103
|
*/
|
|
79
|
-
mode: '
|
|
104
|
+
mode: 'direct' | 'proxy'
|
|
80
105
|
/**
|
|
81
|
-
* Amount of time before we consider a channel creation failed and retry when a new
|
|
106
|
+
* Amount of time before we consider a channel creation failed and retry when a new edge is available
|
|
82
107
|
*
|
|
83
108
|
* @default 1 second
|
|
84
109
|
*/
|
|
85
110
|
timeout?: Duration.DurationInput
|
|
86
111
|
}) => Effect.Effect<WebChannel.WebChannel<MsgListen, MsgSend>, never, Scope.Scope>
|
|
112
|
+
|
|
113
|
+
listenForChannel: Stream.Stream<ListenForChannelResult>
|
|
114
|
+
|
|
115
|
+
/**
|
|
116
|
+
* Creates a WebChannel that is broadcasted to all connected nodes.
|
|
117
|
+
* Messages won't be buffered for nodes that join the network after the broadcast channel has been created.
|
|
118
|
+
*/
|
|
119
|
+
makeBroadcastChannel: <Msg>(args: {
|
|
120
|
+
channelName: string
|
|
121
|
+
schema: Schema.Schema<Msg, any>
|
|
122
|
+
}) => Effect.Effect<WebChannel.WebChannel<Msg, Msg>, never, Scope.Scope>
|
|
87
123
|
}
|
|
88
124
|
|
|
89
|
-
export const makeMeshNode =
|
|
125
|
+
export const makeMeshNode = <TName extends MeshNodeName>(
|
|
126
|
+
nodeName: TName,
|
|
127
|
+
): Effect.Effect<MeshNode<TName>, never, Scope.Scope> =>
|
|
90
128
|
Effect.gen(function* () {
|
|
91
|
-
const
|
|
92
|
-
MeshNodeName,
|
|
93
|
-
{ channel: ConnectionChannel; listenFiber: Fiber.RuntimeFiber<void> }
|
|
94
|
-
>()
|
|
129
|
+
const edgeChannels = new Map<MeshNodeName, { channel: EdgeChannel; listenFiber: Fiber.RuntimeFiber<void> }>()
|
|
95
130
|
|
|
96
131
|
// To avoid unbounded memory growth, we automatically forget about packet ids after a while
|
|
97
|
-
const handledPacketIds =
|
|
132
|
+
const handledPacketIds = yield* TimeoutSet.make(Duration.minutes(1))
|
|
98
133
|
|
|
99
|
-
const
|
|
100
|
-
Effect.acquireRelease(PubSub.shutdown),
|
|
101
|
-
)
|
|
134
|
+
const newEdgeAvailablePubSub = yield* PubSub.unbounded<MeshNodeName>().pipe(Effect.acquireRelease(PubSub.shutdown))
|
|
102
135
|
|
|
103
136
|
// const proxyPacketsToProcess = yield* Queue.unbounded<ProxyQueueItem>().pipe(Effect.acquireRelease(Queue.shutdown))
|
|
104
137
|
// const messagePacketsToProcess = yield* Queue.unbounded<MessageQueueItem>().pipe(
|
|
105
138
|
// Effect.acquireRelease(Queue.shutdown),
|
|
106
139
|
// )
|
|
107
140
|
|
|
108
|
-
const channelMap = new Map<
|
|
141
|
+
const channelMap = new Map<
|
|
142
|
+
ChannelKey,
|
|
143
|
+
{
|
|
144
|
+
queue: Queue.Queue<MessageQueueItem | ProxyQueueItem>
|
|
145
|
+
/** This reference is only kept for debugging purposes */
|
|
146
|
+
debugInfo:
|
|
147
|
+
| {
|
|
148
|
+
channel: WebChannel.WebChannel<any, any>
|
|
149
|
+
target: MeshNodeName
|
|
150
|
+
}
|
|
151
|
+
| undefined
|
|
152
|
+
}
|
|
153
|
+
>()
|
|
154
|
+
|
|
155
|
+
const channelRequestsQueue = yield* Queue.unbounded<ListenForChannelResult>().pipe(
|
|
156
|
+
Effect.acquireRelease(Queue.shutdown),
|
|
157
|
+
)
|
|
158
|
+
|
|
159
|
+
type RequestId = string
|
|
160
|
+
const topologyRequestsMap = new Map<RequestId, Map<MeshNodeName, Set<MeshNodeName>>>()
|
|
161
|
+
|
|
162
|
+
type BroadcastChannelName = string
|
|
163
|
+
const broadcastChannelListenQueueMap = new Map<BroadcastChannelName, Queue.Queue<any>>()
|
|
109
164
|
|
|
110
|
-
const
|
|
165
|
+
const checkTransferableEdges = (packet: typeof WebmeshSchema.DirectChannelPacket.Type) => {
|
|
111
166
|
if (
|
|
112
|
-
(packet._tag === '
|
|
113
|
-
(
|
|
114
|
-
// Either if direct
|
|
115
|
-
|
|
116
|
-
// ... or if no forward-
|
|
117
|
-
![...
|
|
167
|
+
(packet._tag === 'DirectChannelRequest' &&
|
|
168
|
+
(edgeChannels.size === 0 ||
|
|
169
|
+
// Either if direct edge does not support transferables ...
|
|
170
|
+
edgeChannels.get(packet.target)?.channel.supportsTransferables === false)) ||
|
|
171
|
+
// ... or if no forward-edges support transferables
|
|
172
|
+
![...edgeChannels.values()].some((c) => c.channel.supportsTransferables === true)
|
|
118
173
|
) {
|
|
119
|
-
return
|
|
174
|
+
return WebmeshSchema.DirectChannelResponseNoTransferables.make({
|
|
120
175
|
reqId: packet.id,
|
|
121
176
|
channelName: packet.channelName,
|
|
122
177
|
// NOTE for now we're "pretending" that the message is coming from the target node
|
|
123
178
|
// even though we're already handling it here.
|
|
124
179
|
// TODO we should clean this up at some point
|
|
125
180
|
source: packet.target,
|
|
126
|
-
// source: nodeName,
|
|
127
181
|
target: packet.source,
|
|
128
182
|
remainingHops: packet.hops,
|
|
129
183
|
hops: [],
|
|
@@ -131,65 +185,156 @@ export const makeMeshNode = (nodeName: MeshNodeName): Effect.Effect<MeshNode, ne
|
|
|
131
185
|
}
|
|
132
186
|
}
|
|
133
187
|
|
|
134
|
-
const sendPacket = (packet: typeof
|
|
188
|
+
const sendPacket = (packet: typeof WebmeshSchema.Packet.Type) =>
|
|
135
189
|
Effect.gen(function* () {
|
|
136
|
-
|
|
137
|
-
yield* Effect.spanEvent('NetworkConnectionAdded', { packet, nodeName })
|
|
138
|
-
yield* PubSub.publish(newConnectionAvailablePubSub, packet.target)
|
|
190
|
+
// yield* Effect.log(`${nodeName}: sendPacket:${packet._tag} [${packet.id}]`)
|
|
139
191
|
|
|
140
|
-
|
|
192
|
+
if (Schema.is(WebmeshSchema.NetworkEdgeAdded)(packet)) {
|
|
193
|
+
yield* Effect.spanEvent('NetworkEdgeAdded', { packet, nodeName })
|
|
194
|
+
yield* PubSub.publish(newEdgeAvailablePubSub, packet.target)
|
|
195
|
+
|
|
196
|
+
const edgesToForwardTo = Array.from(edgeChannels)
|
|
141
197
|
.filter(([name]) => name !== packet.source)
|
|
142
198
|
.map(([_, con]) => con.channel)
|
|
143
199
|
|
|
144
|
-
yield* Effect.forEach(
|
|
200
|
+
yield* Effect.forEach(edgesToForwardTo, (con) => con.send(packet), { concurrency: 'unbounded' })
|
|
201
|
+
return
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
if (Schema.is(WebmeshSchema.BroadcastChannelPacket)(packet)) {
|
|
205
|
+
const edgesToForwardTo = Array.from(edgeChannels)
|
|
206
|
+
.filter(([name]) => !packet.hops.includes(name))
|
|
207
|
+
.map(([_, con]) => con.channel)
|
|
208
|
+
|
|
209
|
+
const adjustedPacket = {
|
|
210
|
+
...packet,
|
|
211
|
+
hops: [...packet.hops, nodeName],
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
yield* Effect.forEach(edgesToForwardTo, (con) => con.send(adjustedPacket), { concurrency: 'unbounded' })
|
|
215
|
+
|
|
216
|
+
// Don't emit the packet to the own node listen queue
|
|
217
|
+
if (packet.source === nodeName) {
|
|
218
|
+
return
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
const queue = broadcastChannelListenQueueMap.get(packet.channelName)
|
|
222
|
+
// In case this node is listening to this channel, add the packet to the listen queue
|
|
223
|
+
if (queue !== undefined) {
|
|
224
|
+
yield* Queue.offer(queue, packet)
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
return
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
if (Schema.is(WebmeshSchema.NetworkTopologyRequest)(packet)) {
|
|
231
|
+
if (packet.source !== nodeName) {
|
|
232
|
+
const backEdgeName =
|
|
233
|
+
packet.hops.at(-1) ?? shouldNeverHappen(`${nodeName}: Expected hops for packet`, packet)
|
|
234
|
+
const backEdgeChannel = edgeChannels.get(backEdgeName)!.channel
|
|
235
|
+
|
|
236
|
+
// Respond with own edge info
|
|
237
|
+
const response = WebmeshSchema.NetworkTopologyResponse.make({
|
|
238
|
+
reqId: packet.id,
|
|
239
|
+
source: packet.source,
|
|
240
|
+
target: packet.target,
|
|
241
|
+
remainingHops: packet.hops.slice(0, -1),
|
|
242
|
+
nodeName,
|
|
243
|
+
edges: Array.from(edgeChannels.keys()),
|
|
244
|
+
})
|
|
245
|
+
|
|
246
|
+
yield* backEdgeChannel.send(response)
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
// Forward the packet to all edges except the already visited ones
|
|
250
|
+
const edgesToForwardTo = Array.from(edgeChannels)
|
|
251
|
+
.filter(([name]) => !packet.hops.includes(name))
|
|
252
|
+
.map(([_, con]) => con.channel)
|
|
253
|
+
|
|
254
|
+
const adjustedPacket = {
|
|
255
|
+
...packet,
|
|
256
|
+
hops: [...packet.hops, nodeName],
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
yield* Effect.forEach(edgesToForwardTo, (con) => con.send(adjustedPacket), { concurrency: 'unbounded' })
|
|
260
|
+
|
|
145
261
|
return
|
|
146
262
|
}
|
|
147
263
|
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
264
|
+
if (Schema.is(WebmeshSchema.NetworkTopologyResponse)(packet)) {
|
|
265
|
+
if (packet.source === nodeName) {
|
|
266
|
+
const topologyRequestItem = topologyRequestsMap.get(packet.reqId)!
|
|
267
|
+
topologyRequestItem.set(packet.nodeName, new Set(packet.edges))
|
|
268
|
+
} else {
|
|
269
|
+
const remainingHops = packet.remainingHops
|
|
270
|
+
// Forwarding the response to the original sender via the route back
|
|
271
|
+
const routeBack =
|
|
272
|
+
remainingHops.at(-1) ?? shouldNeverHappen(`${nodeName}: Expected remaining hops for packet`, packet)
|
|
273
|
+
const edgeChannel =
|
|
274
|
+
edgeChannels.get(routeBack)?.channel ??
|
|
275
|
+
shouldNeverHappen(
|
|
276
|
+
`${nodeName}: Expected edge channel (${routeBack}) for packet`,
|
|
277
|
+
packet,
|
|
278
|
+
'Available edges:',
|
|
279
|
+
Array.from(edgeChannels.keys()),
|
|
280
|
+
)
|
|
281
|
+
|
|
282
|
+
yield* edgeChannel.send({ ...packet, remainingHops: packet.remainingHops.slice(0, -1) })
|
|
283
|
+
}
|
|
284
|
+
return
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
// We have a direct edge to the target node
|
|
288
|
+
if (edgeChannels.has(packet.target)) {
|
|
289
|
+
const edgeChannel = edgeChannels.get(packet.target)!.channel
|
|
151
290
|
const hops = packet.source === nodeName ? [] : [...packet.hops, nodeName]
|
|
152
|
-
yield*
|
|
291
|
+
yield* Effect.annotateCurrentSpan({ hasDirectEdge: true })
|
|
292
|
+
yield* edgeChannel.send({ ...packet, hops })
|
|
153
293
|
}
|
|
154
294
|
// In this case we have an expected route back we should follow
|
|
155
295
|
// eslint-disable-next-line unicorn/no-negated-condition
|
|
156
296
|
else if (packet.remainingHops !== undefined) {
|
|
157
297
|
const hopTarget =
|
|
158
|
-
packet.remainingHops
|
|
159
|
-
const
|
|
298
|
+
packet.remainingHops.at(-1) ?? shouldNeverHappen(`${nodeName}: Expected remaining hops for packet`, packet)
|
|
299
|
+
const edgeChannel = edgeChannels.get(hopTarget)?.channel
|
|
160
300
|
|
|
161
|
-
if (
|
|
301
|
+
if (edgeChannel === undefined) {
|
|
162
302
|
yield* Effect.logWarning(
|
|
163
|
-
`${nodeName}: Expected to find hop target ${hopTarget} in
|
|
303
|
+
`${nodeName}: Expected to find hop target ${hopTarget} in edges. Dropping packet.`,
|
|
164
304
|
packet,
|
|
165
305
|
)
|
|
166
306
|
return
|
|
167
307
|
}
|
|
168
308
|
|
|
169
|
-
yield*
|
|
309
|
+
yield* edgeChannel.send({
|
|
170
310
|
...packet,
|
|
171
|
-
remainingHops: packet.remainingHops.slice(1),
|
|
311
|
+
remainingHops: packet.remainingHops.slice(0, -1),
|
|
172
312
|
hops: [...packet.hops, nodeName],
|
|
173
313
|
})
|
|
174
314
|
}
|
|
175
|
-
// No route found, forward to all
|
|
315
|
+
// No route found, forward to all edges
|
|
176
316
|
else {
|
|
177
317
|
const hops = packet.source === nodeName ? [] : [...packet.hops, nodeName]
|
|
178
318
|
|
|
179
|
-
// Optimization: filter out
|
|
180
|
-
const
|
|
319
|
+
// Optimization: filter out edge where packet just came from
|
|
320
|
+
const edgesToForwardTo = Array.from(edgeChannels)
|
|
181
321
|
.filter(([name]) => name !== packet.source)
|
|
182
|
-
.map(([
|
|
322
|
+
.map(([name, con]) => ({ name, channel: con.channel }))
|
|
183
323
|
|
|
184
324
|
// TODO if hops-depth=0, we should fail right away with no route found
|
|
185
|
-
if (hops.length === 0 &&
|
|
186
|
-
|
|
325
|
+
if (hops.length === 0 && edgesToForwardTo.length === 0 && LS_DEV) {
|
|
326
|
+
yield* Effect.logWarning(nodeName, 'no route found to', packet.target, packet._tag, 'TODO handle better')
|
|
187
327
|
// TODO return a expected failure
|
|
188
328
|
}
|
|
189
329
|
|
|
190
330
|
const packetToSend = { ...packet, hops }
|
|
331
|
+
// console.debug(nodeName, 'sendPacket:forwarding', packetToSend)
|
|
191
332
|
|
|
192
|
-
yield* Effect.
|
|
333
|
+
yield* Effect.annotateCurrentSpan({ edgesToForwardTo: edgesToForwardTo.map(({ name }) => name) })
|
|
334
|
+
|
|
335
|
+
yield* Effect.forEach(edgesToForwardTo, ({ channel }) => channel.send(packetToSend), {
|
|
336
|
+
concurrency: 'unbounded',
|
|
337
|
+
})
|
|
193
338
|
}
|
|
194
339
|
}).pipe(
|
|
195
340
|
Effect.withSpan(`sendPacket:${packet._tag}:${packet.source}→${packet.target}`, {
|
|
@@ -198,178 +343,378 @@ export const makeMeshNode = (nodeName: MeshNodeName): Effect.Effect<MeshNode, ne
|
|
|
198
343
|
Effect.orDie,
|
|
199
344
|
)
|
|
200
345
|
|
|
201
|
-
const
|
|
202
|
-
target: targetNodeName,
|
|
203
|
-
connectionChannel,
|
|
204
|
-
replaceIfExists = false,
|
|
205
|
-
}) =>
|
|
346
|
+
const addEdge: MeshNode['addEdge'] = ({ target: targetNodeName, edgeChannel, replaceIfExists = false }) =>
|
|
206
347
|
Effect.gen(function* () {
|
|
207
|
-
if (
|
|
348
|
+
if (edgeChannels.has(targetNodeName)) {
|
|
208
349
|
if (replaceIfExists) {
|
|
209
|
-
yield*
|
|
210
|
-
// console.log('interrupting', targetNodeName)
|
|
211
|
-
// yield* Fiber.interrupt(connectionChannels.get(targetNodeName)!.listenFiber)
|
|
350
|
+
yield* removeEdge(targetNodeName).pipe(Effect.orDie)
|
|
212
351
|
} else {
|
|
213
|
-
return yield* new
|
|
352
|
+
return yield* new EdgeAlreadyExistsError({ target: targetNodeName })
|
|
214
353
|
}
|
|
215
354
|
}
|
|
216
355
|
|
|
217
|
-
// TODO use a priority queue instead to prioritize network-changes/
|
|
218
|
-
const listenFiber = yield*
|
|
356
|
+
// TODO use a priority queue instead to prioritize network-changes/edge-requests over payloads
|
|
357
|
+
const listenFiber = yield* edgeChannel.listen.pipe(
|
|
219
358
|
Stream.flatten(),
|
|
220
359
|
Stream.tap((message) =>
|
|
221
360
|
Effect.gen(function* () {
|
|
222
|
-
const packet = yield* Schema.decodeUnknown(
|
|
361
|
+
const packet = yield* Schema.decodeUnknown(WebmeshSchema.Packet)(message)
|
|
223
362
|
|
|
224
|
-
// console.debug(nodeName, '
|
|
363
|
+
// console.debug(nodeName, 'recv', packet._tag, packet.source, packet.target)
|
|
225
364
|
|
|
226
365
|
if (handledPacketIds.has(packet.id)) return
|
|
227
366
|
handledPacketIds.add(packet.id)
|
|
228
367
|
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
if (!channelMap.has(channelKey)) {
|
|
235
|
-
const queue = yield* Queue.unbounded<MessageQueueItem | ProxyQueueItem>().pipe(
|
|
236
|
-
Effect.acquireRelease(Queue.shutdown),
|
|
237
|
-
)
|
|
238
|
-
channelMap.set(channelKey, { queue })
|
|
239
|
-
}
|
|
368
|
+
switch (packet._tag) {
|
|
369
|
+
case 'NetworkEdgeAdded':
|
|
370
|
+
case 'NetworkTopologyRequest':
|
|
371
|
+
case 'NetworkTopologyResponse': {
|
|
372
|
+
yield* sendPacket(packet)
|
|
240
373
|
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
const respondToSender = (outgoingPacket: typeof MeshSchema.Packet.Type) =>
|
|
244
|
-
connectionChannel
|
|
245
|
-
.send(outgoingPacket)
|
|
246
|
-
.pipe(
|
|
247
|
-
Effect.withSpan(
|
|
248
|
-
`respondToSender:${outgoingPacket._tag}:${outgoingPacket.source}→${outgoingPacket.target}`,
|
|
249
|
-
{ attributes: packetAsOtelAttributes(outgoingPacket) },
|
|
250
|
-
),
|
|
251
|
-
Effect.orDie,
|
|
252
|
-
)
|
|
253
|
-
|
|
254
|
-
if (Schema.is(MeshSchema.ProxyChannelPacket)(packet)) {
|
|
255
|
-
yield* Queue.offer(queue, { packet, respondToSender })
|
|
256
|
-
} else if (Schema.is(MeshSchema.MessageChannelPacket)(packet)) {
|
|
257
|
-
yield* Queue.offer(queue, { packet, respondToSender })
|
|
374
|
+
break
|
|
258
375
|
}
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
})
|
|
268
|
-
|
|
376
|
+
default: {
|
|
377
|
+
if (packet.target === nodeName) {
|
|
378
|
+
const channelKey = `target:${packet.source}, channelName:${packet.channelName}` satisfies ChannelKey
|
|
379
|
+
|
|
380
|
+
if (!channelMap.has(channelKey)) {
|
|
381
|
+
const channelQueue = yield* Queue.unbounded<MessageQueueItem | ProxyQueueItem>().pipe(
|
|
382
|
+
Effect.acquireRelease(Queue.shutdown),
|
|
383
|
+
)
|
|
384
|
+
channelMap.set(channelKey, { queue: channelQueue, debugInfo: undefined })
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
const channelQueue = channelMap.get(channelKey)!.queue
|
|
388
|
+
|
|
389
|
+
const respondToSender = (outgoingPacket: typeof WebmeshSchema.Packet.Type) =>
|
|
390
|
+
edgeChannel
|
|
391
|
+
.send(outgoingPacket)
|
|
392
|
+
.pipe(
|
|
393
|
+
Effect.withSpan(
|
|
394
|
+
`respondToSender:${outgoingPacket._tag}:${outgoingPacket.source}→${outgoingPacket.target}`,
|
|
395
|
+
{ attributes: packetAsOtelAttributes(outgoingPacket) },
|
|
396
|
+
),
|
|
397
|
+
Effect.orDie,
|
|
398
|
+
)
|
|
399
|
+
|
|
400
|
+
if (Schema.is(WebmeshSchema.ProxyChannelPacket)(packet)) {
|
|
401
|
+
yield* Queue.offer(channelQueue, { packet, respondToSender })
|
|
402
|
+
} else if (Schema.is(WebmeshSchema.DirectChannelPacket)(packet)) {
|
|
403
|
+
yield* Queue.offer(channelQueue, { packet, respondToSender })
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
if (packet._tag === 'ProxyChannelRequest' || packet._tag === 'DirectChannelRequest') {
|
|
407
|
+
yield* Queue.offer(channelRequestsQueue, {
|
|
408
|
+
channelName: packet.channelName,
|
|
409
|
+
source: packet.source,
|
|
410
|
+
mode: packet._tag === 'ProxyChannelRequest' ? 'proxy' : 'direct',
|
|
411
|
+
})
|
|
412
|
+
}
|
|
413
|
+
} else {
|
|
414
|
+
if (Schema.is(WebmeshSchema.DirectChannelPacket)(packet)) {
|
|
415
|
+
const noTransferableResponse = checkTransferableEdges(packet)
|
|
416
|
+
if (noTransferableResponse !== undefined) {
|
|
417
|
+
yield* Effect.spanEvent(`No transferable edges found for ${packet.source}→${packet.target}`)
|
|
418
|
+
return yield* edgeChannel.send(noTransferableResponse).pipe(
|
|
419
|
+
Effect.withSpan(`sendNoTransferableResponse:${packet.source}→${packet.target}`, {
|
|
420
|
+
attributes: packetAsOtelAttributes(noTransferableResponse),
|
|
421
|
+
}),
|
|
422
|
+
)
|
|
423
|
+
}
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
yield* sendPacket(packet)
|
|
269
427
|
}
|
|
270
428
|
}
|
|
271
|
-
|
|
272
|
-
yield* sendPacket(packet)
|
|
273
429
|
}
|
|
274
430
|
}),
|
|
275
431
|
),
|
|
276
432
|
Stream.runDrain,
|
|
433
|
+
Effect.interruptible,
|
|
277
434
|
Effect.orDie,
|
|
278
435
|
Effect.tapCauseLogPretty,
|
|
279
436
|
Effect.forkScoped,
|
|
280
437
|
)
|
|
281
438
|
|
|
282
|
-
|
|
439
|
+
edgeChannels.set(targetNodeName, { channel: edgeChannel, listenFiber })
|
|
283
440
|
|
|
284
|
-
const
|
|
441
|
+
const edgeAddedPacket = WebmeshSchema.NetworkEdgeAdded.make({
|
|
285
442
|
source: nodeName,
|
|
286
443
|
target: targetNodeName,
|
|
287
444
|
})
|
|
288
|
-
yield* sendPacket(
|
|
445
|
+
yield* sendPacket(edgeAddedPacket).pipe(Effect.orDie)
|
|
289
446
|
}).pipe(
|
|
290
|
-
Effect.
|
|
291
|
-
|
|
447
|
+
Effect.annotateLogs({ 'addEdge:target': targetNodeName, nodeName }),
|
|
448
|
+
Effect.withSpan(`addEdge:${nodeName}→${targetNodeName}`, {
|
|
449
|
+
attributes: { supportsTransferables: edgeChannel.supportsTransferables },
|
|
292
450
|
}),
|
|
293
451
|
) as any // any-cast needed for error/never overload
|
|
294
452
|
|
|
295
|
-
const
|
|
453
|
+
const removeEdge: MeshNode['removeEdge'] = (targetNodeName) =>
|
|
296
454
|
Effect.gen(function* () {
|
|
297
|
-
if (!
|
|
298
|
-
yield* new Cause.NoSuchElementException(`No
|
|
455
|
+
if (!edgeChannels.has(targetNodeName)) {
|
|
456
|
+
yield* new Cause.NoSuchElementException(`No edge found for ${targetNodeName}`)
|
|
299
457
|
}
|
|
300
458
|
|
|
301
|
-
yield* Fiber.interrupt(
|
|
459
|
+
yield* Fiber.interrupt(edgeChannels.get(targetNodeName)!.listenFiber)
|
|
302
460
|
|
|
303
|
-
|
|
461
|
+
edgeChannels.delete(targetNodeName)
|
|
304
462
|
})
|
|
305
463
|
|
|
306
|
-
|
|
464
|
+
const hasChannel: MeshNode['hasChannel'] = ({ target, channelName }) =>
|
|
465
|
+
Effect.sync(() => channelMap.has(`target:${target}, channelName:${channelName}`))
|
|
466
|
+
|
|
467
|
+
// TODO add heartbeat to detect dead edges (for both e2e and proxying)
|
|
307
468
|
// TODO when a channel is established in the same origin, we can use a weblock to detect disconnects
|
|
308
469
|
const makeChannel: MeshNode['makeChannel'] = ({
|
|
309
470
|
target,
|
|
310
471
|
channelName,
|
|
311
472
|
schema: inputSchema,
|
|
312
|
-
// TODO in the future we could have a mode that prefers
|
|
473
|
+
// TODO in the future we could have a mode that prefers directs and then falls back to proxies if needed
|
|
313
474
|
mode,
|
|
314
475
|
timeout = Duration.seconds(1),
|
|
315
476
|
}) =>
|
|
316
477
|
Effect.gen(function* () {
|
|
317
478
|
const schema = WebChannel.mapSchema(inputSchema)
|
|
318
|
-
const channelKey =
|
|
479
|
+
const channelKey = `target:${target}, channelName:${channelName}` satisfies ChannelKey
|
|
319
480
|
|
|
320
|
-
if (
|
|
321
|
-
const
|
|
481
|
+
if (channelMap.has(channelKey)) {
|
|
482
|
+
const existingChannel = channelMap.get(channelKey)!.debugInfo?.channel
|
|
483
|
+
if (existingChannel) {
|
|
484
|
+
shouldNeverHappen(`Channel ${channelKey} already exists`, existingChannel)
|
|
485
|
+
}
|
|
486
|
+
} else {
|
|
487
|
+
const channelQueue = yield* Queue.unbounded<MessageQueueItem | ProxyQueueItem>().pipe(
|
|
322
488
|
Effect.acquireRelease(Queue.shutdown),
|
|
323
489
|
)
|
|
324
|
-
channelMap.set(channelKey, { queue })
|
|
490
|
+
channelMap.set(channelKey, { queue: channelQueue, debugInfo: undefined })
|
|
325
491
|
}
|
|
326
492
|
|
|
327
|
-
const
|
|
493
|
+
const channelQueue = channelMap.get(channelKey)!.queue as Queue.Queue<any>
|
|
328
494
|
|
|
329
495
|
yield* Effect.addFinalizer(() => Effect.sync(() => channelMap.delete(channelKey)))
|
|
330
496
|
|
|
331
|
-
if (mode === '
|
|
332
|
-
|
|
497
|
+
if (mode === 'direct') {
|
|
498
|
+
const incomingPacketsQueue = yield* Queue.unbounded<any>().pipe(Effect.acquireRelease(Queue.shutdown))
|
|
499
|
+
|
|
500
|
+
// We're we're draining the queue into another new queue.
|
|
501
|
+
// It's a bit of a mystery why this is needed, since the unit tests also work without it.
|
|
502
|
+
// But for the LiveStore devtools to actually work, we need to do this.
|
|
503
|
+
// We should figure out some day why this is needed and further simplify if possible.
|
|
504
|
+
yield* Queue.takeBetween(channelQueue, 1, 10).pipe(
|
|
505
|
+
Effect.tap((_) => Queue.offerAll(incomingPacketsQueue, _)),
|
|
506
|
+
Effect.forever,
|
|
507
|
+
Effect.interruptible,
|
|
508
|
+
Effect.tapCauseLogPretty,
|
|
509
|
+
Effect.forkScoped,
|
|
510
|
+
)
|
|
333
511
|
|
|
334
512
|
// NOTE already retries internally when transferables are required
|
|
335
|
-
|
|
513
|
+
const { webChannel, initialEdgeDeferred } = yield* makeDirectChannel({
|
|
336
514
|
nodeName,
|
|
337
|
-
|
|
338
|
-
|
|
515
|
+
incomingPacketsQueue,
|
|
516
|
+
newEdgeAvailablePubSub,
|
|
339
517
|
target,
|
|
340
518
|
channelName,
|
|
341
519
|
schema,
|
|
342
520
|
sendPacket,
|
|
343
|
-
|
|
521
|
+
checkTransferableEdges,
|
|
344
522
|
})
|
|
523
|
+
|
|
524
|
+
channelMap.set(channelKey, { queue: channelQueue, debugInfo: { channel: webChannel, target } })
|
|
525
|
+
|
|
526
|
+
yield* initialEdgeDeferred
|
|
527
|
+
|
|
528
|
+
return webChannel
|
|
345
529
|
} else {
|
|
346
|
-
|
|
530
|
+
const channel = yield* makeProxyChannel({
|
|
347
531
|
nodeName,
|
|
348
|
-
|
|
532
|
+
newEdgeAvailablePubSub,
|
|
349
533
|
target,
|
|
350
534
|
channelName,
|
|
351
535
|
schema,
|
|
352
|
-
queue,
|
|
536
|
+
queue: channelQueue,
|
|
353
537
|
sendPacket,
|
|
354
538
|
})
|
|
539
|
+
|
|
540
|
+
channelMap.set(channelKey, { queue: channelQueue, debugInfo: { channel, target } })
|
|
541
|
+
|
|
542
|
+
return channel
|
|
355
543
|
}
|
|
356
544
|
}).pipe(
|
|
545
|
+
// Effect.timeout(timeout),
|
|
357
546
|
Effect.withSpanScoped(`makeChannel:${nodeName}→${target}(${channelName})`, {
|
|
358
547
|
attributes: { target, channelName, mode, timeout },
|
|
359
548
|
}),
|
|
360
|
-
Effect.annotateLogs({ nodeName }),
|
|
549
|
+
Effect.annotateLogs({ nodeName, target, channelName }),
|
|
361
550
|
)
|
|
362
551
|
|
|
363
|
-
|
|
552
|
+
// TODO consider supporting multiple listeners
|
|
553
|
+
// TODO also provide a way to allow for reconnects
|
|
554
|
+
let listenAlreadyStarted = false
|
|
555
|
+
const listenForChannel: MeshNode['listenForChannel'] = Stream.suspend(() => {
|
|
556
|
+
if (listenAlreadyStarted) {
|
|
557
|
+
return shouldNeverHappen('listenForChannel already started')
|
|
558
|
+
}
|
|
559
|
+
listenAlreadyStarted = true
|
|
560
|
+
|
|
561
|
+
const hash = (res: ListenForChannelResult) => `${res.channelName}:${res.source}:${res.mode}`
|
|
562
|
+
type HashedListenForChannelResult = string
|
|
563
|
+
const seen = new Set<HashedListenForChannelResult>()
|
|
564
|
+
|
|
565
|
+
return Stream.fromQueue(channelRequestsQueue).pipe(
|
|
566
|
+
Stream.filter((res) => {
|
|
567
|
+
const hashed = hash(res)
|
|
568
|
+
if (seen.has(hashed)) {
|
|
569
|
+
return false
|
|
570
|
+
}
|
|
571
|
+
seen.add(hashed)
|
|
572
|
+
return true
|
|
573
|
+
}),
|
|
574
|
+
)
|
|
575
|
+
})
|
|
576
|
+
|
|
577
|
+
const makeBroadcastChannel: MeshNode['makeBroadcastChannel'] = ({ channelName, schema }) =>
|
|
578
|
+
Effect.scopeWithCloseable((scope) =>
|
|
579
|
+
Effect.gen(function* () {
|
|
580
|
+
if (broadcastChannelListenQueueMap.has(channelName)) {
|
|
581
|
+
return shouldNeverHappen(
|
|
582
|
+
`Broadcast channel ${channelName} already exists`,
|
|
583
|
+
broadcastChannelListenQueueMap.get(channelName),
|
|
584
|
+
)
|
|
585
|
+
}
|
|
586
|
+
|
|
587
|
+
const debugInfo = {}
|
|
588
|
+
|
|
589
|
+
const queue = yield* Queue.unbounded<any>().pipe(Effect.acquireRelease(Queue.shutdown))
|
|
590
|
+
broadcastChannelListenQueueMap.set(channelName, queue)
|
|
591
|
+
|
|
592
|
+
const send = (message: any) =>
|
|
593
|
+
Effect.gen(function* () {
|
|
594
|
+
const payload = yield* Schema.encode(schema)(message)
|
|
595
|
+
const packet = WebmeshSchema.BroadcastChannelPacket.make({
|
|
596
|
+
channelName,
|
|
597
|
+
payload,
|
|
598
|
+
source: nodeName,
|
|
599
|
+
target: '-',
|
|
600
|
+
hops: [],
|
|
601
|
+
})
|
|
602
|
+
|
|
603
|
+
yield* sendPacket(packet)
|
|
604
|
+
})
|
|
605
|
+
|
|
606
|
+
const listen = Stream.fromQueue(queue).pipe(
|
|
607
|
+
Stream.filter(Schema.is(WebmeshSchema.BroadcastChannelPacket)),
|
|
608
|
+
Stream.map((_) => Schema.decodeEither(schema)(_.payload)),
|
|
609
|
+
)
|
|
610
|
+
|
|
611
|
+
const closedDeferred = yield* Deferred.make<void>().pipe(Effect.acquireRelease(Deferred.done(Exit.void)))
|
|
612
|
+
|
|
613
|
+
return {
|
|
614
|
+
[WebChannel.WebChannelSymbol]: WebChannel.WebChannelSymbol,
|
|
615
|
+
send,
|
|
616
|
+
listen,
|
|
617
|
+
closedDeferred,
|
|
618
|
+
supportsTransferables: false,
|
|
619
|
+
schema: { listen: schema, send: schema },
|
|
620
|
+
shutdown: Scope.close(scope, Exit.void),
|
|
621
|
+
debugInfo,
|
|
622
|
+
} satisfies WebChannel.WebChannel<any, any>
|
|
623
|
+
}),
|
|
624
|
+
)
|
|
625
|
+
|
|
626
|
+
const edgeKeys: MeshNode['edgeKeys'] = Effect.sync(() => new Set(edgeChannels.keys()))
|
|
627
|
+
|
|
628
|
+
const runtime = yield* Effect.runtime()
|
|
364
629
|
|
|
365
630
|
const debug: MeshNode['debug'] = {
|
|
366
|
-
|
|
631
|
+
print: () => {
|
|
632
|
+
console.log('Webmesh debug info for node:', nodeName)
|
|
633
|
+
|
|
634
|
+
console.log('Edges:', edgeChannels.size)
|
|
635
|
+
for (const [key, value] of edgeChannels) {
|
|
636
|
+
console.log(` ${key}: supportsTransferables=${value.channel.supportsTransferables}`)
|
|
637
|
+
}
|
|
638
|
+
|
|
367
639
|
console.log('Channels:', channelMap.size)
|
|
368
640
|
for (const [key, value] of channelMap) {
|
|
369
|
-
console.log(
|
|
641
|
+
console.log(
|
|
642
|
+
indent(key, 2),
|
|
643
|
+
'\n',
|
|
644
|
+
Object.entries({
|
|
645
|
+
target: value.debugInfo?.target,
|
|
646
|
+
supportsTransferables: value.debugInfo?.channel.supportsTransferables,
|
|
647
|
+
...value.debugInfo?.channel.debugInfo,
|
|
648
|
+
})
|
|
649
|
+
.map(([key, value]) => indent(`${key}=${value}`, 4))
|
|
650
|
+
.join('\n'),
|
|
651
|
+
' ',
|
|
652
|
+
value.debugInfo?.channel,
|
|
653
|
+
'\n',
|
|
654
|
+
indent(`Queue: ${value.queue.unsafeSize().pipe(Option.getOrUndefined)}`, 4),
|
|
655
|
+
value.queue,
|
|
656
|
+
)
|
|
370
657
|
}
|
|
658
|
+
|
|
659
|
+
console.log('Broadcast channels:', broadcastChannelListenQueueMap.size)
|
|
660
|
+
for (const [key, _value] of broadcastChannelListenQueueMap) {
|
|
661
|
+
console.log(indent(key, 2))
|
|
662
|
+
}
|
|
663
|
+
},
|
|
664
|
+
ping: (payload) => {
|
|
665
|
+
Effect.gen(function* () {
|
|
666
|
+
const msg = (via: string) =>
|
|
667
|
+
WebChannel.DebugPingMessage.make({ message: `ping from ${nodeName} via ${via}`, payload })
|
|
668
|
+
|
|
669
|
+
for (const [channelName, con] of edgeChannels) {
|
|
670
|
+
yield* Effect.logDebug(`sending ping via edge ${channelName}`)
|
|
671
|
+
yield* con.channel.send(msg(`edge ${channelName}`) as any)
|
|
672
|
+
}
|
|
673
|
+
|
|
674
|
+
for (const [channelKey, channel] of channelMap) {
|
|
675
|
+
if (channel.debugInfo === undefined) {
|
|
676
|
+
yield* Effect.logDebug(`channel ${channelKey} has no debug info`)
|
|
677
|
+
continue
|
|
678
|
+
}
|
|
679
|
+
// if (channel.debugInfo === undefined) continue
|
|
680
|
+
yield* Effect.logDebug(`sending ping via channel ${channelKey}`)
|
|
681
|
+
yield* channel.debugInfo.channel.send(msg(`channel ${channelKey}`) as any)
|
|
682
|
+
}
|
|
683
|
+
}).pipe(Effect.provide(runtime), Effect.tapCauseLogPretty, Effect.runFork)
|
|
371
684
|
},
|
|
685
|
+
requestTopology: (timeoutMs = 1000) =>
|
|
686
|
+
Effect.gen(function* () {
|
|
687
|
+
const packet = WebmeshSchema.NetworkTopologyRequest.make({
|
|
688
|
+
source: nodeName,
|
|
689
|
+
target: '-',
|
|
690
|
+
hops: [],
|
|
691
|
+
})
|
|
692
|
+
|
|
693
|
+
const item = new Map<MeshNodeName, Set<MeshNodeName>>()
|
|
694
|
+
item.set(nodeName, new Set(edgeChannels.keys()))
|
|
695
|
+
topologyRequestsMap.set(packet.id, item)
|
|
696
|
+
|
|
697
|
+
yield* sendPacket(packet)
|
|
698
|
+
|
|
699
|
+
yield* Effect.logDebug(`Waiting ${timeoutMs}ms for topology response`)
|
|
700
|
+
yield* Effect.sleep(timeoutMs)
|
|
701
|
+
|
|
702
|
+
yield* Effect.logDebug(`Topology response (from ${nodeName}):`)
|
|
703
|
+
for (const [key, value] of item) {
|
|
704
|
+
yield* Effect.logDebug(` node '${key}' has edge to: ${Array.from(value.values()).join(', ')}`)
|
|
705
|
+
}
|
|
706
|
+
}).pipe(Effect.provide(runtime), Effect.tapCauseLogPretty, Effect.runPromise),
|
|
372
707
|
}
|
|
373
708
|
|
|
374
|
-
return {
|
|
375
|
-
|
|
709
|
+
return {
|
|
710
|
+
nodeName,
|
|
711
|
+
addEdge,
|
|
712
|
+
removeEdge,
|
|
713
|
+
hasChannel,
|
|
714
|
+
makeChannel,
|
|
715
|
+
listenForChannel,
|
|
716
|
+
makeBroadcastChannel,
|
|
717
|
+
edgeKeys,
|
|
718
|
+
debug,
|
|
719
|
+
} satisfies MeshNode
|
|
720
|
+
}).pipe(Effect.withSpan(`makeMeshNode:${nodeName}`), Effect.annotateLogs({ 'makeMeshNode.nodeName': nodeName }))
|