@livestore/webmesh 0.4.0 → 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/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 +23 -18
- 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 +20 -21
- package/dist/channel/direct-channel.js.map +1 -1
- package/dist/channel/proxy-channel.d.ts +7 -8
- package/dist/channel/proxy-channel.d.ts.map +1 -1
- package/dist/channel/proxy-channel.js +26 -22
- 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 +3 -2
- 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 +6 -6
- package/dist/node.d.ts.map +1 -1
- package/dist/node.js +22 -21
- package/dist/node.js.map +1 -1
- package/dist/node.test.js +74 -71
- 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 +18 -16
- package/dist/websocket-edge.d.ts.map +1 -1
- package/dist/websocket-edge.js +30 -36
- package/dist/websocket-edge.js.map +1 -1
- package/dist/websocket-edge.test.js +14 -14
- package/dist/websocket-edge.test.js.map +1 -1
- 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 +20 -37
- package/src/channel/direct-channel-internal.ts +30 -22
- package/src/channel/direct-channel.ts +26 -23
- package/src/channel/proxy-channel.ts +39 -36
- package/src/common.ts +6 -3
- package/src/mesh-schema.ts +34 -33
- package/src/node.test.ts +99 -86
- package/src/node.ts +32 -32
- package/src/utils.ts +3 -3
- package/src/websocket-edge.test.ts +14 -14
- package/src/websocket-edge.ts +47 -41
- package/src/worker/mod.ts +89 -0
- package/src/worker/schema.ts +8 -0
package/dist/node.test.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
|
+
import { expect } from 'vitest';
|
|
1
2
|
import { IS_CI, omitUndefineds } from '@livestore/utils';
|
|
2
|
-
import { Chunk, Deferred, Effect, Exit, Schema, Scope, Stream, WebChannel } from '@livestore/utils/effect';
|
|
3
3
|
import { Vitest } from '@livestore/utils-dev/node-vitest';
|
|
4
|
-
import {
|
|
4
|
+
import { Deferred, Effect, Exit, Fiber, Schema, Scope, Stream, WebChannel } from '@livestore/utils/effect';
|
|
5
5
|
import { Packet } from "./mesh-schema.js";
|
|
6
6
|
import { makeMeshNode } from "./node.js";
|
|
7
7
|
// TODO test cases where in-between node only comes online later
|
|
@@ -55,20 +55,23 @@ const createChannel = (source, target, options) => source.makeChannel({
|
|
|
55
55
|
mode: options?.mode ?? 'direct',
|
|
56
56
|
timeout: options?.timeout ?? 200,
|
|
57
57
|
});
|
|
58
|
-
const getFirstMessage = (channel) => channel.listen.pipe(Stream.
|
|
58
|
+
const getFirstMessage = (channel) => channel.listen.pipe(Stream.mapEffect(Effect.fromResult), Stream.take(1), Stream.runCollect, Effect.map(([message]) => message));
|
|
59
59
|
// NOTE we distinguish between undefined and 0 delays as it changes the fiber execution
|
|
60
60
|
const maybeDelay = (delay, label) => (effect) => delay === undefined
|
|
61
61
|
? effect
|
|
62
62
|
: Effect.sleep(delay).pipe(Effect.withSpan(`${label}:delay(${delay})`), Effect.andThen(effect));
|
|
63
63
|
const testTimeout = IS_CI === true ? 30_000 : 1000;
|
|
64
64
|
const propTestTimeout = IS_CI === true ? 60_000 : 20_000;
|
|
65
|
-
// TODO also make work without `Vitest.
|
|
65
|
+
// TODO also make work without `Vitest.live` (i.e. with `Vitest.effect`)
|
|
66
66
|
// probably requires controlling the clocks
|
|
67
67
|
Vitest.describe('webmesh node', { timeout: testTimeout }, () => {
|
|
68
|
-
const Delay = Schema.UndefinedOr(Schema.
|
|
68
|
+
const Delay = Schema.UndefinedOr(Schema.Literals([0, 1, 10, 50]));
|
|
69
69
|
// NOTE for message channels, we test both with and without transferables (i.e. proxying)
|
|
70
|
-
const ChannelType = Schema.
|
|
71
|
-
const NodeNames = Schema.Union(
|
|
70
|
+
const ChannelType = Schema.Literals(['direct', 'proxy(via-messagechannel-edge)', 'proxy']);
|
|
71
|
+
const NodeNames = Schema.Union([
|
|
72
|
+
Schema.Tuple([Schema.Literal('A'), Schema.Literal('B')]),
|
|
73
|
+
Schema.Tuple([Schema.Literal('B'), Schema.Literal('A')]),
|
|
74
|
+
]);
|
|
72
75
|
const fromChannelType = (channelType) => {
|
|
73
76
|
switch (channelType) {
|
|
74
77
|
case 'proxy': {
|
|
@@ -114,10 +117,10 @@ Vitest.describe('webmesh node', { timeout: testTimeout }, () => {
|
|
|
114
117
|
// const connectDelay = undefined
|
|
115
118
|
// const channelType = 'direct'
|
|
116
119
|
// const nodeNames = ['B', 'A'] as const
|
|
117
|
-
// Vitest.
|
|
120
|
+
// Vitest.live(
|
|
118
121
|
// 'a / b connect at different times with different channel types',
|
|
119
122
|
// (test) =>
|
|
120
|
-
Vitest.
|
|
123
|
+
Vitest.live.prop('a / b connect at different times with different channel types', [Delay, Delay, Delay, ChannelType, NodeNames], ([delayX, delayY, connectDelay, channelType, nodeNames], test) => Effect.gen(function* () {
|
|
121
124
|
// console.log({ delayX, delayY, connectDelay, channelType, nodeNames })
|
|
122
125
|
const [nodeNameX, nodeNameY] = nodeNames;
|
|
123
126
|
const nodeX = yield* makeMeshNode(nodeNameX);
|
|
@@ -141,10 +144,10 @@ Vitest.describe('webmesh node', { timeout: testTimeout }, () => {
|
|
|
141
144
|
// const waitForOfflineDelay = undefined
|
|
142
145
|
// const sleepDelay = 0
|
|
143
146
|
// const channelType = 'direct'
|
|
144
|
-
// Vitest.
|
|
147
|
+
// Vitest.live(
|
|
145
148
|
// 'b reconnects',
|
|
146
149
|
// (test) =>
|
|
147
|
-
Vitest.
|
|
150
|
+
Vitest.live.prop('b reconnects', [Delay, Delay, ChannelType], ([waitForOfflineDelay, sleepDelay, channelType], test) => Effect.gen(function* () {
|
|
148
151
|
// console.log({ waitForOfflineDelay, sleepDelay, channelType })
|
|
149
152
|
if (waitForOfflineDelay === undefined) {
|
|
150
153
|
// TODO we still need to fix this scenario but it shouldn't really be common in practice
|
|
@@ -162,7 +165,7 @@ Vitest.describe('webmesh node', { timeout: testTimeout }, () => {
|
|
|
162
165
|
expect(yield* getFirstMessage(channelAToB)).toEqual({ message: 'B1' });
|
|
163
166
|
console.log('nodeACode:waiting for B to be offline');
|
|
164
167
|
if (waitForBToBeOffline !== undefined) {
|
|
165
|
-
yield* waitForBToBeOffline;
|
|
168
|
+
yield* Deferred.await(waitForBToBeOffline);
|
|
166
169
|
}
|
|
167
170
|
yield* channelAToB.send({ message: 'A2' });
|
|
168
171
|
expect(yield* getFirstMessage(channelAToB)).toEqual({ message: 'B2' });
|
|
@@ -194,11 +197,11 @@ Vitest.describe('webmesh node', { timeout: testTimeout }, () => {
|
|
|
194
197
|
}).pipe(Vitest.withTestCtx(test, {
|
|
195
198
|
suffix: `waitForOfflineDelay=${waitForOfflineDelay} sleepDelay=${sleepDelay} channelType=${channelType}`,
|
|
196
199
|
})), { fastCheck: { numRuns: 20 } });
|
|
197
|
-
Vitest.
|
|
200
|
+
Vitest.live('reconnect with re-created node', (test) => Effect.gen(function* () {
|
|
198
201
|
const nodeBgen1Scope = yield* Scope.make();
|
|
199
202
|
const nodeA = yield* makeMeshNode('A');
|
|
200
|
-
const nodeBgen1 = yield* makeMeshNode('B').pipe(Scope.
|
|
201
|
-
yield* connectNodesViaMessageChannel(nodeA, nodeBgen1).pipe(Scope.
|
|
203
|
+
const nodeBgen1 = yield* makeMeshNode('B').pipe(Scope.provide(nodeBgen1Scope));
|
|
204
|
+
yield* connectNodesViaMessageChannel(nodeA, nodeBgen1).pipe(Scope.provide(nodeBgen1Scope));
|
|
202
205
|
// yield* Effect.sleep(100)
|
|
203
206
|
const channelAToBOnce = yield* Effect.cached(createChannel(nodeA, 'B'));
|
|
204
207
|
const nodeACode = Effect.gen(function* () {
|
|
@@ -213,7 +216,7 @@ Vitest.describe('webmesh node', { timeout: testTimeout }, () => {
|
|
|
213
216
|
expect(yield* getFirstMessage(channelBToA)).toEqual({ message: 'A1' });
|
|
214
217
|
// expect(channelBToA.debugInfo.connectCounter).toBe(1)
|
|
215
218
|
});
|
|
216
|
-
yield* Effect.all([nodeACode, nodeBCode(nodeBgen1).pipe(Scope.
|
|
219
|
+
yield* Effect.all([nodeACode, nodeBCode(nodeBgen1).pipe(Scope.provide(nodeBgen1Scope))], {
|
|
217
220
|
concurrency: 'unbounded',
|
|
218
221
|
}).pipe(Effect.withSpan('test1'));
|
|
219
222
|
yield* Scope.close(nodeBgen1Scope, Exit.void);
|
|
@@ -221,7 +224,7 @@ Vitest.describe('webmesh node', { timeout: testTimeout }, () => {
|
|
|
221
224
|
yield* connectNodesViaMessageChannel(nodeA, nodeBgen2, { replaceIfExists: true });
|
|
222
225
|
yield* Effect.all([nodeACode, nodeBCode(nodeBgen2)], { concurrency: 'unbounded' }).pipe(Effect.withSpan('test2'));
|
|
223
226
|
}).pipe(Vitest.withTestCtx(test)));
|
|
224
|
-
const ChannelTypeWithoutMessageChannelProxy = Schema.
|
|
227
|
+
const ChannelTypeWithoutMessageChannelProxy = Schema.Literals(['proxy', 'direct']);
|
|
225
228
|
// TODO there seems to be a flaky case here which gets hit sometimes (e.g. 2025-02-28-17:11)
|
|
226
229
|
// Log output:
|
|
227
230
|
// test: { seed: -964670352, path: "1", endOnFailure: true }
|
|
@@ -237,7 +240,7 @@ Vitest.describe('webmesh node', { timeout: testTimeout }, () => {
|
|
|
237
240
|
// test: Error: Property failed after 2 tests
|
|
238
241
|
// test: { seed: -964670352, path: "1", endOnFailure: true }
|
|
239
242
|
// test: Counterexample: ["direct",["A","B"]]
|
|
240
|
-
Vitest.
|
|
243
|
+
Vitest.live.prop('replace edge while keeping the channel', [ChannelTypeWithoutMessageChannelProxy, NodeNames], ([channelType, nodeNames], test) => Effect.gen(function* () {
|
|
241
244
|
const [nodeNameX, nodeNameY] = nodeNames;
|
|
242
245
|
const nodeX = yield* makeMeshNode(nodeNameX);
|
|
243
246
|
const nodeY = yield* makeMeshNode(nodeNameY);
|
|
@@ -249,7 +252,7 @@ Vitest.describe('webmesh node', { timeout: testTimeout }, () => {
|
|
|
249
252
|
const channelXToY = yield* createChannel(nodeX, nodeLabel.y, { mode });
|
|
250
253
|
yield* channelXToY.send({ message: `${nodeLabel.x}1` });
|
|
251
254
|
expect(yield* getFirstMessage(channelXToY)).toEqual({ message: `${nodeLabel.y}1` });
|
|
252
|
-
yield* waitForEdgeReplacement;
|
|
255
|
+
yield* Deferred.await(waitForEdgeReplacement);
|
|
253
256
|
yield* channelXToY.send({ message: `${nodeLabel.x}2` });
|
|
254
257
|
expect(yield* getFirstMessage(channelXToY)).toEqual({ message: `${nodeLabel.y}2` });
|
|
255
258
|
});
|
|
@@ -271,7 +274,7 @@ Vitest.describe('webmesh node', { timeout: testTimeout }, () => {
|
|
|
271
274
|
})), { fastCheck: { numRuns: 10 } });
|
|
272
275
|
Vitest.describe('TODO improve latency', () => {
|
|
273
276
|
// TODO we need to improve latency when sending messages concurrently
|
|
274
|
-
Vitest.
|
|
277
|
+
Vitest.live.prop('concurrent messages', [ChannelType, Schema.Int.check(Schema.isBetween({ minimum: 1, maximum: 50 }))], ([channelType, count], test) => Effect.gen(function* () {
|
|
275
278
|
const nodeA = yield* makeMeshNode('A');
|
|
276
279
|
const nodeB = yield* makeMeshNode('B');
|
|
277
280
|
const { mode, connectNodes } = fromChannelType(channelType);
|
|
@@ -279,15 +282,15 @@ Vitest.describe('webmesh node', { timeout: testTimeout }, () => {
|
|
|
279
282
|
const nodeACode = Effect.gen(function* () {
|
|
280
283
|
const channelAToB = yield* createChannel(nodeA, 'B', { mode });
|
|
281
284
|
// send 10 times A1
|
|
282
|
-
yield* Effect.forEach(
|
|
283
|
-
expect(yield* channelAToB.listen.pipe(Stream.
|
|
285
|
+
yield* Effect.forEach(Array.from({ length: count }, (_, i) => ({ message: `A${i}` })), channelAToB.send, { concurrency: 'unbounded' });
|
|
286
|
+
expect(yield* channelAToB.listen.pipe(Stream.mapEffect(Effect.fromResult), Stream.take(count), Stream.runCollect)).toEqual(Array.from({ length: count }, (_, i) => ({ message: `B${i}` })));
|
|
284
287
|
// expect(yield* getFirstMessage(channelAToB)).toEqual({ message: 'A2' })
|
|
285
288
|
});
|
|
286
289
|
const nodeBCode = Effect.gen(function* () {
|
|
287
290
|
const channelBToA = yield* createChannel(nodeB, 'A', { mode });
|
|
288
291
|
// send 10 times B1
|
|
289
|
-
yield* Effect.forEach(
|
|
290
|
-
expect(yield* channelBToA.listen.pipe(Stream.
|
|
292
|
+
yield* Effect.forEach(Array.from({ length: count }, (_, i) => ({ message: `B${i}` })), channelBToA.send, { concurrency: 'unbounded' });
|
|
293
|
+
expect(yield* channelBToA.listen.pipe(Stream.mapEffect(Effect.fromResult), Stream.take(count), Stream.runCollect)).toEqual(Array.from({ length: count }, (_, i) => ({ message: `A${i}` })));
|
|
291
294
|
});
|
|
292
295
|
yield* Effect.all([nodeACode, nodeBCode, connectNodes(nodeA, nodeB).pipe(Effect.delay(100))], {
|
|
293
296
|
concurrency: 'unbounded',
|
|
@@ -299,27 +302,27 @@ Vitest.describe('webmesh node', { timeout: testTimeout }, () => {
|
|
|
299
302
|
});
|
|
300
303
|
});
|
|
301
304
|
Vitest.describe('message channel specific tests', () => {
|
|
302
|
-
Vitest.
|
|
305
|
+
Vitest.live('differing initial edge counter', (test) => Effect.gen(function* () {
|
|
303
306
|
const nodeA = yield* makeMeshNode('A');
|
|
304
307
|
const nodeB = yield* makeMeshNode('B');
|
|
305
308
|
yield* connectNodesViaMessageChannel(nodeA, nodeB);
|
|
306
309
|
const messageCount = 3;
|
|
307
310
|
const bFiber = yield* Effect.gen(function* () {
|
|
308
311
|
const channelBToA = yield* createChannel(nodeB, 'A');
|
|
309
|
-
yield* channelBToA.listen.pipe(Stream.
|
|
310
|
-
}).pipe(Effect.scoped, Effect.
|
|
312
|
+
yield* channelBToA.listen.pipe(Stream.mapEffect(Effect.fromResult), Stream.tap((msg) => channelBToA.send({ message: `resp:${msg.message}` })), Stream.take(messageCount), Stream.runDrain);
|
|
313
|
+
}).pipe(Effect.scoped, Effect.forkChild);
|
|
311
314
|
// yield* createChannel(nodeA, 'B').pipe(Effect.andThen(WebChannel.shutdown))
|
|
312
315
|
// // yield* createChannel(nodeA, 'B').pipe(Effect.andThen(WebChannel.shutdown))
|
|
313
316
|
// // yield* createChannel(nodeA, 'B').pipe(Effect.andThen(WebChannel.shutdown))
|
|
314
|
-
yield* Effect.gen(function* () {
|
|
317
|
+
yield* Effect.replicateEffect(Effect.gen(function* () {
|
|
315
318
|
const channelAToB = yield* createChannel(nodeA, 'B');
|
|
316
319
|
yield* channelAToB.send({ message: 'A' });
|
|
317
320
|
expect(yield* getFirstMessage(channelAToB)).toEqual({ message: 'resp:A' });
|
|
318
|
-
}).pipe(Effect.scoped,
|
|
319
|
-
yield* bFiber;
|
|
321
|
+
}).pipe(Effect.scoped), messageCount, { discard: true });
|
|
322
|
+
yield* Fiber.join(bFiber);
|
|
320
323
|
}).pipe(Vitest.withTestCtx(test)));
|
|
321
324
|
});
|
|
322
|
-
Vitest.
|
|
325
|
+
Vitest.live('manual debug test', (test) => Effect.gen(function* () {
|
|
323
326
|
const nodeA = yield* makeMeshNode('A');
|
|
324
327
|
const nodeB = yield* makeMeshNode('B');
|
|
325
328
|
// const connectNodes = connectNodesViaBroadcastChannel
|
|
@@ -338,16 +341,16 @@ Vitest.describe('webmesh node', { timeout: testTimeout }, () => {
|
|
|
338
341
|
concurrency: 'unbounded',
|
|
339
342
|
});
|
|
340
343
|
}).pipe(Vitest.withTestCtx(test)));
|
|
341
|
-
Vitest.
|
|
344
|
+
Vitest.live('broadcast edge with message channel', (test) => Effect.gen(function* () {
|
|
342
345
|
const nodeA = yield* makeMeshNode('A');
|
|
343
346
|
const nodeB = yield* makeMeshNode('B');
|
|
344
347
|
yield* connectNodesViaBroadcastChannel(nodeA, nodeB);
|
|
345
348
|
const err = yield* createChannel(nodeA, 'B', { mode: 'direct' }).pipe(Effect.timeout(200), Effect.flip);
|
|
346
|
-
expect(err._tag).toBe('
|
|
349
|
+
expect(err._tag).toBe('TimeoutError');
|
|
347
350
|
}).pipe(Vitest.withTestCtx(test)));
|
|
348
351
|
});
|
|
349
352
|
Vitest.describe('A <> B <> C', () => {
|
|
350
|
-
Vitest.
|
|
353
|
+
Vitest.live('should work', (test) => Effect.gen(function* () {
|
|
351
354
|
const nodeA = yield* makeMeshNode('A');
|
|
352
355
|
const nodeB = yield* makeMeshNode('B');
|
|
353
356
|
const nodeC = yield* makeMeshNode('C');
|
|
@@ -369,7 +372,7 @@ Vitest.describe('webmesh node', { timeout: testTimeout }, () => {
|
|
|
369
372
|
});
|
|
370
373
|
yield* Effect.all([nodeACode, nodeCCode], { concurrency: 'unbounded' });
|
|
371
374
|
}).pipe(Vitest.withTestCtx(test)));
|
|
372
|
-
Vitest.
|
|
375
|
+
Vitest.live('should work - delayed edge', (test) => Effect.gen(function* () {
|
|
373
376
|
const nodeA = yield* makeMeshNode('A');
|
|
374
377
|
const nodeB = yield* makeMeshNode('B');
|
|
375
378
|
const nodeC = yield* makeMeshNode('C');
|
|
@@ -393,7 +396,7 @@ Vitest.describe('webmesh node', { timeout: testTimeout }, () => {
|
|
|
393
396
|
connectNodes(nodeB, nodeC).pipe(Effect.delay(100), Effect.withSpan('connect-nodeB-nodeC-delay(100)')),
|
|
394
397
|
], { concurrency: 'unbounded' });
|
|
395
398
|
}).pipe(Vitest.withTestCtx(test)));
|
|
396
|
-
Vitest.
|
|
399
|
+
Vitest.live('proxy channel', (test) => Effect.gen(function* () {
|
|
397
400
|
const nodeA = yield* makeMeshNode('A');
|
|
398
401
|
const nodeB = yield* makeMeshNode('B');
|
|
399
402
|
const nodeC = yield* makeMeshNode('C');
|
|
@@ -429,7 +432,7 @@ Vitest.describe('webmesh node', { timeout: testTimeout }, () => {
|
|
|
429
432
|
* 2. Verify the pattern works correctly with forked ACKs
|
|
430
433
|
* 3. Serve as a reference for the fix in proxy-channel.ts
|
|
431
434
|
*/
|
|
432
|
-
Vitest.
|
|
435
|
+
Vitest.live('ACK sending should not block message processing', (test) => Effect.gen(function* () {
|
|
433
436
|
const nodeA = yield* makeMeshNode('A');
|
|
434
437
|
const nodeB = yield* makeMeshNode('B');
|
|
435
438
|
const nodeC = yield* makeMeshNode('C');
|
|
@@ -440,18 +443,18 @@ Vitest.describe('webmesh node', { timeout: testTimeout }, () => {
|
|
|
440
443
|
const nodeACode = Effect.gen(function* () {
|
|
441
444
|
const channelAToC = yield* createChannel(nodeA, 'C', { mode: 'proxy' });
|
|
442
445
|
// Send multiple messages concurrently
|
|
443
|
-
yield* Effect.forEach(
|
|
446
|
+
yield* Effect.forEach(Array.from({ length: messageCount }, (_, i) => ({ message: `A${i}` })), channelAToC.send, { concurrency: 'unbounded' });
|
|
444
447
|
// Receive responses
|
|
445
|
-
const responses = yield* channelAToC.listen.pipe(Stream.
|
|
446
|
-
expect(
|
|
448
|
+
const responses = yield* channelAToC.listen.pipe(Stream.mapEffect(Effect.fromResult), Stream.take(messageCount), Stream.runCollect);
|
|
449
|
+
expect(responses.length).toBe(messageCount);
|
|
447
450
|
});
|
|
448
451
|
const nodeCCode = Effect.gen(function* () {
|
|
449
452
|
const channelCToA = yield* createChannel(nodeC, 'A', { mode: 'proxy' });
|
|
450
453
|
// Send multiple messages concurrently
|
|
451
|
-
yield* Effect.forEach(
|
|
454
|
+
yield* Effect.forEach(Array.from({ length: messageCount }, (_, i) => ({ message: `C${i}` })), channelCToA.send, { concurrency: 'unbounded' });
|
|
452
455
|
// Receive responses
|
|
453
|
-
const responses = yield* channelCToA.listen.pipe(Stream.
|
|
454
|
-
expect(
|
|
456
|
+
const responses = yield* channelCToA.listen.pipe(Stream.mapEffect(Effect.fromResult), Stream.take(messageCount), Stream.runCollect);
|
|
457
|
+
expect(responses.length).toBe(messageCount);
|
|
455
458
|
});
|
|
456
459
|
yield* Effect.all([nodeACode, nodeCCode], { concurrency: 'unbounded' });
|
|
457
460
|
}).pipe(Vitest.withTestCtx(test)));
|
|
@@ -482,7 +485,7 @@ Vitest.describe('webmesh node', { timeout: testTimeout }, () => {
|
|
|
482
485
|
* without being blocked by slow ACK sends. We measure how long it takes to receive
|
|
483
486
|
* all messages - with forked ACKs it should be fast, with blocking ACKs it would be slow.
|
|
484
487
|
*/
|
|
485
|
-
Vitest.
|
|
488
|
+
Vitest.live('messages arrive in listen queue without waiting for ACK send', (test) => Effect.gen(function* () {
|
|
486
489
|
const ACK_DELAY_MS = 50;
|
|
487
490
|
const MESSAGE_COUNT = 5;
|
|
488
491
|
const nodeA = yield* makeMeshNode('A');
|
|
@@ -500,7 +503,7 @@ Vitest.describe('webmesh node', { timeout: testTimeout }, () => {
|
|
|
500
503
|
// Send all messages concurrently - this is key!
|
|
501
504
|
// With forked ACKs, all messages get processed immediately at receiver
|
|
502
505
|
// With blocking ACKs, messages would be processed one at a time with delays
|
|
503
|
-
yield* Effect.forEach(
|
|
506
|
+
yield* Effect.forEach(Array.from({ length: MESSAGE_COUNT }, (_, i) => ({ message: `msg${i}` })), channelAToB.send, { concurrency: 'unbounded' });
|
|
504
507
|
});
|
|
505
508
|
const receiverCode = Effect.gen(function* () {
|
|
506
509
|
const channelBToA = yield* nodeB.makeChannel({
|
|
@@ -520,7 +523,7 @@ Vitest.describe('webmesh node', { timeout: testTimeout }, () => {
|
|
|
520
523
|
},
|
|
521
524
|
},
|
|
522
525
|
});
|
|
523
|
-
yield* channelBToA.listen.pipe(Stream.
|
|
526
|
+
yield* channelBToA.listen.pipe(Stream.mapEffect(Effect.fromResult), Stream.tap((msg) => Effect.sync(() => {
|
|
524
527
|
receivedMessages.push(msg.message);
|
|
525
528
|
})), Stream.take(MESSAGE_COUNT), Stream.runDrain);
|
|
526
529
|
});
|
|
@@ -548,7 +551,7 @@ Vitest.describe('webmesh node', { timeout: testTimeout }, () => {
|
|
|
548
551
|
* This test sends multiple messages concurrently and verifies they're all
|
|
549
552
|
* processed even when ACK sends are slow.
|
|
550
553
|
*/
|
|
551
|
-
Vitest.
|
|
554
|
+
Vitest.live('concurrent messages processed despite slow ACK sends', (test) => Effect.gen(function* () {
|
|
552
555
|
const ACK_DELAY_MS = 50;
|
|
553
556
|
const MESSAGE_COUNT = 5;
|
|
554
557
|
const nodeA = yield* makeMeshNode('A');
|
|
@@ -564,7 +567,7 @@ Vitest.describe('webmesh node', { timeout: testTimeout }, () => {
|
|
|
564
567
|
timeout: 3000,
|
|
565
568
|
});
|
|
566
569
|
// Send all messages concurrently
|
|
567
|
-
yield* Effect.forEach(
|
|
570
|
+
yield* Effect.forEach(Array.from({ length: MESSAGE_COUNT }, (_, i) => ({ message: `msg${i}` })), channelAToB.send, { concurrency: 'unbounded' });
|
|
568
571
|
});
|
|
569
572
|
const receiverCode = Effect.gen(function* () {
|
|
570
573
|
const channelBToA = yield* nodeB.makeChannel({
|
|
@@ -581,7 +584,7 @@ Vitest.describe('webmesh node', { timeout: testTimeout }, () => {
|
|
|
581
584
|
},
|
|
582
585
|
},
|
|
583
586
|
});
|
|
584
|
-
yield* channelBToA.listen.pipe(Stream.
|
|
587
|
+
yield* channelBToA.listen.pipe(Stream.mapEffect(Effect.fromResult), Stream.tap((msg) => Effect.sync(() => {
|
|
585
588
|
receivedMessages.push(msg.message);
|
|
586
589
|
})), Stream.take(MESSAGE_COUNT), Stream.runDrain);
|
|
587
590
|
});
|
|
@@ -598,7 +601,7 @@ Vitest.describe('webmesh node', { timeout: testTimeout }, () => {
|
|
|
598
601
|
console.log(`[regression-test] Elapsed: ${elapsed}ms, Blocking estimate: ${blockingTime}ms`);
|
|
599
602
|
}).pipe(Vitest.withTestCtx(test)));
|
|
600
603
|
});
|
|
601
|
-
Vitest.
|
|
604
|
+
Vitest.live('should fail with timeout due to missing edge', (test) => Effect.gen(function* () {
|
|
602
605
|
const nodeA = yield* makeMeshNode('A');
|
|
603
606
|
const nodeB = yield* makeMeshNode('B');
|
|
604
607
|
const nodeC = yield* makeMeshNode('C');
|
|
@@ -606,35 +609,35 @@ Vitest.describe('webmesh node', { timeout: testTimeout }, () => {
|
|
|
606
609
|
// We're not connecting nodeB and nodeC, so this should fail
|
|
607
610
|
const nodeACode = Effect.gen(function* () {
|
|
608
611
|
const err = yield* createChannel(nodeA, 'C').pipe(Effect.timeout(200), Effect.flip);
|
|
609
|
-
expect(err._tag).toBe('
|
|
612
|
+
expect(err._tag).toBe('TimeoutError');
|
|
610
613
|
});
|
|
611
614
|
const nodeCCode = Effect.gen(function* () {
|
|
612
615
|
const err = yield* createChannel(nodeC, 'A').pipe(Effect.timeout(200), Effect.flip);
|
|
613
|
-
expect(err._tag).toBe('
|
|
616
|
+
expect(err._tag).toBe('TimeoutError');
|
|
614
617
|
});
|
|
615
618
|
yield* Effect.all([nodeACode, nodeCCode], { concurrency: 'unbounded' });
|
|
616
619
|
}).pipe(Vitest.withTestCtx(test)));
|
|
617
|
-
Vitest.
|
|
620
|
+
Vitest.live('should fail with timeout due no transferable', (test) => Effect.gen(function* () {
|
|
618
621
|
const nodeA = yield* makeMeshNode('A');
|
|
619
622
|
const nodeB = yield* makeMeshNode('B');
|
|
620
623
|
yield* connectNodesViaBroadcastChannel(nodeA, nodeB);
|
|
621
624
|
const nodeACode = Effect.gen(function* () {
|
|
622
625
|
const err = yield* createChannel(nodeA, 'B').pipe(Effect.timeout(200), Effect.flip);
|
|
623
|
-
expect(err._tag).toBe('
|
|
626
|
+
expect(err._tag).toBe('TimeoutError');
|
|
624
627
|
});
|
|
625
628
|
const nodeBCode = Effect.gen(function* () {
|
|
626
629
|
const err = yield* createChannel(nodeB, 'A').pipe(Effect.timeout(200), Effect.flip);
|
|
627
|
-
expect(err._tag).toBe('
|
|
630
|
+
expect(err._tag).toBe('TimeoutError');
|
|
628
631
|
});
|
|
629
632
|
yield* Effect.all([nodeACode, nodeBCode], { concurrency: 'unbounded' });
|
|
630
633
|
}).pipe(Vitest.withTestCtx(test)));
|
|
631
|
-
Vitest.
|
|
634
|
+
Vitest.live('reconnect with re-created node', (test) => Effect.gen(function* () {
|
|
632
635
|
const nodeCgen1Scope = yield* Scope.make();
|
|
633
636
|
const nodeA = yield* makeMeshNode('A');
|
|
634
637
|
const nodeB = yield* makeMeshNode('B');
|
|
635
|
-
const nodeCgen1 = yield* makeMeshNode('C').pipe(Scope.
|
|
638
|
+
const nodeCgen1 = yield* makeMeshNode('C').pipe(Scope.provide(nodeCgen1Scope));
|
|
636
639
|
yield* connectNodesViaMessageChannel(nodeA, nodeB);
|
|
637
|
-
yield* connectNodesViaMessageChannel(nodeB, nodeCgen1).pipe(Scope.
|
|
640
|
+
yield* connectNodesViaMessageChannel(nodeB, nodeCgen1).pipe(Scope.provide(nodeCgen1Scope));
|
|
638
641
|
const nodeACode = Effect.gen(function* () {
|
|
639
642
|
const channelAToB = yield* createChannel(nodeA, 'C');
|
|
640
643
|
yield* channelAToB.send({ message: 'A1' });
|
|
@@ -645,7 +648,7 @@ Vitest.describe('webmesh node', { timeout: testTimeout }, () => {
|
|
|
645
648
|
yield* channelBToA.send({ message: 'C1' });
|
|
646
649
|
expect(yield* getFirstMessage(channelBToA)).toEqual({ message: 'A1' });
|
|
647
650
|
});
|
|
648
|
-
yield* Effect.all([nodeACode, nodeCCode(nodeCgen1)], { concurrency: 'unbounded' }).pipe(Effect.withSpan('test1'), Scope.
|
|
651
|
+
yield* Effect.all([nodeACode, nodeCCode(nodeCgen1)], { concurrency: 'unbounded' }).pipe(Effect.withSpan('test1'), Scope.provide(nodeCgen1Scope));
|
|
649
652
|
yield* Scope.close(nodeCgen1Scope, Exit.void);
|
|
650
653
|
const nodeCgen2 = yield* makeMeshNode('C');
|
|
651
654
|
yield* connectNodesViaMessageChannel(nodeB, nodeCgen2, { replaceIfExists: true });
|
|
@@ -660,7 +663,7 @@ Vitest.describe('webmesh node', { timeout: testTimeout }, () => {
|
|
|
660
663
|
* D
|
|
661
664
|
*/
|
|
662
665
|
Vitest.describe('diamond topology', () => {
|
|
663
|
-
Vitest.
|
|
666
|
+
Vitest.live('should work', (test) => Effect.gen(function* () {
|
|
664
667
|
const nodeA = yield* makeMeshNode('A');
|
|
665
668
|
const nodeB = yield* makeMeshNode('B');
|
|
666
669
|
const nodeC = yield* makeMeshNode('C');
|
|
@@ -692,7 +695,7 @@ Vitest.describe('webmesh node', { timeout: testTimeout }, () => {
|
|
|
692
695
|
* Topology: Butterfly topology with two connected hubs (C-D) each serving multiple nodes
|
|
693
696
|
*/
|
|
694
697
|
Vitest.describe('butterfly topology', () => {
|
|
695
|
-
Vitest.
|
|
698
|
+
Vitest.live('should work', (test) => Effect.gen(function* () {
|
|
696
699
|
const nodeA = yield* makeMeshNode('A');
|
|
697
700
|
const nodeB = yield* makeMeshNode('B');
|
|
698
701
|
const nodeC = yield* makeMeshNode('C');
|
|
@@ -721,14 +724,14 @@ Vitest.describe('webmesh node', { timeout: testTimeout }, () => {
|
|
|
721
724
|
Vitest.describe('mixture of direct and proxy edge connections', () => {
|
|
722
725
|
// TODO test case to better guard against case where side A tries to create a proxy channel to B
|
|
723
726
|
// and side B tries to create a direct to A
|
|
724
|
-
Vitest.
|
|
727
|
+
Vitest.live('should work for proxy channels', (test) => Effect.gen(function* () {
|
|
725
728
|
const nodeA = yield* makeMeshNode('A');
|
|
726
729
|
const nodeB = yield* makeMeshNode('B');
|
|
727
730
|
yield* connectNodesViaMessageChannel(nodeB, nodeA);
|
|
728
731
|
const err = yield* connectNodesViaBroadcastChannel(nodeA, nodeB).pipe(Effect.flip);
|
|
729
732
|
expect(err._tag).toBe('EdgeAlreadyExistsError');
|
|
730
733
|
}).pipe(Vitest.withTestCtx(test)));
|
|
731
|
-
Vitest.
|
|
734
|
+
Vitest.live('should work for directs', (test) => Effect.gen(function* () {
|
|
732
735
|
const nodeA = yield* makeMeshNode('A');
|
|
733
736
|
const nodeB = yield* makeMeshNode('B');
|
|
734
737
|
const nodeC = yield* makeMeshNode('C');
|
|
@@ -748,7 +751,7 @@ Vitest.describe('webmesh node', { timeout: testTimeout }, () => {
|
|
|
748
751
|
}).pipe(Vitest.withTestCtx(test)));
|
|
749
752
|
});
|
|
750
753
|
Vitest.describe('listenForChannel', () => {
|
|
751
|
-
Vitest.
|
|
754
|
+
Vitest.live('connect later', (test) => Effect.gen(function* () {
|
|
752
755
|
const nodeA = yield* makeMeshNode('A');
|
|
753
756
|
const mode = 'direct';
|
|
754
757
|
const connect = mode === 'direct' ? connectNodesViaMessageChannel : connectNodesViaBroadcastChannel;
|
|
@@ -772,7 +775,7 @@ Vitest.describe('webmesh node', { timeout: testTimeout }, () => {
|
|
|
772
775
|
yield* Effect.all([nodeACode, nodeBCode.pipe(Effect.delay(500))], { concurrency: 'unbounded' });
|
|
773
776
|
}).pipe(Vitest.withTestCtx(test)));
|
|
774
777
|
// TODO provide a way to allow for reconnecting in the `listenForChannel` case
|
|
775
|
-
Vitest.
|
|
778
|
+
Vitest.live.skip('reconnect', (test) => Effect.gen(function* () {
|
|
776
779
|
const nodeA = yield* makeMeshNode('A');
|
|
777
780
|
const mode = 'direct';
|
|
778
781
|
const connect = mode === 'direct' ? connectNodesViaMessageChannel : connectNodesViaBroadcastChannel;
|
|
@@ -808,7 +811,7 @@ Vitest.describe('webmesh node', { timeout: testTimeout }, () => {
|
|
|
808
811
|
yield* Effect.all([nodeACode, nodeBCode], { concurrency: 'unbounded' });
|
|
809
812
|
}).pipe(Vitest.withTestCtx(test)));
|
|
810
813
|
Vitest.describe('prop tests', { timeout: propTestTimeout }, () => {
|
|
811
|
-
Vitest.
|
|
814
|
+
Vitest.live.prop('listenForChannel A <> B <> C', [Delay, Delay, Delay, Delay, ChannelType], ([delayNodeA, delayNodeC, delayConnectAB, delayConnectBC, channelType], test) => Effect.gen(function* () {
|
|
812
815
|
const nodeA = yield* makeMeshNode('A');
|
|
813
816
|
const nodeB = yield* makeMeshNode('B');
|
|
814
817
|
const nodeC = yield* makeMeshNode('C');
|
|
@@ -844,7 +847,7 @@ Vitest.describe('webmesh node', { timeout: testTimeout }, () => {
|
|
|
844
847
|
});
|
|
845
848
|
});
|
|
846
849
|
Vitest.describe('broadcast channel', () => {
|
|
847
|
-
Vitest.
|
|
850
|
+
Vitest.live('should work', (test) => Effect.gen(function* () {
|
|
848
851
|
const nodeA = yield* makeMeshNode('A');
|
|
849
852
|
const nodeB = yield* makeMeshNode('B');
|
|
850
853
|
const nodeC = yield* makeMeshNode('C');
|
|
@@ -852,12 +855,12 @@ Vitest.describe('webmesh node', { timeout: testTimeout }, () => {
|
|
|
852
855
|
yield* connectNodesViaMessageChannel(nodeB, nodeC);
|
|
853
856
|
const channelOnA = yield* nodeA.makeBroadcastChannel({ channelName: 'test', schema: Schema.String });
|
|
854
857
|
const channelOnC = yield* nodeC.makeBroadcastChannel({ channelName: 'test', schema: Schema.String });
|
|
855
|
-
const listenOnAFiber = yield* channelOnA.listen.pipe(Stream.
|
|
856
|
-
const listenOnCFiber = yield* channelOnC.listen.pipe(Stream.
|
|
858
|
+
const listenOnAFiber = yield* channelOnA.listen.pipe(Stream.mapEffect(Effect.fromResult), Stream.runHead, Effect.flatMap(Effect.fromOption), Effect.forkChild);
|
|
859
|
+
const listenOnCFiber = yield* channelOnC.listen.pipe(Stream.mapEffect(Effect.fromResult), Stream.runHead, Effect.flatMap(Effect.fromOption), Effect.forkChild);
|
|
857
860
|
yield* channelOnA.send('A1');
|
|
858
861
|
yield* channelOnC.send('C1');
|
|
859
|
-
expect(yield* listenOnAFiber).toEqual('C1');
|
|
860
|
-
expect(yield* listenOnCFiber).toEqual('A1');
|
|
862
|
+
expect(yield* Fiber.join(listenOnAFiber)).toEqual('C1');
|
|
863
|
+
expect(yield* Fiber.join(listenOnCFiber)).toEqual('A1');
|
|
861
864
|
}).pipe(Vitest.withTestCtx(test)));
|
|
862
865
|
});
|
|
863
866
|
});
|