@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/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
|
-
const testTimeout = IS_CI ? 30_000 : 1000;
|
|
64
|
-
const propTestTimeout = IS_CI ? 60_000 : 20_000;
|
|
65
|
-
// TODO also make work without `Vitest.
|
|
63
|
+
const testTimeout = IS_CI === true ? 30_000 : 1000;
|
|
64
|
+
const propTestTimeout = IS_CI === true ? 60_000 : 20_000;
|
|
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);
|
|
@@ -136,15 +139,15 @@ Vitest.describe('webmesh node', { timeout: testTimeout }, () => {
|
|
|
136
139
|
});
|
|
137
140
|
yield* Effect.promise(() => nodeX.debug.requestTopology(100));
|
|
138
141
|
}).pipe(Vitest.withTestCtx(test, {
|
|
139
|
-
suffix: `delayX=${delayX} delayY=${delayY} connectDelay=${connectDelay} channelType=${channelType} nodeNames=${nodeNames}`,
|
|
142
|
+
suffix: `delayX=${delayX} delayY=${delayY} connectDelay=${connectDelay} channelType=${channelType} nodeNames=${nodeNames.join(',')}`,
|
|
140
143
|
})));
|
|
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
|
});
|
|
@@ -267,11 +270,11 @@ Vitest.describe('webmesh node', { timeout: testTimeout }, () => {
|
|
|
267
270
|
});
|
|
268
271
|
yield* Effect.all([nodeXCode, nodeYCode], { concurrency: 'unbounded' });
|
|
269
272
|
}).pipe(Vitest.withTestCtx(test, {
|
|
270
|
-
suffix: `channelType=${channelType} nodeNames=${nodeNames}`,
|
|
273
|
+
suffix: `channelType=${channelType} nodeNames=${nodeNames.join(',')}`,
|
|
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');
|
|
@@ -411,7 +414,194 @@ Vitest.describe('webmesh node', { timeout: testTimeout }, () => {
|
|
|
411
414
|
});
|
|
412
415
|
yield* Effect.all([nodeACode, nodeCCode], { concurrency: 'unbounded' });
|
|
413
416
|
}).pipe(Vitest.withTestCtx(test)));
|
|
414
|
-
|
|
417
|
+
/**
|
|
418
|
+
* Pattern test: ACK sending must not block message processing.
|
|
419
|
+
*
|
|
420
|
+
* This test documents the pattern where ACKs are sent fire-and-forget using `Effect.forkScoped`
|
|
421
|
+
* to avoid blocking the message processing loop.
|
|
422
|
+
*
|
|
423
|
+
* Background: Before the fix, ACKs were sent with `await` which blocked message processing.
|
|
424
|
+
* This caused heartbeat timeouts in devtools after ~30 seconds ("Connection to app lost").
|
|
425
|
+
*
|
|
426
|
+
* Note: This unit test passes both with and without the fix because in-memory channels
|
|
427
|
+
* complete ACK sends instantly. The actual regression test is the Playwright test
|
|
428
|
+
* `node-adapter-timeout.play.ts` which uses real network conditions.
|
|
429
|
+
*
|
|
430
|
+
* This test exists to:
|
|
431
|
+
* 1. Document the expected pattern (high message throughput via proxy channels)
|
|
432
|
+
* 2. Verify the pattern works correctly with forked ACKs
|
|
433
|
+
* 3. Serve as a reference for the fix in proxy-channel.ts
|
|
434
|
+
*/
|
|
435
|
+
Vitest.live('ACK sending should not block message processing', (test) => Effect.gen(function* () {
|
|
436
|
+
const nodeA = yield* makeMeshNode('A');
|
|
437
|
+
const nodeB = yield* makeMeshNode('B');
|
|
438
|
+
const nodeC = yield* makeMeshNode('C');
|
|
439
|
+
yield* connectNodesViaBroadcastChannel(nodeA, nodeB);
|
|
440
|
+
yield* connectNodesViaBroadcastChannel(nodeB, nodeC);
|
|
441
|
+
// Send many messages to stress the ACK handling
|
|
442
|
+
const messageCount = 10;
|
|
443
|
+
const nodeACode = Effect.gen(function* () {
|
|
444
|
+
const channelAToC = yield* createChannel(nodeA, 'C', { mode: 'proxy' });
|
|
445
|
+
// Send multiple messages concurrently
|
|
446
|
+
yield* Effect.forEach(Array.from({ length: messageCount }, (_, i) => ({ message: `A${i}` })), channelAToC.send, { concurrency: 'unbounded' });
|
|
447
|
+
// Receive responses
|
|
448
|
+
const responses = yield* channelAToC.listen.pipe(Stream.mapEffect(Effect.fromResult), Stream.take(messageCount), Stream.runCollect);
|
|
449
|
+
expect(responses.length).toBe(messageCount);
|
|
450
|
+
});
|
|
451
|
+
const nodeCCode = Effect.gen(function* () {
|
|
452
|
+
const channelCToA = yield* createChannel(nodeC, 'A', { mode: 'proxy' });
|
|
453
|
+
// Send multiple messages concurrently
|
|
454
|
+
yield* Effect.forEach(Array.from({ length: messageCount }, (_, i) => ({ message: `C${i}` })), channelCToA.send, { concurrency: 'unbounded' });
|
|
455
|
+
// Receive responses
|
|
456
|
+
const responses = yield* channelCToA.listen.pipe(Stream.mapEffect(Effect.fromResult), Stream.take(messageCount), Stream.runCollect);
|
|
457
|
+
expect(responses.length).toBe(messageCount);
|
|
458
|
+
});
|
|
459
|
+
yield* Effect.all([nodeACode, nodeCCode], { concurrency: 'unbounded' });
|
|
460
|
+
}).pipe(Vitest.withTestCtx(test)));
|
|
461
|
+
Vitest.describe('ACK forkScoped regression tests', { timeout: 10_000 }, () => {
|
|
462
|
+
/**
|
|
463
|
+
* REGRESSION TEST: ACK sends must be forked (non-blocking) to allow message processing to continue.
|
|
464
|
+
*
|
|
465
|
+
* This test uses simulation parameters to inject a delay BEFORE the ACK send.
|
|
466
|
+
* It then measures when messages arrive at the receiver's listen queue.
|
|
467
|
+
*
|
|
468
|
+
* With the fix (Effect.forkScoped):
|
|
469
|
+
* - ACK send is forked in the background
|
|
470
|
+
* - Message is added to listen queue IMMEDIATELY (before ACK send completes)
|
|
471
|
+
* - Multiple messages arrive close together regardless of ACK delay
|
|
472
|
+
*
|
|
473
|
+
* Without the fix (blocking yield*):
|
|
474
|
+
* - ACK send blocks the processing loop
|
|
475
|
+
* - Message is added to listen queue AFTER ACK send completes
|
|
476
|
+
* - Messages arrive spread out by the ACK delay
|
|
477
|
+
*
|
|
478
|
+
* To verify this test catches the regression:
|
|
479
|
+
* 1. Temporarily change `Effect.forkScoped` to `yield*` in proxy-channel.ts:319
|
|
480
|
+
* 2. Run this test - it should FAIL because messages arrive too slowly
|
|
481
|
+
* 3. Revert the change - test should PASS
|
|
482
|
+
*/
|
|
483
|
+
/**
|
|
484
|
+
* This test verifies the fix works: messages should arrive at the listen queue
|
|
485
|
+
* without being blocked by slow ACK sends. We measure how long it takes to receive
|
|
486
|
+
* all messages - with forked ACKs it should be fast, with blocking ACKs it would be slow.
|
|
487
|
+
*/
|
|
488
|
+
Vitest.live('messages arrive in listen queue without waiting for ACK send', (test) => Effect.gen(function* () {
|
|
489
|
+
const ACK_DELAY_MS = 50;
|
|
490
|
+
const MESSAGE_COUNT = 5;
|
|
491
|
+
const nodeA = yield* makeMeshNode('A');
|
|
492
|
+
const nodeB = yield* makeMeshNode('B');
|
|
493
|
+
yield* connectNodesViaMessageChannel(nodeA, nodeB);
|
|
494
|
+
const receivedMessages = [];
|
|
495
|
+
const senderCode = Effect.gen(function* () {
|
|
496
|
+
const channelAToB = yield* nodeA.makeChannel({
|
|
497
|
+
target: 'B',
|
|
498
|
+
channelName: 'test-ack-timing',
|
|
499
|
+
schema: ExampleSchema,
|
|
500
|
+
mode: 'proxy',
|
|
501
|
+
timeout: 3000,
|
|
502
|
+
});
|
|
503
|
+
// Send all messages concurrently - this is key!
|
|
504
|
+
// With forked ACKs, all messages get processed immediately at receiver
|
|
505
|
+
// With blocking ACKs, messages would be processed one at a time with delays
|
|
506
|
+
yield* Effect.forEach(Array.from({ length: MESSAGE_COUNT }, (_, i) => ({ message: `msg${i}` })), channelAToB.send, { concurrency: 'unbounded' });
|
|
507
|
+
});
|
|
508
|
+
const receiverCode = Effect.gen(function* () {
|
|
509
|
+
const channelBToA = yield* nodeB.makeChannel({
|
|
510
|
+
target: 'A',
|
|
511
|
+
channelName: 'test-ack-timing',
|
|
512
|
+
schema: ExampleSchema,
|
|
513
|
+
mode: 'proxy',
|
|
514
|
+
timeout: 3000,
|
|
515
|
+
// KEY: Inject delay BEFORE ACK send
|
|
516
|
+
// With forkScoped: message arrives in queue immediately, then ACK is sent in background
|
|
517
|
+
// Without forkScoped: message waits for ACK delay before being added to queue
|
|
518
|
+
simulation: {
|
|
519
|
+
onPayload: {
|
|
520
|
+
beforeAckSend: ACK_DELAY_MS,
|
|
521
|
+
afterAckFork: 0,
|
|
522
|
+
afterListenQueueOffer: 0,
|
|
523
|
+
},
|
|
524
|
+
},
|
|
525
|
+
});
|
|
526
|
+
yield* channelBToA.listen.pipe(Stream.mapEffect(Effect.fromResult), Stream.tap((msg) => Effect.sync(() => {
|
|
527
|
+
receivedMessages.push(msg.message);
|
|
528
|
+
})), Stream.take(MESSAGE_COUNT), Stream.runDrain);
|
|
529
|
+
});
|
|
530
|
+
const startTime = Date.now();
|
|
531
|
+
yield* Effect.all([senderCode, receiverCode], { concurrency: 'unbounded' });
|
|
532
|
+
const elapsed = Date.now() - startTime;
|
|
533
|
+
console.log(`[regression-test-1] Received ${receivedMessages.length} messages in ${elapsed}ms`);
|
|
534
|
+
console.log(`[regression-test-1] Messages: ${receivedMessages.join(', ')}`);
|
|
535
|
+
expect(receivedMessages.length).toBe(MESSAGE_COUNT);
|
|
536
|
+
// With forked ACKs, elapsed time should be much less than MESSAGE_COUNT * ACK_DELAY_MS
|
|
537
|
+
// because ACKs don't block message processing
|
|
538
|
+
const blockingTime = MESSAGE_COUNT * ACK_DELAY_MS;
|
|
539
|
+
console.log(`[regression-test-1] Elapsed: ${elapsed}ms, Blocking estimate: ${blockingTime}ms`);
|
|
540
|
+
// The test passes if messages arrive faster than they would with blocking ACKs
|
|
541
|
+
// Allow some margin for test overhead (2x faster than blocking)
|
|
542
|
+
if (elapsed > blockingTime / 2) {
|
|
543
|
+
throw new Error(`REGRESSION DETECTED: Processing took ${elapsed}ms, blocking estimate: ${blockingTime}ms. ` +
|
|
544
|
+
`With forked ACKs, processing should be much faster. ` +
|
|
545
|
+
`Check that proxy-channel.ts uses Effect.forkScoped for ACK sends.`);
|
|
546
|
+
}
|
|
547
|
+
}).pipe(Vitest.withTestCtx(test)));
|
|
548
|
+
/**
|
|
549
|
+
* Additional test: Verify message processing continues during slow ACK sends.
|
|
550
|
+
*
|
|
551
|
+
* This test sends multiple messages concurrently and verifies they're all
|
|
552
|
+
* processed even when ACK sends are slow.
|
|
553
|
+
*/
|
|
554
|
+
Vitest.live('concurrent messages processed despite slow ACK sends', (test) => Effect.gen(function* () {
|
|
555
|
+
const ACK_DELAY_MS = 50;
|
|
556
|
+
const MESSAGE_COUNT = 5;
|
|
557
|
+
const nodeA = yield* makeMeshNode('A');
|
|
558
|
+
const nodeB = yield* makeMeshNode('B');
|
|
559
|
+
yield* connectNodesViaMessageChannel(nodeA, nodeB);
|
|
560
|
+
const receivedMessages = [];
|
|
561
|
+
const senderCode = Effect.gen(function* () {
|
|
562
|
+
const channelAToB = yield* nodeA.makeChannel({
|
|
563
|
+
target: 'B',
|
|
564
|
+
channelName: 'test-concurrent',
|
|
565
|
+
schema: ExampleSchema,
|
|
566
|
+
mode: 'proxy',
|
|
567
|
+
timeout: 3000,
|
|
568
|
+
});
|
|
569
|
+
// Send all messages concurrently
|
|
570
|
+
yield* Effect.forEach(Array.from({ length: MESSAGE_COUNT }, (_, i) => ({ message: `msg${i}` })), channelAToB.send, { concurrency: 'unbounded' });
|
|
571
|
+
});
|
|
572
|
+
const receiverCode = Effect.gen(function* () {
|
|
573
|
+
const channelBToA = yield* nodeB.makeChannel({
|
|
574
|
+
target: 'A',
|
|
575
|
+
channelName: 'test-concurrent',
|
|
576
|
+
schema: ExampleSchema,
|
|
577
|
+
mode: 'proxy',
|
|
578
|
+
timeout: 3000,
|
|
579
|
+
simulation: {
|
|
580
|
+
onPayload: {
|
|
581
|
+
beforeAckSend: ACK_DELAY_MS,
|
|
582
|
+
afterAckFork: 0,
|
|
583
|
+
afterListenQueueOffer: 0,
|
|
584
|
+
},
|
|
585
|
+
},
|
|
586
|
+
});
|
|
587
|
+
yield* channelBToA.listen.pipe(Stream.mapEffect(Effect.fromResult), Stream.tap((msg) => Effect.sync(() => {
|
|
588
|
+
receivedMessages.push(msg.message);
|
|
589
|
+
})), Stream.take(MESSAGE_COUNT), Stream.runDrain);
|
|
590
|
+
});
|
|
591
|
+
const startTime = Date.now();
|
|
592
|
+
yield* Effect.all([senderCode, receiverCode], { concurrency: 'unbounded' });
|
|
593
|
+
const elapsed = Date.now() - startTime;
|
|
594
|
+
console.log(`[regression-test] Received ${receivedMessages.length} messages in ${elapsed}ms`);
|
|
595
|
+
console.log(`[regression-test] Messages: ${receivedMessages.join(', ')}`);
|
|
596
|
+
// All messages should be received
|
|
597
|
+
expect(receivedMessages.length).toBe(MESSAGE_COUNT);
|
|
598
|
+
// With forked ACKs, elapsed time should be much less than MESSAGE_COUNT * ACK_DELAY_MS
|
|
599
|
+
// because ACKs don't block message processing
|
|
600
|
+
const blockingTime = MESSAGE_COUNT * ACK_DELAY_MS;
|
|
601
|
+
console.log(`[regression-test] Elapsed: ${elapsed}ms, Blocking estimate: ${blockingTime}ms`);
|
|
602
|
+
}).pipe(Vitest.withTestCtx(test)));
|
|
603
|
+
});
|
|
604
|
+
Vitest.live('should fail with timeout due to missing edge', (test) => Effect.gen(function* () {
|
|
415
605
|
const nodeA = yield* makeMeshNode('A');
|
|
416
606
|
const nodeB = yield* makeMeshNode('B');
|
|
417
607
|
const nodeC = yield* makeMeshNode('C');
|
|
@@ -419,35 +609,35 @@ Vitest.describe('webmesh node', { timeout: testTimeout }, () => {
|
|
|
419
609
|
// We're not connecting nodeB and nodeC, so this should fail
|
|
420
610
|
const nodeACode = Effect.gen(function* () {
|
|
421
611
|
const err = yield* createChannel(nodeA, 'C').pipe(Effect.timeout(200), Effect.flip);
|
|
422
|
-
expect(err._tag).toBe('
|
|
612
|
+
expect(err._tag).toBe('TimeoutError');
|
|
423
613
|
});
|
|
424
614
|
const nodeCCode = Effect.gen(function* () {
|
|
425
615
|
const err = yield* createChannel(nodeC, 'A').pipe(Effect.timeout(200), Effect.flip);
|
|
426
|
-
expect(err._tag).toBe('
|
|
616
|
+
expect(err._tag).toBe('TimeoutError');
|
|
427
617
|
});
|
|
428
618
|
yield* Effect.all([nodeACode, nodeCCode], { concurrency: 'unbounded' });
|
|
429
619
|
}).pipe(Vitest.withTestCtx(test)));
|
|
430
|
-
Vitest.
|
|
620
|
+
Vitest.live('should fail with timeout due no transferable', (test) => Effect.gen(function* () {
|
|
431
621
|
const nodeA = yield* makeMeshNode('A');
|
|
432
622
|
const nodeB = yield* makeMeshNode('B');
|
|
433
623
|
yield* connectNodesViaBroadcastChannel(nodeA, nodeB);
|
|
434
624
|
const nodeACode = Effect.gen(function* () {
|
|
435
625
|
const err = yield* createChannel(nodeA, 'B').pipe(Effect.timeout(200), Effect.flip);
|
|
436
|
-
expect(err._tag).toBe('
|
|
626
|
+
expect(err._tag).toBe('TimeoutError');
|
|
437
627
|
});
|
|
438
628
|
const nodeBCode = Effect.gen(function* () {
|
|
439
629
|
const err = yield* createChannel(nodeB, 'A').pipe(Effect.timeout(200), Effect.flip);
|
|
440
|
-
expect(err._tag).toBe('
|
|
630
|
+
expect(err._tag).toBe('TimeoutError');
|
|
441
631
|
});
|
|
442
632
|
yield* Effect.all([nodeACode, nodeBCode], { concurrency: 'unbounded' });
|
|
443
633
|
}).pipe(Vitest.withTestCtx(test)));
|
|
444
|
-
Vitest.
|
|
634
|
+
Vitest.live('reconnect with re-created node', (test) => Effect.gen(function* () {
|
|
445
635
|
const nodeCgen1Scope = yield* Scope.make();
|
|
446
636
|
const nodeA = yield* makeMeshNode('A');
|
|
447
637
|
const nodeB = yield* makeMeshNode('B');
|
|
448
|
-
const nodeCgen1 = yield* makeMeshNode('C').pipe(Scope.
|
|
638
|
+
const nodeCgen1 = yield* makeMeshNode('C').pipe(Scope.provide(nodeCgen1Scope));
|
|
449
639
|
yield* connectNodesViaMessageChannel(nodeA, nodeB);
|
|
450
|
-
yield* connectNodesViaMessageChannel(nodeB, nodeCgen1).pipe(Scope.
|
|
640
|
+
yield* connectNodesViaMessageChannel(nodeB, nodeCgen1).pipe(Scope.provide(nodeCgen1Scope));
|
|
451
641
|
const nodeACode = Effect.gen(function* () {
|
|
452
642
|
const channelAToB = yield* createChannel(nodeA, 'C');
|
|
453
643
|
yield* channelAToB.send({ message: 'A1' });
|
|
@@ -458,7 +648,7 @@ Vitest.describe('webmesh node', { timeout: testTimeout }, () => {
|
|
|
458
648
|
yield* channelBToA.send({ message: 'C1' });
|
|
459
649
|
expect(yield* getFirstMessage(channelBToA)).toEqual({ message: 'A1' });
|
|
460
650
|
});
|
|
461
|
-
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));
|
|
462
652
|
yield* Scope.close(nodeCgen1Scope, Exit.void);
|
|
463
653
|
const nodeCgen2 = yield* makeMeshNode('C');
|
|
464
654
|
yield* connectNodesViaMessageChannel(nodeB, nodeCgen2, { replaceIfExists: true });
|
|
@@ -473,7 +663,7 @@ Vitest.describe('webmesh node', { timeout: testTimeout }, () => {
|
|
|
473
663
|
* D
|
|
474
664
|
*/
|
|
475
665
|
Vitest.describe('diamond topology', () => {
|
|
476
|
-
Vitest.
|
|
666
|
+
Vitest.live('should work', (test) => Effect.gen(function* () {
|
|
477
667
|
const nodeA = yield* makeMeshNode('A');
|
|
478
668
|
const nodeB = yield* makeMeshNode('B');
|
|
479
669
|
const nodeC = yield* makeMeshNode('C');
|
|
@@ -505,7 +695,7 @@ Vitest.describe('webmesh node', { timeout: testTimeout }, () => {
|
|
|
505
695
|
* Topology: Butterfly topology with two connected hubs (C-D) each serving multiple nodes
|
|
506
696
|
*/
|
|
507
697
|
Vitest.describe('butterfly topology', () => {
|
|
508
|
-
Vitest.
|
|
698
|
+
Vitest.live('should work', (test) => Effect.gen(function* () {
|
|
509
699
|
const nodeA = yield* makeMeshNode('A');
|
|
510
700
|
const nodeB = yield* makeMeshNode('B');
|
|
511
701
|
const nodeC = yield* makeMeshNode('C');
|
|
@@ -534,14 +724,14 @@ Vitest.describe('webmesh node', { timeout: testTimeout }, () => {
|
|
|
534
724
|
Vitest.describe('mixture of direct and proxy edge connections', () => {
|
|
535
725
|
// TODO test case to better guard against case where side A tries to create a proxy channel to B
|
|
536
726
|
// and side B tries to create a direct to A
|
|
537
|
-
Vitest.
|
|
727
|
+
Vitest.live('should work for proxy channels', (test) => Effect.gen(function* () {
|
|
538
728
|
const nodeA = yield* makeMeshNode('A');
|
|
539
729
|
const nodeB = yield* makeMeshNode('B');
|
|
540
730
|
yield* connectNodesViaMessageChannel(nodeB, nodeA);
|
|
541
731
|
const err = yield* connectNodesViaBroadcastChannel(nodeA, nodeB).pipe(Effect.flip);
|
|
542
732
|
expect(err._tag).toBe('EdgeAlreadyExistsError');
|
|
543
733
|
}).pipe(Vitest.withTestCtx(test)));
|
|
544
|
-
Vitest.
|
|
734
|
+
Vitest.live('should work for directs', (test) => Effect.gen(function* () {
|
|
545
735
|
const nodeA = yield* makeMeshNode('A');
|
|
546
736
|
const nodeB = yield* makeMeshNode('B');
|
|
547
737
|
const nodeC = yield* makeMeshNode('C');
|
|
@@ -561,7 +751,7 @@ Vitest.describe('webmesh node', { timeout: testTimeout }, () => {
|
|
|
561
751
|
}).pipe(Vitest.withTestCtx(test)));
|
|
562
752
|
});
|
|
563
753
|
Vitest.describe('listenForChannel', () => {
|
|
564
|
-
Vitest.
|
|
754
|
+
Vitest.live('connect later', (test) => Effect.gen(function* () {
|
|
565
755
|
const nodeA = yield* makeMeshNode('A');
|
|
566
756
|
const mode = 'direct';
|
|
567
757
|
const connect = mode === 'direct' ? connectNodesViaMessageChannel : connectNodesViaBroadcastChannel;
|
|
@@ -585,7 +775,7 @@ Vitest.describe('webmesh node', { timeout: testTimeout }, () => {
|
|
|
585
775
|
yield* Effect.all([nodeACode, nodeBCode.pipe(Effect.delay(500))], { concurrency: 'unbounded' });
|
|
586
776
|
}).pipe(Vitest.withTestCtx(test)));
|
|
587
777
|
// TODO provide a way to allow for reconnecting in the `listenForChannel` case
|
|
588
|
-
Vitest.
|
|
778
|
+
Vitest.live.skip('reconnect', (test) => Effect.gen(function* () {
|
|
589
779
|
const nodeA = yield* makeMeshNode('A');
|
|
590
780
|
const mode = 'direct';
|
|
591
781
|
const connect = mode === 'direct' ? connectNodesViaMessageChannel : connectNodesViaBroadcastChannel;
|
|
@@ -621,11 +811,11 @@ Vitest.describe('webmesh node', { timeout: testTimeout }, () => {
|
|
|
621
811
|
yield* Effect.all([nodeACode, nodeBCode], { concurrency: 'unbounded' });
|
|
622
812
|
}).pipe(Vitest.withTestCtx(test)));
|
|
623
813
|
Vitest.describe('prop tests', { timeout: propTestTimeout }, () => {
|
|
624
|
-
Vitest.
|
|
814
|
+
Vitest.live.prop('listenForChannel A <> B <> C', [Delay, Delay, Delay, Delay, ChannelType], ([delayNodeA, delayNodeC, delayConnectAB, delayConnectBC, channelType], test) => Effect.gen(function* () {
|
|
625
815
|
const nodeA = yield* makeMeshNode('A');
|
|
626
816
|
const nodeB = yield* makeMeshNode('B');
|
|
627
817
|
const nodeC = yield* makeMeshNode('C');
|
|
628
|
-
const mode = channelType.includes('proxy') ? 'proxy' : 'direct';
|
|
818
|
+
const mode = channelType.includes('proxy') === true ? 'proxy' : 'direct';
|
|
629
819
|
const connect = channelType === 'direct' ? connectNodesViaMessageChannel : connectNodesViaBroadcastChannel;
|
|
630
820
|
yield* connect(nodeA, nodeB).pipe(maybeDelay(delayConnectAB, 'delayConnectAB'));
|
|
631
821
|
yield* connect(nodeB, nodeC).pipe(maybeDelay(delayConnectBC, 'delayConnectBC'));
|
|
@@ -657,7 +847,7 @@ Vitest.describe('webmesh node', { timeout: testTimeout }, () => {
|
|
|
657
847
|
});
|
|
658
848
|
});
|
|
659
849
|
Vitest.describe('broadcast channel', () => {
|
|
660
|
-
Vitest.
|
|
850
|
+
Vitest.live('should work', (test) => Effect.gen(function* () {
|
|
661
851
|
const nodeA = yield* makeMeshNode('A');
|
|
662
852
|
const nodeB = yield* makeMeshNode('B');
|
|
663
853
|
const nodeC = yield* makeMeshNode('C');
|
|
@@ -665,12 +855,12 @@ Vitest.describe('webmesh node', { timeout: testTimeout }, () => {
|
|
|
665
855
|
yield* connectNodesViaMessageChannel(nodeB, nodeC);
|
|
666
856
|
const channelOnA = yield* nodeA.makeBroadcastChannel({ channelName: 'test', schema: Schema.String });
|
|
667
857
|
const channelOnC = yield* nodeC.makeBroadcastChannel({ channelName: 'test', schema: Schema.String });
|
|
668
|
-
const listenOnAFiber = yield* channelOnA.listen.pipe(Stream.
|
|
669
|
-
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);
|
|
670
860
|
yield* channelOnA.send('A1');
|
|
671
861
|
yield* channelOnC.send('C1');
|
|
672
|
-
expect(yield* listenOnAFiber).toEqual('C1');
|
|
673
|
-
expect(yield* listenOnCFiber).toEqual('A1');
|
|
862
|
+
expect(yield* Fiber.join(listenOnAFiber)).toEqual('C1');
|
|
863
|
+
expect(yield* Fiber.join(listenOnCFiber)).toEqual('A1');
|
|
674
864
|
}).pipe(Vitest.withTestCtx(test)));
|
|
675
865
|
});
|
|
676
866
|
});
|