@livestore/webmesh 0.3.0-dev.21 → 0.3.0-dev.23
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/.tsbuildinfo +1 -1
- package/dist/channel/message-channel-internal.d.ts +3 -3
- package/dist/channel/message-channel-internal.d.ts.map +1 -1
- package/dist/channel/message-channel-internal.js +8 -8
- package/dist/channel/message-channel-internal.js.map +1 -1
- package/dist/channel/message-channel.d.ts +5 -5
- package/dist/channel/message-channel.d.ts.map +1 -1
- package/dist/channel/message-channel.js +22 -22
- package/dist/channel/message-channel.js.map +1 -1
- package/dist/channel/proxy-channel.d.ts +2 -2
- package/dist/channel/proxy-channel.d.ts.map +1 -1
- package/dist/channel/proxy-channel.js +9 -9
- package/dist/channel/proxy-channel.js.map +1 -1
- package/dist/common.d.ts +7 -12
- package/dist/common.d.ts.map +1 -1
- package/dist/common.js +5 -3
- package/dist/common.js.map +1 -1
- package/dist/mesh-schema.d.ts +10 -10
- package/dist/mesh-schema.d.ts.map +1 -1
- package/dist/mesh-schema.js +7 -7
- package/dist/mesh-schema.js.map +1 -1
- package/dist/mod.d.ts +2 -2
- package/dist/mod.d.ts.map +1 -1
- package/dist/mod.js +2 -2
- package/dist/mod.js.map +1 -1
- package/dist/node.d.ts +14 -14
- package/dist/node.d.ts.map +1 -1
- package/dist/node.js +86 -86
- package/dist/node.js.map +1 -1
- package/dist/node.test.js +25 -25
- package/dist/node.test.js.map +1 -1
- package/dist/websocket-connection.d.ts +1 -1
- package/dist/websocket-connection.d.ts.map +1 -1
- package/dist/websocket-connection.js +5 -5
- package/dist/websocket-connection.js.map +1 -1
- package/dist/websocket-edge.d.ts +51 -0
- package/dist/websocket-edge.d.ts.map +1 -0
- package/dist/websocket-edge.js +69 -0
- package/dist/websocket-edge.js.map +1 -0
- package/dist/websocket-server.d.ts +2 -2
- package/dist/websocket-server.d.ts.map +1 -1
- package/dist/websocket-server.js +6 -6
- package/dist/websocket-server.js.map +1 -1
- package/package.json +3 -3
- package/src/channel/message-channel-internal.ts +10 -10
- package/src/channel/message-channel.ts +25 -25
- package/src/channel/proxy-channel.ts +11 -11
- package/src/common.ts +8 -11
- package/src/mesh-schema.ts +9 -9
- package/src/mod.ts +2 -2
- package/src/node.test.ts +25 -25
- package/src/node.ts +102 -113
- package/src/{websocket-connection.ts → websocket-edge.ts} +15 -14
- package/src/websocket-server.ts +9 -9
package/src/node.ts
CHANGED
|
@@ -18,16 +18,16 @@ import {
|
|
|
18
18
|
import { makeMessageChannel } from './channel/message-channel.js'
|
|
19
19
|
import { makeProxyChannel } from './channel/proxy-channel.js'
|
|
20
20
|
import type { ChannelKey, MeshNodeName, MessageQueueItem, ProxyQueueItem } from './common.js'
|
|
21
|
-
import {
|
|
21
|
+
import { EdgeAlreadyExistsError, packetAsOtelAttributes } from './common.js'
|
|
22
22
|
import * as WebmeshSchema from './mesh-schema.js'
|
|
23
23
|
import { TimeoutSet } from './utils.js'
|
|
24
24
|
|
|
25
|
-
type
|
|
25
|
+
type EdgeChannel = WebChannel.WebChannel<typeof WebmeshSchema.Packet.Type, typeof WebmeshSchema.Packet.Type>
|
|
26
26
|
|
|
27
27
|
export interface MeshNode<TName extends MeshNodeName = MeshNodeName> {
|
|
28
28
|
nodeName: TName
|
|
29
29
|
|
|
30
|
-
|
|
30
|
+
edgeKeys: Effect.Effect<Set<MeshNodeName>>
|
|
31
31
|
|
|
32
32
|
debug: {
|
|
33
33
|
print: () => void
|
|
@@ -40,33 +40,33 @@ export interface MeshNode<TName extends MeshNodeName = MeshNodeName> {
|
|
|
40
40
|
}
|
|
41
41
|
|
|
42
42
|
/**
|
|
43
|
-
* Manually adds a
|
|
43
|
+
* Manually adds a edge to get connected to the network of nodes with an existing WebChannel.
|
|
44
44
|
*
|
|
45
|
-
* Assumptions about the WebChannel
|
|
46
|
-
* - 1:1
|
|
45
|
+
* Assumptions about the WebChannel edge:
|
|
46
|
+
* - 1:1 edge
|
|
47
47
|
* - Queues messages internally to never drop messages
|
|
48
48
|
* - Automatically reconnects
|
|
49
49
|
* - Ideally supports transferables
|
|
50
50
|
*/
|
|
51
|
-
|
|
51
|
+
addEdge: {
|
|
52
52
|
(options: {
|
|
53
53
|
target: MeshNodeName
|
|
54
|
-
|
|
54
|
+
edgeChannel: EdgeChannel
|
|
55
55
|
replaceIfExists: true
|
|
56
56
|
}): Effect.Effect<void, never, Scope.Scope>
|
|
57
57
|
(options: {
|
|
58
58
|
target: MeshNodeName
|
|
59
|
-
|
|
59
|
+
edgeChannel: EdgeChannel
|
|
60
60
|
replaceIfExists?: boolean
|
|
61
|
-
}): Effect.Effect<void,
|
|
61
|
+
}): Effect.Effect<void, EdgeAlreadyExistsError, Scope.Scope>
|
|
62
62
|
}
|
|
63
63
|
|
|
64
|
-
|
|
64
|
+
removeEdge: (targetNodeName: MeshNodeName) => Effect.Effect<void, Cause.NoSuchElementException>
|
|
65
65
|
|
|
66
66
|
/**
|
|
67
|
-
* Tries to broker a MessageChannel
|
|
67
|
+
* Tries to broker a MessageChannel edge between the nodes, otherwise will proxy messages via hop-nodes
|
|
68
68
|
*
|
|
69
|
-
* For a channel to successfully open, both sides need to have a
|
|
69
|
+
* For a channel to successfully open, both sides need to have a edge and call `makeChannel`.
|
|
70
70
|
*
|
|
71
71
|
* Example:
|
|
72
72
|
* ```ts
|
|
@@ -95,7 +95,7 @@ export interface MeshNode<TName extends MeshNodeName = MeshNodeName> {
|
|
|
95
95
|
*/
|
|
96
96
|
mode: 'messagechannel' | 'proxy'
|
|
97
97
|
/**
|
|
98
|
-
* Amount of time before we consider a channel creation failed and retry when a new
|
|
98
|
+
* Amount of time before we consider a channel creation failed and retry when a new edge is available
|
|
99
99
|
*
|
|
100
100
|
* @default 1 second
|
|
101
101
|
*/
|
|
@@ -116,17 +116,12 @@ export const makeMeshNode = <TName extends MeshNodeName>(
|
|
|
116
116
|
nodeName: TName,
|
|
117
117
|
): Effect.Effect<MeshNode<TName>, never, Scope.Scope> =>
|
|
118
118
|
Effect.gen(function* () {
|
|
119
|
-
const
|
|
120
|
-
MeshNodeName,
|
|
121
|
-
{ channel: ConnectionChannel; listenFiber: Fiber.RuntimeFiber<void> }
|
|
122
|
-
>()
|
|
119
|
+
const edgeChannels = new Map<MeshNodeName, { channel: EdgeChannel; listenFiber: Fiber.RuntimeFiber<void> }>()
|
|
123
120
|
|
|
124
121
|
// To avoid unbounded memory growth, we automatically forget about packet ids after a while
|
|
125
122
|
const handledPacketIds = new TimeoutSet<string>({ timeout: Duration.minutes(1) })
|
|
126
123
|
|
|
127
|
-
const
|
|
128
|
-
Effect.acquireRelease(PubSub.shutdown),
|
|
129
|
-
)
|
|
124
|
+
const newEdgeAvailablePubSub = yield* PubSub.unbounded<MeshNodeName>().pipe(Effect.acquireRelease(PubSub.shutdown))
|
|
130
125
|
|
|
131
126
|
// const proxyPacketsToProcess = yield* Queue.unbounded<ProxyQueueItem>().pipe(Effect.acquireRelease(Queue.shutdown))
|
|
132
127
|
// const messagePacketsToProcess = yield* Queue.unbounded<MessageQueueItem>().pipe(
|
|
@@ -153,14 +148,14 @@ export const makeMeshNode = <TName extends MeshNodeName>(
|
|
|
153
148
|
type BroadcastChannelName = string
|
|
154
149
|
const broadcastChannelListenQueueMap = new Map<BroadcastChannelName, Queue.Queue<any>>()
|
|
155
150
|
|
|
156
|
-
const
|
|
151
|
+
const checkTransferableEdges = (packet: typeof WebmeshSchema.MessageChannelPacket.Type) => {
|
|
157
152
|
if (
|
|
158
153
|
(packet._tag === 'MessageChannelRequest' &&
|
|
159
|
-
(
|
|
160
|
-
// Either if direct
|
|
161
|
-
|
|
162
|
-
// ... or if no forward-
|
|
163
|
-
![...
|
|
154
|
+
(edgeChannels.size === 0 ||
|
|
155
|
+
// Either if direct edge does not support transferables ...
|
|
156
|
+
edgeChannels.get(packet.target)?.channel.supportsTransferables === false)) ||
|
|
157
|
+
// ... or if no forward-edges support transferables
|
|
158
|
+
![...edgeChannels.values()].some((c) => c.channel.supportsTransferables === true)
|
|
164
159
|
) {
|
|
165
160
|
return WebmeshSchema.MessageChannelResponseNoTransferables.make({
|
|
166
161
|
reqId: packet.id,
|
|
@@ -181,20 +176,20 @@ export const makeMeshNode = <TName extends MeshNodeName>(
|
|
|
181
176
|
Effect.gen(function* () {
|
|
182
177
|
// yield* Effect.log(`${nodeName}: sendPacket:${packet._tag} [${packet.id}]`)
|
|
183
178
|
|
|
184
|
-
if (Schema.is(WebmeshSchema.
|
|
185
|
-
yield* Effect.spanEvent('
|
|
186
|
-
yield* PubSub.publish(
|
|
179
|
+
if (Schema.is(WebmeshSchema.NetworkEdgeAdded)(packet)) {
|
|
180
|
+
yield* Effect.spanEvent('NetworkEdgeAdded', { packet, nodeName })
|
|
181
|
+
yield* PubSub.publish(newEdgeAvailablePubSub, packet.target)
|
|
187
182
|
|
|
188
|
-
const
|
|
183
|
+
const edgesToForwardTo = Array.from(edgeChannels)
|
|
189
184
|
.filter(([name]) => name !== packet.source)
|
|
190
185
|
.map(([_, con]) => con.channel)
|
|
191
186
|
|
|
192
|
-
yield* Effect.forEach(
|
|
187
|
+
yield* Effect.forEach(edgesToForwardTo, (con) => con.send(packet), { concurrency: 'unbounded' })
|
|
193
188
|
return
|
|
194
189
|
}
|
|
195
190
|
|
|
196
191
|
if (Schema.is(WebmeshSchema.BroadcastChannelPacket)(packet)) {
|
|
197
|
-
const
|
|
192
|
+
const edgesToForwardTo = Array.from(edgeChannels)
|
|
198
193
|
.filter(([name]) => !packet.hops.includes(name))
|
|
199
194
|
.map(([_, con]) => con.channel)
|
|
200
195
|
|
|
@@ -203,7 +198,7 @@ export const makeMeshNode = <TName extends MeshNodeName>(
|
|
|
203
198
|
hops: [...packet.hops, nodeName],
|
|
204
199
|
}
|
|
205
200
|
|
|
206
|
-
yield* Effect.forEach(
|
|
201
|
+
yield* Effect.forEach(edgesToForwardTo, (con) => con.send(adjustedPacket), { concurrency: 'unbounded' })
|
|
207
202
|
|
|
208
203
|
// Don't emit the packet to the own node listen queue
|
|
209
204
|
if (packet.source === nodeName) {
|
|
@@ -219,27 +214,27 @@ export const makeMeshNode = <TName extends MeshNodeName>(
|
|
|
219
214
|
return
|
|
220
215
|
}
|
|
221
216
|
|
|
222
|
-
if (Schema.is(WebmeshSchema.
|
|
217
|
+
if (Schema.is(WebmeshSchema.NetworkTopologyRequest)(packet)) {
|
|
223
218
|
if (packet.source !== nodeName) {
|
|
224
|
-
const
|
|
219
|
+
const backEdgeName =
|
|
225
220
|
packet.hops.at(-1) ?? shouldNeverHappen(`${nodeName}: Expected hops for packet`, packet)
|
|
226
|
-
const
|
|
221
|
+
const backEdgeChannel = edgeChannels.get(backEdgeName)!.channel
|
|
227
222
|
|
|
228
|
-
// Respond with own
|
|
229
|
-
const response = WebmeshSchema.
|
|
223
|
+
// Respond with own edge info
|
|
224
|
+
const response = WebmeshSchema.NetworkTopologyResponse.make({
|
|
230
225
|
reqId: packet.id,
|
|
231
226
|
source: packet.source,
|
|
232
227
|
target: packet.target,
|
|
233
228
|
remainingHops: packet.hops.slice(0, -1),
|
|
234
229
|
nodeName,
|
|
235
|
-
|
|
230
|
+
edges: Array.from(edgeChannels.keys()),
|
|
236
231
|
})
|
|
237
232
|
|
|
238
|
-
yield*
|
|
233
|
+
yield* backEdgeChannel.send(response)
|
|
239
234
|
}
|
|
240
235
|
|
|
241
|
-
// Forward the packet to all
|
|
242
|
-
const
|
|
236
|
+
// Forward the packet to all edges except the already visited ones
|
|
237
|
+
const edgesToForwardTo = Array.from(edgeChannels)
|
|
243
238
|
.filter(([name]) => !packet.hops.includes(name))
|
|
244
239
|
.map(([_, con]) => con.channel)
|
|
245
240
|
|
|
@@ -248,72 +243,72 @@ export const makeMeshNode = <TName extends MeshNodeName>(
|
|
|
248
243
|
hops: [...packet.hops, nodeName],
|
|
249
244
|
}
|
|
250
245
|
|
|
251
|
-
yield* Effect.forEach(
|
|
246
|
+
yield* Effect.forEach(edgesToForwardTo, (con) => con.send(adjustedPacket), { concurrency: 'unbounded' })
|
|
252
247
|
|
|
253
248
|
return
|
|
254
249
|
}
|
|
255
250
|
|
|
256
|
-
if (Schema.is(WebmeshSchema.
|
|
251
|
+
if (Schema.is(WebmeshSchema.NetworkTopologyResponse)(packet)) {
|
|
257
252
|
if (packet.source === nodeName) {
|
|
258
253
|
const topologyRequestItem = topologyRequestsMap.get(packet.reqId)!
|
|
259
|
-
topologyRequestItem.set(packet.nodeName, new Set(packet.
|
|
254
|
+
topologyRequestItem.set(packet.nodeName, new Set(packet.edges))
|
|
260
255
|
} else {
|
|
261
256
|
const remainingHops = packet.remainingHops
|
|
262
257
|
// Forwarding the response to the original sender via the route back
|
|
263
258
|
const routeBack =
|
|
264
259
|
remainingHops.at(-1) ?? shouldNeverHappen(`${nodeName}: Expected remaining hops for packet`, packet)
|
|
265
|
-
const
|
|
266
|
-
|
|
260
|
+
const edgeChannel =
|
|
261
|
+
edgeChannels.get(routeBack)?.channel ??
|
|
267
262
|
shouldNeverHappen(
|
|
268
|
-
`${nodeName}: Expected
|
|
263
|
+
`${nodeName}: Expected edge channel (${routeBack}) for packet`,
|
|
269
264
|
packet,
|
|
270
|
-
'Available
|
|
271
|
-
Array.from(
|
|
265
|
+
'Available edges:',
|
|
266
|
+
Array.from(edgeChannels.keys()),
|
|
272
267
|
)
|
|
273
268
|
|
|
274
|
-
yield*
|
|
269
|
+
yield* edgeChannel.send({ ...packet, remainingHops: packet.remainingHops.slice(0, -1) })
|
|
275
270
|
}
|
|
276
271
|
return
|
|
277
272
|
}
|
|
278
273
|
|
|
279
|
-
// We have a direct
|
|
280
|
-
if (
|
|
281
|
-
const
|
|
274
|
+
// We have a direct edge to the target node
|
|
275
|
+
if (edgeChannels.has(packet.target)) {
|
|
276
|
+
const edgeChannel = edgeChannels.get(packet.target)!.channel
|
|
282
277
|
const hops = packet.source === nodeName ? [] : [...packet.hops, nodeName]
|
|
283
|
-
yield*
|
|
278
|
+
yield* edgeChannel.send({ ...packet, hops })
|
|
284
279
|
}
|
|
285
280
|
// In this case we have an expected route back we should follow
|
|
286
281
|
// eslint-disable-next-line unicorn/no-negated-condition
|
|
287
282
|
else if (packet.remainingHops !== undefined) {
|
|
288
283
|
const hopTarget =
|
|
289
284
|
packet.remainingHops.at(-1) ?? shouldNeverHappen(`${nodeName}: Expected remaining hops for packet`, packet)
|
|
290
|
-
const
|
|
285
|
+
const edgeChannel = edgeChannels.get(hopTarget)?.channel
|
|
291
286
|
|
|
292
|
-
if (
|
|
287
|
+
if (edgeChannel === undefined) {
|
|
293
288
|
yield* Effect.logWarning(
|
|
294
|
-
`${nodeName}: Expected to find hop target ${hopTarget} in
|
|
289
|
+
`${nodeName}: Expected to find hop target ${hopTarget} in edges. Dropping packet.`,
|
|
295
290
|
packet,
|
|
296
291
|
)
|
|
297
292
|
return
|
|
298
293
|
}
|
|
299
294
|
|
|
300
|
-
yield*
|
|
295
|
+
yield* edgeChannel.send({
|
|
301
296
|
...packet,
|
|
302
297
|
remainingHops: packet.remainingHops.slice(0, -1),
|
|
303
298
|
hops: [...packet.hops, nodeName],
|
|
304
299
|
})
|
|
305
300
|
}
|
|
306
|
-
// No route found, forward to all
|
|
301
|
+
// No route found, forward to all edges
|
|
307
302
|
else {
|
|
308
303
|
const hops = packet.source === nodeName ? [] : [...packet.hops, nodeName]
|
|
309
304
|
|
|
310
|
-
// Optimization: filter out
|
|
311
|
-
const
|
|
305
|
+
// Optimization: filter out edge where packet just came from
|
|
306
|
+
const edgesToForwardTo = Array.from(edgeChannels)
|
|
312
307
|
.filter(([name]) => name !== packet.source)
|
|
313
308
|
.map(([_, con]) => con.channel)
|
|
314
309
|
|
|
315
310
|
// TODO if hops-depth=0, we should fail right away with no route found
|
|
316
|
-
if (hops.length === 0 &&
|
|
311
|
+
if (hops.length === 0 && edgesToForwardTo.length === 0 && LS_DEV) {
|
|
317
312
|
console.log(nodeName, 'no route found', packet._tag, 'TODO handle better')
|
|
318
313
|
// TODO return a expected failure
|
|
319
314
|
}
|
|
@@ -321,7 +316,7 @@ export const makeMeshNode = <TName extends MeshNodeName>(
|
|
|
321
316
|
const packetToSend = { ...packet, hops }
|
|
322
317
|
// console.debug(nodeName, 'sendPacket:forwarding', packetToSend)
|
|
323
318
|
|
|
324
|
-
yield* Effect.forEach(
|
|
319
|
+
yield* Effect.forEach(edgesToForwardTo, (con) => con.send(packetToSend), { concurrency: 'unbounded' })
|
|
325
320
|
}
|
|
326
321
|
}).pipe(
|
|
327
322
|
Effect.withSpan(`sendPacket:${packet._tag}:${packet.source}→${packet.target}`, {
|
|
@@ -330,24 +325,20 @@ export const makeMeshNode = <TName extends MeshNodeName>(
|
|
|
330
325
|
Effect.orDie,
|
|
331
326
|
)
|
|
332
327
|
|
|
333
|
-
const
|
|
334
|
-
target: targetNodeName,
|
|
335
|
-
connectionChannel,
|
|
336
|
-
replaceIfExists = false,
|
|
337
|
-
}) =>
|
|
328
|
+
const addEdge: MeshNode['addEdge'] = ({ target: targetNodeName, edgeChannel, replaceIfExists = false }) =>
|
|
338
329
|
Effect.gen(function* () {
|
|
339
|
-
if (
|
|
330
|
+
if (edgeChannels.has(targetNodeName)) {
|
|
340
331
|
if (replaceIfExists) {
|
|
341
|
-
yield*
|
|
332
|
+
yield* removeEdge(targetNodeName).pipe(Effect.orDie)
|
|
342
333
|
// console.log('interrupting', targetNodeName)
|
|
343
|
-
// yield* Fiber.interrupt(
|
|
334
|
+
// yield* Fiber.interrupt(edgeChannels.get(targetNodeName)!.listenFiber)
|
|
344
335
|
} else {
|
|
345
|
-
return yield* new
|
|
336
|
+
return yield* new EdgeAlreadyExistsError({ target: targetNodeName })
|
|
346
337
|
}
|
|
347
338
|
}
|
|
348
339
|
|
|
349
|
-
// TODO use a priority queue instead to prioritize network-changes/
|
|
350
|
-
const listenFiber = yield*
|
|
340
|
+
// TODO use a priority queue instead to prioritize network-changes/edge-requests over payloads
|
|
341
|
+
const listenFiber = yield* edgeChannel.listen.pipe(
|
|
351
342
|
Stream.flatten(),
|
|
352
343
|
Stream.tap((message) =>
|
|
353
344
|
Effect.gen(function* () {
|
|
@@ -359,9 +350,9 @@ export const makeMeshNode = <TName extends MeshNodeName>(
|
|
|
359
350
|
handledPacketIds.add(packet.id)
|
|
360
351
|
|
|
361
352
|
switch (packet._tag) {
|
|
362
|
-
case '
|
|
363
|
-
case '
|
|
364
|
-
case '
|
|
353
|
+
case 'NetworkEdgeAdded':
|
|
354
|
+
case 'NetworkTopologyRequest':
|
|
355
|
+
case 'NetworkTopologyResponse': {
|
|
365
356
|
yield* sendPacket(packet)
|
|
366
357
|
|
|
367
358
|
break
|
|
@@ -380,7 +371,7 @@ export const makeMeshNode = <TName extends MeshNodeName>(
|
|
|
380
371
|
const queue = channelMap.get(channelKey)!.queue
|
|
381
372
|
|
|
382
373
|
const respondToSender = (outgoingPacket: typeof WebmeshSchema.Packet.Type) =>
|
|
383
|
-
|
|
374
|
+
edgeChannel
|
|
384
375
|
.send(outgoingPacket)
|
|
385
376
|
.pipe(
|
|
386
377
|
Effect.withSpan(
|
|
@@ -397,12 +388,10 @@ export const makeMeshNode = <TName extends MeshNodeName>(
|
|
|
397
388
|
}
|
|
398
389
|
} else {
|
|
399
390
|
if (Schema.is(WebmeshSchema.MessageChannelPacket)(packet)) {
|
|
400
|
-
const noTransferableResponse =
|
|
391
|
+
const noTransferableResponse = checkTransferableEdges(packet)
|
|
401
392
|
if (noTransferableResponse !== undefined) {
|
|
402
|
-
yield* Effect.spanEvent(
|
|
403
|
-
|
|
404
|
-
)
|
|
405
|
-
return yield* connectionChannel.send(noTransferableResponse).pipe(
|
|
393
|
+
yield* Effect.spanEvent(`No transferable edges found for ${packet.source}→${packet.target}`)
|
|
394
|
+
return yield* edgeChannel.send(noTransferableResponse).pipe(
|
|
406
395
|
Effect.withSpan(`sendNoTransferableResponse:${packet.source}→${packet.target}`, {
|
|
407
396
|
attributes: packetAsOtelAttributes(noTransferableResponse),
|
|
408
397
|
}),
|
|
@@ -422,31 +411,31 @@ export const makeMeshNode = <TName extends MeshNodeName>(
|
|
|
422
411
|
Effect.forkScoped,
|
|
423
412
|
)
|
|
424
413
|
|
|
425
|
-
|
|
414
|
+
edgeChannels.set(targetNodeName, { channel: edgeChannel, listenFiber })
|
|
426
415
|
|
|
427
|
-
const
|
|
416
|
+
const edgeAddedPacket = WebmeshSchema.NetworkEdgeAdded.make({
|
|
428
417
|
source: nodeName,
|
|
429
418
|
target: targetNodeName,
|
|
430
419
|
})
|
|
431
|
-
yield* sendPacket(
|
|
420
|
+
yield* sendPacket(edgeAddedPacket).pipe(Effect.orDie)
|
|
432
421
|
}).pipe(
|
|
433
|
-
Effect.withSpan(`
|
|
434
|
-
attributes: { supportsTransferables:
|
|
422
|
+
Effect.withSpan(`addEdge:${nodeName}→${targetNodeName}`, {
|
|
423
|
+
attributes: { supportsTransferables: edgeChannel.supportsTransferables },
|
|
435
424
|
}),
|
|
436
425
|
) as any // any-cast needed for error/never overload
|
|
437
426
|
|
|
438
|
-
const
|
|
427
|
+
const removeEdge: MeshNode['removeEdge'] = (targetNodeName) =>
|
|
439
428
|
Effect.gen(function* () {
|
|
440
|
-
if (!
|
|
441
|
-
yield* new Cause.NoSuchElementException(`No
|
|
429
|
+
if (!edgeChannels.has(targetNodeName)) {
|
|
430
|
+
yield* new Cause.NoSuchElementException(`No edge found for ${targetNodeName}`)
|
|
442
431
|
}
|
|
443
432
|
|
|
444
|
-
yield* Fiber.interrupt(
|
|
433
|
+
yield* Fiber.interrupt(edgeChannels.get(targetNodeName)!.listenFiber)
|
|
445
434
|
|
|
446
|
-
|
|
435
|
+
edgeChannels.delete(targetNodeName)
|
|
447
436
|
})
|
|
448
437
|
|
|
449
|
-
// TODO add heartbeat to detect dead
|
|
438
|
+
// TODO add heartbeat to detect dead edges (for both e2e and proxying)
|
|
450
439
|
// TODO when a channel is established in the same origin, we can use a weblock to detect disconnects
|
|
451
440
|
const makeChannel: MeshNode['makeChannel'] = ({
|
|
452
441
|
target,
|
|
@@ -491,26 +480,26 @@ export const makeMeshNode = <TName extends MeshNodeName>(
|
|
|
491
480
|
)
|
|
492
481
|
|
|
493
482
|
// NOTE already retries internally when transferables are required
|
|
494
|
-
const { webChannel,
|
|
483
|
+
const { webChannel, initialEdgeDeferred } = yield* makeMessageChannel({
|
|
495
484
|
nodeName,
|
|
496
485
|
incomingPacketsQueue,
|
|
497
|
-
|
|
486
|
+
newEdgeAvailablePubSub,
|
|
498
487
|
target,
|
|
499
488
|
channelName,
|
|
500
489
|
schema,
|
|
501
490
|
sendPacket,
|
|
502
|
-
|
|
491
|
+
checkTransferableEdges,
|
|
503
492
|
})
|
|
504
493
|
|
|
505
494
|
channelMap.set(channelKey, { queue, debugInfo: { channel: webChannel, target } })
|
|
506
495
|
|
|
507
|
-
yield*
|
|
496
|
+
yield* initialEdgeDeferred
|
|
508
497
|
|
|
509
498
|
return webChannel
|
|
510
499
|
} else {
|
|
511
500
|
const channel = yield* makeProxyChannel({
|
|
512
501
|
nodeName,
|
|
513
|
-
|
|
502
|
+
newEdgeAvailablePubSub,
|
|
514
503
|
target,
|
|
515
504
|
channelName,
|
|
516
505
|
schema,
|
|
@@ -579,7 +568,7 @@ export const makeMeshNode = <TName extends MeshNodeName>(
|
|
|
579
568
|
}),
|
|
580
569
|
)
|
|
581
570
|
|
|
582
|
-
const
|
|
571
|
+
const edgeKeys: MeshNode['edgeKeys'] = Effect.sync(() => new Set(edgeChannels.keys()))
|
|
583
572
|
|
|
584
573
|
const runtime = yield* Effect.runtime()
|
|
585
574
|
|
|
@@ -587,8 +576,8 @@ export const makeMeshNode = <TName extends MeshNodeName>(
|
|
|
587
576
|
print: () => {
|
|
588
577
|
console.log('Webmesh debug info for node:', nodeName)
|
|
589
578
|
|
|
590
|
-
console.log('
|
|
591
|
-
for (const [key, value] of
|
|
579
|
+
console.log('Edges:', edgeChannels.size)
|
|
580
|
+
for (const [key, value] of edgeChannels) {
|
|
592
581
|
console.log(` ${key}: supportsTransferables=${value.channel.supportsTransferables}`)
|
|
593
582
|
}
|
|
594
583
|
|
|
@@ -620,11 +609,11 @@ export const makeMeshNode = <TName extends MeshNodeName>(
|
|
|
620
609
|
ping: (payload) => {
|
|
621
610
|
Effect.gen(function* () {
|
|
622
611
|
const msg = (via: string) =>
|
|
623
|
-
WebChannel.DebugPingMessage.make({ message: `ping from ${nodeName} via
|
|
612
|
+
WebChannel.DebugPingMessage.make({ message: `ping from ${nodeName} via edge ${via}`, payload })
|
|
624
613
|
|
|
625
|
-
for (const [channelName, con] of
|
|
626
|
-
yield* Effect.logDebug(`sending ping via
|
|
627
|
-
yield* con.channel.send(msg(`
|
|
614
|
+
for (const [channelName, con] of edgeChannels) {
|
|
615
|
+
yield* Effect.logDebug(`sending ping via edge ${channelName}`)
|
|
616
|
+
yield* con.channel.send(msg(`edge ${channelName}`) as any)
|
|
628
617
|
}
|
|
629
618
|
|
|
630
619
|
for (const [channelKey, channel] of channelMap) {
|
|
@@ -636,14 +625,14 @@ export const makeMeshNode = <TName extends MeshNodeName>(
|
|
|
636
625
|
},
|
|
637
626
|
requestTopology: (timeoutMs = 1000) =>
|
|
638
627
|
Effect.gen(function* () {
|
|
639
|
-
const packet = WebmeshSchema.
|
|
628
|
+
const packet = WebmeshSchema.NetworkTopologyRequest.make({
|
|
640
629
|
source: nodeName,
|
|
641
630
|
target: '-',
|
|
642
631
|
hops: [],
|
|
643
632
|
})
|
|
644
633
|
|
|
645
634
|
const item = new Map<MeshNodeName, Set<MeshNodeName>>()
|
|
646
|
-
item.set(nodeName, new Set(
|
|
635
|
+
item.set(nodeName, new Set(edgeChannels.keys()))
|
|
647
636
|
topologyRequestsMap.set(packet.id, item)
|
|
648
637
|
|
|
649
638
|
yield* sendPacket(packet)
|
|
@@ -659,11 +648,11 @@ export const makeMeshNode = <TName extends MeshNodeName>(
|
|
|
659
648
|
|
|
660
649
|
return {
|
|
661
650
|
nodeName,
|
|
662
|
-
|
|
663
|
-
|
|
651
|
+
addEdge,
|
|
652
|
+
removeEdge,
|
|
664
653
|
makeChannel,
|
|
665
654
|
makeBroadcastChannel,
|
|
666
|
-
|
|
655
|
+
edgeKeys,
|
|
667
656
|
debug,
|
|
668
657
|
} satisfies MeshNode
|
|
669
658
|
}).pipe(Effect.withSpan(`makeMeshNode:${nodeName}`))
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import type { HttpClient } from '@livestore/utils/effect'
|
|
1
2
|
import {
|
|
2
3
|
Deferred,
|
|
3
4
|
Effect,
|
|
@@ -16,18 +17,18 @@ import type * as NodeWebSocket from 'ws'
|
|
|
16
17
|
import * as WebmeshSchema from './mesh-schema.js'
|
|
17
18
|
import type { MeshNode } from './node.js'
|
|
18
19
|
|
|
19
|
-
export class
|
|
20
|
+
export class WSEdgeInit extends Schema.TaggedStruct('WSEdgeInit', {
|
|
20
21
|
from: Schema.String,
|
|
21
22
|
}) {}
|
|
22
23
|
|
|
23
|
-
export class
|
|
24
|
+
export class WSEdgePayload extends Schema.TaggedStruct('WSEdgePayload', {
|
|
24
25
|
from: Schema.String,
|
|
25
26
|
payload: Schema.Any,
|
|
26
27
|
}) {}
|
|
27
28
|
|
|
28
|
-
export class
|
|
29
|
+
export class WSEdgeMessage extends Schema.Union(WSEdgeInit, WSEdgePayload) {}
|
|
29
30
|
|
|
30
|
-
export const MessageMsgPack = Schema.MsgPack(
|
|
31
|
+
export const MessageMsgPack = Schema.MsgPack(WSEdgeMessage)
|
|
31
32
|
|
|
32
33
|
export type SocketType =
|
|
33
34
|
| {
|
|
@@ -46,7 +47,7 @@ export const connectViaWebSocket = ({
|
|
|
46
47
|
node: MeshNode
|
|
47
48
|
url: string
|
|
48
49
|
reconnect?: Schedule.Schedule<unknown> | false
|
|
49
|
-
}): Effect.Effect<void, never, Scope.Scope> =>
|
|
50
|
+
}): Effect.Effect<void, never, Scope.Scope | HttpClient.HttpClient> =>
|
|
50
51
|
Effect.gen(function* () {
|
|
51
52
|
const disconnected = yield* Deferred.make<void>()
|
|
52
53
|
|
|
@@ -54,14 +55,14 @@ export const connectViaWebSocket = ({
|
|
|
54
55
|
|
|
55
56
|
socket.addEventListener('close', () => Deferred.unsafeDone(disconnected, Exit.void))
|
|
56
57
|
|
|
57
|
-
const
|
|
58
|
+
const edgeChannel = yield* makeWebSocketEdge(socket, { _tag: 'leaf', from: node.nodeName })
|
|
58
59
|
|
|
59
|
-
yield* node.
|
|
60
|
+
yield* node.addEdge({ target: 'ws', edgeChannel: edgeChannel.webChannel, replaceIfExists: true })
|
|
60
61
|
|
|
61
62
|
yield* disconnected
|
|
62
63
|
}).pipe(Effect.scoped, Effect.forever, Effect.catchTag('WebSocketError', Effect.orDie))
|
|
63
64
|
|
|
64
|
-
export const
|
|
65
|
+
export const makeWebSocketEdge = (
|
|
65
66
|
socket: globalThis.WebSocket | NodeWebSocket.WebSocket,
|
|
66
67
|
socketType: SocketType,
|
|
67
68
|
): Effect.Effect<
|
|
@@ -70,7 +71,7 @@ export const makeWebSocketConnection = (
|
|
|
70
71
|
from: string
|
|
71
72
|
},
|
|
72
73
|
never,
|
|
73
|
-
Scope.Scope
|
|
74
|
+
Scope.Scope | HttpClient.HttpClient
|
|
74
75
|
> =>
|
|
75
76
|
Effect.scopeWithCloseable((scope) =>
|
|
76
77
|
Effect.gen(function* () {
|
|
@@ -89,7 +90,7 @@ export const makeWebSocketConnection = (
|
|
|
89
90
|
Stream.flatten(),
|
|
90
91
|
Stream.tap((msg) =>
|
|
91
92
|
Effect.gen(function* () {
|
|
92
|
-
if (msg._tag === '
|
|
93
|
+
if (msg._tag === 'WSEdgeInit') {
|
|
93
94
|
yield* Deferred.succeed(fromDeferred, msg.from)
|
|
94
95
|
} else {
|
|
95
96
|
const decodedPayload = yield* Schema.decode(schema.listen)(msg.payload)
|
|
@@ -104,7 +105,7 @@ export const makeWebSocketConnection = (
|
|
|
104
105
|
)
|
|
105
106
|
|
|
106
107
|
const initHandshake = (from: string) =>
|
|
107
|
-
socket.send(Schema.encodeSync(MessageMsgPack)({ _tag: '
|
|
108
|
+
socket.send(Schema.encodeSync(MessageMsgPack)({ _tag: 'WSEdgeInit', from }))
|
|
108
109
|
|
|
109
110
|
if (socketType._tag === 'leaf') {
|
|
110
111
|
initHandshake(socketType.from)
|
|
@@ -136,12 +137,12 @@ export const makeWebSocketConnection = (
|
|
|
136
137
|
Effect.gen(function* () {
|
|
137
138
|
yield* isConnectedLatch.await
|
|
138
139
|
const payload = yield* Schema.encode(schema.send)(message)
|
|
139
|
-
socket.send(Schema.encodeSync(MessageMsgPack)({ _tag: '
|
|
140
|
+
socket.send(Schema.encodeSync(MessageMsgPack)({ _tag: 'WSEdgePayload', payload, from }))
|
|
140
141
|
})
|
|
141
142
|
|
|
142
143
|
const listen = Stream.fromQueue(listenQueue).pipe(
|
|
143
144
|
Stream.map(Either.right),
|
|
144
|
-
WebChannel.listenToDebugPing('websocket-
|
|
145
|
+
WebChannel.listenToDebugPing('websocket-edge'),
|
|
145
146
|
)
|
|
146
147
|
|
|
147
148
|
const webChannel = {
|
|
@@ -155,5 +156,5 @@ export const makeWebSocketConnection = (
|
|
|
155
156
|
} satisfies WebChannel.WebChannel<typeof WebmeshSchema.Packet.Type, typeof WebmeshSchema.Packet.Type>
|
|
156
157
|
|
|
157
158
|
return { webChannel, from }
|
|
158
|
-
}).pipe(Effect.withSpanScoped('
|
|
159
|
+
}).pipe(Effect.withSpanScoped('makeWebSocketEdge')),
|
|
159
160
|
)
|
package/src/websocket-server.ts
CHANGED
|
@@ -1,16 +1,16 @@
|
|
|
1
1
|
import { UnexpectedError } from '@livestore/common'
|
|
2
|
-
import type { Scope } from '@livestore/utils/effect'
|
|
2
|
+
import type { HttpClient, Scope } from '@livestore/utils/effect'
|
|
3
3
|
import { Effect, FiberSet } from '@livestore/utils/effect'
|
|
4
4
|
import * as WebSocket from 'ws'
|
|
5
5
|
|
|
6
6
|
import { makeMeshNode } from './node.js'
|
|
7
|
-
import {
|
|
7
|
+
import { makeWebSocketEdge } from './websocket-edge.js'
|
|
8
8
|
|
|
9
9
|
export const makeWebSocketServer = ({
|
|
10
10
|
relayNodeName,
|
|
11
11
|
}: {
|
|
12
12
|
relayNodeName: string
|
|
13
|
-
}): Effect.Effect<WebSocket.WebSocketServer, never, Scope.Scope> =>
|
|
13
|
+
}): Effect.Effect<WebSocket.WebSocketServer, never, Scope.Scope | HttpClient.HttpClient> =>
|
|
14
14
|
Effect.gen(function* () {
|
|
15
15
|
const server = new WebSocket.WebSocketServer({ noServer: true })
|
|
16
16
|
|
|
@@ -30,22 +30,22 @@ export const makeWebSocketServer = ({
|
|
|
30
30
|
|
|
31
31
|
const node = yield* makeMeshNode(relayNodeName)
|
|
32
32
|
|
|
33
|
-
const runtime = yield* Effect.runtime<
|
|
33
|
+
const runtime = yield* Effect.runtime<HttpClient.HttpClient>()
|
|
34
34
|
|
|
35
35
|
const fiberSet = yield* FiberSet.make()
|
|
36
36
|
|
|
37
37
|
// TODO handle node disconnects (i.e. remove respective connection)
|
|
38
38
|
server.on('connection', (socket) => {
|
|
39
39
|
Effect.gen(function* () {
|
|
40
|
-
const { webChannel, from } = yield*
|
|
40
|
+
const { webChannel, from } = yield* makeWebSocketEdge(socket, { _tag: 'relay' })
|
|
41
41
|
|
|
42
|
-
yield* node.
|
|
43
|
-
yield* Effect.log(`WS Relay ${relayNodeName}: added
|
|
42
|
+
yield* node.addEdge({ target: from, edgeChannel: webChannel, replaceIfExists: true })
|
|
43
|
+
yield* Effect.log(`WS Relay ${relayNodeName}: added edge from '${from}'`)
|
|
44
44
|
|
|
45
45
|
socket.addEventListener('close', () =>
|
|
46
46
|
Effect.gen(function* () {
|
|
47
|
-
yield* node.
|
|
48
|
-
yield* Effect.log(`WS Relay ${relayNodeName}: removed
|
|
47
|
+
yield* node.removeEdge(from)
|
|
48
|
+
yield* Effect.log(`WS Relay ${relayNodeName}: removed edge from '${from}'`)
|
|
49
49
|
}).pipe(Effect.provide(runtime), Effect.tapCauseLogPretty, Effect.runFork),
|
|
50
50
|
)
|
|
51
51
|
|