@livestore/webmesh 0.0.0-snapshot-1d99fea7d2ce2c7a5d9ed0a3752f8a7bda6bc3db

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 (54) hide show
  1. package/README.md +5 -0
  2. package/dist/.tsbuildinfo +1 -0
  3. package/dist/channel/message-channel.d.ts +20 -0
  4. package/dist/channel/message-channel.d.ts.map +1 -0
  5. package/dist/channel/message-channel.js +183 -0
  6. package/dist/channel/message-channel.js.map +1 -0
  7. package/dist/channel/proxy-channel.d.ts +19 -0
  8. package/dist/channel/proxy-channel.d.ts.map +1 -0
  9. package/dist/channel/proxy-channel.js +179 -0
  10. package/dist/channel/proxy-channel.js.map +1 -0
  11. package/dist/common.d.ts +83 -0
  12. package/dist/common.d.ts.map +1 -0
  13. package/dist/common.js +13 -0
  14. package/dist/common.js.map +1 -0
  15. package/dist/mesh-schema.d.ts +104 -0
  16. package/dist/mesh-schema.d.ts.map +1 -0
  17. package/dist/mesh-schema.js +77 -0
  18. package/dist/mesh-schema.js.map +1 -0
  19. package/dist/mod.d.ts +5 -0
  20. package/dist/mod.d.ts.map +1 -0
  21. package/dist/mod.js +5 -0
  22. package/dist/mod.js.map +1 -0
  23. package/dist/node.d.ts +65 -0
  24. package/dist/node.d.ts.map +1 -0
  25. package/dist/node.js +216 -0
  26. package/dist/node.js.map +1 -0
  27. package/dist/node.test.d.ts +2 -0
  28. package/dist/node.test.d.ts.map +1 -0
  29. package/dist/node.test.js +351 -0
  30. package/dist/node.test.js.map +1 -0
  31. package/dist/utils.d.ts +19 -0
  32. package/dist/utils.d.ts.map +1 -0
  33. package/dist/utils.js +41 -0
  34. package/dist/utils.js.map +1 -0
  35. package/dist/websocket-connection.d.ts +51 -0
  36. package/dist/websocket-connection.d.ts.map +1 -0
  37. package/dist/websocket-connection.js +74 -0
  38. package/dist/websocket-connection.js.map +1 -0
  39. package/dist/websocket-server.d.ts +7 -0
  40. package/dist/websocket-server.d.ts.map +1 -0
  41. package/dist/websocket-server.js +24 -0
  42. package/dist/websocket-server.js.map +1 -0
  43. package/package.json +32 -0
  44. package/src/channel/message-channel.ts +354 -0
  45. package/src/channel/proxy-channel.ts +332 -0
  46. package/src/common.ts +36 -0
  47. package/src/mesh-schema.ts +94 -0
  48. package/src/mod.ts +4 -0
  49. package/src/node.test.ts +533 -0
  50. package/src/node.ts +408 -0
  51. package/src/utils.ts +47 -0
  52. package/src/websocket-connection.ts +158 -0
  53. package/src/websocket-server.ts +40 -0
  54. package/tsconfig.json +11 -0
@@ -0,0 +1,533 @@
1
+ import { IS_CI } from '@livestore/utils'
2
+ import { Chunk, Deferred, Effect, identity, Layer, Logger, Schema, Stream, WebChannel } from '@livestore/utils/effect'
3
+ import { OtelLiveHttp } from '@livestore/utils/node'
4
+ import { Vitest } from '@livestore/utils/node-vitest'
5
+ import { expect } from 'vitest'
6
+
7
+ import { Packet } from './mesh-schema.js'
8
+ import type { MeshNode } from './node.js'
9
+ import { makeMeshNode } from './node.js'
10
+
11
+ // TODO test cases where in-between node only comes online later
12
+ // TODO test cases where other side tries to reconnect
13
+ // TODO test combination of connection types (message, proxy)
14
+ // TODO test "diamond shape" topology (A <> B1, A <> B2, B1 <> C, B2 <> C)
15
+ // TODO test cases where multiple entities try to claim to be the same channel end (e.g. A,B,B)
16
+ // TODO write tests with worker threads
17
+
18
+ const ExampleSchema = Schema.Struct({ message: Schema.String })
19
+
20
+ const connectNodesViaMessageChannel = (nodeA: MeshNode, nodeB: MeshNode) =>
21
+ Effect.gen(function* () {
22
+ const mc = new MessageChannel()
23
+ const meshChannelAToB = yield* WebChannel.messagePortChannel({ port: mc.port1, schema: Packet })
24
+ const meshChannelBToA = yield* WebChannel.messagePortChannel({ port: mc.port2, schema: Packet })
25
+
26
+ yield* nodeA.addConnection({ target: nodeB.nodeName, connectionChannel: meshChannelAToB })
27
+ yield* nodeB.addConnection({ target: nodeA.nodeName, connectionChannel: meshChannelBToA })
28
+
29
+ return mc
30
+ }).pipe(Effect.withSpan(`connectNodesViaMessageChannel:${nodeA.nodeName}↔${nodeB.nodeName}`))
31
+
32
+ const connectNodesViaBroadcastChannel = (nodeA: MeshNode, nodeB: MeshNode) =>
33
+ Effect.gen(function* () {
34
+ // Need to instantiate two different channels because they filter out messages they sent themselves
35
+ const broadcastWebChannelA = yield* WebChannel.broadcastChannelWithAck({
36
+ channelName: `${nodeA.nodeName}↔${nodeB.nodeName}`,
37
+ listenSchema: Packet,
38
+ sendSchema: Packet,
39
+ })
40
+
41
+ const broadcastWebChannelB = yield* WebChannel.broadcastChannelWithAck({
42
+ channelName: `${nodeA.nodeName}↔${nodeB.nodeName}`,
43
+ listenSchema: Packet,
44
+ sendSchema: Packet,
45
+ })
46
+
47
+ yield* nodeA.addConnection({ target: nodeB.nodeName, connectionChannel: broadcastWebChannelA })
48
+ yield* nodeB.addConnection({ target: nodeA.nodeName, connectionChannel: broadcastWebChannelB })
49
+ }).pipe(Effect.withSpan(`connectNodesViaBroadcastChannel:${nodeA.nodeName}↔${nodeB.nodeName}`))
50
+
51
+ const createChannel = (source: MeshNode, target: string, options?: Partial<Parameters<MeshNode['makeChannel']>[0]>) =>
52
+ source.makeChannel({
53
+ target,
54
+ channelName: options?.channelName ?? 'test',
55
+ schema: ExampleSchema,
56
+ // transferables: options?.transferables ?? 'prefer',
57
+ mode: options?.mode ?? 'messagechannel',
58
+ timeout: options?.timeout ?? 200,
59
+ })
60
+
61
+ const getFirstMessage = <T1, T2>(channel: WebChannel.WebChannel<T1, T2>) =>
62
+ channel.listen.pipe(
63
+ Stream.flatten(),
64
+ Stream.take(1),
65
+ Stream.runCollect,
66
+ Effect.map(([message]) => message),
67
+ )
68
+
69
+ // NOTE we distinguish between undefined and 0 delays as it changes the fiber execution
70
+ const maybeDelay =
71
+ (delay: number | undefined, label: string) =>
72
+ <A, E, R>(effect: Effect.Effect<A, E, R>) =>
73
+ delay === undefined
74
+ ? effect
75
+ : Effect.sleep(delay).pipe(Effect.withSpan(`${label}:delay(${delay})`), Effect.andThen(effect))
76
+
77
+ const testTimeout = IS_CI ? 30_000 : 500
78
+
79
+ // TODO also make work without `Vitest.scopedLive` (i.e. with `Vitest.scoped`)
80
+ // probably requires controlling the clocks
81
+ Vitest.describe('webmesh node', { timeout: testTimeout }, () => {
82
+ Vitest.describe('A <> B', () => {
83
+ Vitest.describe('prop tests', () => {
84
+ const Delay = Schema.UndefinedOr(Schema.Literal(0, 1, 10, 50))
85
+ // NOTE for message channels, we test both with and without transferables (i.e. proxying)
86
+ const ChannelType = Schema.Literal('messagechannel', 'messagechannel.proxy', 'proxy')
87
+
88
+ const fromChannelType = (
89
+ channelType: typeof ChannelType.Type,
90
+ ): {
91
+ mode: 'messagechannel' | 'proxy'
92
+ connectNodes: typeof connectNodesViaMessageChannel | typeof connectNodesViaBroadcastChannel
93
+ } => {
94
+ switch (channelType) {
95
+ case 'proxy': {
96
+ return { mode: 'proxy', connectNodes: connectNodesViaBroadcastChannel }
97
+ }
98
+ case 'messagechannel': {
99
+ return { mode: 'messagechannel', connectNodes: connectNodesViaMessageChannel }
100
+ }
101
+ case 'messagechannel.proxy': {
102
+ return { mode: 'proxy', connectNodes: connectNodesViaMessageChannel }
103
+ }
104
+ }
105
+ }
106
+
107
+ Vitest.scopedLive.prop(
108
+ // Vitest.scopedLive.only(
109
+ 'a / b connect at different times with different channel types',
110
+ [Delay, Delay, Delay, ChannelType],
111
+ ([delayA, delayB, connectDelay, channelType], test) =>
112
+ // (test) =>
113
+ Effect.gen(function* () {
114
+ // const delayA = 1
115
+ // const delayB = 10
116
+ // const connectDelay = 10
117
+ // const channelType = 'message.prefer'
118
+ // console.log('delayA', delayA, 'delayB', delayB, 'connectDelay', connectDelay, 'channelType', channelType)
119
+
120
+ const nodeA = yield* makeMeshNode('A')
121
+ const nodeB = yield* makeMeshNode('B')
122
+
123
+ const { mode, connectNodes } = fromChannelType(channelType)
124
+
125
+ const nodeACode = Effect.gen(function* () {
126
+ const channelAToB = yield* createChannel(nodeA, 'B', { mode })
127
+
128
+ yield* channelAToB.send({ message: 'A1' })
129
+ expect(yield* getFirstMessage(channelAToB)).toEqual({ message: 'A2' })
130
+ })
131
+
132
+ const nodeBCode = Effect.gen(function* () {
133
+ const channelBToA = yield* createChannel(nodeB, 'A', { mode })
134
+
135
+ yield* channelBToA.send({ message: 'A2' })
136
+ expect(yield* getFirstMessage(channelBToA)).toEqual({ message: 'A1' })
137
+ })
138
+
139
+ yield* Effect.all(
140
+ [
141
+ connectNodes(nodeA, nodeB).pipe(maybeDelay(connectDelay, 'connectNodes')),
142
+ nodeACode.pipe(maybeDelay(delayA, 'nodeACode')),
143
+ nodeBCode.pipe(maybeDelay(delayB, 'nodeBCode')),
144
+ ],
145
+ { concurrency: 'unbounded' },
146
+ )
147
+ }).pipe(
148
+ withCtx(test, { skipOtel: true, suffix: `delayA=${delayA} delayB=${delayB} channelType=${channelType}` }),
149
+ ),
150
+ )
151
+
152
+ // Vitest.scopedLive.only(
153
+ // 'reconnects',
154
+ // (test) =>
155
+ Vitest.scopedLive.prop(
156
+ 'b reconnects',
157
+ [Delay, Delay, ChannelType],
158
+ ([waitForOfflineDelay, sleepDelay, channelType], test) =>
159
+ Effect.gen(function* () {
160
+ // const waitForOfflineDelay = 0
161
+ // const sleepDelay = 10
162
+ // const channelType = 'proxy'
163
+ // console.log(
164
+ // 'waitForOfflineDelay',
165
+ // waitForOfflineDelay,
166
+ // 'sleepDelay',
167
+ // sleepDelay,
168
+ // 'channelType',
169
+ // channelType,
170
+ // )
171
+
172
+ const nodeA = yield* makeMeshNode('A')
173
+ const nodeB = yield* makeMeshNode('B')
174
+
175
+ const { mode, connectNodes } = fromChannelType(channelType)
176
+
177
+ // TODO also optionally delay the connection
178
+ yield* connectNodes(nodeA, nodeB)
179
+
180
+ const waitForBToBeOffline =
181
+ waitForOfflineDelay === undefined ? undefined : yield* Deferred.make<void, never>()
182
+
183
+ const nodeACode = Effect.gen(function* () {
184
+ const channelAToB = yield* createChannel(nodeA, 'B', { mode })
185
+
186
+ yield* channelAToB.send({ message: 'A1' })
187
+ expect(yield* getFirstMessage(channelAToB)).toEqual({ message: 'B1' })
188
+
189
+ if (waitForBToBeOffline !== undefined) {
190
+ yield* waitForBToBeOffline
191
+ }
192
+
193
+ yield* channelAToB.send({ message: 'A2' })
194
+ expect(yield* getFirstMessage(channelAToB)).toEqual({ message: 'B2' })
195
+ })
196
+
197
+ // Simulating node b going offline and then coming back online
198
+ const nodeBCode = Effect.gen(function* () {
199
+ yield* Effect.gen(function* () {
200
+ const channelBToA = yield* createChannel(nodeB, 'A', { mode })
201
+
202
+ yield* channelBToA.send({ message: 'B1' })
203
+ expect(yield* getFirstMessage(channelBToA)).toEqual({ message: 'A1' })
204
+ }).pipe(Effect.scoped)
205
+
206
+ if (waitForBToBeOffline !== undefined) {
207
+ yield* Deferred.succeed(waitForBToBeOffline, void 0)
208
+ }
209
+
210
+ if (sleepDelay !== undefined) {
211
+ yield* Effect.sleep(sleepDelay).pipe(Effect.withSpan(`B:sleep(${sleepDelay})`))
212
+ }
213
+
214
+ yield* Effect.gen(function* () {
215
+ const channelBToA = yield* createChannel(nodeB, 'A', { mode })
216
+
217
+ yield* channelBToA.send({ message: 'B2' })
218
+ expect(yield* getFirstMessage(channelBToA)).toEqual({ message: 'A2' })
219
+ }).pipe(Effect.scoped)
220
+ })
221
+
222
+ yield* Effect.all([nodeACode, nodeBCode], { concurrency: 'unbounded' })
223
+ }).pipe(
224
+ withCtx(test, {
225
+ skipOtel: true,
226
+ suffix: `waitForOfflineDelay=${waitForOfflineDelay} sleepDelay=${sleepDelay} channelType=${channelType}`,
227
+ }),
228
+ ),
229
+ )
230
+
231
+ const ChannelTypeWithoutMessageChannelProxy = Schema.Literal('proxy', 'messagechannel')
232
+ Vitest.scopedLive.prop(
233
+ 'replace connection while keeping the channel',
234
+ [ChannelTypeWithoutMessageChannelProxy],
235
+ ([channelType], test) =>
236
+ Effect.gen(function* () {
237
+ const nodeA = yield* makeMeshNode('A')
238
+ const nodeB = yield* makeMeshNode('B')
239
+
240
+ const { mode, connectNodes } = fromChannelType(channelType)
241
+
242
+ yield* connectNodes(nodeA, nodeB)
243
+
244
+ const waitForConnectionReplacement = yield* Deferred.make<void>()
245
+
246
+ const nodeACode = Effect.gen(function* () {
247
+ const channelAToB = yield* createChannel(nodeA, 'B', { mode })
248
+
249
+ yield* channelAToB.send({ message: 'A1' })
250
+ expect(yield* getFirstMessage(channelAToB)).toEqual({ message: 'B1' })
251
+
252
+ yield* waitForConnectionReplacement
253
+
254
+ yield* channelAToB.send({ message: 'A2' })
255
+ expect(yield* getFirstMessage(channelAToB)).toEqual({ message: 'B2' })
256
+ })
257
+
258
+ const nodeBCode = Effect.gen(function* () {
259
+ const channelBToA = yield* createChannel(nodeB, 'A', { mode })
260
+
261
+ yield* channelBToA.send({ message: 'B1' })
262
+ expect(yield* getFirstMessage(channelBToA)).toEqual({ message: 'A1' })
263
+
264
+ // Switch out connection while keeping the channel
265
+ yield* nodeA.removeConnection('B')
266
+ yield* nodeB.removeConnection('A')
267
+ yield* connectNodes(nodeA, nodeB)
268
+ yield* Deferred.succeed(waitForConnectionReplacement, void 0)
269
+
270
+ yield* channelBToA.send({ message: 'B2' })
271
+ expect(yield* getFirstMessage(channelBToA)).toEqual({ message: 'A2' })
272
+ })
273
+
274
+ yield* Effect.all([nodeACode, nodeBCode], { concurrency: 'unbounded' })
275
+ }).pipe(withCtx(test, { skipOtel: true, suffix: `channelType=${channelType}` })),
276
+ )
277
+
278
+ Vitest.describe.todo('TODO improve latency', () => {
279
+ // TODO we need to improve latency when sending messages concurrently
280
+ Vitest.scopedLive.prop(
281
+ 'concurrent messages',
282
+ [ChannelType, Schema.Int.pipe(Schema.between(1, 50))],
283
+ ([channelType, count], test) =>
284
+ Effect.gen(function* () {
285
+ const nodeA = yield* makeMeshNode('A')
286
+ const nodeB = yield* makeMeshNode('B')
287
+
288
+ const { mode, connectNodes } = fromChannelType(channelType)
289
+ console.log('channelType', channelType, 'mode', mode)
290
+
291
+ const nodeACode = Effect.gen(function* () {
292
+ const channelAToB = yield* createChannel(nodeA, 'B', { mode })
293
+
294
+ // send 10 times A1
295
+ yield* Effect.forEach(
296
+ Chunk.makeBy(count, (i) => ({ message: `A${i}` })),
297
+ channelAToB.send,
298
+ { concurrency: 'unbounded' },
299
+ )
300
+
301
+ expect(yield* channelAToB.listen.pipe(Stream.flatten(), Stream.take(count), Stream.runCollect)).toEqual(
302
+ Chunk.makeBy(count, (i) => ({ message: `B${i}` })),
303
+ )
304
+ // expect(yield* getFirstMessage(channelAToB)).toEqual({ message: 'A2' })
305
+ })
306
+
307
+ const nodeBCode = Effect.gen(function* () {
308
+ const channelBToA = yield* createChannel(nodeB, 'A', { mode })
309
+
310
+ // send 10 times B1
311
+ yield* Effect.forEach(
312
+ Chunk.makeBy(count, (i) => ({ message: `B${i}` })),
313
+ channelBToA.send,
314
+ { concurrency: 'unbounded' },
315
+ )
316
+
317
+ expect(yield* channelBToA.listen.pipe(Stream.flatten(), Stream.take(count), Stream.runCollect)).toEqual(
318
+ Chunk.makeBy(count, (i) => ({ message: `A${i}` })),
319
+ )
320
+ })
321
+
322
+ yield* Effect.all([nodeACode, nodeBCode, connectNodes(nodeA, nodeB).pipe(Effect.delay(100))], {
323
+ concurrency: 'unbounded',
324
+ })
325
+ }).pipe(withCtx(test, { skipOtel: false, suffix: `channelType=${channelType} count=${count}` })),
326
+ )
327
+ })
328
+ })
329
+
330
+ Vitest.scopedLive('manual debug test', (test) =>
331
+ Effect.gen(function* () {
332
+ const nodeA = yield* makeMeshNode('A')
333
+ const nodeB = yield* makeMeshNode('B')
334
+
335
+ // const connectNodes = connectNodesViaBroadcastChannel
336
+ const connectNodes = connectNodesViaMessageChannel
337
+
338
+ const nodeACode = Effect.gen(function* () {
339
+ const channelAToB = yield* createChannel(nodeA, 'B')
340
+
341
+ yield* channelAToB.send({ message: 'A1' })
342
+ expect(yield* getFirstMessage(channelAToB)).toEqual({ message: 'A2' })
343
+ })
344
+
345
+ const nodeBCode = Effect.gen(function* () {
346
+ const channelBToA = yield* createChannel(nodeB, 'A')
347
+
348
+ yield* channelBToA.send({ message: 'A2' })
349
+ expect(yield* getFirstMessage(channelBToA)).toEqual({ message: 'A1' })
350
+ })
351
+
352
+ yield* Effect.all([nodeACode, nodeBCode, connectNodes(nodeA, nodeB).pipe(Effect.delay(100))], {
353
+ concurrency: 'unbounded',
354
+ })
355
+ }).pipe(withCtx(test)),
356
+ )
357
+
358
+ Vitest.scopedLive('broadcast connection with message channel', (test) =>
359
+ Effect.gen(function* () {
360
+ const nodeA = yield* makeMeshNode('A')
361
+ const nodeB = yield* makeMeshNode('B')
362
+
363
+ yield* connectNodesViaBroadcastChannel(nodeA, nodeB)
364
+
365
+ const err = yield* createChannel(nodeA, 'B', { mode: 'messagechannel' }).pipe(Effect.timeout(200), Effect.flip)
366
+ expect(err._tag).toBe('TimeoutException')
367
+ }).pipe(withCtx(test)),
368
+ )
369
+ })
370
+
371
+ Vitest.describe('A <> B <> C', () => {
372
+ Vitest.scopedLive('should work', (test) =>
373
+ Effect.gen(function* () {
374
+ const nodeA = yield* makeMeshNode('A')
375
+ const nodeB = yield* makeMeshNode('B')
376
+ const nodeC = yield* makeMeshNode('C')
377
+
378
+ yield* connectNodesViaMessageChannel(nodeA, nodeB)
379
+ yield* connectNodesViaMessageChannel(nodeB, nodeC)
380
+
381
+ const nodeACode = Effect.gen(function* () {
382
+ const channelAToC = yield* createChannel(nodeA, 'C')
383
+
384
+ yield* channelAToC.send({ message: 'A1' })
385
+ expect(yield* getFirstMessage(channelAToC)).toEqual({ message: 'C1' })
386
+ expect(yield* getFirstMessage(channelAToC)).toEqual({ message: 'C2' })
387
+ })
388
+
389
+ const nodeCCode = Effect.gen(function* () {
390
+ const channelCToA = yield* createChannel(nodeC, 'A')
391
+ yield* channelCToA.send({ message: 'C1' })
392
+ yield* channelCToA.send({ message: 'C2' })
393
+ yield* channelCToA.send({ message: 'C3' })
394
+ expect(yield* getFirstMessage(channelCToA)).toEqual({ message: 'A1' })
395
+ })
396
+
397
+ yield* Effect.all([nodeACode, nodeCCode], { concurrency: 'unbounded' })
398
+ }).pipe(withCtx(test)),
399
+ )
400
+
401
+ Vitest.scopedLive('should work - delayed connection', (test) =>
402
+ Effect.gen(function* () {
403
+ const nodeA = yield* makeMeshNode('A')
404
+ const nodeB = yield* makeMeshNode('B')
405
+ const nodeC = yield* makeMeshNode('C')
406
+
407
+ const connectNodes = connectNodesViaMessageChannel
408
+ // const connectNodes = connectNodesViaBroadcastChannel
409
+ yield* connectNodes(nodeA, nodeB)
410
+ // yield* connectNodes(nodeB, nodeC)
411
+
412
+ const nodeACode = Effect.gen(function* () {
413
+ const channelAToC = yield* createChannel(nodeA, 'C')
414
+
415
+ yield* channelAToC.send({ message: 'A1' })
416
+ expect(yield* getFirstMessage(channelAToC)).toEqual({ message: 'C1' })
417
+ })
418
+
419
+ const nodeCCode = Effect.gen(function* () {
420
+ const channelCToA = yield* createChannel(nodeC, 'A')
421
+ yield* channelCToA.send({ message: 'C1' })
422
+ expect(yield* getFirstMessage(channelCToA)).toEqual({ message: 'A1' })
423
+ })
424
+
425
+ yield* Effect.all([nodeACode, nodeCCode, connectNodes(nodeB, nodeC).pipe(Effect.delay(100))], {
426
+ concurrency: 'unbounded',
427
+ })
428
+ }).pipe(withCtx(test)),
429
+ )
430
+
431
+ Vitest.scopedLive('proxy channel', (test) =>
432
+ Effect.gen(function* () {
433
+ const nodeA = yield* makeMeshNode('A')
434
+ const nodeB = yield* makeMeshNode('B')
435
+ const nodeC = yield* makeMeshNode('C')
436
+
437
+ yield* connectNodesViaBroadcastChannel(nodeA, nodeB)
438
+ yield* connectNodesViaBroadcastChannel(nodeB, nodeC)
439
+
440
+ const nodeACode = Effect.gen(function* () {
441
+ const channelAToC = yield* createChannel(nodeA, 'C', { mode: 'proxy' })
442
+ yield* channelAToC.send({ message: 'A1' })
443
+ expect(yield* getFirstMessage(channelAToC)).toEqual({ message: 'hello from nodeC' })
444
+ })
445
+
446
+ const nodeCCode = Effect.gen(function* () {
447
+ const channelCToA = yield* createChannel(nodeC, 'A', { mode: 'proxy' })
448
+ yield* channelCToA.send({ message: 'hello from nodeC' })
449
+ expect(yield* getFirstMessage(channelCToA)).toEqual({ message: 'A1' })
450
+ })
451
+
452
+ yield* Effect.all([nodeACode, nodeCCode], { concurrency: 'unbounded' })
453
+ }).pipe(withCtx(test)),
454
+ )
455
+
456
+ Vitest.scopedLive('should fail', (test) =>
457
+ Effect.gen(function* () {
458
+ const nodeA = yield* makeMeshNode('A')
459
+ const nodeB = yield* makeMeshNode('B')
460
+ const nodeC = yield* makeMeshNode('C')
461
+
462
+ yield* connectNodesViaMessageChannel(nodeA, nodeB)
463
+ // We're not connecting nodeB and nodeC, so this should fail
464
+
465
+ const nodeACode = Effect.gen(function* () {
466
+ const err = yield* createChannel(nodeA, 'C').pipe(Effect.timeout(200), Effect.flip)
467
+ expect(err._tag).toBe('TimeoutException')
468
+ })
469
+
470
+ const nodeCCode = Effect.gen(function* () {
471
+ const err = yield* createChannel(nodeC, 'A').pipe(Effect.timeout(200), Effect.flip)
472
+ expect(err._tag).toBe('TimeoutException')
473
+ })
474
+
475
+ yield* Effect.all([nodeACode, nodeCCode], { concurrency: 'unbounded' })
476
+ }).pipe(withCtx(test)),
477
+ )
478
+ })
479
+
480
+ Vitest.describe('mixture of messagechannel and proxy connections', () => {
481
+ // TODO test case to better guard against case where side A tries to create a proxy channel to B
482
+ // and side B tries to create a messagechannel to A
483
+ Vitest.scopedLive('should work for proxy channels', (test) =>
484
+ Effect.gen(function* () {
485
+ const nodeA = yield* makeMeshNode('A')
486
+ const nodeB = yield* makeMeshNode('B')
487
+
488
+ yield* connectNodesViaMessageChannel(nodeB, nodeA)
489
+ const err = yield* connectNodesViaBroadcastChannel(nodeA, nodeB).pipe(Effect.flip)
490
+
491
+ expect(err._tag).toBe('ConnectionAlreadyExistsError')
492
+ }).pipe(withCtx(test)),
493
+ )
494
+
495
+ // TODO this currently fails but should work. probably needs some more guarding internally.
496
+ Vitest.scopedLive.skip('should work for messagechannels', (test) =>
497
+ Effect.gen(function* () {
498
+ const nodeA = yield* makeMeshNode('A')
499
+ const nodeB = yield* makeMeshNode('B')
500
+
501
+ yield* connectNodesViaMessageChannel(nodeB, nodeA)
502
+ yield* connectNodesViaBroadcastChannel(nodeA, nodeB)
503
+
504
+ const nodeACode = Effect.gen(function* () {
505
+ const channelAToB = yield* createChannel(nodeA, 'B', { mode: 'messagechannel' })
506
+ yield* channelAToB.send({ message: 'A1' })
507
+ expect(yield* getFirstMessage(channelAToB)).toEqual({ message: 'B1' })
508
+ })
509
+
510
+ const nodeBCode = Effect.gen(function* () {
511
+ const channelBToA = yield* createChannel(nodeB, 'A', { mode: 'messagechannel' })
512
+ yield* channelBToA.send({ message: 'B1' })
513
+ expect(yield* getFirstMessage(channelBToA)).toEqual({ message: 'A1' })
514
+ })
515
+
516
+ yield* Effect.all([nodeACode, nodeBCode], { concurrency: 'unbounded' })
517
+ }).pipe(withCtx(test)),
518
+ )
519
+ })
520
+ })
521
+
522
+ const otelLayer = IS_CI ? Layer.empty : OtelLiveHttp({ serviceName: 'webmesh-node-test', skipLogUrl: false })
523
+
524
+ const withCtx =
525
+ (testContext: Vitest.TaskContext, { suffix, skipOtel = false }: { suffix?: string; skipOtel?: boolean } = {}) =>
526
+ <A, E, R>(self: Effect.Effect<A, E, R>) =>
527
+ self.pipe(
528
+ Effect.timeout(testTimeout),
529
+ Effect.provide(Logger.pretty),
530
+ Effect.scoped, // We need to scope the effect manually here because otherwise the span is not closed
531
+ Effect.withSpan(`${testContext.task.suite?.name}:${testContext.task.name}${suffix ? `:${suffix}` : ''}`),
532
+ skipOtel ? identity : Effect.provide(otelLayer),
533
+ )