@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.
Files changed (56) hide show
  1. package/dist/.tsbuildinfo +1 -1
  2. package/dist/channel/direct-channel-internal.d.ts +2 -3
  3. package/dist/channel/direct-channel-internal.d.ts.map +1 -1
  4. package/dist/channel/direct-channel-internal.js +23 -18
  5. package/dist/channel/direct-channel-internal.js.map +1 -1
  6. package/dist/channel/direct-channel.d.ts.map +1 -1
  7. package/dist/channel/direct-channel.js +20 -21
  8. package/dist/channel/direct-channel.js.map +1 -1
  9. package/dist/channel/proxy-channel.d.ts +7 -8
  10. package/dist/channel/proxy-channel.d.ts.map +1 -1
  11. package/dist/channel/proxy-channel.js +26 -22
  12. package/dist/channel/proxy-channel.js.map +1 -1
  13. package/dist/common.d.ts +33 -35
  14. package/dist/common.d.ts.map +1 -1
  15. package/dist/common.js +3 -2
  16. package/dist/common.js.map +1 -1
  17. package/dist/mesh-schema.d.ts +262 -136
  18. package/dist/mesh-schema.d.ts.map +1 -1
  19. package/dist/mesh-schema.js +45 -44
  20. package/dist/mesh-schema.js.map +1 -1
  21. package/dist/node.d.ts +6 -6
  22. package/dist/node.d.ts.map +1 -1
  23. package/dist/node.js +22 -21
  24. package/dist/node.js.map +1 -1
  25. package/dist/node.test.js +74 -71
  26. package/dist/node.test.js.map +1 -1
  27. package/dist/utils.d.ts +1 -1
  28. package/dist/utils.d.ts.map +1 -1
  29. package/dist/utils.js.map +1 -1
  30. package/dist/websocket-edge.d.ts +18 -16
  31. package/dist/websocket-edge.d.ts.map +1 -1
  32. package/dist/websocket-edge.js +30 -36
  33. package/dist/websocket-edge.js.map +1 -1
  34. package/dist/websocket-edge.test.js +14 -14
  35. package/dist/websocket-edge.test.js.map +1 -1
  36. package/dist/worker/mod.d.ts +24 -0
  37. package/dist/worker/mod.d.ts.map +1 -0
  38. package/dist/worker/mod.js +44 -0
  39. package/dist/worker/mod.js.map +1 -0
  40. package/dist/worker/schema.d.ts +10 -0
  41. package/dist/worker/schema.d.ts.map +1 -0
  42. package/dist/worker/schema.js +7 -0
  43. package/dist/worker/schema.js.map +1 -0
  44. package/package.json +20 -37
  45. package/src/channel/direct-channel-internal.ts +30 -22
  46. package/src/channel/direct-channel.ts +26 -23
  47. package/src/channel/proxy-channel.ts +39 -36
  48. package/src/common.ts +6 -3
  49. package/src/mesh-schema.ts +34 -33
  50. package/src/node.test.ts +99 -86
  51. package/src/node.ts +32 -32
  52. package/src/utils.ts +3 -3
  53. package/src/websocket-edge.test.ts +14 -14
  54. package/src/websocket-edge.ts +47 -41
  55. package/src/worker/mod.ts +89 -0
  56. 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 { expect } from 'vitest';
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.flatten(), Stream.take(1), Stream.runCollect, Effect.map(([message]) => message));
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.scopedLive` (i.e. with `Vitest.scoped`)
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.Literal(0, 1, 10, 50));
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.Literal('direct', 'proxy(via-messagechannel-edge)', 'proxy');
71
- const NodeNames = Schema.Union(Schema.Tuple(Schema.Literal('A'), Schema.Literal('B')), Schema.Tuple(Schema.Literal('B'), Schema.Literal('A')));
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.scopedLive(
120
+ // Vitest.live(
118
121
  // 'a / b connect at different times with different channel types',
119
122
  // (test) =>
120
- Vitest.scopedLive.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* () {
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.scopedLive(
147
+ // Vitest.live(
145
148
  // 'b reconnects',
146
149
  // (test) =>
147
- Vitest.scopedLive.prop('b reconnects', [Delay, Delay, ChannelType], ([waitForOfflineDelay, sleepDelay, channelType], test) => Effect.gen(function* () {
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.scopedLive('reconnect with re-created node', (test) => Effect.gen(function* () {
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.extend(nodeBgen1Scope));
201
- yield* connectNodesViaMessageChannel(nodeA, nodeBgen1).pipe(Scope.extend(nodeBgen1Scope));
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.extend(nodeBgen1Scope))], {
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.Literal('proxy', 'direct');
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.scopedLive.prop('replace edge while keeping the channel', [ChannelTypeWithoutMessageChannelProxy, NodeNames], ([channelType, nodeNames], test) => Effect.gen(function* () {
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.scopedLive.prop('concurrent messages', [ChannelType, Schema.Int.pipe(Schema.between(1, 50))], ([channelType, count], test) => Effect.gen(function* () {
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(Chunk.makeBy(count, (i) => ({ message: `A${i}` })), channelAToB.send, { concurrency: 'unbounded' });
283
- expect(yield* channelAToB.listen.pipe(Stream.flatten(), Stream.take(count), Stream.runCollect)).toEqual(Chunk.makeBy(count, (i) => ({ message: `B${i}` })));
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(Chunk.makeBy(count, (i) => ({ message: `B${i}` })), channelBToA.send, { concurrency: 'unbounded' });
290
- expect(yield* channelBToA.listen.pipe(Stream.flatten(), Stream.take(count), Stream.runCollect)).toEqual(Chunk.makeBy(count, (i) => ({ message: `A${i}` })));
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.scopedLive('differing initial edge counter', (test) => Effect.gen(function* () {
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.flatten(), Stream.tap((msg) => channelBToA.send({ message: `resp:${msg.message}` })), Stream.take(messageCount), Stream.runDrain);
310
- }).pipe(Effect.scoped, Effect.fork);
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, Effect.repeatN(messageCount));
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.scopedLive('manual debug test', (test) => Effect.gen(function* () {
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.scopedLive('broadcast edge with message channel', (test) => Effect.gen(function* () {
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('TimeoutException');
349
+ expect(err._tag).toBe('TimeoutError');
347
350
  }).pipe(Vitest.withTestCtx(test)));
348
351
  });
349
352
  Vitest.describe('A <> B <> C', () => {
350
- Vitest.scopedLive('should work', (test) => Effect.gen(function* () {
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.scopedLive('should work - delayed edge', (test) => Effect.gen(function* () {
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.scopedLive('proxy channel', (test) => Effect.gen(function* () {
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.scopedLive('ACK sending should not block message processing', (test) => Effect.gen(function* () {
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(Chunk.makeBy(messageCount, (i) => ({ message: `A${i}` })), channelAToC.send, { concurrency: 'unbounded' });
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.flatten(), Stream.take(messageCount), Stream.runCollect);
446
- expect(Chunk.size(responses)).toBe(messageCount);
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(Chunk.makeBy(messageCount, (i) => ({ message: `C${i}` })), channelCToA.send, { concurrency: 'unbounded' });
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.flatten(), Stream.take(messageCount), Stream.runCollect);
454
- expect(Chunk.size(responses)).toBe(messageCount);
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.scopedLive('messages arrive in listen queue without waiting for ACK send', (test) => Effect.gen(function* () {
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(Chunk.makeBy(MESSAGE_COUNT, (i) => ({ message: `msg${i}` })), channelAToB.send, { concurrency: 'unbounded' });
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.flatten(), Stream.tap((msg) => Effect.sync(() => {
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.scopedLive('concurrent messages processed despite slow ACK sends', (test) => Effect.gen(function* () {
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(Chunk.makeBy(MESSAGE_COUNT, (i) => ({ message: `msg${i}` })), channelAToB.send, { concurrency: 'unbounded' });
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.flatten(), Stream.tap((msg) => Effect.sync(() => {
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.scopedLive('should fail with timeout due to missing edge', (test) => Effect.gen(function* () {
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('TimeoutException');
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('TimeoutException');
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.scopedLive('should fail with timeout due no transferable', (test) => Effect.gen(function* () {
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('TimeoutException');
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('TimeoutException');
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.scopedLive('reconnect with re-created node', (test) => Effect.gen(function* () {
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.extend(nodeCgen1Scope));
638
+ const nodeCgen1 = yield* makeMeshNode('C').pipe(Scope.provide(nodeCgen1Scope));
636
639
  yield* connectNodesViaMessageChannel(nodeA, nodeB);
637
- yield* connectNodesViaMessageChannel(nodeB, nodeCgen1).pipe(Scope.extend(nodeCgen1Scope));
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.extend(nodeCgen1Scope));
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.scopedLive('should work', (test) => Effect.gen(function* () {
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.scopedLive('should work', (test) => Effect.gen(function* () {
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.scopedLive('should work for proxy channels', (test) => Effect.gen(function* () {
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.scopedLive('should work for directs', (test) => Effect.gen(function* () {
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.scopedLive('connect later', (test) => Effect.gen(function* () {
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.scopedLive.skip('reconnect', (test) => Effect.gen(function* () {
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.scopedLive.prop('listenForChannel A <> B <> C', [Delay, Delay, Delay, Delay, ChannelType], ([delayNodeA, delayNodeC, delayConnectAB, delayConnectBC, channelType], test) => Effect.gen(function* () {
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.scopedLive('should work', (test) => Effect.gen(function* () {
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.flatten(), Stream.runHead, Effect.flatten, Effect.fork);
856
- const listenOnCFiber = yield* channelOnC.listen.pipe(Stream.flatten(), Stream.runHead, Effect.flatten, Effect.fork);
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
  });