@livestore/webmesh 0.3.0-dev.2 → 0.3.0-dev.21

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