@livestore/webmesh 0.3.0-dev.19 → 0.3.0-dev.21
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 +3 -1
- package/dist/.tsbuildinfo +1 -1
- package/dist/channel/message-channel-internal.js +1 -1
- package/dist/channel/message-channel-internal.js.map +1 -1
- package/dist/channel/message-channel.js +1 -1
- package/dist/channel/message-channel.js.map +1 -1
- package/dist/channel/proxy-channel.d.ts.map +1 -1
- package/dist/channel/proxy-channel.js +10 -6
- package/dist/channel/proxy-channel.js.map +1 -1
- package/dist/common.d.ts +24 -1
- package/dist/common.d.ts.map +1 -1
- package/dist/mesh-schema.d.ts +45 -1
- package/dist/mesh-schema.d.ts.map +1 -1
- package/dist/mesh-schema.js +32 -2
- package/dist/mesh-schema.js.map +1 -1
- package/dist/node.d.ts +18 -7
- package/dist/node.d.ts.map +1 -1
- package/dist/node.js +182 -46
- package/dist/node.js.map +1 -1
- package/dist/node.test.js +54 -0
- package/dist/node.test.js.map +1 -1
- package/dist/websocket-connection.d.ts +5 -5
- package/dist/websocket-connection.d.ts.map +1 -1
- package/dist/websocket-connection.js +5 -4
- package/dist/websocket-connection.js.map +1 -1
- package/package.json +5 -5
- package/src/channel/message-channel-internal.ts +1 -1
- package/src/channel/message-channel.ts +1 -1
- package/src/channel/proxy-channel.ts +10 -6
- package/src/common.ts +1 -1
- package/src/mesh-schema.ts +40 -2
- package/src/node.test.ts +81 -0
- package/src/node.ts +273 -70
- package/src/websocket-connection.ts +9 -4
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'
|
|
@@ -17,13 +19,13 @@ 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
21
|
import { ConnectionAlreadyExistsError, packetAsOtelAttributes } from './common.js'
|
|
20
|
-
import * as
|
|
22
|
+
import * as WebmeshSchema from './mesh-schema.js'
|
|
21
23
|
import { TimeoutSet } from './utils.js'
|
|
22
24
|
|
|
23
|
-
type ConnectionChannel = WebChannel.WebChannel<typeof
|
|
25
|
+
type ConnectionChannel = 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
|
connectionKeys: Effect.Effect<Set<MeshNodeName>>
|
|
29
31
|
|
|
@@ -31,6 +33,10 @@ export interface MeshNode {
|
|
|
31
33
|
print: () => void
|
|
32
34
|
/** Sends a ping message to all connected nodes and channels */
|
|
33
35
|
ping: (payload?: string) => void
|
|
36
|
+
/**
|
|
37
|
+
* Requests the topology of the network from all connected nodes
|
|
38
|
+
*/
|
|
39
|
+
requestTopology: (timeoutMs?: number) => Promise<void>
|
|
34
40
|
}
|
|
35
41
|
|
|
36
42
|
/**
|
|
@@ -95,9 +101,20 @@ export interface MeshNode {
|
|
|
95
101
|
*/
|
|
96
102
|
timeout?: Duration.DurationInput
|
|
97
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>
|
|
98
113
|
}
|
|
99
114
|
|
|
100
|
-
export const makeMeshNode =
|
|
115
|
+
export const makeMeshNode = <TName extends MeshNodeName>(
|
|
116
|
+
nodeName: TName,
|
|
117
|
+
): Effect.Effect<MeshNode<TName>, never, Scope.Scope> =>
|
|
101
118
|
Effect.gen(function* () {
|
|
102
119
|
const connectionChannels = new Map<
|
|
103
120
|
MeshNodeName,
|
|
@@ -130,7 +147,13 @@ export const makeMeshNode = (nodeName: MeshNodeName): Effect.Effect<MeshNode, ne
|
|
|
130
147
|
}
|
|
131
148
|
>()
|
|
132
149
|
|
|
133
|
-
|
|
150
|
+
type RequestId = string
|
|
151
|
+
const topologyRequestsMap = new Map<RequestId, Map<MeshNodeName, Set<MeshNodeName>>>()
|
|
152
|
+
|
|
153
|
+
type BroadcastChannelName = string
|
|
154
|
+
const broadcastChannelListenQueueMap = new Map<BroadcastChannelName, Queue.Queue<any>>()
|
|
155
|
+
|
|
156
|
+
const checkTransferableConnections = (packet: typeof WebmeshSchema.MessageChannelPacket.Type) => {
|
|
134
157
|
if (
|
|
135
158
|
(packet._tag === 'MessageChannelRequest' &&
|
|
136
159
|
(connectionChannels.size === 0 ||
|
|
@@ -139,7 +162,7 @@ export const makeMeshNode = (nodeName: MeshNodeName): Effect.Effect<MeshNode, ne
|
|
|
139
162
|
// ... or if no forward-connections support transferables
|
|
140
163
|
![...connectionChannels.values()].some((c) => c.channel.supportsTransferables === true)
|
|
141
164
|
) {
|
|
142
|
-
return
|
|
165
|
+
return WebmeshSchema.MessageChannelResponseNoTransferables.make({
|
|
143
166
|
reqId: packet.id,
|
|
144
167
|
channelName: packet.channelName,
|
|
145
168
|
// NOTE for now we're "pretending" that the message is coming from the target node
|
|
@@ -154,11 +177,11 @@ export const makeMeshNode = (nodeName: MeshNodeName): Effect.Effect<MeshNode, ne
|
|
|
154
177
|
}
|
|
155
178
|
}
|
|
156
179
|
|
|
157
|
-
const sendPacket = (packet: typeof
|
|
180
|
+
const sendPacket = (packet: typeof WebmeshSchema.Packet.Type) =>
|
|
158
181
|
Effect.gen(function* () {
|
|
159
182
|
// yield* Effect.log(`${nodeName}: sendPacket:${packet._tag} [${packet.id}]`)
|
|
160
183
|
|
|
161
|
-
if (Schema.is(
|
|
184
|
+
if (Schema.is(WebmeshSchema.NetworkConnectionAdded)(packet)) {
|
|
162
185
|
yield* Effect.spanEvent('NetworkConnectionAdded', { packet, nodeName })
|
|
163
186
|
yield* PubSub.publish(newConnectionAvailablePubSub, packet.target)
|
|
164
187
|
|
|
@@ -170,6 +193,89 @@ export const makeMeshNode = (nodeName: MeshNodeName): Effect.Effect<MeshNode, ne
|
|
|
170
193
|
return
|
|
171
194
|
}
|
|
172
195
|
|
|
196
|
+
if (Schema.is(WebmeshSchema.BroadcastChannelPacket)(packet)) {
|
|
197
|
+
const connectionsToForwardTo = Array.from(connectionChannels)
|
|
198
|
+
.filter(([name]) => !packet.hops.includes(name))
|
|
199
|
+
.map(([_, con]) => con.channel)
|
|
200
|
+
|
|
201
|
+
const adjustedPacket = {
|
|
202
|
+
...packet,
|
|
203
|
+
hops: [...packet.hops, nodeName],
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
yield* Effect.forEach(connectionsToForwardTo, (con) => con.send(adjustedPacket), { concurrency: 'unbounded' })
|
|
207
|
+
|
|
208
|
+
// Don't emit the packet to the own node listen queue
|
|
209
|
+
if (packet.source === nodeName) {
|
|
210
|
+
return
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
const queue = broadcastChannelListenQueueMap.get(packet.channelName)
|
|
214
|
+
// In case this node is listening to this channel, add the packet to the listen queue
|
|
215
|
+
if (queue !== undefined) {
|
|
216
|
+
yield* Queue.offer(queue, packet)
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
return
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
if (Schema.is(WebmeshSchema.NetworkConnectionTopologyRequest)(packet)) {
|
|
223
|
+
if (packet.source !== nodeName) {
|
|
224
|
+
const backConnectionName =
|
|
225
|
+
packet.hops.at(-1) ?? shouldNeverHappen(`${nodeName}: Expected hops for packet`, packet)
|
|
226
|
+
const backConnectionChannel = connectionChannels.get(backConnectionName)!.channel
|
|
227
|
+
|
|
228
|
+
// Respond with own connection info
|
|
229
|
+
const response = WebmeshSchema.NetworkConnectionTopologyResponse.make({
|
|
230
|
+
reqId: packet.id,
|
|
231
|
+
source: packet.source,
|
|
232
|
+
target: packet.target,
|
|
233
|
+
remainingHops: packet.hops.slice(0, -1),
|
|
234
|
+
nodeName,
|
|
235
|
+
connections: Array.from(connectionChannels.keys()),
|
|
236
|
+
})
|
|
237
|
+
|
|
238
|
+
yield* backConnectionChannel.send(response)
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
// Forward the packet to all connections except the already visited ones
|
|
242
|
+
const connectionsToForwardTo = Array.from(connectionChannels)
|
|
243
|
+
.filter(([name]) => !packet.hops.includes(name))
|
|
244
|
+
.map(([_, con]) => con.channel)
|
|
245
|
+
|
|
246
|
+
const adjustedPacket = {
|
|
247
|
+
...packet,
|
|
248
|
+
hops: [...packet.hops, nodeName],
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
yield* Effect.forEach(connectionsToForwardTo, (con) => con.send(adjustedPacket), { concurrency: 'unbounded' })
|
|
252
|
+
|
|
253
|
+
return
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
if (Schema.is(WebmeshSchema.NetworkConnectionTopologyResponse)(packet)) {
|
|
257
|
+
if (packet.source === nodeName) {
|
|
258
|
+
const topologyRequestItem = topologyRequestsMap.get(packet.reqId)!
|
|
259
|
+
topologyRequestItem.set(packet.nodeName, new Set(packet.connections))
|
|
260
|
+
} else {
|
|
261
|
+
const remainingHops = packet.remainingHops
|
|
262
|
+
// Forwarding the response to the original sender via the route back
|
|
263
|
+
const routeBack =
|
|
264
|
+
remainingHops.at(-1) ?? shouldNeverHappen(`${nodeName}: Expected remaining hops for packet`, packet)
|
|
265
|
+
const connectionChannel =
|
|
266
|
+
connectionChannels.get(routeBack)?.channel ??
|
|
267
|
+
shouldNeverHappen(
|
|
268
|
+
`${nodeName}: Expected connection channel (${routeBack}) for packet`,
|
|
269
|
+
packet,
|
|
270
|
+
'Available connections:',
|
|
271
|
+
Array.from(connectionChannels.keys()),
|
|
272
|
+
)
|
|
273
|
+
|
|
274
|
+
yield* connectionChannel.send({ ...packet, remainingHops: packet.remainingHops.slice(0, -1) })
|
|
275
|
+
}
|
|
276
|
+
return
|
|
277
|
+
}
|
|
278
|
+
|
|
173
279
|
// We have a direct connection to the target node
|
|
174
280
|
if (connectionChannels.has(packet.target)) {
|
|
175
281
|
const connectionChannel = connectionChannels.get(packet.target)!.channel
|
|
@@ -180,7 +286,7 @@ export const makeMeshNode = (nodeName: MeshNodeName): Effect.Effect<MeshNode, ne
|
|
|
180
286
|
// eslint-disable-next-line unicorn/no-negated-condition
|
|
181
287
|
else if (packet.remainingHops !== undefined) {
|
|
182
288
|
const hopTarget =
|
|
183
|
-
packet.remainingHops
|
|
289
|
+
packet.remainingHops.at(-1) ?? shouldNeverHappen(`${nodeName}: Expected remaining hops for packet`, packet)
|
|
184
290
|
const connectionChannel = connectionChannels.get(hopTarget)?.channel
|
|
185
291
|
|
|
186
292
|
if (connectionChannel === undefined) {
|
|
@@ -193,7 +299,7 @@ export const makeMeshNode = (nodeName: MeshNodeName): Effect.Effect<MeshNode, ne
|
|
|
193
299
|
|
|
194
300
|
yield* connectionChannel.send({
|
|
195
301
|
...packet,
|
|
196
|
-
remainingHops: packet.remainingHops.slice(1),
|
|
302
|
+
remainingHops: packet.remainingHops.slice(0, -1),
|
|
197
303
|
hops: [...packet.hops, nodeName],
|
|
198
304
|
})
|
|
199
305
|
}
|
|
@@ -245,57 +351,68 @@ export const makeMeshNode = (nodeName: MeshNodeName): Effect.Effect<MeshNode, ne
|
|
|
245
351
|
Stream.flatten(),
|
|
246
352
|
Stream.tap((message) =>
|
|
247
353
|
Effect.gen(function* () {
|
|
248
|
-
const packet = yield* Schema.decodeUnknown(
|
|
354
|
+
const packet = yield* Schema.decodeUnknown(WebmeshSchema.Packet)(message)
|
|
249
355
|
|
|
250
356
|
// console.debug(nodeName, 'received', packet._tag, packet.source, packet.target)
|
|
251
357
|
|
|
252
358
|
if (handledPacketIds.has(packet.id)) return
|
|
253
359
|
handledPacketIds.add(packet.id)
|
|
254
360
|
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
361
|
+
switch (packet._tag) {
|
|
362
|
+
case 'NetworkConnectionAdded':
|
|
363
|
+
case 'NetworkConnectionTopologyRequest':
|
|
364
|
+
case 'NetworkConnectionTopologyResponse': {
|
|
365
|
+
yield* sendPacket(packet)
|
|
259
366
|
|
|
260
|
-
|
|
261
|
-
const queue = yield* Queue.unbounded<MessageQueueItem | ProxyQueueItem>().pipe(
|
|
262
|
-
Effect.acquireRelease(Queue.shutdown),
|
|
263
|
-
)
|
|
264
|
-
channelMap.set(channelKey, { queue, debugInfo: undefined })
|
|
367
|
+
break
|
|
265
368
|
}
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
})
|
|
294
|
-
|
|
369
|
+
default: {
|
|
370
|
+
if (packet.target === nodeName) {
|
|
371
|
+
const channelKey = `target:${packet.source}, channelName:${packet.channelName}` satisfies ChannelKey
|
|
372
|
+
|
|
373
|
+
if (!channelMap.has(channelKey)) {
|
|
374
|
+
const queue = yield* Queue.unbounded<MessageQueueItem | ProxyQueueItem>().pipe(
|
|
375
|
+
Effect.acquireRelease(Queue.shutdown),
|
|
376
|
+
)
|
|
377
|
+
channelMap.set(channelKey, { queue, debugInfo: undefined })
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
const queue = channelMap.get(channelKey)!.queue
|
|
381
|
+
|
|
382
|
+
const respondToSender = (outgoingPacket: typeof WebmeshSchema.Packet.Type) =>
|
|
383
|
+
connectionChannel
|
|
384
|
+
.send(outgoingPacket)
|
|
385
|
+
.pipe(
|
|
386
|
+
Effect.withSpan(
|
|
387
|
+
`respondToSender:${outgoingPacket._tag}:${outgoingPacket.source}→${outgoingPacket.target}`,
|
|
388
|
+
{ attributes: packetAsOtelAttributes(outgoingPacket) },
|
|
389
|
+
),
|
|
390
|
+
Effect.orDie,
|
|
391
|
+
)
|
|
392
|
+
|
|
393
|
+
if (Schema.is(WebmeshSchema.ProxyChannelPacket)(packet)) {
|
|
394
|
+
yield* Queue.offer(queue, { packet, respondToSender })
|
|
395
|
+
} else if (Schema.is(WebmeshSchema.MessageChannelPacket)(packet)) {
|
|
396
|
+
yield* Queue.offer(queue, { packet, respondToSender })
|
|
397
|
+
}
|
|
398
|
+
} else {
|
|
399
|
+
if (Schema.is(WebmeshSchema.MessageChannelPacket)(packet)) {
|
|
400
|
+
const noTransferableResponse = checkTransferableConnections(packet)
|
|
401
|
+
if (noTransferableResponse !== undefined) {
|
|
402
|
+
yield* Effect.spanEvent(
|
|
403
|
+
`No transferable connections found for ${packet.source}→${packet.target}`,
|
|
404
|
+
)
|
|
405
|
+
return yield* connectionChannel.send(noTransferableResponse).pipe(
|
|
406
|
+
Effect.withSpan(`sendNoTransferableResponse:${packet.source}→${packet.target}`, {
|
|
407
|
+
attributes: packetAsOtelAttributes(noTransferableResponse),
|
|
408
|
+
}),
|
|
409
|
+
)
|
|
410
|
+
}
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
yield* sendPacket(packet)
|
|
295
414
|
}
|
|
296
415
|
}
|
|
297
|
-
|
|
298
|
-
yield* sendPacket(packet)
|
|
299
416
|
}
|
|
300
417
|
}),
|
|
301
418
|
),
|
|
@@ -307,11 +424,11 @@ export const makeMeshNode = (nodeName: MeshNodeName): Effect.Effect<MeshNode, ne
|
|
|
307
424
|
|
|
308
425
|
connectionChannels.set(targetNodeName, { channel: connectionChannel, listenFiber })
|
|
309
426
|
|
|
310
|
-
const connectionAddedPacket =
|
|
427
|
+
const connectionAddedPacket = WebmeshSchema.NetworkConnectionAdded.make({
|
|
311
428
|
source: nodeName,
|
|
312
429
|
target: targetNodeName,
|
|
313
430
|
})
|
|
314
|
-
yield* sendPacket(connectionAddedPacket).pipe(Effect.
|
|
431
|
+
yield* sendPacket(connectionAddedPacket).pipe(Effect.orDie)
|
|
315
432
|
}).pipe(
|
|
316
433
|
Effect.withSpan(`addConnection:${nodeName}→${targetNodeName}`, {
|
|
317
434
|
attributes: { supportsTransferables: connectionChannel.supportsTransferables },
|
|
@@ -341,7 +458,7 @@ export const makeMeshNode = (nodeName: MeshNodeName): Effect.Effect<MeshNode, ne
|
|
|
341
458
|
}) =>
|
|
342
459
|
Effect.gen(function* () {
|
|
343
460
|
const schema = WebChannel.mapSchema(inputSchema)
|
|
344
|
-
const channelKey =
|
|
461
|
+
const channelKey = `target:${target}, channelName:${channelName}` satisfies ChannelKey
|
|
345
462
|
|
|
346
463
|
if (channelMap.has(channelKey)) {
|
|
347
464
|
const existingChannel = channelMap.get(channelKey)!.debugInfo?.channel
|
|
@@ -413,10 +530,63 @@ export const makeMeshNode = (nodeName: MeshNodeName): Effect.Effect<MeshNode, ne
|
|
|
413
530
|
Effect.annotateLogs({ nodeName }),
|
|
414
531
|
)
|
|
415
532
|
|
|
533
|
+
const makeBroadcastChannel: MeshNode['makeBroadcastChannel'] = ({ channelName, schema }) =>
|
|
534
|
+
Effect.scopeWithCloseable((scope) =>
|
|
535
|
+
Effect.gen(function* () {
|
|
536
|
+
if (broadcastChannelListenQueueMap.has(channelName)) {
|
|
537
|
+
return shouldNeverHappen(
|
|
538
|
+
`Broadcast channel ${channelName} already exists`,
|
|
539
|
+
broadcastChannelListenQueueMap.get(channelName),
|
|
540
|
+
)
|
|
541
|
+
}
|
|
542
|
+
|
|
543
|
+
const debugInfo = {}
|
|
544
|
+
|
|
545
|
+
const queue = yield* Queue.unbounded<any>().pipe(Effect.acquireRelease(Queue.shutdown))
|
|
546
|
+
broadcastChannelListenQueueMap.set(channelName, queue)
|
|
547
|
+
|
|
548
|
+
const send = (message: any) =>
|
|
549
|
+
Effect.gen(function* () {
|
|
550
|
+
const payload = yield* Schema.encode(schema)(message)
|
|
551
|
+
const packet = WebmeshSchema.BroadcastChannelPacket.make({
|
|
552
|
+
channelName,
|
|
553
|
+
payload,
|
|
554
|
+
source: nodeName,
|
|
555
|
+
target: '-',
|
|
556
|
+
hops: [],
|
|
557
|
+
})
|
|
558
|
+
|
|
559
|
+
yield* sendPacket(packet)
|
|
560
|
+
})
|
|
561
|
+
|
|
562
|
+
const listen = Stream.fromQueue(queue).pipe(
|
|
563
|
+
Stream.filter(Schema.is(WebmeshSchema.BroadcastChannelPacket)),
|
|
564
|
+
Stream.map((_) => Schema.decodeEither(schema)(_.payload)),
|
|
565
|
+
)
|
|
566
|
+
|
|
567
|
+
const closedDeferred = yield* Deferred.make<void>().pipe(Effect.acquireRelease(Deferred.done(Exit.void)))
|
|
568
|
+
|
|
569
|
+
return {
|
|
570
|
+
[WebChannel.WebChannelSymbol]: WebChannel.WebChannelSymbol,
|
|
571
|
+
send,
|
|
572
|
+
listen,
|
|
573
|
+
closedDeferred,
|
|
574
|
+
supportsTransferables: true,
|
|
575
|
+
schema: { listen: schema, send: schema },
|
|
576
|
+
shutdown: Scope.close(scope, Exit.void),
|
|
577
|
+
debugInfo,
|
|
578
|
+
} satisfies WebChannel.WebChannel<any, any>
|
|
579
|
+
}),
|
|
580
|
+
)
|
|
581
|
+
|
|
416
582
|
const connectionKeys: MeshNode['connectionKeys'] = Effect.sync(() => new Set(connectionChannels.keys()))
|
|
417
583
|
|
|
584
|
+
const runtime = yield* Effect.runtime()
|
|
585
|
+
|
|
418
586
|
const debug: MeshNode['debug'] = {
|
|
419
587
|
print: () => {
|
|
588
|
+
console.log('Webmesh debug info for node:', nodeName)
|
|
589
|
+
|
|
420
590
|
console.log('Connections:', connectionChannels.size)
|
|
421
591
|
for (const [key, value] of connectionChannels) {
|
|
422
592
|
console.log(` ${key}: supportsTransferables=${value.channel.supportsTransferables}`)
|
|
@@ -441,26 +611,59 @@ export const makeMeshNode = (nodeName: MeshNodeName): Effect.Effect<MeshNode, ne
|
|
|
441
611
|
value.queue,
|
|
442
612
|
)
|
|
443
613
|
}
|
|
614
|
+
|
|
615
|
+
console.log('Broadcast channels:', broadcastChannelListenQueueMap.size)
|
|
616
|
+
for (const [key, _value] of broadcastChannelListenQueueMap) {
|
|
617
|
+
console.log(indent(key, 2))
|
|
618
|
+
}
|
|
444
619
|
},
|
|
445
620
|
ping: (payload) => {
|
|
446
621
|
Effect.gen(function* () {
|
|
447
|
-
const msg = (via: string) =>
|
|
448
|
-
|
|
449
|
-
return WebChannel.DebugPingMessage.make({ message: `ping from ${nodeName} via ${via}`, payload })
|
|
450
|
-
}
|
|
622
|
+
const msg = (via: string) =>
|
|
623
|
+
WebChannel.DebugPingMessage.make({ message: `ping from ${nodeName} via connection ${via}`, payload })
|
|
451
624
|
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
625
|
+
for (const [channelName, con] of connectionChannels) {
|
|
626
|
+
yield* Effect.logDebug(`sending ping via connection ${channelName}`)
|
|
627
|
+
yield* con.channel.send(msg(`connection ${channelName}`) as any)
|
|
628
|
+
}
|
|
455
629
|
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
(
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
}).pipe(Effect.runFork)
|
|
630
|
+
for (const [channelKey, channel] of channelMap) {
|
|
631
|
+
if (channel.debugInfo === undefined) continue
|
|
632
|
+
yield* Effect.logDebug(`sending ping via channel ${channelKey}`)
|
|
633
|
+
yield* channel.debugInfo.channel.send(msg(`channel ${channelKey}`) as any)
|
|
634
|
+
}
|
|
635
|
+
}).pipe(Effect.provide(runtime), Effect.tapCauseLogPretty, Effect.runFork)
|
|
462
636
|
},
|
|
637
|
+
requestTopology: (timeoutMs = 1000) =>
|
|
638
|
+
Effect.gen(function* () {
|
|
639
|
+
const packet = WebmeshSchema.NetworkConnectionTopologyRequest.make({
|
|
640
|
+
source: nodeName,
|
|
641
|
+
target: '-',
|
|
642
|
+
hops: [],
|
|
643
|
+
})
|
|
644
|
+
|
|
645
|
+
const item = new Map<MeshNodeName, Set<MeshNodeName>>()
|
|
646
|
+
item.set(nodeName, new Set(connectionChannels.keys()))
|
|
647
|
+
topologyRequestsMap.set(packet.id, item)
|
|
648
|
+
|
|
649
|
+
yield* sendPacket(packet)
|
|
650
|
+
|
|
651
|
+
yield* Effect.logDebug(`Waiting ${timeoutMs}ms for topology response`)
|
|
652
|
+
yield* Effect.sleep(timeoutMs)
|
|
653
|
+
|
|
654
|
+
for (const [key, value] of item) {
|
|
655
|
+
yield* Effect.logDebug(`node '${key}' is connected to: ${Array.from(value.values()).join(', ')}`)
|
|
656
|
+
}
|
|
657
|
+
}).pipe(Effect.provide(runtime), Effect.tapCauseLogPretty, Effect.runPromise),
|
|
463
658
|
}
|
|
464
659
|
|
|
465
|
-
return {
|
|
660
|
+
return {
|
|
661
|
+
nodeName,
|
|
662
|
+
addConnection,
|
|
663
|
+
removeConnection,
|
|
664
|
+
makeChannel,
|
|
665
|
+
makeBroadcastChannel,
|
|
666
|
+
connectionKeys,
|
|
667
|
+
debug,
|
|
668
|
+
} satisfies MeshNode
|
|
466
669
|
}).pipe(Effect.withSpan(`makeMeshNode:${nodeName}`))
|
|
@@ -82,6 +82,8 @@ export const makeWebSocketConnection = (
|
|
|
82
82
|
Effect.acquireRelease(Queue.shutdown),
|
|
83
83
|
)
|
|
84
84
|
|
|
85
|
+
const schema = WebChannel.mapSchema(WebmeshSchema.Packet)
|
|
86
|
+
|
|
85
87
|
yield* Stream.fromEventListener<MessageEvent>(socket as any, 'message').pipe(
|
|
86
88
|
Stream.map((msg) => Schema.decodeUnknownEither(MessageMsgPack)(new Uint8Array(msg.data))),
|
|
87
89
|
Stream.flatten(),
|
|
@@ -90,7 +92,7 @@ export const makeWebSocketConnection = (
|
|
|
90
92
|
if (msg._tag === 'WSConnectionInit') {
|
|
91
93
|
yield* Deferred.succeed(fromDeferred, msg.from)
|
|
92
94
|
} else {
|
|
93
|
-
const decodedPayload = yield* Schema.decode(
|
|
95
|
+
const decodedPayload = yield* Schema.decode(schema.listen)(msg.payload)
|
|
94
96
|
yield* Queue.offer(listenQueue, decodedPayload)
|
|
95
97
|
}
|
|
96
98
|
}),
|
|
@@ -133,18 +135,21 @@ export const makeWebSocketConnection = (
|
|
|
133
135
|
const send = (message: typeof WebmeshSchema.Packet.Type) =>
|
|
134
136
|
Effect.gen(function* () {
|
|
135
137
|
yield* isConnectedLatch.await
|
|
136
|
-
const payload = yield* Schema.encode(
|
|
138
|
+
const payload = yield* Schema.encode(schema.send)(message)
|
|
137
139
|
socket.send(Schema.encodeSync(MessageMsgPack)({ _tag: 'WSConnectionPayload', payload, from }))
|
|
138
140
|
})
|
|
139
141
|
|
|
140
|
-
const listen = Stream.fromQueue(listenQueue).pipe(
|
|
142
|
+
const listen = Stream.fromQueue(listenQueue).pipe(
|
|
143
|
+
Stream.map(Either.right),
|
|
144
|
+
WebChannel.listenToDebugPing('websocket-connection'),
|
|
145
|
+
)
|
|
141
146
|
|
|
142
147
|
const webChannel = {
|
|
143
148
|
[WebChannel.WebChannelSymbol]: WebChannel.WebChannelSymbol,
|
|
144
149
|
send,
|
|
145
150
|
listen,
|
|
146
151
|
closedDeferred,
|
|
147
|
-
schema
|
|
152
|
+
schema,
|
|
148
153
|
supportsTransferables: false,
|
|
149
154
|
shutdown: Scope.close(scope, Exit.void),
|
|
150
155
|
} satisfies WebChannel.WebChannel<typeof WebmeshSchema.Packet.Type, typeof WebmeshSchema.Packet.Type>
|