@livestore/webmesh 0.3.0-dev.3 → 0.3.0-dev.31

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 (64) hide show
  1. package/README.md +26 -0
  2. package/dist/.tsbuildinfo +1 -1
  3. package/dist/channel/message-channel-internal.d.ts +26 -0
  4. package/dist/channel/message-channel-internal.d.ts.map +1 -0
  5. package/dist/channel/message-channel-internal.js +217 -0
  6. package/dist/channel/message-channel-internal.js.map +1 -0
  7. package/dist/channel/message-channel.d.ts +21 -19
  8. package/dist/channel/message-channel.d.ts.map +1 -1
  9. package/dist/channel/message-channel.js +132 -162
  10. package/dist/channel/message-channel.js.map +1 -1
  11. package/dist/channel/proxy-channel.d.ts +3 -3
  12. package/dist/channel/proxy-channel.d.ts.map +1 -1
  13. package/dist/channel/proxy-channel.js +38 -19
  14. package/dist/channel/proxy-channel.js.map +1 -1
  15. package/dist/common.d.ts +36 -14
  16. package/dist/common.d.ts.map +1 -1
  17. package/dist/common.js +7 -4
  18. package/dist/common.js.map +1 -1
  19. package/dist/mesh-schema.d.ts +71 -5
  20. package/dist/mesh-schema.d.ts.map +1 -1
  21. package/dist/mesh-schema.js +55 -6
  22. package/dist/mesh-schema.js.map +1 -1
  23. package/dist/mod.d.ts +2 -2
  24. package/dist/mod.d.ts.map +1 -1
  25. package/dist/mod.js +2 -2
  26. package/dist/mod.js.map +1 -1
  27. package/dist/node.d.ts +43 -21
  28. package/dist/node.d.ts.map +1 -1
  29. package/dist/node.js +273 -100
  30. package/dist/node.js.map +1 -1
  31. package/dist/node.test.d.ts +1 -1
  32. package/dist/node.test.d.ts.map +1 -1
  33. package/dist/node.test.js +391 -156
  34. package/dist/node.test.js.map +1 -1
  35. package/dist/utils.d.ts +4 -4
  36. package/dist/utils.d.ts.map +1 -1
  37. package/dist/utils.js +7 -1
  38. package/dist/utils.js.map +1 -1
  39. package/dist/websocket-edge.d.ts +52 -0
  40. package/dist/websocket-edge.d.ts.map +1 -0
  41. package/dist/websocket-edge.js +85 -0
  42. package/dist/websocket-edge.js.map +1 -0
  43. package/package.json +5 -6
  44. package/src/channel/message-channel-internal.ts +356 -0
  45. package/src/channel/message-channel.ts +190 -310
  46. package/src/channel/proxy-channel.ts +259 -231
  47. package/src/common.ts +12 -13
  48. package/src/mesh-schema.ts +62 -6
  49. package/src/mod.ts +2 -2
  50. package/src/node.test.ts +554 -189
  51. package/src/node.ts +421 -138
  52. package/src/utils.ts +13 -2
  53. package/src/websocket-edge.ts +177 -0
  54. package/tmp/pack.tgz +0 -0
  55. package/dist/websocket-connection.d.ts +0 -51
  56. package/dist/websocket-connection.d.ts.map +0 -1
  57. package/dist/websocket-connection.js +0 -74
  58. package/dist/websocket-connection.js.map +0 -1
  59. package/dist/websocket-server.d.ts +0 -7
  60. package/dist/websocket-server.d.ts.map +0 -1
  61. package/dist/websocket-server.js +0 -24
  62. package/dist/websocket-server.js.map +0 -1
  63. package/src/websocket-connection.ts +0 -158
  64. package/src/websocket-server.ts +0 -40
package/src/node.test.ts CHANGED
@@ -1,4 +1,19 @@
1
- import { Chunk, Deferred, Effect, identity, Layer, Logger, Schema, Stream, WebChannel } from '@livestore/utils/effect'
1
+ import '@livestore/utils/node-vitest-polyfill'
2
+
3
+ import { IS_CI } from '@livestore/utils'
4
+ import {
5
+ Chunk,
6
+ Deferred,
7
+ Effect,
8
+ Exit,
9
+ identity,
10
+ Layer,
11
+ Logger,
12
+ Schema,
13
+ Scope,
14
+ Stream,
15
+ WebChannel,
16
+ } from '@livestore/utils/effect'
2
17
  import { OtelLiveHttp } from '@livestore/utils/node'
3
18
  import { Vitest } from '@livestore/utils/node-vitest'
4
19
  import { expect } from 'vitest'
@@ -9,42 +24,54 @@ import { makeMeshNode } from './node.js'
9
24
 
10
25
  // TODO test cases where in-between node only comes online later
11
26
  // TODO test cases where other side tries to reconnect
12
- // TODO test combination of connection types (message, proxy)
27
+ // TODO test combination of channel types (message, proxy)
13
28
  // TODO test "diamond shape" topology (A <> B1, A <> B2, B1 <> C, B2 <> C)
14
29
  // TODO test cases where multiple entities try to claim to be the same channel end (e.g. A,B,B)
15
30
  // TODO write tests with worker threads
16
31
 
17
32
  const ExampleSchema = Schema.Struct({ message: Schema.String })
18
33
 
19
- const connectNodesViaMessageChannel = (nodeA: MeshNode, nodeB: MeshNode) =>
34
+ const connectNodesViaMessageChannel = (nodeA: MeshNode, nodeB: MeshNode, options?: { replaceIfExists?: boolean }) =>
20
35
  Effect.gen(function* () {
21
36
  const mc = new MessageChannel()
22
37
  const meshChannelAToB = yield* WebChannel.messagePortChannel({ port: mc.port1, schema: Packet })
23
38
  const meshChannelBToA = yield* WebChannel.messagePortChannel({ port: mc.port2, schema: Packet })
24
39
 
25
- yield* nodeA.addConnection({ target: nodeB.nodeName, connectionChannel: meshChannelAToB })
26
- yield* nodeB.addConnection({ target: nodeA.nodeName, connectionChannel: meshChannelBToA })
27
-
28
- return mc
40
+ yield* nodeA.addEdge({
41
+ target: nodeB.nodeName,
42
+ edgeChannel: meshChannelAToB,
43
+ replaceIfExists: options?.replaceIfExists,
44
+ })
45
+ yield* nodeB.addEdge({
46
+ target: nodeA.nodeName,
47
+ edgeChannel: meshChannelBToA,
48
+ replaceIfExists: options?.replaceIfExists,
49
+ })
29
50
  }).pipe(Effect.withSpan(`connectNodesViaMessageChannel:${nodeA.nodeName}↔${nodeB.nodeName}`))
30
51
 
31
- const connectNodesViaBroadcastChannel = (nodeA: MeshNode, nodeB: MeshNode) =>
52
+ const connectNodesViaBroadcastChannel = (nodeA: MeshNode, nodeB: MeshNode, options?: { replaceIfExists?: boolean }) =>
32
53
  Effect.gen(function* () {
33
54
  // Need to instantiate two different channels because they filter out messages they sent themselves
34
55
  const broadcastWebChannelA = yield* WebChannel.broadcastChannelWithAck({
35
56
  channelName: `${nodeA.nodeName}↔${nodeB.nodeName}`,
36
- listenSchema: Packet,
37
- sendSchema: Packet,
57
+ schema: Packet,
38
58
  })
39
59
 
40
60
  const broadcastWebChannelB = yield* WebChannel.broadcastChannelWithAck({
41
61
  channelName: `${nodeA.nodeName}↔${nodeB.nodeName}`,
42
- listenSchema: Packet,
43
- sendSchema: Packet,
62
+ schema: Packet,
44
63
  })
45
64
 
46
- yield* nodeA.addConnection({ target: nodeB.nodeName, connectionChannel: broadcastWebChannelA })
47
- yield* nodeB.addConnection({ target: nodeA.nodeName, connectionChannel: broadcastWebChannelB })
65
+ yield* nodeA.addEdge({
66
+ target: nodeB.nodeName,
67
+ edgeChannel: broadcastWebChannelA,
68
+ replaceIfExists: options?.replaceIfExists,
69
+ })
70
+ yield* nodeB.addEdge({
71
+ target: nodeA.nodeName,
72
+ edgeChannel: broadcastWebChannelB,
73
+ replaceIfExists: options?.replaceIfExists,
74
+ })
48
75
  }).pipe(Effect.withSpan(`connectNodesViaBroadcastChannel:${nodeA.nodeName}↔${nodeB.nodeName}`))
49
76
 
50
77
  const createChannel = (source: MeshNode, target: string, options?: Partial<Parameters<MeshNode['makeChannel']>[0]>) =>
@@ -73,206 +100,319 @@ const maybeDelay =
73
100
  ? effect
74
101
  : Effect.sleep(delay).pipe(Effect.withSpan(`${label}:delay(${delay})`), Effect.andThen(effect))
75
102
 
103
+ const testTimeout = IS_CI ? 30_000 : 1000
104
+ const propTestTimeout = IS_CI ? 60_000 : 20_000
105
+
76
106
  // TODO also make work without `Vitest.scopedLive` (i.e. with `Vitest.scoped`)
77
107
  // probably requires controlling the clocks
78
- Vitest.describe('webmesh node', { timeout: 1000 }, () => {
79
- Vitest.describe('A <> B', () => {
80
- Vitest.describe('prop tests', { timeout: 10_000 }, () => {
81
- const Delay = Schema.UndefinedOr(Schema.Literal(0, 1, 10, 50))
82
- // NOTE for message channels, we test both with and without transferables (i.e. proxying)
83
- const ChannelType = Schema.Literal('messagechannel', 'messagechannel.proxy', 'proxy')
84
-
85
- const fromChannelType = (
86
- channelType: typeof ChannelType.Type,
87
- ): {
88
- mode: 'messagechannel' | 'proxy'
89
- connectNodes: typeof connectNodesViaMessageChannel | typeof connectNodesViaBroadcastChannel
90
- } => {
91
- switch (channelType) {
92
- case 'proxy': {
93
- return { mode: 'proxy', connectNodes: connectNodesViaBroadcastChannel }
94
- }
95
- case 'messagechannel': {
96
- return { mode: 'messagechannel', connectNodes: connectNodesViaMessageChannel }
97
- }
98
- case 'messagechannel.proxy': {
99
- return { mode: 'proxy', connectNodes: connectNodesViaMessageChannel }
100
- }
101
- }
108
+ Vitest.describe('webmesh node', { timeout: testTimeout }, () => {
109
+ const Delay = Schema.UndefinedOr(Schema.Literal(0, 1, 10, 50))
110
+ // NOTE for message channels, we test both with and without transferables (i.e. proxying)
111
+ const ChannelType = Schema.Literal('messagechannel', 'messagechannel.proxy', 'proxy')
112
+ const NodeNames = Schema.Union(
113
+ Schema.Tuple(Schema.Literal('A'), Schema.Literal('B')),
114
+ Schema.Tuple(Schema.Literal('B'), Schema.Literal('A')),
115
+ )
116
+
117
+ const fromChannelType = (
118
+ channelType: typeof ChannelType.Type,
119
+ ): {
120
+ mode: 'messagechannel' | 'proxy'
121
+ connectNodes: typeof connectNodesViaMessageChannel | typeof connectNodesViaBroadcastChannel
122
+ } => {
123
+ switch (channelType) {
124
+ case 'proxy': {
125
+ return { mode: 'proxy', connectNodes: connectNodesViaBroadcastChannel }
126
+ }
127
+ case 'messagechannel': {
128
+ return { mode: 'messagechannel', connectNodes: connectNodesViaMessageChannel }
102
129
  }
130
+ case 'messagechannel.proxy': {
131
+ return { mode: 'proxy', connectNodes: connectNodesViaMessageChannel }
132
+ }
133
+ }
134
+ }
135
+
136
+ const exchangeMessages = ({
137
+ nodeX,
138
+ nodeY,
139
+ channelType,
140
+ // numberOfMessages = 1,
141
+ delays,
142
+ }: {
143
+ nodeX: MeshNode
144
+ nodeY: MeshNode
145
+ channelType: 'messagechannel' | 'proxy' | 'messagechannel.proxy'
146
+ numberOfMessages?: number
147
+ delays?: { x?: number; y?: number; connect?: number }
148
+ }) =>
149
+ Effect.gen(function* () {
150
+ const nodeLabel = { x: nodeX.nodeName, y: nodeY.nodeName }
151
+ const { mode, connectNodes } = fromChannelType(channelType)
152
+
153
+ const nodeXCode = Effect.gen(function* () {
154
+ const channelXToY = yield* createChannel(nodeX, nodeY.nodeName, { mode })
155
+
156
+ yield* channelXToY.send({ message: `${nodeLabel.x}1` })
157
+ // console.log('channelXToY', channelXToY.debugInfo)
158
+ expect(yield* getFirstMessage(channelXToY)).toEqual({ message: `${nodeLabel.y}1` })
159
+ // expect(channelXToY.debugInfo.connectCounter).toBe(1)
160
+ })
161
+
162
+ const nodeYCode = Effect.gen(function* () {
163
+ const channelYToX = yield* createChannel(nodeY, nodeX.nodeName, { mode })
103
164
 
165
+ yield* channelYToX.send({ message: `${nodeLabel.y}1` })
166
+ // console.log('channelYToX', channelYToX.debugInfo)
167
+ expect(yield* getFirstMessage(channelYToX)).toEqual({ message: `${nodeLabel.x}1` })
168
+ // expect(channelYToX.debugInfo.connectCounter).toBe(1)
169
+ })
170
+
171
+ yield* Effect.all(
172
+ [
173
+ connectNodes(nodeX, nodeY).pipe(maybeDelay(delays?.connect, 'connectNodes')),
174
+ nodeXCode.pipe(maybeDelay(delays?.x, `node${nodeLabel.x}Code`)),
175
+ nodeYCode.pipe(maybeDelay(delays?.y, `node${nodeLabel.y}Code`)),
176
+ ],
177
+ { concurrency: 'unbounded' },
178
+ ).pipe(Effect.withSpan(`exchangeMessages(${nodeLabel.x}↔${nodeLabel.y})`))
179
+ })
180
+ Vitest.describe('A <> B', () => {
181
+ Vitest.describe('prop tests', { timeout: propTestTimeout }, () => {
182
+ // const delayX = 40
183
+ // const delayY = undefined
184
+ // const connectDelay = undefined
185
+ // const channelType = 'messagechannel'
186
+ // const nodeNames = ['B', 'A'] as const
187
+ // Vitest.scopedLive(
188
+ // 'a / b connect at different times with different channel types',
189
+ // (test) =>
104
190
  Vitest.scopedLive.prop(
105
- // Vitest.scopedLive.only(
106
191
  'a / b connect at different times with different channel types',
107
- [Delay, Delay, Delay, ChannelType],
108
- ([delayA, delayB, connectDelay, channelType], test) =>
109
- // (test) =>
192
+ [Delay, Delay, Delay, ChannelType, NodeNames],
193
+ ([delayX, delayY, connectDelay, channelType, nodeNames], test) =>
110
194
  Effect.gen(function* () {
111
- // const delayA = 1
112
- // const delayB = 10
113
- // const connectDelay = 10
114
- // const channelType = 'message.prefer'
115
- // console.log('delayA', delayA, 'delayB', delayB, 'connectDelay', connectDelay, 'channelType', channelType)
116
-
117
- const nodeA = yield* makeMeshNode('A')
118
- const nodeB = yield* makeMeshNode('B')
195
+ // console.log({ delayX, delayY, connectDelay, channelType, nodeNames })
119
196
 
120
- const { mode, connectNodes } = fromChannelType(channelType)
197
+ const [nodeNameX, nodeNameY] = nodeNames
198
+ const nodeX = yield* makeMeshNode(nodeNameX)
199
+ const nodeY = yield* makeMeshNode(nodeNameY)
121
200
 
122
- const nodeACode = Effect.gen(function* () {
123
- const channelAToB = yield* createChannel(nodeA, 'B', { mode })
124
-
125
- yield* channelAToB.send({ message: 'A1' })
126
- expect(yield* getFirstMessage(channelAToB)).toEqual({ message: 'A2' })
127
- })
128
-
129
- const nodeBCode = Effect.gen(function* () {
130
- const channelBToA = yield* createChannel(nodeB, 'A', { mode })
131
-
132
- yield* channelBToA.send({ message: 'A2' })
133
- expect(yield* getFirstMessage(channelBToA)).toEqual({ message: 'A1' })
201
+ yield* exchangeMessages({
202
+ nodeX,
203
+ nodeY,
204
+ channelType,
205
+ delays: { x: delayX, y: delayY, connect: connectDelay },
134
206
  })
135
207
 
136
- yield* Effect.all(
137
- [
138
- connectNodes(nodeA, nodeB).pipe(maybeDelay(connectDelay, 'connectNodes')),
139
- nodeACode.pipe(maybeDelay(delayA, 'nodeACode')),
140
- nodeBCode.pipe(maybeDelay(delayB, 'nodeBCode')),
141
- ],
142
- { concurrency: 'unbounded' },
143
- )
208
+ yield* Effect.promise(() => nodeX.debug.requestTopology(100))
144
209
  }).pipe(
145
- withCtx(test, { skipOtel: true, suffix: `delayA=${delayA} delayB=${delayB} channelType=${channelType}` }),
210
+ withCtx(test, {
211
+ skipOtel: true,
212
+ suffix: `delayX=${delayX} delayY=${delayY} connectDelay=${connectDelay} channelType=${channelType} nodeNames=${nodeNames}`,
213
+ }),
146
214
  ),
215
+ // { fastCheck: { numRuns: 20 } },
147
216
  )
148
217
 
149
- // Vitest.scopedLive.only(
150
- // 'reconnects',
151
- // (test) =>
152
- Vitest.scopedLive.prop(
153
- 'b reconnects',
154
- [Delay, Delay, ChannelType],
155
- ([waitForOfflineDelay, sleepDelay, channelType], test) =>
156
- Effect.gen(function* () {
157
- // const waitForOfflineDelay = 0
158
- // const sleepDelay = 10
159
- // const channelType = 'proxy'
160
- // console.log(
161
- // 'waitForOfflineDelay',
162
- // waitForOfflineDelay,
163
- // 'sleepDelay',
164
- // sleepDelay,
165
- // 'channelType',
166
- // channelType,
167
- // )
168
-
169
- const nodeA = yield* makeMeshNode('A')
170
- const nodeB = yield* makeMeshNode('B')
218
+ {
219
+ // const waitForOfflineDelay = undefined
220
+ // const sleepDelay = 0
221
+ // const channelType = 'messagechannel'
222
+ // Vitest.scopedLive(
223
+ // 'b reconnects',
224
+ // (test) =>
225
+ Vitest.scopedLive.prop(
226
+ 'b reconnects',
227
+ [Delay, Delay, ChannelType],
228
+ ([waitForOfflineDelay, sleepDelay, channelType], test) =>
229
+ Effect.gen(function* () {
230
+ // console.log({ waitForOfflineDelay, sleepDelay, channelType })
171
231
 
172
- const { mode, connectNodes } = fromChannelType(channelType)
232
+ if (waitForOfflineDelay === undefined) {
233
+ // TODO we still need to fix this scenario but it shouldn't really be common in practice
234
+ return
235
+ }
173
236
 
174
- // TODO also optionally delay the connection
175
- yield* connectNodes(nodeA, nodeB)
237
+ const nodeA = yield* makeMeshNode('A')
238
+ const nodeB = yield* makeMeshNode('B')
176
239
 
177
- const waitForBToBeOffline =
178
- waitForOfflineDelay === undefined ? undefined : yield* Deferred.make<void, never>()
240
+ const { mode, connectNodes } = fromChannelType(channelType)
179
241
 
180
- const nodeACode = Effect.gen(function* () {
181
- const channelAToB = yield* createChannel(nodeA, 'B', { mode })
242
+ // TODO also optionally delay the edge
243
+ yield* connectNodes(nodeA, nodeB)
182
244
 
183
- yield* channelAToB.send({ message: 'A1' })
184
- expect(yield* getFirstMessage(channelAToB)).toEqual({ message: 'B1' })
245
+ const waitForBToBeOffline =
246
+ waitForOfflineDelay === undefined ? undefined : yield* Deferred.make<void, never>()
185
247
 
186
- if (waitForBToBeOffline !== undefined) {
187
- yield* waitForBToBeOffline
188
- }
248
+ const nodeACode = Effect.gen(function* () {
249
+ const channelAToB = yield* createChannel(nodeA, 'B', { mode })
250
+ yield* channelAToB.send({ message: 'A1' })
251
+ expect(yield* getFirstMessage(channelAToB)).toEqual({ message: 'B1' })
189
252
 
190
- yield* channelAToB.send({ message: 'A2' })
191
- expect(yield* getFirstMessage(channelAToB)).toEqual({ message: 'B2' })
192
- })
253
+ console.log('nodeACode:waiting for B to be offline')
254
+ if (waitForBToBeOffline !== undefined) {
255
+ yield* waitForBToBeOffline
256
+ }
193
257
 
194
- // Simulating node b going offline and then coming back online
195
- const nodeBCode = Effect.gen(function* () {
196
- yield* Effect.gen(function* () {
197
- const channelBToA = yield* createChannel(nodeB, 'A', { mode })
258
+ yield* channelAToB.send({ message: 'A2' })
259
+ expect(yield* getFirstMessage(channelAToB)).toEqual({ message: 'B2' })
260
+ })
198
261
 
199
- yield* channelBToA.send({ message: 'B1' })
200
- expect(yield* getFirstMessage(channelBToA)).toEqual({ message: 'A1' })
201
- }).pipe(Effect.scoped)
262
+ // Simulating node b going offline and then coming back online
263
+ // This test also illustrates why we need a ack-message channel since otherwise
264
+ // sent messages might get lost
265
+ const nodeBCode = Effect.gen(function* () {
266
+ yield* Effect.gen(function* () {
267
+ const channelBToA = yield* createChannel(nodeB, 'A', { mode })
202
268
 
203
- if (waitForBToBeOffline !== undefined) {
204
- yield* Deferred.succeed(waitForBToBeOffline, void 0)
205
- }
269
+ yield* channelBToA.send({ message: 'B1' })
270
+ expect(yield* getFirstMessage(channelBToA)).toEqual({ message: 'A1' })
271
+ }).pipe(Effect.scoped, Effect.withSpan('nodeBCode:part1'))
206
272
 
207
- if (sleepDelay !== undefined) {
208
- yield* Effect.sleep(sleepDelay).pipe(Effect.withSpan(`B:sleep(${sleepDelay})`))
209
- }
273
+ console.log('nodeBCode:B node going offline')
274
+ if (waitForBToBeOffline !== undefined) {
275
+ yield* Deferred.succeed(waitForBToBeOffline, void 0)
276
+ }
210
277
 
211
- yield* Effect.gen(function* () {
212
- const channelBToA = yield* createChannel(nodeB, 'A', { mode })
278
+ if (sleepDelay !== undefined) {
279
+ yield* Effect.sleep(sleepDelay).pipe(Effect.withSpan(`B:sleep(${sleepDelay})`))
280
+ }
281
+
282
+ // Recreating the channel
283
+ yield* Effect.gen(function* () {
284
+ const channelBToA = yield* createChannel(nodeB, 'A', { mode })
213
285
 
214
- yield* channelBToA.send({ message: 'B2' })
215
- expect(yield* getFirstMessage(channelBToA)).toEqual({ message: 'A2' })
216
- }).pipe(Effect.scoped)
286
+ yield* channelBToA.send({ message: 'B2' })
287
+ expect(yield* getFirstMessage(channelBToA)).toEqual({ message: 'A2' })
288
+ }).pipe(Effect.scoped, Effect.withSpan('nodeBCode:part2'))
289
+ })
290
+
291
+ yield* Effect.all([nodeACode, nodeBCode], { concurrency: 'unbounded' }).pipe(Effect.withSpan('test'))
292
+ }).pipe(
293
+ withCtx(test, {
294
+ skipOtel: true,
295
+ suffix: `waitForOfflineDelay=${waitForOfflineDelay} sleepDelay=${sleepDelay} channelType=${channelType}`,
296
+ }),
297
+ ),
298
+ { fastCheck: { numRuns: 20 } },
299
+ )
300
+ }
301
+
302
+ Vitest.scopedLive('reconnect with re-created node', (test) =>
303
+ Effect.gen(function* () {
304
+ const nodeBgen1Scope = yield* Scope.make()
305
+
306
+ const nodeA = yield* makeMeshNode('A')
307
+ const nodeBgen1 = yield* makeMeshNode('B').pipe(Scope.extend(nodeBgen1Scope))
308
+
309
+ yield* connectNodesViaMessageChannel(nodeA, nodeBgen1).pipe(Scope.extend(nodeBgen1Scope))
310
+
311
+ // yield* Effect.sleep(100)
312
+
313
+ const channelAToBOnce = yield* Effect.cached(createChannel(nodeA, 'B'))
314
+ const nodeACode = Effect.gen(function* () {
315
+ const channelAToB = yield* channelAToBOnce
316
+ yield* channelAToB.send({ message: 'A1' })
317
+ expect(yield* getFirstMessage(channelAToB)).toEqual({ message: 'B1' })
318
+ // expect(channelAToB.debugInfo.connectCounter).toBe(1)
319
+ })
320
+
321
+ const nodeBCode = (nodeB: MeshNode) =>
322
+ Effect.gen(function* () {
323
+ const channelBToA = yield* createChannel(nodeB, 'A')
324
+
325
+ yield* channelBToA.send({ message: 'B1' })
326
+ expect(yield* getFirstMessage(channelBToA)).toEqual({ message: 'A1' })
327
+ // expect(channelBToA.debugInfo.connectCounter).toBe(1)
217
328
  })
218
329
 
219
- yield* Effect.all([nodeACode, nodeBCode], { concurrency: 'unbounded' })
220
- }).pipe(
221
- withCtx(test, {
222
- skipOtel: true,
223
- suffix: `waitForOfflineDelay=${waitForOfflineDelay} sleepDelay=${sleepDelay} channelType=${channelType}`,
224
- }),
225
- ),
330
+ yield* Effect.all([nodeACode, nodeBCode(nodeBgen1).pipe(Scope.extend(nodeBgen1Scope))], {
331
+ concurrency: 'unbounded',
332
+ }).pipe(Effect.withSpan('test1'))
333
+
334
+ yield* Scope.close(nodeBgen1Scope, Exit.void)
335
+
336
+ const nodeBgen2 = yield* makeMeshNode('B')
337
+ yield* connectNodesViaMessageChannel(nodeA, nodeBgen2, { replaceIfExists: true })
338
+
339
+ yield* Effect.all([nodeACode, nodeBCode(nodeBgen2)], { concurrency: 'unbounded' }).pipe(
340
+ Effect.withSpan('test2'),
341
+ )
342
+ }).pipe(withCtx(test)),
226
343
  )
227
344
 
228
345
  const ChannelTypeWithoutMessageChannelProxy = Schema.Literal('proxy', 'messagechannel')
346
+ // TODO there seems to be a flaky case here which gets hit sometimes (e.g. 2025-02-28-17:11)
347
+ // Log output:
348
+ // test: { seed: -964670352, path: "1", endOnFailure: true }
349
+ // test: Counterexample: ["messagechannel",["A","B"]]
350
+ // test: Shrunk 0 time(s)
351
+ // test: Got AssertionError: expected { _tag: 'MessageChannelPing' } to deeply equal { message: 'A1' }
352
+ // test: at next (/Users/schickling/Code/overtone/submodules/livestore/packages/@livestore/webmesh/src/node.test.ts:376:59)
353
+ // test: at prop tests:replace edge while keeping the channel:channelType=messagechannel nodeNames=A,B (/Users/schickling/Code/overtone/submodules/livestore/packages/@livestore/webmesh/src/node.test.ts:801:14)
354
+ // test: Hint: Enable verbose mode in order to have the list of all failing values encountered during the run
355
+ // test: ✓ webmesh node > A <> B > prop tests > TODO improve latency > concurrent messages 2110ms
356
+ // test: ⎯⎯⎯⎯⎯⎯⎯ Failed Tests 1 ⎯⎯⎯⎯⎯⎯⎯
357
+ // test: FAIL src/node.test.ts > webmesh node > A <> B > prop tests > replace edge while keeping the channel
358
+ // test: Error: Property failed after 2 tests
359
+ // test: { seed: -964670352, path: "1", endOnFailure: true }
360
+ // test: Counterexample: ["messagechannel",["A","B"]]
229
361
  Vitest.scopedLive.prop(
230
- 'replace connection while keeping the channel',
231
- [ChannelTypeWithoutMessageChannelProxy],
232
- ([channelType], test) =>
362
+ 'replace edge while keeping the channel',
363
+ [ChannelTypeWithoutMessageChannelProxy, NodeNames],
364
+ ([channelType, nodeNames], test) =>
233
365
  Effect.gen(function* () {
234
- const nodeA = yield* makeMeshNode('A')
235
- const nodeB = yield* makeMeshNode('B')
366
+ const [nodeNameX, nodeNameY] = nodeNames
367
+ const nodeX = yield* makeMeshNode(nodeNameX)
368
+ const nodeY = yield* makeMeshNode(nodeNameY)
369
+ const nodeLabel = { x: nodeX.nodeName, y: nodeY.nodeName }
236
370
 
237
371
  const { mode, connectNodes } = fromChannelType(channelType)
238
372
 
239
- yield* connectNodes(nodeA, nodeB)
373
+ yield* connectNodes(nodeX, nodeY)
240
374
 
241
- const waitForConnectionReplacement = yield* Deferred.make<void>()
375
+ const waitForEdgeReplacement = yield* Deferred.make<void>()
242
376
 
243
- const nodeACode = Effect.gen(function* () {
244
- const channelAToB = yield* createChannel(nodeA, 'B', { mode })
377
+ const nodeXCode = Effect.gen(function* () {
378
+ const channelXToY = yield* createChannel(nodeX, nodeLabel.y, { mode })
245
379
 
246
- yield* channelAToB.send({ message: 'A1' })
247
- expect(yield* getFirstMessage(channelAToB)).toEqual({ message: 'B1' })
380
+ yield* channelXToY.send({ message: `${nodeLabel.x}1` })
381
+ expect(yield* getFirstMessage(channelXToY)).toEqual({ message: `${nodeLabel.y}1` })
248
382
 
249
- yield* waitForConnectionReplacement
383
+ yield* waitForEdgeReplacement
250
384
 
251
- yield* channelAToB.send({ message: 'A2' })
252
- expect(yield* getFirstMessage(channelAToB)).toEqual({ message: 'B2' })
385
+ yield* channelXToY.send({ message: `${nodeLabel.x}2` })
386
+ expect(yield* getFirstMessage(channelXToY)).toEqual({ message: `${nodeLabel.y}2` })
253
387
  })
254
388
 
255
- const nodeBCode = Effect.gen(function* () {
256
- const channelBToA = yield* createChannel(nodeB, 'A', { mode })
389
+ const nodeYCode = Effect.gen(function* () {
390
+ const channelYToX = yield* createChannel(nodeY, nodeLabel.x, { mode })
257
391
 
258
- yield* channelBToA.send({ message: 'B1' })
259
- expect(yield* getFirstMessage(channelBToA)).toEqual({ message: 'A1' })
392
+ yield* channelYToX.send({ message: `${nodeLabel.y}1` })
393
+ expect(yield* getFirstMessage(channelYToX)).toEqual({ message: `${nodeLabel.x}1` })
260
394
 
261
- // Switch out connection while keeping the channel
262
- yield* nodeA.removeConnection('B')
263
- yield* nodeB.removeConnection('A')
264
- yield* connectNodes(nodeA, nodeB)
265
- yield* Deferred.succeed(waitForConnectionReplacement, void 0)
395
+ // Switch out edge while keeping the channel
396
+ yield* nodeX.removeEdge(nodeLabel.y)
397
+ yield* nodeY.removeEdge(nodeLabel.x)
398
+ yield* connectNodes(nodeX, nodeY)
399
+ yield* Deferred.succeed(waitForEdgeReplacement, void 0)
266
400
 
267
- yield* channelBToA.send({ message: 'B2' })
268
- expect(yield* getFirstMessage(channelBToA)).toEqual({ message: 'A2' })
401
+ yield* channelYToX.send({ message: `${nodeLabel.y}2` })
402
+ expect(yield* getFirstMessage(channelYToX)).toEqual({ message: `${nodeLabel.x}2` })
269
403
  })
270
404
 
271
- yield* Effect.all([nodeACode, nodeBCode], { concurrency: 'unbounded' })
272
- }).pipe(withCtx(test, { skipOtel: true, suffix: `channelType=${channelType}` })),
405
+ yield* Effect.all([nodeXCode, nodeYCode], { concurrency: 'unbounded' })
406
+ }).pipe(
407
+ withCtx(test, {
408
+ skipOtel: true,
409
+ suffix: `channelType=${channelType} nodeNames=${nodeNames}`,
410
+ }),
411
+ ),
412
+ { fastCheck: { numRuns: 10 } },
273
413
  )
274
414
 
275
- Vitest.describe.todo('TODO improve latency', () => {
415
+ Vitest.describe('TODO improve latency', () => {
276
416
  // TODO we need to improve latency when sending messages concurrently
277
417
  Vitest.scopedLive.prop(
278
418
  'concurrent messages',
@@ -319,12 +459,52 @@ Vitest.describe('webmesh node', { timeout: 1000 }, () => {
319
459
  yield* Effect.all([nodeACode, nodeBCode, connectNodes(nodeA, nodeB).pipe(Effect.delay(100))], {
320
460
  concurrency: 'unbounded',
321
461
  })
322
- }).pipe(withCtx(test, { skipOtel: false, suffix: `channelType=${channelType} count=${count}` })),
323
- { timeout: 30_000 },
462
+ }).pipe(
463
+ withCtx(test, {
464
+ skipOtel: true,
465
+ suffix: `channelType=${channelType} count=${count}`,
466
+ timeout: testTimeout * 2,
467
+ }),
468
+ ),
469
+ { fastCheck: { numRuns: 10 } },
324
470
  )
325
471
  })
326
472
  })
327
473
 
474
+ Vitest.describe('message channel specific tests', () => {
475
+ Vitest.scopedLive('differing initial edge counter', (test) =>
476
+ Effect.gen(function* () {
477
+ const nodeA = yield* makeMeshNode('A')
478
+ const nodeB = yield* makeMeshNode('B')
479
+
480
+ yield* connectNodesViaMessageChannel(nodeA, nodeB)
481
+
482
+ const messageCount = 3
483
+
484
+ const bFiber = yield* Effect.gen(function* () {
485
+ const channelBToA = yield* createChannel(nodeB, 'A')
486
+ yield* channelBToA.listen.pipe(
487
+ Stream.flatten(),
488
+ Stream.tap((msg) => channelBToA.send({ message: `resp:${msg.message}` })),
489
+ Stream.take(messageCount),
490
+ Stream.runDrain,
491
+ )
492
+ }).pipe(Effect.scoped, Effect.fork)
493
+
494
+ // yield* createChannel(nodeA, 'B').pipe(Effect.andThen(WebChannel.shutdown))
495
+ // // yield* createChannel(nodeA, 'B').pipe(Effect.andThen(WebChannel.shutdown))
496
+ // // yield* createChannel(nodeA, 'B').pipe(Effect.andThen(WebChannel.shutdown))
497
+ yield* Effect.gen(function* () {
498
+ const channelAToB = yield* createChannel(nodeA, 'B')
499
+ yield* channelAToB.send({ message: 'A' })
500
+ expect(yield* getFirstMessage(channelAToB)).toEqual({ message: 'resp:A' })
501
+ }).pipe(Effect.scoped, Effect.repeatN(messageCount))
502
+
503
+ yield* bFiber
504
+ }).pipe(withCtx(test)),
505
+ )
506
+ })
507
+
328
508
  Vitest.scopedLive('manual debug test', (test) =>
329
509
  Effect.gen(function* () {
330
510
  const nodeA = yield* makeMeshNode('A')
@@ -353,7 +533,7 @@ Vitest.describe('webmesh node', { timeout: 1000 }, () => {
353
533
  }).pipe(withCtx(test)),
354
534
  )
355
535
 
356
- Vitest.scopedLive('broadcast connection with message channel', (test) =>
536
+ Vitest.scopedLive('broadcast edge with message channel', (test) =>
357
537
  Effect.gen(function* () {
358
538
  const nodeA = yield* makeMeshNode('A')
359
539
  const nodeB = yield* makeMeshNode('B')
@@ -382,6 +562,7 @@ Vitest.describe('webmesh node', { timeout: 1000 }, () => {
382
562
  yield* channelAToC.send({ message: 'A1' })
383
563
  expect(yield* getFirstMessage(channelAToC)).toEqual({ message: 'C1' })
384
564
  expect(yield* getFirstMessage(channelAToC)).toEqual({ message: 'C2' })
565
+ expect(yield* getFirstMessage(channelAToC)).toEqual({ message: 'C3' })
385
566
  })
386
567
 
387
568
  const nodeCCode = Effect.gen(function* () {
@@ -396,7 +577,7 @@ Vitest.describe('webmesh node', { timeout: 1000 }, () => {
396
577
  }).pipe(withCtx(test)),
397
578
  )
398
579
 
399
- Vitest.scopedLive('should work - delayed connection', (test) =>
580
+ Vitest.scopedLive('should work - delayed edge', (test) =>
400
581
  Effect.gen(function* () {
401
582
  const nodeA = yield* makeMeshNode('A')
402
583
  const nodeB = yield* makeMeshNode('B')
@@ -420,9 +601,14 @@ Vitest.describe('webmesh node', { timeout: 1000 }, () => {
420
601
  expect(yield* getFirstMessage(channelCToA)).toEqual({ message: 'A1' })
421
602
  })
422
603
 
423
- yield* Effect.all([nodeACode, nodeCCode, connectNodes(nodeB, nodeC).pipe(Effect.delay(100))], {
424
- concurrency: 'unbounded',
425
- })
604
+ yield* Effect.all(
605
+ [
606
+ nodeACode,
607
+ nodeCCode,
608
+ connectNodes(nodeB, nodeC).pipe(Effect.delay(100), Effect.withSpan('connect-nodeB-nodeC-delay(100)')),
609
+ ],
610
+ { concurrency: 'unbounded' },
611
+ )
426
612
  }).pipe(withCtx(test)),
427
613
  )
428
614
 
@@ -451,7 +637,7 @@ Vitest.describe('webmesh node', { timeout: 1000 }, () => {
451
637
  }).pipe(withCtx(test)),
452
638
  )
453
639
 
454
- Vitest.scopedLive('should fail', (test) =>
640
+ Vitest.scopedLive('should fail with timeout due to missing edge', (test) =>
455
641
  Effect.gen(function* () {
456
642
  const nodeA = yield* makeMeshNode('A')
457
643
  const nodeB = yield* makeMeshNode('B')
@@ -473,9 +659,153 @@ Vitest.describe('webmesh node', { timeout: 1000 }, () => {
473
659
  yield* Effect.all([nodeACode, nodeCCode], { concurrency: 'unbounded' })
474
660
  }).pipe(withCtx(test)),
475
661
  )
662
+
663
+ Vitest.scopedLive('should fail with timeout due no transferable', (test) =>
664
+ Effect.gen(function* () {
665
+ const nodeA = yield* makeMeshNode('A')
666
+ const nodeB = yield* makeMeshNode('B')
667
+
668
+ yield* connectNodesViaBroadcastChannel(nodeA, nodeB)
669
+
670
+ const nodeACode = Effect.gen(function* () {
671
+ const err = yield* createChannel(nodeA, 'B').pipe(Effect.timeout(200), Effect.flip)
672
+ expect(err._tag).toBe('TimeoutException')
673
+ })
674
+
675
+ const nodeBCode = Effect.gen(function* () {
676
+ const err = yield* createChannel(nodeB, 'A').pipe(Effect.timeout(200), Effect.flip)
677
+ expect(err._tag).toBe('TimeoutException')
678
+ })
679
+
680
+ yield* Effect.all([nodeACode, nodeBCode], { concurrency: 'unbounded' })
681
+ }).pipe(withCtx(test)),
682
+ )
683
+
684
+ Vitest.scopedLive('reconnect with re-created node', (test) =>
685
+ Effect.gen(function* () {
686
+ const nodeCgen1Scope = yield* Scope.make()
687
+
688
+ const nodeA = yield* makeMeshNode('A')
689
+ const nodeB = yield* makeMeshNode('B')
690
+ const nodeCgen1 = yield* makeMeshNode('C').pipe(Scope.extend(nodeCgen1Scope))
691
+
692
+ yield* connectNodesViaMessageChannel(nodeA, nodeB)
693
+ yield* connectNodesViaMessageChannel(nodeB, nodeCgen1).pipe(Scope.extend(nodeCgen1Scope))
694
+
695
+ const nodeACode = Effect.gen(function* () {
696
+ const channelAToB = yield* createChannel(nodeA, 'C')
697
+
698
+ yield* channelAToB.send({ message: 'A1' })
699
+ expect(yield* getFirstMessage(channelAToB)).toEqual({ message: 'C1' })
700
+ })
701
+
702
+ const nodeCCode = (nodeB: MeshNode) =>
703
+ Effect.gen(function* () {
704
+ const channelBToA = yield* createChannel(nodeB, 'A')
705
+
706
+ yield* channelBToA.send({ message: 'C1' })
707
+ expect(yield* getFirstMessage(channelBToA)).toEqual({ message: 'A1' })
708
+ })
709
+
710
+ yield* Effect.all([nodeACode, nodeCCode(nodeCgen1)], { concurrency: 'unbounded' }).pipe(
711
+ Effect.withSpan('test1'),
712
+ Scope.extend(nodeCgen1Scope),
713
+ )
714
+
715
+ yield* Scope.close(nodeCgen1Scope, Exit.void)
716
+
717
+ const nodeCgen2 = yield* makeMeshNode('C')
718
+ yield* connectNodesViaMessageChannel(nodeB, nodeCgen2, { replaceIfExists: true })
719
+
720
+ yield* Effect.all([nodeACode, nodeCCode(nodeCgen2)], { concurrency: 'unbounded' }).pipe(
721
+ Effect.withSpan('test2'),
722
+ )
723
+ }).pipe(withCtx(test)),
724
+ )
725
+ })
726
+
727
+ /**
728
+ * A
729
+ * / \
730
+ * B C
731
+ * \ /
732
+ * D
733
+ */
734
+ Vitest.describe('diamond topology', () => {
735
+ Vitest.scopedLive('should work', (test) =>
736
+ Effect.gen(function* () {
737
+ const nodeA = yield* makeMeshNode('A')
738
+ const nodeB = yield* makeMeshNode('B')
739
+ const nodeC = yield* makeMeshNode('C')
740
+ const nodeD = yield* makeMeshNode('D')
741
+
742
+ yield* connectNodesViaMessageChannel(nodeA, nodeB)
743
+ yield* connectNodesViaMessageChannel(nodeA, nodeC)
744
+ yield* connectNodesViaMessageChannel(nodeB, nodeD)
745
+ yield* connectNodesViaMessageChannel(nodeC, nodeD)
746
+
747
+ const nodeACode = Effect.gen(function* () {
748
+ const channelAToD = yield* createChannel(nodeA, 'D')
749
+ yield* channelAToD.send({ message: 'A1' })
750
+ expect(yield* getFirstMessage(channelAToD)).toEqual({ message: 'D1' })
751
+ })
752
+
753
+ const nodeDCode = Effect.gen(function* () {
754
+ const channelDToA = yield* createChannel(nodeD, 'A')
755
+ yield* channelDToA.send({ message: 'D1' })
756
+ expect(yield* getFirstMessage(channelDToA)).toEqual({ message: 'A1' })
757
+ })
758
+
759
+ yield* Effect.all([nodeACode, nodeDCode], { concurrency: 'unbounded' })
760
+ }).pipe(withCtx(test)),
761
+ )
762
+ })
763
+
764
+ /**
765
+ * A E
766
+ * \ /
767
+ * C---D
768
+ * / \
769
+ * B F
770
+ *
771
+ * Topology: Butterfly topology with two connected hubs (C-D) each serving multiple nodes
772
+ */
773
+ Vitest.describe('butterfly topology', () => {
774
+ Vitest.scopedLive('should work', (test) =>
775
+ Effect.gen(function* () {
776
+ const nodeA = yield* makeMeshNode('A')
777
+ const nodeB = yield* makeMeshNode('B')
778
+ const nodeC = yield* makeMeshNode('C')
779
+ const nodeD = yield* makeMeshNode('D')
780
+ const nodeE = yield* makeMeshNode('E')
781
+ const nodeF = yield* makeMeshNode('F')
782
+
783
+ yield* connectNodesViaMessageChannel(nodeA, nodeC)
784
+ yield* connectNodesViaMessageChannel(nodeB, nodeC)
785
+ yield* connectNodesViaMessageChannel(nodeC, nodeD)
786
+ yield* connectNodesViaMessageChannel(nodeD, nodeE)
787
+ yield* connectNodesViaMessageChannel(nodeD, nodeF)
788
+
789
+ yield* Effect.promise(() => nodeA.debug.requestTopology(100))
790
+
791
+ const nodeACode = Effect.gen(function* () {
792
+ const channelAToE = yield* createChannel(nodeA, 'E')
793
+ yield* channelAToE.send({ message: 'A1' })
794
+ expect(yield* getFirstMessage(channelAToE)).toEqual({ message: 'E1' })
795
+ })
796
+
797
+ const nodeECode = Effect.gen(function* () {
798
+ const channelEToA = yield* createChannel(nodeE, 'A')
799
+ yield* channelEToA.send({ message: 'E1' })
800
+ expect(yield* getFirstMessage(channelEToA)).toEqual({ message: 'A1' })
801
+ })
802
+
803
+ yield* Effect.all([nodeACode, nodeECode], { concurrency: 'unbounded' })
804
+ }).pipe(withCtx(test)),
805
+ )
476
806
  })
477
807
 
478
- Vitest.describe('mixture of messagechannel and proxy connections', () => {
808
+ Vitest.describe('mixture of messagechannel and proxy edge connections', () => {
479
809
  // TODO test case to better guard against case where side A tries to create a proxy channel to B
480
810
  // and side B tries to create a messagechannel to A
481
811
  Vitest.scopedLive('should work for proxy channels', (test) =>
@@ -486,47 +816,82 @@ Vitest.describe('webmesh node', { timeout: 1000 }, () => {
486
816
  yield* connectNodesViaMessageChannel(nodeB, nodeA)
487
817
  const err = yield* connectNodesViaBroadcastChannel(nodeA, nodeB).pipe(Effect.flip)
488
818
 
489
- expect(err._tag).toBe('ConnectionAlreadyExistsError')
819
+ expect(err._tag).toBe('EdgeAlreadyExistsError')
490
820
  }).pipe(withCtx(test)),
491
821
  )
492
822
 
493
- // TODO this currently fails but should work. probably needs some more guarding internally.
494
- Vitest.scopedLive.skip('should work for messagechannels', (test) =>
823
+ Vitest.scopedLive('should work for messagechannels', (test) =>
495
824
  Effect.gen(function* () {
496
825
  const nodeA = yield* makeMeshNode('A')
497
826
  const nodeB = yield* makeMeshNode('B')
827
+ const nodeC = yield* makeMeshNode('C')
498
828
 
499
829
  yield* connectNodesViaMessageChannel(nodeB, nodeA)
500
- yield* connectNodesViaBroadcastChannel(nodeA, nodeB)
830
+ yield* connectNodesViaBroadcastChannel(nodeB, nodeC)
501
831
 
502
832
  const nodeACode = Effect.gen(function* () {
503
- const channelAToB = yield* createChannel(nodeA, 'B', { mode: 'messagechannel' })
504
- yield* channelAToB.send({ message: 'A1' })
505
- expect(yield* getFirstMessage(channelAToB)).toEqual({ message: 'B1' })
833
+ const channelAToC = yield* createChannel(nodeA, 'C', { mode: 'proxy' })
834
+ yield* channelAToC.send({ message: 'A1' })
835
+ expect(yield* getFirstMessage(channelAToC)).toEqual({ message: 'C1' })
506
836
  })
507
837
 
508
- const nodeBCode = Effect.gen(function* () {
509
- const channelBToA = yield* createChannel(nodeB, 'A', { mode: 'messagechannel' })
510
- yield* channelBToA.send({ message: 'B1' })
511
- expect(yield* getFirstMessage(channelBToA)).toEqual({ message: 'A1' })
838
+ const nodeCCode = Effect.gen(function* () {
839
+ const channelCToA = yield* createChannel(nodeC, 'A', { mode: 'proxy' })
840
+ yield* channelCToA.send({ message: 'C1' })
841
+ expect(yield* getFirstMessage(channelCToA)).toEqual({ message: 'A1' })
512
842
  })
513
843
 
514
- yield* Effect.all([nodeACode, nodeBCode], { concurrency: 'unbounded' })
844
+ yield* Effect.all([nodeACode, nodeCCode], { concurrency: 'unbounded' })
515
845
  }).pipe(withCtx(test)),
516
846
  )
517
847
  })
518
- })
519
848
 
520
- const envTruish = (env: string | undefined) => env !== undefined && env !== 'false' && env !== '0'
521
- const isCi = envTruish(process.env.CI)
849
+ Vitest.describe('broadcast channel', () => {
850
+ Vitest.scopedLive('should work', (test) =>
851
+ Effect.gen(function* () {
852
+ const nodeA = yield* makeMeshNode('A')
853
+ const nodeB = yield* makeMeshNode('B')
854
+ const nodeC = yield* makeMeshNode('C')
855
+
856
+ yield* connectNodesViaMessageChannel(nodeA, nodeB)
857
+ yield* connectNodesViaMessageChannel(nodeB, nodeC)
858
+
859
+ const channelOnA = yield* nodeA.makeBroadcastChannel({ channelName: 'test', schema: Schema.String })
860
+ const channelOnC = yield* nodeC.makeBroadcastChannel({ channelName: 'test', schema: Schema.String })
861
+
862
+ const listenOnAFiber = yield* channelOnA.listen.pipe(
863
+ Stream.flatten(),
864
+ Stream.runHead,
865
+ Effect.flatten,
866
+ Effect.fork,
867
+ )
868
+ const listenOnCFiber = yield* channelOnC.listen.pipe(
869
+ Stream.flatten(),
870
+ Stream.runHead,
871
+ Effect.flatten,
872
+ Effect.fork,
873
+ )
874
+
875
+ yield* channelOnA.send('A1')
876
+ yield* channelOnC.send('C1')
877
+
878
+ expect(yield* listenOnAFiber).toEqual('C1')
879
+ expect(yield* listenOnCFiber).toEqual('A1')
880
+ }).pipe(withCtx(test)),
881
+ )
882
+ })
883
+ })
522
884
 
523
- const otelLayer = isCi ? Layer.empty : OtelLiveHttp({ serviceName: 'webmesh-node-test', skipLogUrl: false })
885
+ const otelLayer = IS_CI ? Layer.empty : OtelLiveHttp({ serviceName: 'webmesh-node-test', skipLogUrl: false })
524
886
 
525
887
  const withCtx =
526
- (testContext: Vitest.TaskContext, { suffix, skipOtel = false }: { suffix?: string; skipOtel?: boolean } = {}) =>
888
+ (
889
+ testContext: Vitest.TaskContext,
890
+ { suffix, skipOtel = false, timeout = testTimeout }: { suffix?: string; skipOtel?: boolean; timeout?: number } = {},
891
+ ) =>
527
892
  <A, E, R>(self: Effect.Effect<A, E, R>) =>
528
893
  self.pipe(
529
- Effect.timeout(isCi ? 10_000 : 500),
894
+ Effect.timeout(timeout),
530
895
  Effect.provide(Logger.pretty),
531
896
  Effect.scoped, // We need to scope the effect manually here because otherwise the span is not closed
532
897
  Effect.withSpan(`${testContext.task.suite?.name}:${testContext.task.name}${suffix ? `:${suffix}` : ''}`),