@livestore/webmesh 0.0.0-snapshot-1d99fea7d2ce2c7a5d9ed0a3752f8a7bda6bc3db → 0.0.0-snapshot-aed277ba0960f72b8d464508961ab4aec1881230

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 (43) hide show
  1. package/README.md +19 -1
  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 +202 -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 +125 -162
  10. package/dist/channel/message-channel.js.map +1 -1
  11. package/dist/channel/proxy-channel.d.ts +2 -2
  12. package/dist/channel/proxy-channel.d.ts.map +1 -1
  13. package/dist/channel/proxy-channel.js +7 -5
  14. package/dist/channel/proxy-channel.js.map +1 -1
  15. package/dist/common.d.ts +8 -4
  16. package/dist/common.d.ts.map +1 -1
  17. package/dist/common.js +2 -1
  18. package/dist/common.js.map +1 -1
  19. package/dist/mesh-schema.d.ts +23 -1
  20. package/dist/mesh-schema.d.ts.map +1 -1
  21. package/dist/mesh-schema.js +21 -2
  22. package/dist/mesh-schema.js.map +1 -1
  23. package/dist/node.d.ts +12 -1
  24. package/dist/node.d.ts.map +1 -1
  25. package/dist/node.js +39 -9
  26. package/dist/node.js.map +1 -1
  27. package/dist/node.test.d.ts +1 -1
  28. package/dist/node.test.d.ts.map +1 -1
  29. package/dist/node.test.js +256 -124
  30. package/dist/node.test.js.map +1 -1
  31. package/dist/websocket-connection.d.ts +1 -2
  32. package/dist/websocket-connection.d.ts.map +1 -1
  33. package/dist/websocket-connection.js +5 -4
  34. package/dist/websocket-connection.js.map +1 -1
  35. package/package.json +3 -3
  36. package/src/channel/message-channel-internal.ts +337 -0
  37. package/src/channel/message-channel.ts +177 -308
  38. package/src/channel/proxy-channel.ts +238 -230
  39. package/src/common.ts +3 -1
  40. package/src/mesh-schema.ts +20 -2
  41. package/src/node.test.ts +367 -150
  42. package/src/node.ts +68 -12
  43. package/src/websocket-connection.ts +83 -79
package/src/node.test.ts CHANGED
@@ -1,5 +1,19 @@
1
+ import '@livestore/utils/node-vitest-polyfill'
2
+
1
3
  import { IS_CI } from '@livestore/utils'
2
- import { Chunk, Deferred, Effect, identity, Layer, Logger, Schema, Stream, WebChannel } from '@livestore/utils/effect'
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'
3
17
  import { OtelLiveHttp } from '@livestore/utils/node'
4
18
  import { Vitest } from '@livestore/utils/node-vitest'
5
19
  import { expect } from 'vitest'
@@ -17,35 +31,47 @@ import { makeMeshNode } from './node.js'
17
31
 
18
32
  const ExampleSchema = Schema.Struct({ message: Schema.String })
19
33
 
20
- const connectNodesViaMessageChannel = (nodeA: MeshNode, nodeB: MeshNode) =>
34
+ const connectNodesViaMessageChannel = (nodeA: MeshNode, nodeB: MeshNode, options?: { replaceIfExists?: boolean }) =>
21
35
  Effect.gen(function* () {
22
36
  const mc = new MessageChannel()
23
37
  const meshChannelAToB = yield* WebChannel.messagePortChannel({ port: mc.port1, schema: Packet })
24
38
  const meshChannelBToA = yield* WebChannel.messagePortChannel({ port: mc.port2, schema: Packet })
25
39
 
26
- yield* nodeA.addConnection({ target: nodeB.nodeName, connectionChannel: meshChannelAToB })
27
- yield* nodeB.addConnection({ target: nodeA.nodeName, connectionChannel: meshChannelBToA })
28
-
29
- return mc
40
+ yield* nodeA.addConnection({
41
+ target: nodeB.nodeName,
42
+ connectionChannel: meshChannelAToB,
43
+ replaceIfExists: options?.replaceIfExists,
44
+ })
45
+ yield* nodeB.addConnection({
46
+ target: nodeA.nodeName,
47
+ connectionChannel: meshChannelBToA,
48
+ replaceIfExists: options?.replaceIfExists,
49
+ })
30
50
  }).pipe(Effect.withSpan(`connectNodesViaMessageChannel:${nodeA.nodeName}↔${nodeB.nodeName}`))
31
51
 
32
- const connectNodesViaBroadcastChannel = (nodeA: MeshNode, nodeB: MeshNode) =>
52
+ const connectNodesViaBroadcastChannel = (nodeA: MeshNode, nodeB: MeshNode, options?: { replaceIfExists?: boolean }) =>
33
53
  Effect.gen(function* () {
34
54
  // Need to instantiate two different channels because they filter out messages they sent themselves
35
55
  const broadcastWebChannelA = yield* WebChannel.broadcastChannelWithAck({
36
56
  channelName: `${nodeA.nodeName}↔${nodeB.nodeName}`,
37
- listenSchema: Packet,
38
- sendSchema: Packet,
57
+ schema: Packet,
39
58
  })
40
59
 
41
60
  const broadcastWebChannelB = yield* WebChannel.broadcastChannelWithAck({
42
61
  channelName: `${nodeA.nodeName}↔${nodeB.nodeName}`,
43
- listenSchema: Packet,
44
- sendSchema: Packet,
62
+ schema: Packet,
45
63
  })
46
64
 
47
- yield* nodeA.addConnection({ target: nodeB.nodeName, connectionChannel: broadcastWebChannelA })
48
- yield* nodeB.addConnection({ target: nodeA.nodeName, connectionChannel: broadcastWebChannelB })
65
+ yield* nodeA.addConnection({
66
+ target: nodeB.nodeName,
67
+ connectionChannel: broadcastWebChannelA,
68
+ replaceIfExists: options?.replaceIfExists,
69
+ })
70
+ yield* nodeB.addConnection({
71
+ target: nodeA.nodeName,
72
+ connectionChannel: broadcastWebChannelB,
73
+ replaceIfExists: options?.replaceIfExists,
74
+ })
49
75
  }).pipe(Effect.withSpan(`connectNodesViaBroadcastChannel:${nodeA.nodeName}↔${nodeB.nodeName}`))
50
76
 
51
77
  const createChannel = (source: MeshNode, target: string, options?: Partial<Parameters<MeshNode['makeChannel']>[0]>) =>
@@ -74,16 +100,21 @@ const maybeDelay =
74
100
  ? effect
75
101
  : Effect.sleep(delay).pipe(Effect.withSpan(`${label}:delay(${delay})`), Effect.andThen(effect))
76
102
 
77
- const testTimeout = IS_CI ? 30_000 : 500
103
+ const testTimeout = IS_CI ? 30_000 : 1000
104
+ const propTestTimeout = IS_CI ? 60_000 : 20_000
78
105
 
79
106
  // TODO also make work without `Vitest.scopedLive` (i.e. with `Vitest.scoped`)
80
107
  // probably requires controlling the clocks
81
108
  Vitest.describe('webmesh node', { timeout: testTimeout }, () => {
82
109
  Vitest.describe('A <> B', () => {
83
- Vitest.describe('prop tests', () => {
110
+ Vitest.describe('prop tests', { timeout: propTestTimeout }, () => {
84
111
  const Delay = Schema.UndefinedOr(Schema.Literal(0, 1, 10, 50))
85
112
  // NOTE for message channels, we test both with and without transferables (i.e. proxying)
86
113
  const ChannelType = Schema.Literal('messagechannel', 'messagechannel.proxy', 'proxy')
114
+ const NodeNames = Schema.Union(
115
+ Schema.Tuple(Schema.Literal('A'), Schema.Literal('B')),
116
+ Schema.Tuple(Schema.Literal('B'), Schema.Literal('A')),
117
+ )
87
118
 
88
119
  const fromChannelType = (
89
120
  channelType: typeof ChannelType.Type,
@@ -104,178 +135,268 @@ Vitest.describe('webmesh node', { timeout: testTimeout }, () => {
104
135
  }
105
136
  }
106
137
 
138
+ const exchangeMessages = ({
139
+ nodeX,
140
+ nodeY,
141
+ channelType,
142
+ // numberOfMessages = 1,
143
+ delays,
144
+ }: {
145
+ nodeX: MeshNode
146
+ nodeY: MeshNode
147
+ channelType: 'messagechannel' | 'proxy' | 'messagechannel.proxy'
148
+ numberOfMessages?: number
149
+ delays?: { x?: number; y?: number; connect?: number }
150
+ }) =>
151
+ Effect.gen(function* () {
152
+ const nodeLabel = { x: nodeX.nodeName, y: nodeY.nodeName }
153
+ const { mode, connectNodes } = fromChannelType(channelType)
154
+
155
+ const nodeXCode = Effect.gen(function* () {
156
+ const channelXToY = yield* createChannel(nodeX, nodeY.nodeName, { mode })
157
+
158
+ yield* channelXToY.send({ message: `${nodeLabel.x}1` })
159
+ // console.log('channelXToY', channelXToY.debugInfo)
160
+ expect(yield* getFirstMessage(channelXToY)).toEqual({ message: `${nodeLabel.y}1` })
161
+ // expect(channelXToY.debugInfo.connectCounter).toBe(1)
162
+ })
163
+
164
+ const nodeYCode = Effect.gen(function* () {
165
+ const channelYToX = yield* createChannel(nodeY, nodeX.nodeName, { mode })
166
+
167
+ yield* channelYToX.send({ message: `${nodeLabel.y}1` })
168
+ // console.log('channelYToX', channelYToX.debugInfo)
169
+ expect(yield* getFirstMessage(channelYToX)).toEqual({ message: `${nodeLabel.x}1` })
170
+ // expect(channelYToX.debugInfo.connectCounter).toBe(1)
171
+ })
172
+
173
+ yield* Effect.all(
174
+ [
175
+ connectNodes(nodeX, nodeY).pipe(maybeDelay(delays?.connect, 'connectNodes')),
176
+ nodeXCode.pipe(maybeDelay(delays?.x, `node${nodeLabel.x}Code`)),
177
+ nodeYCode.pipe(maybeDelay(delays?.y, `node${nodeLabel.y}Code`)),
178
+ ],
179
+ { concurrency: 'unbounded' },
180
+ ).pipe(Effect.withSpan(`exchangeMessages(${nodeLabel.x}↔${nodeLabel.y})`))
181
+ })
182
+
183
+ // const delayX = 40
184
+ // const delayY = undefined
185
+ // const connectDelay = undefined
186
+ // const channelType = 'messagechannel'
187
+ // const nodeNames = ['B', 'A'] as const
188
+ // Vitest.scopedLive(
189
+ // 'a / b connect at different times with different channel types',
190
+ // (test) =>
107
191
  Vitest.scopedLive.prop(
108
- // Vitest.scopedLive.only(
109
192
  'a / b connect at different times with different channel types',
110
- [Delay, Delay, Delay, ChannelType],
111
- ([delayA, delayB, connectDelay, channelType], test) =>
112
- // (test) =>
193
+ [Delay, Delay, Delay, ChannelType, NodeNames],
194
+ ([delayX, delayY, connectDelay, channelType, nodeNames], test) =>
113
195
  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)
196
+ // console.log({ delayX, delayY, connectDelay, channelType, nodeNames })
119
197
 
120
- const nodeA = yield* makeMeshNode('A')
121
- const nodeB = yield* makeMeshNode('B')
198
+ const [nodeNameX, nodeNameY] = nodeNames
199
+ const nodeX = yield* makeMeshNode(nodeNameX)
200
+ const nodeY = yield* makeMeshNode(nodeNameY)
122
201
 
123
- const { mode, connectNodes } = fromChannelType(channelType)
202
+ yield* exchangeMessages({
203
+ nodeX,
204
+ nodeY,
205
+ channelType,
206
+ delays: { x: delayX, y: delayY, connect: connectDelay },
207
+ })
208
+ }).pipe(
209
+ withCtx(test, {
210
+ skipOtel: true,
211
+ suffix: `delayX=${delayX} delayY=${delayY} connectDelay=${connectDelay} channelType=${channelType} nodeNames=${nodeNames}`,
212
+ }),
213
+ ),
214
+ // { fastCheck: { numRuns: 20 } },
215
+ )
124
216
 
125
- const nodeACode = Effect.gen(function* () {
126
- const channelAToB = yield* createChannel(nodeA, 'B', { mode })
217
+ {
218
+ // const waitForOfflineDelay = undefined
219
+ // const sleepDelay = 0
220
+ // const channelType = 'messagechannel'
221
+ // Vitest.scopedLive(
222
+ // 'b reconnects',
223
+ // (test) =>
224
+ Vitest.scopedLive.prop(
225
+ 'b reconnects',
226
+ [Delay, Delay, ChannelType],
227
+ ([waitForOfflineDelay, sleepDelay, channelType], test) =>
228
+ Effect.gen(function* () {
229
+ // console.log({ waitForOfflineDelay, sleepDelay, channelType })
127
230
 
128
- yield* channelAToB.send({ message: 'A1' })
129
- expect(yield* getFirstMessage(channelAToB)).toEqual({ message: 'A2' })
130
- })
231
+ if (waitForOfflineDelay === undefined) {
232
+ // TODO we still need to fix this scenario but it shouldn't really be common in practice
233
+ return
234
+ }
131
235
 
132
- const nodeBCode = Effect.gen(function* () {
133
- const channelBToA = yield* createChannel(nodeB, 'A', { mode })
236
+ const nodeA = yield* makeMeshNode('A')
237
+ const nodeB = yield* makeMeshNode('B')
134
238
 
135
- yield* channelBToA.send({ message: 'A2' })
136
- expect(yield* getFirstMessage(channelBToA)).toEqual({ message: 'A1' })
137
- })
239
+ const { mode, connectNodes } = fromChannelType(channelType)
138
240
 
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
- )
241
+ // TODO also optionally delay the connection
242
+ yield* connectNodes(nodeA, nodeB)
151
243
 
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')
244
+ const waitForBToBeOffline =
245
+ waitForOfflineDelay === undefined ? undefined : yield* Deferred.make<void, never>()
174
246
 
175
- const { mode, connectNodes } = fromChannelType(channelType)
247
+ const nodeACode = Effect.gen(function* () {
248
+ const channelAToB = yield* createChannel(nodeA, 'B', { mode })
249
+ yield* channelAToB.send({ message: 'A1' })
250
+ expect(yield* getFirstMessage(channelAToB)).toEqual({ message: 'B1' })
176
251
 
177
- // TODO also optionally delay the connection
178
- yield* connectNodes(nodeA, nodeB)
252
+ console.log('nodeACode:waiting for B to be offline')
253
+ if (waitForBToBeOffline !== undefined) {
254
+ yield* waitForBToBeOffline
255
+ }
179
256
 
180
- const waitForBToBeOffline =
181
- waitForOfflineDelay === undefined ? undefined : yield* Deferred.make<void, never>()
257
+ yield* channelAToB.send({ message: 'A2' })
258
+ expect(yield* getFirstMessage(channelAToB)).toEqual({ message: 'B2' })
259
+ })
182
260
 
183
- const nodeACode = Effect.gen(function* () {
184
- const channelAToB = yield* createChannel(nodeA, 'B', { mode })
261
+ // Simulating node b going offline and then coming back online
262
+ // This test also illustrates why we need a ack-message channel since otherwise
263
+ // sent messages might get lost
264
+ const nodeBCode = Effect.gen(function* () {
265
+ yield* Effect.gen(function* () {
266
+ const channelBToA = yield* createChannel(nodeB, 'A', { mode })
185
267
 
186
- yield* channelAToB.send({ message: 'A1' })
187
- expect(yield* getFirstMessage(channelAToB)).toEqual({ message: 'B1' })
268
+ yield* channelBToA.send({ message: 'B1' })
269
+ expect(yield* getFirstMessage(channelBToA)).toEqual({ message: 'A1' })
270
+ }).pipe(Effect.scoped, Effect.withSpan('nodeBCode:part1'))
188
271
 
189
- if (waitForBToBeOffline !== undefined) {
190
- yield* waitForBToBeOffline
191
- }
272
+ console.log('nodeBCode:B node going offline')
273
+ if (waitForBToBeOffline !== undefined) {
274
+ yield* Deferred.succeed(waitForBToBeOffline, void 0)
275
+ }
192
276
 
193
- yield* channelAToB.send({ message: 'A2' })
194
- expect(yield* getFirstMessage(channelAToB)).toEqual({ message: 'B2' })
195
- })
277
+ if (sleepDelay !== undefined) {
278
+ yield* Effect.sleep(sleepDelay).pipe(Effect.withSpan(`B:sleep(${sleepDelay})`))
279
+ }
196
280
 
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 })
281
+ // Recreating the channel
282
+ yield* Effect.gen(function* () {
283
+ const channelBToA = yield* createChannel(nodeB, 'A', { mode })
201
284
 
202
- yield* channelBToA.send({ message: 'B1' })
203
- expect(yield* getFirstMessage(channelBToA)).toEqual({ message: 'A1' })
204
- }).pipe(Effect.scoped)
285
+ yield* channelBToA.send({ message: 'B2' })
286
+ expect(yield* getFirstMessage(channelBToA)).toEqual({ message: 'A2' })
287
+ }).pipe(Effect.scoped, Effect.withSpan('nodeBCode:part2'))
288
+ })
205
289
 
206
- if (waitForBToBeOffline !== undefined) {
207
- yield* Deferred.succeed(waitForBToBeOffline, void 0)
208
- }
290
+ yield* Effect.all([nodeACode, nodeBCode], { concurrency: 'unbounded' }).pipe(Effect.withSpan('test'))
291
+ }).pipe(
292
+ withCtx(test, {
293
+ skipOtel: true,
294
+ suffix: `waitForOfflineDelay=${waitForOfflineDelay} sleepDelay=${sleepDelay} channelType=${channelType}`,
295
+ }),
296
+ ),
297
+ { fastCheck: { numRuns: 20 } },
298
+ )
299
+ }
209
300
 
210
- if (sleepDelay !== undefined) {
211
- yield* Effect.sleep(sleepDelay).pipe(Effect.withSpan(`B:sleep(${sleepDelay})`))
212
- }
301
+ Vitest.scopedLive('reconnect with re-created node', (test) =>
302
+ Effect.gen(function* () {
303
+ const nodeBgen1Scope = yield* Scope.make()
213
304
 
214
- yield* Effect.gen(function* () {
215
- const channelBToA = yield* createChannel(nodeB, 'A', { mode })
305
+ const nodeA = yield* makeMeshNode('A')
306
+ const nodeBgen1 = yield* makeMeshNode('B').pipe(Scope.extend(nodeBgen1Scope))
216
307
 
217
- yield* channelBToA.send({ message: 'B2' })
218
- expect(yield* getFirstMessage(channelBToA)).toEqual({ message: 'A2' })
219
- }).pipe(Effect.scoped)
308
+ yield* connectNodesViaMessageChannel(nodeA, nodeBgen1).pipe(Scope.extend(nodeBgen1Scope))
309
+
310
+ // yield* Effect.sleep(100)
311
+
312
+ const channelAToBOnce = yield* Effect.cached(createChannel(nodeA, 'B'))
313
+ const nodeACode = Effect.gen(function* () {
314
+ const channelAToB = yield* channelAToBOnce
315
+ yield* channelAToB.send({ message: 'A1' })
316
+ expect(yield* getFirstMessage(channelAToB)).toEqual({ message: 'B1' })
317
+ // expect(channelAToB.debugInfo.connectCounter).toBe(1)
318
+ })
319
+
320
+ const nodeBCode = (nodeB: MeshNode) =>
321
+ Effect.gen(function* () {
322
+ const channelBToA = yield* createChannel(nodeB, 'A')
323
+
324
+ yield* channelBToA.send({ message: 'B1' })
325
+ expect(yield* getFirstMessage(channelBToA)).toEqual({ message: 'A1' })
326
+ // expect(channelBToA.debugInfo.connectCounter).toBe(1)
220
327
  })
221
328
 
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
- ),
329
+ yield* Effect.all([nodeACode, nodeBCode(nodeBgen1).pipe(Scope.extend(nodeBgen1Scope))], {
330
+ concurrency: 'unbounded',
331
+ }).pipe(Effect.withSpan('test1'))
332
+
333
+ yield* Scope.close(nodeBgen1Scope, Exit.void)
334
+
335
+ const nodeBgen2 = yield* makeMeshNode('B')
336
+ yield* connectNodesViaMessageChannel(nodeA, nodeBgen2, { replaceIfExists: true })
337
+
338
+ yield* Effect.all([nodeACode, nodeBCode(nodeBgen2)], { concurrency: 'unbounded' }).pipe(
339
+ Effect.withSpan('test2'),
340
+ )
341
+ }).pipe(withCtx(test)),
229
342
  )
230
343
 
231
344
  const ChannelTypeWithoutMessageChannelProxy = Schema.Literal('proxy', 'messagechannel')
232
345
  Vitest.scopedLive.prop(
233
346
  'replace connection while keeping the channel',
234
- [ChannelTypeWithoutMessageChannelProxy],
235
- ([channelType], test) =>
347
+ [ChannelTypeWithoutMessageChannelProxy, NodeNames],
348
+ ([channelType, nodeNames], test) =>
236
349
  Effect.gen(function* () {
237
- const nodeA = yield* makeMeshNode('A')
238
- const nodeB = yield* makeMeshNode('B')
350
+ const [nodeNameX, nodeNameY] = nodeNames
351
+ const nodeX = yield* makeMeshNode(nodeNameX)
352
+ const nodeY = yield* makeMeshNode(nodeNameY)
353
+ const nodeLabel = { x: nodeX.nodeName, y: nodeY.nodeName }
239
354
 
240
355
  const { mode, connectNodes } = fromChannelType(channelType)
241
356
 
242
- yield* connectNodes(nodeA, nodeB)
357
+ yield* connectNodes(nodeX, nodeY)
243
358
 
244
359
  const waitForConnectionReplacement = yield* Deferred.make<void>()
245
360
 
246
- const nodeACode = Effect.gen(function* () {
247
- const channelAToB = yield* createChannel(nodeA, 'B', { mode })
361
+ const nodeXCode = Effect.gen(function* () {
362
+ const channelXToY = yield* createChannel(nodeX, nodeLabel.y, { mode })
248
363
 
249
- yield* channelAToB.send({ message: 'A1' })
250
- expect(yield* getFirstMessage(channelAToB)).toEqual({ message: 'B1' })
364
+ yield* channelXToY.send({ message: `${nodeLabel.x}1` })
365
+ expect(yield* getFirstMessage(channelXToY)).toEqual({ message: `${nodeLabel.y}1` })
251
366
 
252
367
  yield* waitForConnectionReplacement
253
368
 
254
- yield* channelAToB.send({ message: 'A2' })
255
- expect(yield* getFirstMessage(channelAToB)).toEqual({ message: 'B2' })
369
+ yield* channelXToY.send({ message: `${nodeLabel.x}2` })
370
+ expect(yield* getFirstMessage(channelXToY)).toEqual({ message: `${nodeLabel.y}2` })
256
371
  })
257
372
 
258
- const nodeBCode = Effect.gen(function* () {
259
- const channelBToA = yield* createChannel(nodeB, 'A', { mode })
373
+ const nodeYCode = Effect.gen(function* () {
374
+ const channelYToX = yield* createChannel(nodeY, nodeLabel.x, { mode })
260
375
 
261
- yield* channelBToA.send({ message: 'B1' })
262
- expect(yield* getFirstMessage(channelBToA)).toEqual({ message: 'A1' })
376
+ yield* channelYToX.send({ message: `${nodeLabel.y}1` })
377
+ expect(yield* getFirstMessage(channelYToX)).toEqual({ message: `${nodeLabel.x}1` })
263
378
 
264
379
  // Switch out connection while keeping the channel
265
- yield* nodeA.removeConnection('B')
266
- yield* nodeB.removeConnection('A')
267
- yield* connectNodes(nodeA, nodeB)
380
+ yield* nodeX.removeConnection(nodeLabel.y)
381
+ yield* nodeY.removeConnection(nodeLabel.x)
382
+ yield* connectNodes(nodeX, nodeY)
268
383
  yield* Deferred.succeed(waitForConnectionReplacement, void 0)
269
384
 
270
- yield* channelBToA.send({ message: 'B2' })
271
- expect(yield* getFirstMessage(channelBToA)).toEqual({ message: 'A2' })
385
+ yield* channelYToX.send({ message: `${nodeLabel.y}2` })
386
+ expect(yield* getFirstMessage(channelYToX)).toEqual({ message: `${nodeLabel.x}2` })
272
387
  })
273
388
 
274
- yield* Effect.all([nodeACode, nodeBCode], { concurrency: 'unbounded' })
275
- }).pipe(withCtx(test, { skipOtel: true, suffix: `channelType=${channelType}` })),
389
+ yield* Effect.all([nodeXCode, nodeYCode], { concurrency: 'unbounded' })
390
+ }).pipe(
391
+ withCtx(test, {
392
+ skipOtel: true,
393
+ suffix: `channelType=${channelType} nodeNames=${nodeNames}`,
394
+ }),
395
+ ),
396
+ { fastCheck: { numRuns: 10 } },
276
397
  )
277
398
 
278
- Vitest.describe.todo('TODO improve latency', () => {
399
+ Vitest.describe('TODO improve latency', () => {
279
400
  // TODO we need to improve latency when sending messages concurrently
280
401
  Vitest.scopedLive.prop(
281
402
  'concurrent messages',
@@ -322,7 +443,14 @@ Vitest.describe('webmesh node', { timeout: testTimeout }, () => {
322
443
  yield* Effect.all([nodeACode, nodeBCode, connectNodes(nodeA, nodeB).pipe(Effect.delay(100))], {
323
444
  concurrency: 'unbounded',
324
445
  })
325
- }).pipe(withCtx(test, { skipOtel: false, suffix: `channelType=${channelType} count=${count}` })),
446
+ }).pipe(
447
+ withCtx(test, {
448
+ skipOtel: true,
449
+ suffix: `channelType=${channelType} count=${count}`,
450
+ timeout: testTimeout * 2,
451
+ }),
452
+ ),
453
+ { fastCheck: { numRuns: 10 } },
326
454
  )
327
455
  })
328
456
  })
@@ -384,6 +512,7 @@ Vitest.describe('webmesh node', { timeout: testTimeout }, () => {
384
512
  yield* channelAToC.send({ message: 'A1' })
385
513
  expect(yield* getFirstMessage(channelAToC)).toEqual({ message: 'C1' })
386
514
  expect(yield* getFirstMessage(channelAToC)).toEqual({ message: 'C2' })
515
+ expect(yield* getFirstMessage(channelAToC)).toEqual({ message: 'C3' })
387
516
  })
388
517
 
389
518
  const nodeCCode = Effect.gen(function* () {
@@ -422,9 +551,14 @@ Vitest.describe('webmesh node', { timeout: testTimeout }, () => {
422
551
  expect(yield* getFirstMessage(channelCToA)).toEqual({ message: 'A1' })
423
552
  })
424
553
 
425
- yield* Effect.all([nodeACode, nodeCCode, connectNodes(nodeB, nodeC).pipe(Effect.delay(100))], {
426
- concurrency: 'unbounded',
427
- })
554
+ yield* Effect.all(
555
+ [
556
+ nodeACode,
557
+ nodeCCode,
558
+ connectNodes(nodeB, nodeC).pipe(Effect.delay(100), Effect.withSpan('connect-nodeB-nodeC-delay(100)')),
559
+ ],
560
+ { concurrency: 'unbounded' },
561
+ )
428
562
  }).pipe(withCtx(test)),
429
563
  )
430
564
 
@@ -475,6 +609,85 @@ Vitest.describe('webmesh node', { timeout: testTimeout }, () => {
475
609
  yield* Effect.all([nodeACode, nodeCCode], { concurrency: 'unbounded' })
476
610
  }).pipe(withCtx(test)),
477
611
  )
612
+
613
+ Vitest.scopedLive('reconnect with re-created node', (test) =>
614
+ Effect.gen(function* () {
615
+ const nodeCgen1Scope = yield* Scope.make()
616
+
617
+ const nodeA = yield* makeMeshNode('A')
618
+ const nodeB = yield* makeMeshNode('B')
619
+ const nodeCgen1 = yield* makeMeshNode('C').pipe(Scope.extend(nodeCgen1Scope))
620
+
621
+ yield* connectNodesViaMessageChannel(nodeA, nodeB)
622
+ yield* connectNodesViaMessageChannel(nodeB, nodeCgen1).pipe(Scope.extend(nodeCgen1Scope))
623
+
624
+ const nodeACode = Effect.gen(function* () {
625
+ const channelAToB = yield* createChannel(nodeA, 'C')
626
+
627
+ yield* channelAToB.send({ message: 'A1' })
628
+ expect(yield* getFirstMessage(channelAToB)).toEqual({ message: 'C1' })
629
+ })
630
+
631
+ const nodeCCode = (nodeB: MeshNode) =>
632
+ Effect.gen(function* () {
633
+ const channelBToA = yield* createChannel(nodeB, 'A')
634
+
635
+ yield* channelBToA.send({ message: 'C1' })
636
+ expect(yield* getFirstMessage(channelBToA)).toEqual({ message: 'A1' })
637
+ })
638
+
639
+ yield* Effect.all([nodeACode, nodeCCode(nodeCgen1)], { concurrency: 'unbounded' }).pipe(
640
+ Effect.withSpan('test1'),
641
+ Scope.extend(nodeCgen1Scope),
642
+ )
643
+
644
+ yield* Scope.close(nodeCgen1Scope, Exit.void)
645
+
646
+ const nodeCgen2 = yield* makeMeshNode('C')
647
+ yield* connectNodesViaMessageChannel(nodeB, nodeCgen2, { replaceIfExists: true })
648
+
649
+ yield* Effect.all([nodeACode, nodeCCode(nodeCgen2)], { concurrency: 'unbounded' }).pipe(
650
+ Effect.withSpan('test2'),
651
+ )
652
+ }).pipe(withCtx(test)),
653
+ )
654
+ })
655
+
656
+ /**
657
+ * A
658
+ * / \
659
+ * B C
660
+ * \ /
661
+ * D
662
+ */
663
+ Vitest.describe('diamond topology', () => {
664
+ Vitest.scopedLive('should work', (test) =>
665
+ Effect.gen(function* () {
666
+ const nodeA = yield* makeMeshNode('A')
667
+ const nodeB = yield* makeMeshNode('B')
668
+ const nodeC = yield* makeMeshNode('C')
669
+ const nodeD = yield* makeMeshNode('D')
670
+
671
+ yield* connectNodesViaMessageChannel(nodeA, nodeB)
672
+ yield* connectNodesViaMessageChannel(nodeA, nodeC)
673
+ yield* connectNodesViaMessageChannel(nodeB, nodeD)
674
+ yield* connectNodesViaMessageChannel(nodeC, nodeD)
675
+
676
+ const nodeACode = Effect.gen(function* () {
677
+ const channelAToD = yield* createChannel(nodeA, 'D')
678
+ yield* channelAToD.send({ message: 'A1' })
679
+ expect(yield* getFirstMessage(channelAToD)).toEqual({ message: 'D1' })
680
+ })
681
+
682
+ const nodeDCode = Effect.gen(function* () {
683
+ const channelDToA = yield* createChannel(nodeD, 'A')
684
+ yield* channelDToA.send({ message: 'D1' })
685
+ expect(yield* getFirstMessage(channelDToA)).toEqual({ message: 'A1' })
686
+ })
687
+
688
+ yield* Effect.all([nodeACode, nodeDCode], { concurrency: 'unbounded' })
689
+ }).pipe(withCtx(test)),
690
+ )
478
691
  })
479
692
 
480
693
  Vitest.describe('mixture of messagechannel and proxy connections', () => {
@@ -493,27 +706,28 @@ Vitest.describe('webmesh node', { timeout: testTimeout }, () => {
493
706
  )
494
707
 
495
708
  // TODO this currently fails but should work. probably needs some more guarding internally.
496
- Vitest.scopedLive.skip('should work for messagechannels', (test) =>
709
+ Vitest.scopedLive('should work for messagechannels', (test) =>
497
710
  Effect.gen(function* () {
498
711
  const nodeA = yield* makeMeshNode('A')
499
712
  const nodeB = yield* makeMeshNode('B')
713
+ const nodeC = yield* makeMeshNode('C')
500
714
 
501
715
  yield* connectNodesViaMessageChannel(nodeB, nodeA)
502
- yield* connectNodesViaBroadcastChannel(nodeA, nodeB)
716
+ yield* connectNodesViaBroadcastChannel(nodeB, nodeC)
503
717
 
504
718
  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' })
719
+ const channelAToC = yield* createChannel(nodeA, 'C', { mode: 'proxy' })
720
+ yield* channelAToC.send({ message: 'A1' })
721
+ expect(yield* getFirstMessage(channelAToC)).toEqual({ message: 'C1' })
508
722
  })
509
723
 
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' })
724
+ const nodeCCode = Effect.gen(function* () {
725
+ const channelCToA = yield* createChannel(nodeC, 'A', { mode: 'proxy' })
726
+ yield* channelCToA.send({ message: 'C1' })
727
+ expect(yield* getFirstMessage(channelCToA)).toEqual({ message: 'A1' })
514
728
  })
515
729
 
516
- yield* Effect.all([nodeACode, nodeBCode], { concurrency: 'unbounded' })
730
+ yield* Effect.all([nodeACode, nodeCCode], { concurrency: 'unbounded' })
517
731
  }).pipe(withCtx(test)),
518
732
  )
519
733
  })
@@ -522,10 +736,13 @@ Vitest.describe('webmesh node', { timeout: testTimeout }, () => {
522
736
  const otelLayer = IS_CI ? Layer.empty : OtelLiveHttp({ serviceName: 'webmesh-node-test', skipLogUrl: false })
523
737
 
524
738
  const withCtx =
525
- (testContext: Vitest.TaskContext, { suffix, skipOtel = false }: { suffix?: string; skipOtel?: boolean } = {}) =>
739
+ (
740
+ testContext: Vitest.TaskContext,
741
+ { suffix, skipOtel = false, timeout = testTimeout }: { suffix?: string; skipOtel?: boolean; timeout?: number } = {},
742
+ ) =>
526
743
  <A, E, R>(self: Effect.Effect<A, E, R>) =>
527
744
  self.pipe(
528
- Effect.timeout(testTimeout),
745
+ Effect.timeout(timeout),
529
746
  Effect.provide(Logger.pretty),
530
747
  Effect.scoped, // We need to scope the effect manually here because otherwise the span is not closed
531
748
  Effect.withSpan(`${testContext.task.suite?.name}:${testContext.task.name}${suffix ? `:${suffix}` : ''}`),