@livestore/webmesh 0.3.0-dev.10 → 0.3.0-dev.12

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 (51) hide show
  1. package/README.md +20 -1
  2. package/dist/.tsbuildinfo +1 -1
  3. package/dist/channel/message-channel copy.d.ts +9 -0
  4. package/dist/channel/message-channel copy.d.ts.map +1 -0
  5. package/dist/channel/message-channel copy.js +137 -0
  6. package/dist/channel/message-channel copy.js.map +1 -0
  7. package/dist/channel/message-channel-internal copy.d.ts +42 -0
  8. package/dist/channel/message-channel-internal copy.d.ts.map +1 -0
  9. package/dist/channel/message-channel-internal copy.js +239 -0
  10. package/dist/channel/message-channel-internal copy.js.map +1 -0
  11. package/dist/channel/message-channel-internal.d.ts +26 -0
  12. package/dist/channel/message-channel-internal.d.ts.map +1 -0
  13. package/dist/channel/message-channel-internal.js +217 -0
  14. package/dist/channel/message-channel-internal.js.map +1 -0
  15. package/dist/channel/message-channel.d.ts +21 -19
  16. package/dist/channel/message-channel.d.ts.map +1 -1
  17. package/dist/channel/message-channel.js +128 -162
  18. package/dist/channel/message-channel.js.map +1 -1
  19. package/dist/channel/proxy-channel.d.ts +2 -2
  20. package/dist/channel/proxy-channel.d.ts.map +1 -1
  21. package/dist/channel/proxy-channel.js +7 -5
  22. package/dist/channel/proxy-channel.js.map +1 -1
  23. package/dist/common.d.ts +8 -4
  24. package/dist/common.d.ts.map +1 -1
  25. package/dist/common.js +2 -1
  26. package/dist/common.js.map +1 -1
  27. package/dist/mesh-schema.d.ts +23 -1
  28. package/dist/mesh-schema.d.ts.map +1 -1
  29. package/dist/mesh-schema.js +21 -2
  30. package/dist/mesh-schema.js.map +1 -1
  31. package/dist/node.d.ts +12 -1
  32. package/dist/node.d.ts.map +1 -1
  33. package/dist/node.js +40 -9
  34. package/dist/node.js.map +1 -1
  35. package/dist/node.test.d.ts +1 -1
  36. package/dist/node.test.d.ts.map +1 -1
  37. package/dist/node.test.js +300 -147
  38. package/dist/node.test.js.map +1 -1
  39. package/dist/websocket-connection.d.ts +1 -2
  40. package/dist/websocket-connection.d.ts.map +1 -1
  41. package/dist/websocket-connection.js +5 -4
  42. package/dist/websocket-connection.js.map +1 -1
  43. package/package.json +3 -3
  44. package/src/channel/message-channel-internal.ts +356 -0
  45. package/src/channel/message-channel.ts +183 -311
  46. package/src/channel/proxy-channel.ts +238 -230
  47. package/src/common.ts +3 -1
  48. package/src/mesh-schema.ts +20 -2
  49. package/src/node.test.ts +426 -177
  50. package/src/node.ts +70 -12
  51. package/src/websocket-connection.ts +83 -79
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'
@@ -16,35 +31,47 @@ import { makeMeshNode } from './node.js'
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.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
+ })
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.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
+ })
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,302 @@ 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 })
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
+ })
103
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)
195
+ // console.log({ delayX, delayY, connectDelay, channelType, nodeNames })
116
196
 
117
- const nodeA = yield* makeMeshNode('A')
118
- const nodeB = yield* makeMeshNode('B')
197
+ const [nodeNameX, nodeNameY] = nodeNames
198
+ const nodeX = yield* makeMeshNode(nodeNameX)
199
+ const nodeY = yield* makeMeshNode(nodeNameY)
119
200
 
120
- const { mode, connectNodes } = fromChannelType(channelType)
201
+ yield* exchangeMessages({
202
+ nodeX,
203
+ nodeY,
204
+ channelType,
205
+ delays: { x: delayX, y: delayY, connect: connectDelay },
206
+ })
207
+ }).pipe(
208
+ withCtx(test, {
209
+ skipOtel: true,
210
+ suffix: `delayX=${delayX} delayY=${delayY} connectDelay=${connectDelay} channelType=${channelType} nodeNames=${nodeNames}`,
211
+ }),
212
+ ),
213
+ // { fastCheck: { numRuns: 20 } },
214
+ )
121
215
 
122
- const nodeACode = Effect.gen(function* () {
123
- const channelAToB = yield* createChannel(nodeA, 'B', { mode })
216
+ {
217
+ // const waitForOfflineDelay = undefined
218
+ // const sleepDelay = 0
219
+ // const channelType = 'messagechannel'
220
+ // Vitest.scopedLive(
221
+ // 'b reconnects',
222
+ // (test) =>
223
+ Vitest.scopedLive.prop(
224
+ 'b reconnects',
225
+ [Delay, Delay, ChannelType],
226
+ ([waitForOfflineDelay, sleepDelay, channelType], test) =>
227
+ Effect.gen(function* () {
228
+ // console.log({ waitForOfflineDelay, sleepDelay, channelType })
124
229
 
125
- yield* channelAToB.send({ message: 'A1' })
126
- expect(yield* getFirstMessage(channelAToB)).toEqual({ message: 'A2' })
127
- })
230
+ if (waitForOfflineDelay === undefined) {
231
+ // TODO we still need to fix this scenario but it shouldn't really be common in practice
232
+ return
233
+ }
128
234
 
129
- const nodeBCode = Effect.gen(function* () {
130
- const channelBToA = yield* createChannel(nodeB, 'A', { mode })
235
+ const nodeA = yield* makeMeshNode('A')
236
+ const nodeB = yield* makeMeshNode('B')
131
237
 
132
- yield* channelBToA.send({ message: 'A2' })
133
- expect(yield* getFirstMessage(channelBToA)).toEqual({ message: 'A1' })
134
- })
238
+ const { mode, connectNodes } = fromChannelType(channelType)
135
239
 
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
- )
144
- }).pipe(
145
- withCtx(test, { skipOtel: true, suffix: `delayA=${delayA} delayB=${delayB} channelType=${channelType}` }),
146
- ),
147
- )
240
+ // TODO also optionally delay the connection
241
+ yield* connectNodes(nodeA, nodeB)
148
242
 
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')
243
+ const waitForBToBeOffline =
244
+ waitForOfflineDelay === undefined ? undefined : yield* Deferred.make<void, never>()
171
245
 
172
- const { mode, connectNodes } = fromChannelType(channelType)
246
+ const nodeACode = Effect.gen(function* () {
247
+ const channelAToB = yield* createChannel(nodeA, 'B', { mode })
248
+ yield* channelAToB.send({ message: 'A1' })
249
+ expect(yield* getFirstMessage(channelAToB)).toEqual({ message: 'B1' })
173
250
 
174
- // TODO also optionally delay the connection
175
- yield* connectNodes(nodeA, nodeB)
251
+ console.log('nodeACode:waiting for B to be offline')
252
+ if (waitForBToBeOffline !== undefined) {
253
+ yield* waitForBToBeOffline
254
+ }
176
255
 
177
- const waitForBToBeOffline =
178
- waitForOfflineDelay === undefined ? undefined : yield* Deferred.make<void, never>()
256
+ yield* channelAToB.send({ message: 'A2' })
257
+ expect(yield* getFirstMessage(channelAToB)).toEqual({ message: 'B2' })
258
+ })
179
259
 
180
- const nodeACode = Effect.gen(function* () {
181
- const channelAToB = yield* createChannel(nodeA, 'B', { mode })
260
+ // Simulating node b going offline and then coming back online
261
+ // This test also illustrates why we need a ack-message channel since otherwise
262
+ // sent messages might get lost
263
+ const nodeBCode = Effect.gen(function* () {
264
+ yield* Effect.gen(function* () {
265
+ const channelBToA = yield* createChannel(nodeB, 'A', { mode })
182
266
 
183
- yield* channelAToB.send({ message: 'A1' })
184
- expect(yield* getFirstMessage(channelAToB)).toEqual({ message: 'B1' })
267
+ yield* channelBToA.send({ message: 'B1' })
268
+ expect(yield* getFirstMessage(channelBToA)).toEqual({ message: 'A1' })
269
+ }).pipe(Effect.scoped, Effect.withSpan('nodeBCode:part1'))
185
270
 
186
- if (waitForBToBeOffline !== undefined) {
187
- yield* waitForBToBeOffline
188
- }
271
+ console.log('nodeBCode:B node going offline')
272
+ if (waitForBToBeOffline !== undefined) {
273
+ yield* Deferred.succeed(waitForBToBeOffline, void 0)
274
+ }
189
275
 
190
- yield* channelAToB.send({ message: 'A2' })
191
- expect(yield* getFirstMessage(channelAToB)).toEqual({ message: 'B2' })
192
- })
276
+ if (sleepDelay !== undefined) {
277
+ yield* Effect.sleep(sleepDelay).pipe(Effect.withSpan(`B:sleep(${sleepDelay})`))
278
+ }
193
279
 
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 })
280
+ // Recreating the channel
281
+ yield* Effect.gen(function* () {
282
+ const channelBToA = yield* createChannel(nodeB, 'A', { mode })
198
283
 
199
- yield* channelBToA.send({ message: 'B1' })
200
- expect(yield* getFirstMessage(channelBToA)).toEqual({ message: 'A1' })
201
- }).pipe(Effect.scoped)
284
+ yield* channelBToA.send({ message: 'B2' })
285
+ expect(yield* getFirstMessage(channelBToA)).toEqual({ message: 'A2' })
286
+ }).pipe(Effect.scoped, Effect.withSpan('nodeBCode:part2'))
287
+ })
202
288
 
203
- if (waitForBToBeOffline !== undefined) {
204
- yield* Deferred.succeed(waitForBToBeOffline, void 0)
205
- }
289
+ yield* Effect.all([nodeACode, nodeBCode], { concurrency: 'unbounded' }).pipe(Effect.withSpan('test'))
290
+ }).pipe(
291
+ withCtx(test, {
292
+ skipOtel: true,
293
+ suffix: `waitForOfflineDelay=${waitForOfflineDelay} sleepDelay=${sleepDelay} channelType=${channelType}`,
294
+ }),
295
+ ),
296
+ { fastCheck: { numRuns: 20 } },
297
+ )
298
+ }
206
299
 
207
- if (sleepDelay !== undefined) {
208
- yield* Effect.sleep(sleepDelay).pipe(Effect.withSpan(`B:sleep(${sleepDelay})`))
209
- }
300
+ Vitest.scopedLive('reconnect with re-created node', (test) =>
301
+ Effect.gen(function* () {
302
+ const nodeBgen1Scope = yield* Scope.make()
210
303
 
211
- yield* Effect.gen(function* () {
212
- const channelBToA = yield* createChannel(nodeB, 'A', { mode })
304
+ const nodeA = yield* makeMeshNode('A')
305
+ const nodeBgen1 = yield* makeMeshNode('B').pipe(Scope.extend(nodeBgen1Scope))
306
+
307
+ yield* connectNodesViaMessageChannel(nodeA, nodeBgen1).pipe(Scope.extend(nodeBgen1Scope))
308
+
309
+ // yield* Effect.sleep(100)
310
+
311
+ const channelAToBOnce = yield* Effect.cached(createChannel(nodeA, 'B'))
312
+ const nodeACode = Effect.gen(function* () {
313
+ const channelAToB = yield* channelAToBOnce
314
+ yield* channelAToB.send({ message: 'A1' })
315
+ expect(yield* getFirstMessage(channelAToB)).toEqual({ message: 'B1' })
316
+ // expect(channelAToB.debugInfo.connectCounter).toBe(1)
317
+ })
213
318
 
214
- yield* channelBToA.send({ message: 'B2' })
215
- expect(yield* getFirstMessage(channelBToA)).toEqual({ message: 'A2' })
216
- }).pipe(Effect.scoped)
319
+ const nodeBCode = (nodeB: MeshNode) =>
320
+ Effect.gen(function* () {
321
+ const channelBToA = yield* createChannel(nodeB, 'A')
322
+
323
+ yield* channelBToA.send({ message: 'B1' })
324
+ expect(yield* getFirstMessage(channelBToA)).toEqual({ message: 'A1' })
325
+ // expect(channelBToA.debugInfo.connectCounter).toBe(1)
217
326
  })
218
327
 
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
- ),
328
+ yield* Effect.all([nodeACode, nodeBCode(nodeBgen1).pipe(Scope.extend(nodeBgen1Scope))], {
329
+ concurrency: 'unbounded',
330
+ }).pipe(Effect.withSpan('test1'))
331
+
332
+ yield* Scope.close(nodeBgen1Scope, Exit.void)
333
+
334
+ const nodeBgen2 = yield* makeMeshNode('B')
335
+ yield* connectNodesViaMessageChannel(nodeA, nodeBgen2, { replaceIfExists: true })
336
+
337
+ yield* Effect.all([nodeACode, nodeBCode(nodeBgen2)], { concurrency: 'unbounded' }).pipe(
338
+ Effect.withSpan('test2'),
339
+ )
340
+ }).pipe(withCtx(test)),
226
341
  )
227
342
 
228
343
  const ChannelTypeWithoutMessageChannelProxy = Schema.Literal('proxy', 'messagechannel')
229
344
  Vitest.scopedLive.prop(
230
345
  'replace connection while keeping the channel',
231
- [ChannelTypeWithoutMessageChannelProxy],
232
- ([channelType], test) =>
346
+ [ChannelTypeWithoutMessageChannelProxy, NodeNames],
347
+ ([channelType, nodeNames], test) =>
233
348
  Effect.gen(function* () {
234
- const nodeA = yield* makeMeshNode('A')
235
- const nodeB = yield* makeMeshNode('B')
349
+ const [nodeNameX, nodeNameY] = nodeNames
350
+ const nodeX = yield* makeMeshNode(nodeNameX)
351
+ const nodeY = yield* makeMeshNode(nodeNameY)
352
+ const nodeLabel = { x: nodeX.nodeName, y: nodeY.nodeName }
236
353
 
237
354
  const { mode, connectNodes } = fromChannelType(channelType)
238
355
 
239
- yield* connectNodes(nodeA, nodeB)
356
+ yield* connectNodes(nodeX, nodeY)
240
357
 
241
358
  const waitForConnectionReplacement = yield* Deferred.make<void>()
242
359
 
243
- const nodeACode = Effect.gen(function* () {
244
- const channelAToB = yield* createChannel(nodeA, 'B', { mode })
360
+ const nodeXCode = Effect.gen(function* () {
361
+ const channelXToY = yield* createChannel(nodeX, nodeLabel.y, { mode })
245
362
 
246
- yield* channelAToB.send({ message: 'A1' })
247
- expect(yield* getFirstMessage(channelAToB)).toEqual({ message: 'B1' })
363
+ yield* channelXToY.send({ message: `${nodeLabel.x}1` })
364
+ expect(yield* getFirstMessage(channelXToY)).toEqual({ message: `${nodeLabel.y}1` })
248
365
 
249
366
  yield* waitForConnectionReplacement
250
367
 
251
- yield* channelAToB.send({ message: 'A2' })
252
- expect(yield* getFirstMessage(channelAToB)).toEqual({ message: 'B2' })
368
+ yield* channelXToY.send({ message: `${nodeLabel.x}2` })
369
+ expect(yield* getFirstMessage(channelXToY)).toEqual({ message: `${nodeLabel.y}2` })
253
370
  })
254
371
 
255
- const nodeBCode = Effect.gen(function* () {
256
- const channelBToA = yield* createChannel(nodeB, 'A', { mode })
372
+ const nodeYCode = Effect.gen(function* () {
373
+ const channelYToX = yield* createChannel(nodeY, nodeLabel.x, { mode })
257
374
 
258
- yield* channelBToA.send({ message: 'B1' })
259
- expect(yield* getFirstMessage(channelBToA)).toEqual({ message: 'A1' })
375
+ yield* channelYToX.send({ message: `${nodeLabel.y}1` })
376
+ expect(yield* getFirstMessage(channelYToX)).toEqual({ message: `${nodeLabel.x}1` })
260
377
 
261
378
  // Switch out connection while keeping the channel
262
- yield* nodeA.removeConnection('B')
263
- yield* nodeB.removeConnection('A')
264
- yield* connectNodes(nodeA, nodeB)
379
+ yield* nodeX.removeConnection(nodeLabel.y)
380
+ yield* nodeY.removeConnection(nodeLabel.x)
381
+ yield* connectNodes(nodeX, nodeY)
265
382
  yield* Deferred.succeed(waitForConnectionReplacement, void 0)
266
383
 
267
- yield* channelBToA.send({ message: 'B2' })
268
- expect(yield* getFirstMessage(channelBToA)).toEqual({ message: 'A2' })
384
+ yield* channelYToX.send({ message: `${nodeLabel.y}2` })
385
+ expect(yield* getFirstMessage(channelYToX)).toEqual({ message: `${nodeLabel.x}2` })
269
386
  })
270
387
 
271
- yield* Effect.all([nodeACode, nodeBCode], { concurrency: 'unbounded' })
272
- }).pipe(withCtx(test, { skipOtel: true, suffix: `channelType=${channelType}` })),
388
+ yield* Effect.all([nodeXCode, nodeYCode], { concurrency: 'unbounded' })
389
+ }).pipe(
390
+ withCtx(test, {
391
+ skipOtel: true,
392
+ suffix: `channelType=${channelType} nodeNames=${nodeNames}`,
393
+ }),
394
+ ),
395
+ { fastCheck: { numRuns: 10 } },
273
396
  )
274
397
 
275
- Vitest.describe.todo('TODO improve latency', () => {
398
+ Vitest.describe('TODO improve latency', () => {
276
399
  // TODO we need to improve latency when sending messages concurrently
277
400
  Vitest.scopedLive.prop(
278
401
  'concurrent messages',
@@ -319,12 +442,52 @@ Vitest.describe('webmesh node', { timeout: 1000 }, () => {
319
442
  yield* Effect.all([nodeACode, nodeBCode, connectNodes(nodeA, nodeB).pipe(Effect.delay(100))], {
320
443
  concurrency: 'unbounded',
321
444
  })
322
- }).pipe(withCtx(test, { skipOtel: false, suffix: `channelType=${channelType} count=${count}` })),
323
- { timeout: 30_000 },
445
+ }).pipe(
446
+ withCtx(test, {
447
+ skipOtel: true,
448
+ suffix: `channelType=${channelType} count=${count}`,
449
+ timeout: testTimeout * 2,
450
+ }),
451
+ ),
452
+ { fastCheck: { numRuns: 10 } },
324
453
  )
325
454
  })
326
455
  })
327
456
 
457
+ Vitest.describe('message channel specific tests', () => {
458
+ Vitest.scopedLive('differing initial connection counter', (test) =>
459
+ Effect.gen(function* () {
460
+ const nodeA = yield* makeMeshNode('A')
461
+ const nodeB = yield* makeMeshNode('B')
462
+
463
+ yield* connectNodesViaMessageChannel(nodeA, nodeB)
464
+
465
+ const messageCount = 3
466
+
467
+ const bFiber = yield* Effect.gen(function* () {
468
+ const channelBToA = yield* createChannel(nodeB, 'A')
469
+ yield* channelBToA.listen.pipe(
470
+ Stream.flatten(),
471
+ Stream.tap((msg) => channelBToA.send({ message: `resp:${msg.message}` })),
472
+ Stream.take(messageCount),
473
+ Stream.runDrain,
474
+ )
475
+ }).pipe(Effect.scoped, Effect.fork)
476
+
477
+ // yield* createChannel(nodeA, 'B').pipe(Effect.andThen(WebChannel.shutdown))
478
+ // // yield* createChannel(nodeA, 'B').pipe(Effect.andThen(WebChannel.shutdown))
479
+ // // yield* createChannel(nodeA, 'B').pipe(Effect.andThen(WebChannel.shutdown))
480
+ yield* Effect.gen(function* () {
481
+ const channelAToB = yield* createChannel(nodeA, 'B')
482
+ yield* channelAToB.send({ message: 'A' })
483
+ expect(yield* getFirstMessage(channelAToB)).toEqual({ message: 'resp:A' })
484
+ }).pipe(Effect.scoped, Effect.repeatN(messageCount))
485
+
486
+ yield* bFiber
487
+ }).pipe(withCtx(test)),
488
+ )
489
+ })
490
+
328
491
  Vitest.scopedLive('manual debug test', (test) =>
329
492
  Effect.gen(function* () {
330
493
  const nodeA = yield* makeMeshNode('A')
@@ -382,6 +545,7 @@ Vitest.describe('webmesh node', { timeout: 1000 }, () => {
382
545
  yield* channelAToC.send({ message: 'A1' })
383
546
  expect(yield* getFirstMessage(channelAToC)).toEqual({ message: 'C1' })
384
547
  expect(yield* getFirstMessage(channelAToC)).toEqual({ message: 'C2' })
548
+ expect(yield* getFirstMessage(channelAToC)).toEqual({ message: 'C3' })
385
549
  })
386
550
 
387
551
  const nodeCCode = Effect.gen(function* () {
@@ -420,9 +584,14 @@ Vitest.describe('webmesh node', { timeout: 1000 }, () => {
420
584
  expect(yield* getFirstMessage(channelCToA)).toEqual({ message: 'A1' })
421
585
  })
422
586
 
423
- yield* Effect.all([nodeACode, nodeCCode, connectNodes(nodeB, nodeC).pipe(Effect.delay(100))], {
424
- concurrency: 'unbounded',
425
- })
587
+ yield* Effect.all(
588
+ [
589
+ nodeACode,
590
+ nodeCCode,
591
+ connectNodes(nodeB, nodeC).pipe(Effect.delay(100), Effect.withSpan('connect-nodeB-nodeC-delay(100)')),
592
+ ],
593
+ { concurrency: 'unbounded' },
594
+ )
426
595
  }).pipe(withCtx(test)),
427
596
  )
428
597
 
@@ -473,6 +642,85 @@ Vitest.describe('webmesh node', { timeout: 1000 }, () => {
473
642
  yield* Effect.all([nodeACode, nodeCCode], { concurrency: 'unbounded' })
474
643
  }).pipe(withCtx(test)),
475
644
  )
645
+
646
+ Vitest.scopedLive('reconnect with re-created node', (test) =>
647
+ Effect.gen(function* () {
648
+ const nodeCgen1Scope = yield* Scope.make()
649
+
650
+ const nodeA = yield* makeMeshNode('A')
651
+ const nodeB = yield* makeMeshNode('B')
652
+ const nodeCgen1 = yield* makeMeshNode('C').pipe(Scope.extend(nodeCgen1Scope))
653
+
654
+ yield* connectNodesViaMessageChannel(nodeA, nodeB)
655
+ yield* connectNodesViaMessageChannel(nodeB, nodeCgen1).pipe(Scope.extend(nodeCgen1Scope))
656
+
657
+ const nodeACode = Effect.gen(function* () {
658
+ const channelAToB = yield* createChannel(nodeA, 'C')
659
+
660
+ yield* channelAToB.send({ message: 'A1' })
661
+ expect(yield* getFirstMessage(channelAToB)).toEqual({ message: 'C1' })
662
+ })
663
+
664
+ const nodeCCode = (nodeB: MeshNode) =>
665
+ Effect.gen(function* () {
666
+ const channelBToA = yield* createChannel(nodeB, 'A')
667
+
668
+ yield* channelBToA.send({ message: 'C1' })
669
+ expect(yield* getFirstMessage(channelBToA)).toEqual({ message: 'A1' })
670
+ })
671
+
672
+ yield* Effect.all([nodeACode, nodeCCode(nodeCgen1)], { concurrency: 'unbounded' }).pipe(
673
+ Effect.withSpan('test1'),
674
+ Scope.extend(nodeCgen1Scope),
675
+ )
676
+
677
+ yield* Scope.close(nodeCgen1Scope, Exit.void)
678
+
679
+ const nodeCgen2 = yield* makeMeshNode('C')
680
+ yield* connectNodesViaMessageChannel(nodeB, nodeCgen2, { replaceIfExists: true })
681
+
682
+ yield* Effect.all([nodeACode, nodeCCode(nodeCgen2)], { concurrency: 'unbounded' }).pipe(
683
+ Effect.withSpan('test2'),
684
+ )
685
+ }).pipe(withCtx(test)),
686
+ )
687
+ })
688
+
689
+ /**
690
+ * A
691
+ * / \
692
+ * B C
693
+ * \ /
694
+ * D
695
+ */
696
+ Vitest.describe('diamond topology', () => {
697
+ Vitest.scopedLive('should work', (test) =>
698
+ Effect.gen(function* () {
699
+ const nodeA = yield* makeMeshNode('A')
700
+ const nodeB = yield* makeMeshNode('B')
701
+ const nodeC = yield* makeMeshNode('C')
702
+ const nodeD = yield* makeMeshNode('D')
703
+
704
+ yield* connectNodesViaMessageChannel(nodeA, nodeB)
705
+ yield* connectNodesViaMessageChannel(nodeA, nodeC)
706
+ yield* connectNodesViaMessageChannel(nodeB, nodeD)
707
+ yield* connectNodesViaMessageChannel(nodeC, nodeD)
708
+
709
+ const nodeACode = Effect.gen(function* () {
710
+ const channelAToD = yield* createChannel(nodeA, 'D')
711
+ yield* channelAToD.send({ message: 'A1' })
712
+ expect(yield* getFirstMessage(channelAToD)).toEqual({ message: 'D1' })
713
+ })
714
+
715
+ const nodeDCode = Effect.gen(function* () {
716
+ const channelDToA = yield* createChannel(nodeD, 'A')
717
+ yield* channelDToA.send({ message: 'D1' })
718
+ expect(yield* getFirstMessage(channelDToA)).toEqual({ message: 'A1' })
719
+ })
720
+
721
+ yield* Effect.all([nodeACode, nodeDCode], { concurrency: 'unbounded' })
722
+ }).pipe(withCtx(test)),
723
+ )
476
724
  })
477
725
 
478
726
  Vitest.describe('mixture of messagechannel and proxy connections', () => {
@@ -491,42 +739,43 @@ Vitest.describe('webmesh node', { timeout: 1000 }, () => {
491
739
  )
492
740
 
493
741
  // TODO this currently fails but should work. probably needs some more guarding internally.
494
- Vitest.scopedLive.skip('should work for messagechannels', (test) =>
742
+ Vitest.scopedLive('should work for messagechannels', (test) =>
495
743
  Effect.gen(function* () {
496
744
  const nodeA = yield* makeMeshNode('A')
497
745
  const nodeB = yield* makeMeshNode('B')
746
+ const nodeC = yield* makeMeshNode('C')
498
747
 
499
748
  yield* connectNodesViaMessageChannel(nodeB, nodeA)
500
- yield* connectNodesViaBroadcastChannel(nodeA, nodeB)
749
+ yield* connectNodesViaBroadcastChannel(nodeB, nodeC)
501
750
 
502
751
  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' })
752
+ const channelAToC = yield* createChannel(nodeA, 'C', { mode: 'proxy' })
753
+ yield* channelAToC.send({ message: 'A1' })
754
+ expect(yield* getFirstMessage(channelAToC)).toEqual({ message: 'C1' })
506
755
  })
507
756
 
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' })
757
+ const nodeCCode = Effect.gen(function* () {
758
+ const channelCToA = yield* createChannel(nodeC, 'A', { mode: 'proxy' })
759
+ yield* channelCToA.send({ message: 'C1' })
760
+ expect(yield* getFirstMessage(channelCToA)).toEqual({ message: 'A1' })
512
761
  })
513
762
 
514
- yield* Effect.all([nodeACode, nodeBCode], { concurrency: 'unbounded' })
763
+ yield* Effect.all([nodeACode, nodeCCode], { concurrency: 'unbounded' })
515
764
  }).pipe(withCtx(test)),
516
765
  )
517
766
  })
518
767
  })
519
768
 
520
- const envTruish = (env: string | undefined) => env !== undefined && env !== 'false' && env !== '0'
521
- const isCi = envTruish(process.env.CI)
522
-
523
- const otelLayer = isCi ? Layer.empty : OtelLiveHttp({ serviceName: 'webmesh-node-test', skipLogUrl: false })
769
+ const otelLayer = IS_CI ? Layer.empty : OtelLiveHttp({ serviceName: 'webmesh-node-test', skipLogUrl: false })
524
770
 
525
771
  const withCtx =
526
- (testContext: Vitest.TaskContext, { suffix, skipOtel = false }: { suffix?: string; skipOtel?: boolean } = {}) =>
772
+ (
773
+ testContext: Vitest.TaskContext,
774
+ { suffix, skipOtel = false, timeout = testTimeout }: { suffix?: string; skipOtel?: boolean; timeout?: number } = {},
775
+ ) =>
527
776
  <A, E, R>(self: Effect.Effect<A, E, R>) =>
528
777
  self.pipe(
529
- Effect.timeout(isCi ? 10_000 : 500),
778
+ Effect.timeout(timeout),
530
779
  Effect.provide(Logger.pretty),
531
780
  Effect.scoped, // We need to scope the effect manually here because otherwise the span is not closed
532
781
  Effect.withSpan(`${testContext.task.suite?.name}:${testContext.task.name}${suffix ? `:${suffix}` : ''}`),