@livestore/webmesh 0.3.0-dev.4 → 0.3.0-dev.40

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