@livestore/webmesh 0.4.0-dev.9 → 0.5.0-dev.0
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 +5 -3
- package/dist/.tsbuildinfo +1 -1
- package/dist/channel/direct-channel-internal.d.ts +2 -3
- package/dist/channel/direct-channel-internal.d.ts.map +1 -1
- package/dist/channel/direct-channel-internal.js +35 -38
- package/dist/channel/direct-channel-internal.js.map +1 -1
- package/dist/channel/direct-channel.d.ts.map +1 -1
- package/dist/channel/direct-channel.js +22 -23
- package/dist/channel/direct-channel.js.map +1 -1
- package/dist/channel/proxy-channel.d.ts +29 -5
- package/dist/channel/proxy-channel.d.ts.map +1 -1
- package/dist/channel/proxy-channel.js +87 -37
- package/dist/channel/proxy-channel.js.map +1 -1
- package/dist/common.d.ts +33 -35
- package/dist/common.d.ts.map +1 -1
- package/dist/common.js +7 -7
- package/dist/common.js.map +1 -1
- package/dist/mesh-schema.d.ts +262 -136
- package/dist/mesh-schema.d.ts.map +1 -1
- package/dist/mesh-schema.js +45 -44
- package/dist/mesh-schema.js.map +1 -1
- package/dist/node.d.ts +12 -6
- package/dist/node.d.ts.map +1 -1
- package/dist/node.js +47 -44
- package/dist/node.js.map +1 -1
- package/dist/node.test.js +253 -63
- package/dist/node.test.js.map +1 -1
- package/dist/utils.d.ts +1 -1
- package/dist/utils.d.ts.map +1 -1
- package/dist/utils.js.map +1 -1
- package/dist/websocket-edge.d.ts +20 -18
- package/dist/websocket-edge.d.ts.map +1 -1
- package/dist/websocket-edge.js +33 -41
- package/dist/websocket-edge.js.map +1 -1
- package/dist/websocket-edge.test.d.ts +7 -0
- package/dist/websocket-edge.test.d.ts.map +1 -0
- package/dist/websocket-edge.test.js +74 -0
- package/dist/websocket-edge.test.js.map +1 -0
- package/dist/worker/mod.d.ts +24 -0
- package/dist/worker/mod.d.ts.map +1 -0
- package/dist/worker/mod.js +44 -0
- package/dist/worker/mod.js.map +1 -0
- package/dist/worker/schema.d.ts +10 -0
- package/dist/worker/schema.d.ts.map +1 -0
- package/dist/worker/schema.js +7 -0
- package/dist/worker/schema.js.map +1 -0
- package/package.json +48 -12
- package/src/channel/direct-channel-internal.ts +41 -47
- package/src/channel/direct-channel.ts +28 -25
- package/src/channel/proxy-channel.ts +120 -57
- package/src/common.ts +9 -7
- package/src/mesh-schema.ts +34 -33
- package/src/node.test.ts +356 -80
- package/src/node.ts +63 -55
- package/src/utils.ts +3 -3
- package/src/websocket-edge.test.ts +98 -0
- package/src/websocket-edge.ts +52 -48
- package/src/worker/mod.ts +89 -0
- package/src/worker/schema.ts +8 -0
package/src/node.ts
CHANGED
|
@@ -6,7 +6,6 @@ import {
|
|
|
6
6
|
Effect,
|
|
7
7
|
Exit,
|
|
8
8
|
Fiber,
|
|
9
|
-
Option,
|
|
10
9
|
PubSub,
|
|
11
10
|
Queue,
|
|
12
11
|
Schema,
|
|
@@ -16,7 +15,7 @@ import {
|
|
|
16
15
|
} from '@livestore/utils/effect'
|
|
17
16
|
|
|
18
17
|
import { makeDirectChannel } from './channel/direct-channel.ts'
|
|
19
|
-
import { makeProxyChannel } from './channel/proxy-channel.ts'
|
|
18
|
+
import { makeProxyChannel, type ProxyChannelSimulationParams } from './channel/proxy-channel.ts'
|
|
20
19
|
import type { ChannelKey, ListenForChannelResult, MeshNodeName, MessageQueueItem, ProxyQueueItem } from './common.ts'
|
|
21
20
|
import { EdgeAlreadyExistsError, packetAsOtelAttributes } from './common.ts'
|
|
22
21
|
import * as WebmeshSchema from './mesh-schema.ts'
|
|
@@ -61,7 +60,7 @@ export interface MeshNode<TName extends MeshNodeName = MeshNodeName> {
|
|
|
61
60
|
}): Effect.Effect<void, EdgeAlreadyExistsError, Scope.Scope>
|
|
62
61
|
}
|
|
63
62
|
|
|
64
|
-
removeEdge: (targetNodeName: MeshNodeName) => Effect.Effect<void, Cause.
|
|
63
|
+
removeEdge: (targetNodeName: MeshNodeName) => Effect.Effect<void, Cause.NoSuchElementError>
|
|
65
64
|
|
|
66
65
|
hasChannel: ({
|
|
67
66
|
target,
|
|
@@ -93,10 +92,10 @@ export interface MeshNode<TName extends MeshNodeName = MeshNodeName> {
|
|
|
93
92
|
*/
|
|
94
93
|
channelName: string
|
|
95
94
|
schema:
|
|
96
|
-
| Schema.
|
|
95
|
+
| Schema.Codec<MsgListen | MsgSend, any>
|
|
97
96
|
| {
|
|
98
|
-
listen: Schema.
|
|
99
|
-
send: Schema.
|
|
97
|
+
listen: Schema.Codec<MsgListen, any>
|
|
98
|
+
send: Schema.Codec<MsgSend, any>
|
|
100
99
|
}
|
|
101
100
|
/**
|
|
102
101
|
* If possible, prefer using a DirectChannel with transferables (i.e. transferring memory instead of copying it).
|
|
@@ -107,13 +106,18 @@ export interface MeshNode<TName extends MeshNodeName = MeshNodeName> {
|
|
|
107
106
|
*
|
|
108
107
|
* @default 1 second
|
|
109
108
|
*/
|
|
110
|
-
timeout?: Duration.
|
|
109
|
+
timeout?: Duration.Input
|
|
111
110
|
/**
|
|
112
111
|
* If true, will close an existing channel if it exists.
|
|
113
112
|
*
|
|
114
113
|
* @default false
|
|
115
114
|
*/
|
|
116
115
|
closeExisting?: boolean
|
|
116
|
+
/**
|
|
117
|
+
* Optional simulation parameters for testing timing-sensitive behavior.
|
|
118
|
+
* Only used when mode is 'proxy'.
|
|
119
|
+
*/
|
|
120
|
+
simulation?: ProxyChannelSimulationParams
|
|
117
121
|
}) => Effect.Effect<WebChannel.WebChannel<MsgListen, MsgSend>, never, Scope.Scope>
|
|
118
122
|
|
|
119
123
|
listenForChannel: Stream.Stream<ListenForChannelResult>
|
|
@@ -124,7 +128,7 @@ export interface MeshNode<TName extends MeshNodeName = MeshNodeName> {
|
|
|
124
128
|
*/
|
|
125
129
|
makeBroadcastChannel: <Msg>(args: {
|
|
126
130
|
channelName: string
|
|
127
|
-
schema: Schema.
|
|
131
|
+
schema: Schema.Codec<Msg, any>
|
|
128
132
|
}) => Effect.Effect<WebChannel.WebChannel<Msg, Msg>, never, Scope.Scope>
|
|
129
133
|
}
|
|
130
134
|
|
|
@@ -132,16 +136,17 @@ export const makeMeshNode = <TName extends MeshNodeName>(
|
|
|
132
136
|
nodeName: TName,
|
|
133
137
|
): Effect.Effect<MeshNode<TName>, never, Scope.Scope> =>
|
|
134
138
|
Effect.gen(function* () {
|
|
135
|
-
const edgeChannels = new Map<MeshNodeName, { channel: EdgeChannel; listenFiber: Fiber.
|
|
139
|
+
const edgeChannels = new Map<MeshNodeName, { channel: EdgeChannel; listenFiber: Fiber.Fiber<void> }>()
|
|
136
140
|
|
|
137
141
|
// To avoid unbounded memory growth, we automatically forget about packet ids after a while
|
|
138
142
|
const handledPacketIds = yield* TimeoutSet.make(Duration.minutes(1))
|
|
139
143
|
|
|
140
|
-
const newEdgeAvailablePubSub = yield* PubSub.unbounded<MeshNodeName>()
|
|
144
|
+
const newEdgeAvailablePubSub = yield* Effect.acquireRelease(PubSub.unbounded<MeshNodeName>(), PubSub.shutdown)
|
|
141
145
|
|
|
142
|
-
// const proxyPacketsToProcess = yield* Queue.unbounded<ProxyQueueItem>()
|
|
143
|
-
// const messagePacketsToProcess = yield*
|
|
144
|
-
//
|
|
146
|
+
// const proxyPacketsToProcess = yield* Effect.acquireRelease(Queue.unbounded<ProxyQueueItem>(), Queue.shutdown)
|
|
147
|
+
// const messagePacketsToProcess = yield* Effect.acquireRelease(
|
|
148
|
+
// Queue.unbounded<MessageQueueItem>(),
|
|
149
|
+
// Queue.shutdown,
|
|
145
150
|
// )
|
|
146
151
|
|
|
147
152
|
const channelMap = new Map<
|
|
@@ -158,9 +163,7 @@ export const makeMeshNode = <TName extends MeshNodeName>(
|
|
|
158
163
|
}
|
|
159
164
|
>()
|
|
160
165
|
|
|
161
|
-
const channelRequestsQueue = yield* Queue.unbounded<ListenForChannelResult>().
|
|
162
|
-
Effect.acquireRelease(Queue.shutdown),
|
|
163
|
-
)
|
|
166
|
+
const channelRequestsQueue = yield* Effect.acquireRelease(Queue.unbounded<ListenForChannelResult>(), Queue.shutdown)
|
|
164
167
|
|
|
165
168
|
type RequestId = string
|
|
166
169
|
const topologyRequestsMap = new Map<RequestId, Map<MeshNodeName, Set<MeshNodeName>>>()
|
|
@@ -173,7 +176,7 @@ export const makeMeshNode = <TName extends MeshNodeName>(
|
|
|
173
176
|
(packet._tag === 'DirectChannelRequest' &&
|
|
174
177
|
(edgeChannels.size === 0 || // Either if direct edge does not support transferables ...
|
|
175
178
|
edgeChannels.get(packet.target)?.channel.supportsTransferables === false)) || // ... or if no forward-edges support transferables
|
|
176
|
-
|
|
179
|
+
[...edgeChannels.values()].some((c) => c.channel.supportsTransferables) === false
|
|
177
180
|
) {
|
|
178
181
|
return WebmeshSchema.DirectChannelResponseNoTransferables.make({
|
|
179
182
|
reqId: packet.id,
|
|
@@ -187,13 +190,14 @@ export const makeMeshNode = <TName extends MeshNodeName>(
|
|
|
187
190
|
hops: [],
|
|
188
191
|
})
|
|
189
192
|
}
|
|
193
|
+
return undefined
|
|
190
194
|
}
|
|
191
195
|
|
|
192
196
|
const sendPacket = (packet: typeof WebmeshSchema.Packet.Type) =>
|
|
193
197
|
Effect.gen(function* () {
|
|
194
198
|
// yield* Effect.log(`${nodeName}: sendPacket:${packet._tag} [${packet.id}]`)
|
|
195
199
|
|
|
196
|
-
if (Schema.is(WebmeshSchema.NetworkEdgeAdded)(packet)) {
|
|
200
|
+
if (Schema.is(WebmeshSchema.NetworkEdgeAdded)(packet) === true) {
|
|
197
201
|
yield* Effect.spanEvent('NetworkEdgeAdded', { packet, nodeName })
|
|
198
202
|
yield* PubSub.publish(newEdgeAvailablePubSub, packet.target)
|
|
199
203
|
|
|
@@ -205,7 +209,7 @@ export const makeMeshNode = <TName extends MeshNodeName>(
|
|
|
205
209
|
return
|
|
206
210
|
}
|
|
207
211
|
|
|
208
|
-
if (Schema.is(WebmeshSchema.BroadcastChannelPacket)(packet)) {
|
|
212
|
+
if (Schema.is(WebmeshSchema.BroadcastChannelPacket)(packet) === true) {
|
|
209
213
|
const edgesToForwardTo = Array.from(edgeChannels)
|
|
210
214
|
.filter(([name]) => !packet.hops.includes(name))
|
|
211
215
|
.map(([_, con]) => con.channel)
|
|
@@ -231,7 +235,7 @@ export const makeMeshNode = <TName extends MeshNodeName>(
|
|
|
231
235
|
return
|
|
232
236
|
}
|
|
233
237
|
|
|
234
|
-
if (Schema.is(WebmeshSchema.NetworkTopologyRequest)(packet)) {
|
|
238
|
+
if (Schema.is(WebmeshSchema.NetworkTopologyRequest)(packet) === true) {
|
|
235
239
|
if (packet.source !== nodeName) {
|
|
236
240
|
const backEdgeName =
|
|
237
241
|
packet.hops.at(-1) ?? shouldNeverHappen(`${nodeName}: Expected hops for packet`, packet)
|
|
@@ -265,7 +269,7 @@ export const makeMeshNode = <TName extends MeshNodeName>(
|
|
|
265
269
|
return
|
|
266
270
|
}
|
|
267
271
|
|
|
268
|
-
if (Schema.is(WebmeshSchema.NetworkTopologyResponse)(packet)) {
|
|
272
|
+
if (Schema.is(WebmeshSchema.NetworkTopologyResponse)(packet) === true) {
|
|
269
273
|
if (packet.source === nodeName) {
|
|
270
274
|
const topologyRequestItem = topologyRequestsMap.get(packet.reqId)!
|
|
271
275
|
topologyRequestItem.set(packet.nodeName, new Set(packet.edges))
|
|
@@ -289,7 +293,7 @@ export const makeMeshNode = <TName extends MeshNodeName>(
|
|
|
289
293
|
}
|
|
290
294
|
|
|
291
295
|
// We have a direct edge to the target node
|
|
292
|
-
if (edgeChannels.has(packet.target)) {
|
|
296
|
+
if (edgeChannels.has(packet.target) === true) {
|
|
293
297
|
const edgeChannel = edgeChannels.get(packet.target)!.channel
|
|
294
298
|
const hops = packet.source === nodeName ? [] : [...packet.hops, nodeName]
|
|
295
299
|
yield* Effect.annotateCurrentSpan({ hasDirectEdge: true })
|
|
@@ -325,7 +329,7 @@ export const makeMeshNode = <TName extends MeshNodeName>(
|
|
|
325
329
|
.map(([name, con]) => ({ name, channel: con.channel }))
|
|
326
330
|
|
|
327
331
|
// TODO if hops-depth=0, we should fail right away with no route found
|
|
328
|
-
if (hops.length === 0 && edgesToForwardTo.length === 0 && LS_DEV) {
|
|
332
|
+
if (hops.length === 0 && edgesToForwardTo.length === 0 && LS_DEV === true) {
|
|
329
333
|
yield* Effect.logWarning(nodeName, 'no route found to', packet.target, packet._tag, 'TODO handle better')
|
|
330
334
|
// TODO return a expected failure
|
|
331
335
|
}
|
|
@@ -348,8 +352,8 @@ export const makeMeshNode = <TName extends MeshNodeName>(
|
|
|
348
352
|
|
|
349
353
|
const addEdge: MeshNode['addEdge'] = ({ target: targetNodeName, edgeChannel, replaceIfExists = false }) =>
|
|
350
354
|
Effect.gen(function* () {
|
|
351
|
-
if (edgeChannels.has(targetNodeName)) {
|
|
352
|
-
if (replaceIfExists) {
|
|
355
|
+
if (edgeChannels.has(targetNodeName) === true) {
|
|
356
|
+
if (replaceIfExists === true) {
|
|
353
357
|
yield* removeEdge(targetNodeName).pipe(Effect.orDie)
|
|
354
358
|
} else {
|
|
355
359
|
return yield* new EdgeAlreadyExistsError({ target: targetNodeName })
|
|
@@ -358,14 +362,14 @@ export const makeMeshNode = <TName extends MeshNodeName>(
|
|
|
358
362
|
|
|
359
363
|
// TODO use a priority queue instead to prioritize network-changes/edge-requests over payloads
|
|
360
364
|
const listenFiber = yield* edgeChannel.listen.pipe(
|
|
361
|
-
Stream.
|
|
365
|
+
Stream.mapEffect(Effect.fromResult),
|
|
362
366
|
Stream.tap((message) =>
|
|
363
367
|
Effect.gen(function* () {
|
|
364
|
-
const packet = yield* Schema.
|
|
368
|
+
const packet = yield* Schema.decodeUnknownEffect(WebmeshSchema.Packet)(message)
|
|
365
369
|
|
|
366
370
|
// console.debug(nodeName, 'recv', packet._tag, packet.source, packet.target)
|
|
367
371
|
|
|
368
|
-
if (handledPacketIds.has(packet.id)) return
|
|
372
|
+
if (handledPacketIds.has(packet.id) === true) return
|
|
369
373
|
handledPacketIds.add(packet.id)
|
|
370
374
|
|
|
371
375
|
switch (packet._tag) {
|
|
@@ -380,9 +384,10 @@ export const makeMeshNode = <TName extends MeshNodeName>(
|
|
|
380
384
|
if (packet.target === nodeName) {
|
|
381
385
|
const channelKey = `target:${packet.source}, channelName:${packet.channelName}` satisfies ChannelKey
|
|
382
386
|
|
|
383
|
-
if (
|
|
384
|
-
const channelQueue = yield*
|
|
385
|
-
|
|
387
|
+
if (channelMap.has(channelKey) === false) {
|
|
388
|
+
const channelQueue = yield* Effect.acquireRelease(
|
|
389
|
+
Queue.unbounded<MessageQueueItem | ProxyQueueItem>(),
|
|
390
|
+
Queue.shutdown,
|
|
386
391
|
)
|
|
387
392
|
channelMap.set(channelKey, { queue: channelQueue, debugInfo: undefined })
|
|
388
393
|
}
|
|
@@ -400,9 +405,9 @@ export const makeMeshNode = <TName extends MeshNodeName>(
|
|
|
400
405
|
Effect.orDie,
|
|
401
406
|
)
|
|
402
407
|
|
|
403
|
-
if (Schema.is(WebmeshSchema.ProxyChannelPacket)(packet)) {
|
|
408
|
+
if (Schema.is(WebmeshSchema.ProxyChannelPacket)(packet) === true) {
|
|
404
409
|
yield* Queue.offer(channelQueue, { packet, respondToSender })
|
|
405
|
-
} else if (Schema.is(WebmeshSchema.DirectChannelPacket)(packet)) {
|
|
410
|
+
} else if (Schema.is(WebmeshSchema.DirectChannelPacket)(packet) === true) {
|
|
406
411
|
yield* Queue.offer(channelQueue, { packet, respondToSender })
|
|
407
412
|
}
|
|
408
413
|
|
|
@@ -414,7 +419,7 @@ export const makeMeshNode = <TName extends MeshNodeName>(
|
|
|
414
419
|
})
|
|
415
420
|
}
|
|
416
421
|
} else {
|
|
417
|
-
if (Schema.is(WebmeshSchema.DirectChannelPacket)(packet)) {
|
|
422
|
+
if (Schema.is(WebmeshSchema.DirectChannelPacket)(packet) === true) {
|
|
418
423
|
const noTransferableResponse = checkTransferableEdges(packet)
|
|
419
424
|
if (noTransferableResponse !== undefined) {
|
|
420
425
|
yield* Effect.spanEvent(`No transferable edges found for ${packet.source}→${packet.target}`)
|
|
@@ -455,8 +460,8 @@ export const makeMeshNode = <TName extends MeshNodeName>(
|
|
|
455
460
|
|
|
456
461
|
const removeEdge: MeshNode['removeEdge'] = (targetNodeName) =>
|
|
457
462
|
Effect.gen(function* () {
|
|
458
|
-
if (
|
|
459
|
-
yield* new Cause.
|
|
463
|
+
if (edgeChannels.has(targetNodeName) === false) {
|
|
464
|
+
return yield* new Cause.NoSuchElementError(`No edge found for ${targetNodeName}`)
|
|
460
465
|
}
|
|
461
466
|
|
|
462
467
|
yield* Fiber.interrupt(edgeChannels.get(targetNodeName)!.listenFiber)
|
|
@@ -477,15 +482,16 @@ export const makeMeshNode = <TName extends MeshNodeName>(
|
|
|
477
482
|
mode,
|
|
478
483
|
timeout = Duration.seconds(1),
|
|
479
484
|
closeExisting = false,
|
|
485
|
+
simulation,
|
|
480
486
|
}) =>
|
|
481
487
|
Effect.gen(function* () {
|
|
482
488
|
const schema = WebChannel.mapSchema(inputSchema)
|
|
483
489
|
const channelKey = `target:${target}, channelName:${channelName}` satisfies ChannelKey
|
|
484
490
|
|
|
485
|
-
if (channelMap.has(channelKey)) {
|
|
491
|
+
if (channelMap.has(channelKey) === true) {
|
|
486
492
|
const existingChannel = channelMap.get(channelKey)!.debugInfo?.channel
|
|
487
|
-
if (existingChannel) {
|
|
488
|
-
if (closeExisting) {
|
|
493
|
+
if (existingChannel !== undefined) {
|
|
494
|
+
if (closeExisting === true) {
|
|
489
495
|
yield* existingChannel.shutdown
|
|
490
496
|
channelMap.delete(channelKey)
|
|
491
497
|
} else {
|
|
@@ -495,8 +501,9 @@ export const makeMeshNode = <TName extends MeshNodeName>(
|
|
|
495
501
|
}
|
|
496
502
|
|
|
497
503
|
if (channelMap.has(channelKey) === false) {
|
|
498
|
-
const channelQueue = yield*
|
|
499
|
-
|
|
504
|
+
const channelQueue = yield* Effect.acquireRelease(
|
|
505
|
+
Queue.unbounded<MessageQueueItem | ProxyQueueItem>(),
|
|
506
|
+
Queue.shutdown,
|
|
500
507
|
)
|
|
501
508
|
channelMap.set(channelKey, { queue: channelQueue, debugInfo: undefined })
|
|
502
509
|
}
|
|
@@ -506,7 +513,7 @@ export const makeMeshNode = <TName extends MeshNodeName>(
|
|
|
506
513
|
yield* Effect.addFinalizer(() => Effect.sync(() => channelMap.delete(channelKey)))
|
|
507
514
|
|
|
508
515
|
if (mode === 'direct') {
|
|
509
|
-
const incomingPacketsQueue = yield* Queue.unbounded<any>()
|
|
516
|
+
const incomingPacketsQueue = yield* Effect.acquireRelease(Queue.unbounded<any>(), Queue.shutdown)
|
|
510
517
|
|
|
511
518
|
// We're we're draining the queue into another new queue.
|
|
512
519
|
// It's a bit of a mystery why this is needed, since the unit tests also work without it.
|
|
@@ -534,7 +541,7 @@ export const makeMeshNode = <TName extends MeshNodeName>(
|
|
|
534
541
|
|
|
535
542
|
channelMap.set(channelKey, { queue: channelQueue, debugInfo: { channel: webChannel, target } })
|
|
536
543
|
|
|
537
|
-
yield* initialEdgeDeferred
|
|
544
|
+
yield* Deferred.await(initialEdgeDeferred)
|
|
538
545
|
|
|
539
546
|
return webChannel
|
|
540
547
|
} else {
|
|
@@ -546,6 +553,7 @@ export const makeMeshNode = <TName extends MeshNodeName>(
|
|
|
546
553
|
schema,
|
|
547
554
|
queue: channelQueue,
|
|
548
555
|
sendPacket,
|
|
556
|
+
...(simulation !== undefined ? { simulation } : {}),
|
|
549
557
|
})
|
|
550
558
|
|
|
551
559
|
channelMap.set(channelKey, { queue: channelQueue, debugInfo: { channel, target } })
|
|
@@ -564,7 +572,7 @@ export const makeMeshNode = <TName extends MeshNodeName>(
|
|
|
564
572
|
// TODO also provide a way to allow for reconnects
|
|
565
573
|
let listenAlreadyStarted = false
|
|
566
574
|
const listenForChannel: MeshNode['listenForChannel'] = Stream.suspend(() => {
|
|
567
|
-
if (listenAlreadyStarted) {
|
|
575
|
+
if (listenAlreadyStarted === true) {
|
|
568
576
|
return shouldNeverHappen('listenForChannel already started')
|
|
569
577
|
}
|
|
570
578
|
listenAlreadyStarted = true
|
|
@@ -576,7 +584,7 @@ export const makeMeshNode = <TName extends MeshNodeName>(
|
|
|
576
584
|
return Stream.fromQueue(channelRequestsQueue).pipe(
|
|
577
585
|
Stream.filter((res) => {
|
|
578
586
|
const hashed = hash(res)
|
|
579
|
-
if (seen.has(hashed)) {
|
|
587
|
+
if (seen.has(hashed) === true) {
|
|
580
588
|
return false
|
|
581
589
|
}
|
|
582
590
|
seen.add(hashed)
|
|
@@ -588,7 +596,7 @@ export const makeMeshNode = <TName extends MeshNodeName>(
|
|
|
588
596
|
const makeBroadcastChannel: MeshNode['makeBroadcastChannel'] = ({ channelName, schema }) =>
|
|
589
597
|
Effect.scopeWithCloseable((scope) =>
|
|
590
598
|
Effect.gen(function* () {
|
|
591
|
-
if (broadcastChannelListenQueueMap.has(channelName)) {
|
|
599
|
+
if (broadcastChannelListenQueueMap.has(channelName) === true) {
|
|
592
600
|
return shouldNeverHappen(
|
|
593
601
|
`Broadcast channel ${channelName} already exists`,
|
|
594
602
|
broadcastChannelListenQueueMap.get(channelName),
|
|
@@ -597,12 +605,12 @@ export const makeMeshNode = <TName extends MeshNodeName>(
|
|
|
597
605
|
|
|
598
606
|
const debugInfo = {}
|
|
599
607
|
|
|
600
|
-
const queue = yield* Queue.unbounded<any>()
|
|
608
|
+
const queue = yield* Effect.acquireRelease(Queue.unbounded<any>(), Queue.shutdown)
|
|
601
609
|
broadcastChannelListenQueueMap.set(channelName, queue)
|
|
602
610
|
|
|
603
611
|
const send = (message: any) =>
|
|
604
612
|
Effect.gen(function* () {
|
|
605
|
-
const payload = yield* Schema.
|
|
613
|
+
const payload = yield* Schema.encodeEffect(schema)(message)
|
|
606
614
|
const packet = WebmeshSchema.BroadcastChannelPacket.make({
|
|
607
615
|
channelName,
|
|
608
616
|
payload,
|
|
@@ -616,10 +624,10 @@ export const makeMeshNode = <TName extends MeshNodeName>(
|
|
|
616
624
|
|
|
617
625
|
const listen = Stream.fromQueue(queue).pipe(
|
|
618
626
|
Stream.filter(Schema.is(WebmeshSchema.BroadcastChannelPacket)),
|
|
619
|
-
Stream.map((_) => Schema.
|
|
627
|
+
Stream.map((_) => Schema.decodeResult(schema)(_.payload)),
|
|
620
628
|
)
|
|
621
629
|
|
|
622
|
-
const closedDeferred = yield* Deferred.make<void>()
|
|
630
|
+
const closedDeferred = yield* Effect.acquireRelease(Deferred.make<void>(), Deferred.done(Exit.void))
|
|
623
631
|
|
|
624
632
|
return {
|
|
625
633
|
[WebChannel.WebChannelSymbol]: WebChannel.WebChannelSymbol,
|
|
@@ -636,7 +644,7 @@ export const makeMeshNode = <TName extends MeshNodeName>(
|
|
|
636
644
|
|
|
637
645
|
const edgeKeys: MeshNode['edgeKeys'] = Effect.sync(() => new Set(edgeChannels.keys()))
|
|
638
646
|
|
|
639
|
-
const
|
|
647
|
+
const services = yield* Effect.context()
|
|
640
648
|
|
|
641
649
|
const debug: MeshNode['debug'] = {
|
|
642
650
|
print: () => {
|
|
@@ -657,12 +665,12 @@ export const makeMeshNode = <TName extends MeshNodeName>(
|
|
|
657
665
|
supportsTransferables: value.debugInfo?.channel.supportsTransferables,
|
|
658
666
|
...value.debugInfo?.channel.debugInfo,
|
|
659
667
|
})
|
|
660
|
-
.map(([key, value]) => indent(`${key}=${value}`, 4))
|
|
668
|
+
.map(([key, value]) => indent(`${key}=${String(value)}`, 4))
|
|
661
669
|
.join('\n'),
|
|
662
670
|
' ',
|
|
663
671
|
value.debugInfo?.channel,
|
|
664
672
|
'\n',
|
|
665
|
-
indent(`Queue: ${value.queue
|
|
673
|
+
indent(`Queue: ${Queue.sizeUnsafe(value.queue)}`, 4),
|
|
666
674
|
value.queue,
|
|
667
675
|
)
|
|
668
676
|
}
|
|
@@ -691,7 +699,7 @@ export const makeMeshNode = <TName extends MeshNodeName>(
|
|
|
691
699
|
yield* Effect.logDebug(`sending ping via channel ${channelKey}`)
|
|
692
700
|
yield* channel.debugInfo.channel.send(msg(`channel ${channelKey}`) as any)
|
|
693
701
|
}
|
|
694
|
-
}).pipe(Effect.provide(
|
|
702
|
+
}).pipe(Effect.provide(services), Effect.tapCauseLogPretty, Effect.runFork)
|
|
695
703
|
},
|
|
696
704
|
requestTopology: (timeoutMs = 1000) =>
|
|
697
705
|
Effect.gen(function* () {
|
|
@@ -714,7 +722,7 @@ export const makeMeshNode = <TName extends MeshNodeName>(
|
|
|
714
722
|
for (const [key, value] of item) {
|
|
715
723
|
yield* Effect.logDebug(` node '${key}' has edge to: ${Array.from(value.values()).join(', ')}`)
|
|
716
724
|
}
|
|
717
|
-
}).pipe(Effect.provide(
|
|
725
|
+
}).pipe(Effect.provide(services), Effect.tapCauseLogPretty, Effect.runPromise),
|
|
718
726
|
}
|
|
719
727
|
|
|
720
728
|
return {
|
package/src/utils.ts
CHANGED
|
@@ -6,14 +6,14 @@ import { Duration, Effect } from '@livestore/utils/effect'
|
|
|
6
6
|
*/
|
|
7
7
|
export class TimeoutSet<V> {
|
|
8
8
|
private values = new Map<V, number>()
|
|
9
|
-
private timeoutHandle:
|
|
9
|
+
private timeoutHandle: ReturnType<typeof setTimeout> | undefined
|
|
10
10
|
private readonly timeoutMs: number
|
|
11
11
|
|
|
12
|
-
private constructor({ timeout }: { timeout: Duration.
|
|
12
|
+
private constructor({ timeout }: { timeout: Duration.Input }) {
|
|
13
13
|
this.timeoutMs = Duration.toMillis(timeout)
|
|
14
14
|
}
|
|
15
15
|
|
|
16
|
-
static make = (timeout: Duration.
|
|
16
|
+
static make = (timeout: Duration.Input) =>
|
|
17
17
|
Effect.gen(function* () {
|
|
18
18
|
const timeoutSet = new TimeoutSet({ timeout })
|
|
19
19
|
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tests for websocket-edge.ts
|
|
3
|
+
*
|
|
4
|
+
* These tests verify basic WebSocket edge functionality.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { expect } from 'vitest'
|
|
8
|
+
|
|
9
|
+
import { Vitest } from '@livestore/utils-dev/node-vitest'
|
|
10
|
+
import { Effect, Result, Schema, WebChannel } from '@livestore/utils/effect'
|
|
11
|
+
|
|
12
|
+
import * as MeshSchema from './mesh-schema.ts'
|
|
13
|
+
import { MessageMsgpack, WSEdgeInit, WSEdgePayload } from './websocket-edge.ts'
|
|
14
|
+
|
|
15
|
+
Vitest.describe('websocket-edge', () => {
|
|
16
|
+
/**
|
|
17
|
+
* Test that WSEdgeInit messages can be encoded/decoded via MessageMsgpack.
|
|
18
|
+
*/
|
|
19
|
+
Vitest.live('should encode/decode WSEdgeInit', (test) =>
|
|
20
|
+
Effect.gen(function* () {
|
|
21
|
+
const initMessage = WSEdgeInit.make({ from: 'test-node' })
|
|
22
|
+
|
|
23
|
+
// Encode to msgpack
|
|
24
|
+
const encoded = yield* Schema.encodeEffect(MessageMsgpack)(initMessage)
|
|
25
|
+
|
|
26
|
+
// Decode back
|
|
27
|
+
const decoded = yield* Schema.decodeEffect(MessageMsgpack)(encoded)
|
|
28
|
+
|
|
29
|
+
expect(decoded._tag).toBe('WSEdgeInit')
|
|
30
|
+
if (decoded._tag === 'WSEdgeInit') {
|
|
31
|
+
expect(decoded.from).toBe('test-node')
|
|
32
|
+
}
|
|
33
|
+
}).pipe(Vitest.withTestCtx(test)),
|
|
34
|
+
)
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Test that WSEdgePayload messages with valid Packet payloads work correctly.
|
|
38
|
+
*/
|
|
39
|
+
Vitest.live('should encode/decode WSEdgePayload with Packet', (test) =>
|
|
40
|
+
Effect.gen(function* () {
|
|
41
|
+
const packet = {
|
|
42
|
+
_tag: 'NetworkEdgeAdded' as const,
|
|
43
|
+
id: 'test-id',
|
|
44
|
+
source: 'node-a',
|
|
45
|
+
target: 'node-b',
|
|
46
|
+
}
|
|
47
|
+
const wsMessage = WSEdgePayload.make({ from: 'test-node', payload: packet })
|
|
48
|
+
|
|
49
|
+
// Encode to msgpack
|
|
50
|
+
const encoded = yield* Schema.encodeEffect(MessageMsgpack)(wsMessage)
|
|
51
|
+
|
|
52
|
+
// Decode back
|
|
53
|
+
const decoded = yield* Schema.decodeEffect(MessageMsgpack)(encoded)
|
|
54
|
+
|
|
55
|
+
expect(decoded._tag).toBe('WSEdgePayload')
|
|
56
|
+
if (decoded._tag === 'WSEdgePayload') {
|
|
57
|
+
expect(decoded.from).toBe('test-node')
|
|
58
|
+
expect(decoded.payload).toEqual(packet)
|
|
59
|
+
}
|
|
60
|
+
}).pipe(Vitest.withTestCtx(test)),
|
|
61
|
+
)
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* Test that mapSchema(Packet) includes WebChannel internal messages.
|
|
65
|
+
* This is important because channels can send/receive WebChannel.Ping/Pong/DebugPing.
|
|
66
|
+
*/
|
|
67
|
+
Vitest.live('mapSchema should include WebChannel messages in schema', (test) =>
|
|
68
|
+
Effect.gen(function* () {
|
|
69
|
+
const schema = WebChannel.mapSchema(MeshSchema.Packet)
|
|
70
|
+
|
|
71
|
+
// WebChannel.Ping should be decodable via the wrapped listen schema
|
|
72
|
+
const pingPayload = { _tag: 'WebChannel.Ping' as const, requestId: 'test-123' }
|
|
73
|
+
const result = Schema.decodeUnknownResult(schema.listen)(pingPayload)
|
|
74
|
+
|
|
75
|
+
// mapSchema adds WebChannel messages to the schema
|
|
76
|
+
expect(Result.isSuccess(result)).toBe(true)
|
|
77
|
+
}).pipe(Vitest.withTestCtx(test)),
|
|
78
|
+
)
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* Test that valid webmesh packets can be decoded.
|
|
82
|
+
*/
|
|
83
|
+
Vitest.live('should decode valid webmesh packets', (test) =>
|
|
84
|
+
Effect.gen(function* () {
|
|
85
|
+
const schema = WebChannel.mapSchema(MeshSchema.Packet)
|
|
86
|
+
|
|
87
|
+
const packet = {
|
|
88
|
+
_tag: 'NetworkEdgeAdded' as const,
|
|
89
|
+
id: 'test-id',
|
|
90
|
+
source: 'node-a',
|
|
91
|
+
target: 'node-b',
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
const result = yield* Schema.decodeUnknownEffect(schema.listen)(packet)
|
|
95
|
+
expect(result._tag).toBe('NetworkEdgeAdded')
|
|
96
|
+
}).pipe(Vitest.withTestCtx(test)),
|
|
97
|
+
)
|
|
98
|
+
})
|