@livestore/webmesh 0.0.0-snapshot-1d99fea7d2ce2c7a5d9ed0a3752f8a7bda6bc3db → 0.0.0-snapshot-7d3074f682f31cfc38b26ed2c4c2972ce1e9121e

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 +20 -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 +217 -0
  6. package/dist/channel/message-channel-internal.js.map +1 -0
  7. package/dist/channel/message-channel.d.ts +21 -19
  8. package/dist/channel/message-channel.d.ts.map +1 -1
  9. package/dist/channel/message-channel.js +132 -162
  10. package/dist/channel/message-channel.js.map +1 -1
  11. package/dist/channel/proxy-channel.d.ts +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 +40 -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 +312 -146
  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 +356 -0
  37. package/src/channel/message-channel.ts +190 -310
  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 +444 -174
  42. package/src/node.ts +70 -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,208 +100,302 @@ 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
- Vitest.describe('A <> B', () => {
83
- Vitest.describe('prop tests', () => {
84
- const Delay = Schema.UndefinedOr(Schema.Literal(0, 1, 10, 50))
85
- // NOTE for message channels, we test both with and without transferables (i.e. proxying)
86
- const ChannelType = Schema.Literal('messagechannel', 'messagechannel.proxy', 'proxy')
87
-
88
- const fromChannelType = (
89
- channelType: typeof ChannelType.Type,
90
- ): {
91
- mode: 'messagechannel' | 'proxy'
92
- connectNodes: typeof connectNodesViaMessageChannel | typeof connectNodesViaBroadcastChannel
93
- } => {
94
- switch (channelType) {
95
- case 'proxy': {
96
- return { mode: 'proxy', connectNodes: connectNodesViaBroadcastChannel }
97
- }
98
- case 'messagechannel': {
99
- return { mode: 'messagechannel', connectNodes: connectNodesViaMessageChannel }
100
- }
101
- case 'messagechannel.proxy': {
102
- return { mode: 'proxy', connectNodes: connectNodesViaMessageChannel }
103
- }
104
- }
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 }
129
+ }
130
+ case 'messagechannel.proxy': {
131
+ return { mode: 'proxy', connectNodes: connectNodesViaMessageChannel }
105
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 })
106
164
 
165
+ yield* channelYToX.send({ message: `${nodeLabel.y}1` })
166
+ // console.log('channelYToX', channelYToX.debugInfo)
167
+ expect(yield* getFirstMessage(channelYToX)).toEqual({ message: `${nodeLabel.x}1` })
168
+ // expect(channelYToX.debugInfo.connectCounter).toBe(1)
169
+ })
170
+
171
+ yield* Effect.all(
172
+ [
173
+ connectNodes(nodeX, nodeY).pipe(maybeDelay(delays?.connect, 'connectNodes')),
174
+ nodeXCode.pipe(maybeDelay(delays?.x, `node${nodeLabel.x}Code`)),
175
+ nodeYCode.pipe(maybeDelay(delays?.y, `node${nodeLabel.y}Code`)),
176
+ ],
177
+ { concurrency: 'unbounded' },
178
+ ).pipe(Effect.withSpan(`exchangeMessages(${nodeLabel.x}↔${nodeLabel.y})`))
179
+ })
180
+ Vitest.describe('A <> B', () => {
181
+ Vitest.describe('prop tests', { timeout: propTestTimeout }, () => {
182
+ // const delayX = 40
183
+ // const delayY = undefined
184
+ // const connectDelay = undefined
185
+ // const channelType = 'messagechannel'
186
+ // const nodeNames = ['B', 'A'] as const
187
+ // Vitest.scopedLive(
188
+ // 'a / b connect at different times with different channel types',
189
+ // (test) =>
107
190
  Vitest.scopedLive.prop(
108
- // Vitest.scopedLive.only(
109
191
  'a / b connect at different times with different channel types',
110
- [Delay, Delay, Delay, ChannelType],
111
- ([delayA, delayB, connectDelay, channelType], test) =>
112
- // (test) =>
192
+ [Delay, Delay, Delay, ChannelType, NodeNames],
193
+ ([delayX, delayY, connectDelay, channelType, nodeNames], test) =>
113
194
  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)
195
+ // console.log({ delayX, delayY, connectDelay, channelType, nodeNames })
119
196
 
120
- const nodeA = yield* makeMeshNode('A')
121
- const nodeB = yield* makeMeshNode('B')
197
+ const [nodeNameX, nodeNameY] = nodeNames
198
+ const nodeX = yield* makeMeshNode(nodeNameX)
199
+ const nodeY = yield* makeMeshNode(nodeNameY)
122
200
 
123
- 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
+ )
124
215
 
125
- const nodeACode = Effect.gen(function* () {
126
- 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 })
127
229
 
128
- yield* channelAToB.send({ message: 'A1' })
129
- expect(yield* getFirstMessage(channelAToB)).toEqual({ message: 'A2' })
130
- })
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
+ }
131
234
 
132
- const nodeBCode = Effect.gen(function* () {
133
- const channelBToA = yield* createChannel(nodeB, 'A', { mode })
235
+ const nodeA = yield* makeMeshNode('A')
236
+ const nodeB = yield* makeMeshNode('B')
134
237
 
135
- yield* channelBToA.send({ message: 'A2' })
136
- expect(yield* getFirstMessage(channelBToA)).toEqual({ message: 'A1' })
137
- })
238
+ const { mode, connectNodes } = fromChannelType(channelType)
138
239
 
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
- )
240
+ // TODO also optionally delay the connection
241
+ yield* connectNodes(nodeA, nodeB)
151
242
 
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')
243
+ const waitForBToBeOffline =
244
+ waitForOfflineDelay === undefined ? undefined : yield* Deferred.make<void, never>()
174
245
 
175
- 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' })
176
250
 
177
- // TODO also optionally delay the connection
178
- yield* connectNodes(nodeA, nodeB)
251
+ console.log('nodeACode:waiting for B to be offline')
252
+ if (waitForBToBeOffline !== undefined) {
253
+ yield* waitForBToBeOffline
254
+ }
179
255
 
180
- const waitForBToBeOffline =
181
- waitForOfflineDelay === undefined ? undefined : yield* Deferred.make<void, never>()
256
+ yield* channelAToB.send({ message: 'A2' })
257
+ expect(yield* getFirstMessage(channelAToB)).toEqual({ message: 'B2' })
258
+ })
182
259
 
183
- const nodeACode = Effect.gen(function* () {
184
- 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 })
185
266
 
186
- yield* channelAToB.send({ message: 'A1' })
187
- 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'))
188
270
 
189
- if (waitForBToBeOffline !== undefined) {
190
- yield* waitForBToBeOffline
191
- }
271
+ console.log('nodeBCode:B node going offline')
272
+ if (waitForBToBeOffline !== undefined) {
273
+ yield* Deferred.succeed(waitForBToBeOffline, void 0)
274
+ }
192
275
 
193
- yield* channelAToB.send({ message: 'A2' })
194
- expect(yield* getFirstMessage(channelAToB)).toEqual({ message: 'B2' })
195
- })
276
+ if (sleepDelay !== undefined) {
277
+ yield* Effect.sleep(sleepDelay).pipe(Effect.withSpan(`B:sleep(${sleepDelay})`))
278
+ }
196
279
 
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 })
280
+ // Recreating the channel
281
+ yield* Effect.gen(function* () {
282
+ const channelBToA = yield* createChannel(nodeB, 'A', { mode })
201
283
 
202
- yield* channelBToA.send({ message: 'B1' })
203
- expect(yield* getFirstMessage(channelBToA)).toEqual({ message: 'A1' })
204
- }).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
+ })
205
288
 
206
- if (waitForBToBeOffline !== undefined) {
207
- yield* Deferred.succeed(waitForBToBeOffline, void 0)
208
- }
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
+ }
209
299
 
210
- if (sleepDelay !== undefined) {
211
- yield* Effect.sleep(sleepDelay).pipe(Effect.withSpan(`B:sleep(${sleepDelay})`))
212
- }
300
+ Vitest.scopedLive('reconnect with re-created node', (test) =>
301
+ Effect.gen(function* () {
302
+ const nodeBgen1Scope = yield* Scope.make()
213
303
 
214
- yield* Effect.gen(function* () {
215
- 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
+ })
318
+
319
+ const nodeBCode = (nodeB: MeshNode) =>
320
+ Effect.gen(function* () {
321
+ const channelBToA = yield* createChannel(nodeB, 'A')
216
322
 
217
- yield* channelBToA.send({ message: 'B2' })
218
- expect(yield* getFirstMessage(channelBToA)).toEqual({ message: 'A2' })
219
- }).pipe(Effect.scoped)
323
+ yield* channelBToA.send({ message: 'B1' })
324
+ expect(yield* getFirstMessage(channelBToA)).toEqual({ message: 'A1' })
325
+ // expect(channelBToA.debugInfo.connectCounter).toBe(1)
220
326
  })
221
327
 
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
- ),
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)),
229
341
  )
230
342
 
231
343
  const ChannelTypeWithoutMessageChannelProxy = Schema.Literal('proxy', 'messagechannel')
232
344
  Vitest.scopedLive.prop(
233
345
  'replace connection while keeping the channel',
234
- [ChannelTypeWithoutMessageChannelProxy],
235
- ([channelType], test) =>
346
+ [ChannelTypeWithoutMessageChannelProxy, NodeNames],
347
+ ([channelType, nodeNames], test) =>
236
348
  Effect.gen(function* () {
237
- const nodeA = yield* makeMeshNode('A')
238
- 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 }
239
353
 
240
354
  const { mode, connectNodes } = fromChannelType(channelType)
241
355
 
242
- yield* connectNodes(nodeA, nodeB)
356
+ yield* connectNodes(nodeX, nodeY)
243
357
 
244
358
  const waitForConnectionReplacement = yield* Deferred.make<void>()
245
359
 
246
- const nodeACode = Effect.gen(function* () {
247
- const channelAToB = yield* createChannel(nodeA, 'B', { mode })
360
+ const nodeXCode = Effect.gen(function* () {
361
+ const channelXToY = yield* createChannel(nodeX, nodeLabel.y, { mode })
248
362
 
249
- yield* channelAToB.send({ message: 'A1' })
250
- 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` })
251
365
 
252
366
  yield* waitForConnectionReplacement
253
367
 
254
- yield* channelAToB.send({ message: 'A2' })
255
- 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` })
256
370
  })
257
371
 
258
- const nodeBCode = Effect.gen(function* () {
259
- const channelBToA = yield* createChannel(nodeB, 'A', { mode })
372
+ const nodeYCode = Effect.gen(function* () {
373
+ const channelYToX = yield* createChannel(nodeY, nodeLabel.x, { mode })
260
374
 
261
- yield* channelBToA.send({ message: 'B1' })
262
- 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` })
263
377
 
264
378
  // Switch out connection while keeping the channel
265
- yield* nodeA.removeConnection('B')
266
- yield* nodeB.removeConnection('A')
267
- yield* connectNodes(nodeA, nodeB)
379
+ yield* nodeX.removeConnection(nodeLabel.y)
380
+ yield* nodeY.removeConnection(nodeLabel.x)
381
+ yield* connectNodes(nodeX, nodeY)
268
382
  yield* Deferred.succeed(waitForConnectionReplacement, void 0)
269
383
 
270
- yield* channelBToA.send({ message: 'B2' })
271
- 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` })
272
386
  })
273
387
 
274
- yield* Effect.all([nodeACode, nodeBCode], { concurrency: 'unbounded' })
275
- }).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 } },
276
396
  )
277
397
 
278
- Vitest.describe.todo('TODO improve latency', () => {
398
+ Vitest.describe('TODO improve latency', () => {
279
399
  // TODO we need to improve latency when sending messages concurrently
280
400
  Vitest.scopedLive.prop(
281
401
  'concurrent messages',
@@ -322,11 +442,52 @@ Vitest.describe('webmesh node', { timeout: testTimeout }, () => {
322
442
  yield* Effect.all([nodeACode, nodeBCode, connectNodes(nodeA, nodeB).pipe(Effect.delay(100))], {
323
443
  concurrency: 'unbounded',
324
444
  })
325
- }).pipe(withCtx(test, { skipOtel: false, suffix: `channelType=${channelType} count=${count}` })),
445
+ }).pipe(
446
+ withCtx(test, {
447
+ skipOtel: true,
448
+ suffix: `channelType=${channelType} count=${count}`,
449
+ timeout: testTimeout * 2,
450
+ }),
451
+ ),
452
+ { fastCheck: { numRuns: 10 } },
326
453
  )
327
454
  })
328
455
  })
329
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
+
330
491
  Vitest.scopedLive('manual debug test', (test) =>
331
492
  Effect.gen(function* () {
332
493
  const nodeA = yield* makeMeshNode('A')
@@ -384,6 +545,7 @@ Vitest.describe('webmesh node', { timeout: testTimeout }, () => {
384
545
  yield* channelAToC.send({ message: 'A1' })
385
546
  expect(yield* getFirstMessage(channelAToC)).toEqual({ message: 'C1' })
386
547
  expect(yield* getFirstMessage(channelAToC)).toEqual({ message: 'C2' })
548
+ expect(yield* getFirstMessage(channelAToC)).toEqual({ message: 'C3' })
387
549
  })
388
550
 
389
551
  const nodeCCode = Effect.gen(function* () {
@@ -422,9 +584,14 @@ Vitest.describe('webmesh node', { timeout: testTimeout }, () => {
422
584
  expect(yield* getFirstMessage(channelCToA)).toEqual({ message: 'A1' })
423
585
  })
424
586
 
425
- yield* Effect.all([nodeACode, nodeCCode, connectNodes(nodeB, nodeC).pipe(Effect.delay(100))], {
426
- concurrency: 'unbounded',
427
- })
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
+ )
428
595
  }).pipe(withCtx(test)),
429
596
  )
430
597
 
@@ -453,7 +620,7 @@ Vitest.describe('webmesh node', { timeout: testTimeout }, () => {
453
620
  }).pipe(withCtx(test)),
454
621
  )
455
622
 
456
- Vitest.scopedLive('should fail', (test) =>
623
+ Vitest.scopedLive('should fail with timeout due to missing connection', (test) =>
457
624
  Effect.gen(function* () {
458
625
  const nodeA = yield* makeMeshNode('A')
459
626
  const nodeB = yield* makeMeshNode('B')
@@ -475,6 +642,106 @@ Vitest.describe('webmesh node', { timeout: testTimeout }, () => {
475
642
  yield* Effect.all([nodeACode, nodeCCode], { concurrency: 'unbounded' })
476
643
  }).pipe(withCtx(test)),
477
644
  )
645
+
646
+ Vitest.scopedLive('should fail with timeout due no transferable', (test) =>
647
+ Effect.gen(function* () {
648
+ const nodeA = yield* makeMeshNode('A')
649
+ const nodeB = yield* makeMeshNode('B')
650
+
651
+ yield* connectNodesViaBroadcastChannel(nodeA, nodeB)
652
+
653
+ const nodeACode = Effect.gen(function* () {
654
+ const err = yield* createChannel(nodeA, 'B').pipe(Effect.timeout(200), Effect.flip)
655
+ expect(err._tag).toBe('TimeoutException')
656
+ })
657
+
658
+ const nodeBCode = Effect.gen(function* () {
659
+ const err = yield* createChannel(nodeB, 'A').pipe(Effect.timeout(200), Effect.flip)
660
+ expect(err._tag).toBe('TimeoutException')
661
+ })
662
+
663
+ yield* Effect.all([nodeACode, nodeBCode], { concurrency: 'unbounded' })
664
+ }).pipe(withCtx(test)),
665
+ )
666
+
667
+ Vitest.scopedLive('reconnect with re-created node', (test) =>
668
+ Effect.gen(function* () {
669
+ const nodeCgen1Scope = yield* Scope.make()
670
+
671
+ const nodeA = yield* makeMeshNode('A')
672
+ const nodeB = yield* makeMeshNode('B')
673
+ const nodeCgen1 = yield* makeMeshNode('C').pipe(Scope.extend(nodeCgen1Scope))
674
+
675
+ yield* connectNodesViaMessageChannel(nodeA, nodeB)
676
+ yield* connectNodesViaMessageChannel(nodeB, nodeCgen1).pipe(Scope.extend(nodeCgen1Scope))
677
+
678
+ const nodeACode = Effect.gen(function* () {
679
+ const channelAToB = yield* createChannel(nodeA, 'C')
680
+
681
+ yield* channelAToB.send({ message: 'A1' })
682
+ expect(yield* getFirstMessage(channelAToB)).toEqual({ message: 'C1' })
683
+ })
684
+
685
+ const nodeCCode = (nodeB: MeshNode) =>
686
+ Effect.gen(function* () {
687
+ const channelBToA = yield* createChannel(nodeB, 'A')
688
+
689
+ yield* channelBToA.send({ message: 'C1' })
690
+ expect(yield* getFirstMessage(channelBToA)).toEqual({ message: 'A1' })
691
+ })
692
+
693
+ yield* Effect.all([nodeACode, nodeCCode(nodeCgen1)], { concurrency: 'unbounded' }).pipe(
694
+ Effect.withSpan('test1'),
695
+ Scope.extend(nodeCgen1Scope),
696
+ )
697
+
698
+ yield* Scope.close(nodeCgen1Scope, Exit.void)
699
+
700
+ const nodeCgen2 = yield* makeMeshNode('C')
701
+ yield* connectNodesViaMessageChannel(nodeB, nodeCgen2, { replaceIfExists: true })
702
+
703
+ yield* Effect.all([nodeACode, nodeCCode(nodeCgen2)], { concurrency: 'unbounded' }).pipe(
704
+ Effect.withSpan('test2'),
705
+ )
706
+ }).pipe(withCtx(test)),
707
+ )
708
+ })
709
+
710
+ /**
711
+ * A
712
+ * / \
713
+ * B C
714
+ * \ /
715
+ * D
716
+ */
717
+ Vitest.describe('diamond topology', () => {
718
+ Vitest.scopedLive('should work', (test) =>
719
+ Effect.gen(function* () {
720
+ const nodeA = yield* makeMeshNode('A')
721
+ const nodeB = yield* makeMeshNode('B')
722
+ const nodeC = yield* makeMeshNode('C')
723
+ const nodeD = yield* makeMeshNode('D')
724
+
725
+ yield* connectNodesViaMessageChannel(nodeA, nodeB)
726
+ yield* connectNodesViaMessageChannel(nodeA, nodeC)
727
+ yield* connectNodesViaMessageChannel(nodeB, nodeD)
728
+ yield* connectNodesViaMessageChannel(nodeC, nodeD)
729
+
730
+ const nodeACode = Effect.gen(function* () {
731
+ const channelAToD = yield* createChannel(nodeA, 'D')
732
+ yield* channelAToD.send({ message: 'A1' })
733
+ expect(yield* getFirstMessage(channelAToD)).toEqual({ message: 'D1' })
734
+ })
735
+
736
+ const nodeDCode = Effect.gen(function* () {
737
+ const channelDToA = yield* createChannel(nodeD, 'A')
738
+ yield* channelDToA.send({ message: 'D1' })
739
+ expect(yield* getFirstMessage(channelDToA)).toEqual({ message: 'A1' })
740
+ })
741
+
742
+ yield* Effect.all([nodeACode, nodeDCode], { concurrency: 'unbounded' })
743
+ }).pipe(withCtx(test)),
744
+ )
478
745
  })
479
746
 
480
747
  Vitest.describe('mixture of messagechannel and proxy connections', () => {
@@ -492,28 +759,28 @@ Vitest.describe('webmesh node', { timeout: testTimeout }, () => {
492
759
  }).pipe(withCtx(test)),
493
760
  )
494
761
 
495
- // TODO this currently fails but should work. probably needs some more guarding internally.
496
- Vitest.scopedLive.skip('should work for messagechannels', (test) =>
762
+ Vitest.scopedLive('should work for messagechannels', (test) =>
497
763
  Effect.gen(function* () {
498
764
  const nodeA = yield* makeMeshNode('A')
499
765
  const nodeB = yield* makeMeshNode('B')
766
+ const nodeC = yield* makeMeshNode('C')
500
767
 
501
768
  yield* connectNodesViaMessageChannel(nodeB, nodeA)
502
- yield* connectNodesViaBroadcastChannel(nodeA, nodeB)
769
+ yield* connectNodesViaBroadcastChannel(nodeB, nodeC)
503
770
 
504
771
  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' })
772
+ const channelAToC = yield* createChannel(nodeA, 'C', { mode: 'proxy' })
773
+ yield* channelAToC.send({ message: 'A1' })
774
+ expect(yield* getFirstMessage(channelAToC)).toEqual({ message: 'C1' })
508
775
  })
509
776
 
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' })
777
+ const nodeCCode = Effect.gen(function* () {
778
+ const channelCToA = yield* createChannel(nodeC, 'A', { mode: 'proxy' })
779
+ yield* channelCToA.send({ message: 'C1' })
780
+ expect(yield* getFirstMessage(channelCToA)).toEqual({ message: 'A1' })
514
781
  })
515
782
 
516
- yield* Effect.all([nodeACode, nodeBCode], { concurrency: 'unbounded' })
783
+ yield* Effect.all([nodeACode, nodeCCode], { concurrency: 'unbounded' })
517
784
  }).pipe(withCtx(test)),
518
785
  )
519
786
  })
@@ -522,10 +789,13 @@ Vitest.describe('webmesh node', { timeout: testTimeout }, () => {
522
789
  const otelLayer = IS_CI ? Layer.empty : OtelLiveHttp({ serviceName: 'webmesh-node-test', skipLogUrl: false })
523
790
 
524
791
  const withCtx =
525
- (testContext: Vitest.TaskContext, { suffix, skipOtel = false }: { suffix?: string; skipOtel?: boolean } = {}) =>
792
+ (
793
+ testContext: Vitest.TaskContext,
794
+ { suffix, skipOtel = false, timeout = testTimeout }: { suffix?: string; skipOtel?: boolean; timeout?: number } = {},
795
+ ) =>
526
796
  <A, E, R>(self: Effect.Effect<A, E, R>) =>
527
797
  self.pipe(
528
- Effect.timeout(testTimeout),
798
+ Effect.timeout(timeout),
529
799
  Effect.provide(Logger.pretty),
530
800
  Effect.scoped, // We need to scope the effect manually here because otherwise the span is not closed
531
801
  Effect.withSpan(`${testContext.task.suite?.name}:${testContext.task.name}${suffix ? `:${suffix}` : ''}`),