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