@livestore/webmesh 0.0.0-snapshot-12c4570d047bcfaefc85bc6243c1b61f57580913 → 0.0.0-snapshot-b12fe44662a4a2f917d402ea2562edb3c404f26a
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/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 +16 -5
- package/dist/node.d.ts.map +1 -1
- package/dist/node.js +177 -41
- 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.map +1 -1
- package/dist/websocket-connection.js +5 -4
- package/dist/websocket-connection.js.map +1 -1
- package/package.json +3 -3
- package/src/channel/message-channel-internal.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 +265 -62
- 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'
|
|
@@ -22,8 +24,8 @@ import { TimeoutSet } from './utils.js'
|
|
|
22
24
|
|
|
23
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,6 +147,12 @@ export const makeMeshNode = (nodeName: MeshNodeName): Effect.Effect<MeshNode, ne
|
|
|
130
147
|
}
|
|
131
148
|
>()
|
|
132
149
|
|
|
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
|
+
|
|
133
156
|
const checkTransferableConnections = (packet: typeof WebmeshSchema.MessageChannelPacket.Type) => {
|
|
134
157
|
if (
|
|
135
158
|
(packet._tag === 'MessageChannelRequest' &&
|
|
@@ -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
|
}
|
|
@@ -252,50 +358,61 @@ export const makeMeshNode = (nodeName: MeshNodeName): Effect.Effect<MeshNode, ne
|
|
|
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
|
),
|
|
@@ -311,7 +428,7 @@ export const makeMeshNode = (nodeName: MeshNodeName): Effect.Effect<MeshNode, ne
|
|
|
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>
|