@livestore/webmesh 0.3.0-dev.5 → 0.3.0-dev.50
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.
- package/README.md +43 -0
- package/dist/.tsbuildinfo +1 -1
- package/dist/channel/direct-channel-internal.d.ts +26 -0
- package/dist/channel/direct-channel-internal.d.ts.map +1 -0
- package/dist/channel/direct-channel-internal.js +217 -0
- package/dist/channel/direct-channel-internal.js.map +1 -0
- package/dist/channel/direct-channel.d.ts +22 -0
- package/dist/channel/direct-channel.d.ts.map +1 -0
- package/dist/channel/direct-channel.js +153 -0
- package/dist/channel/direct-channel.js.map +1 -0
- package/dist/channel/proxy-channel.d.ts +3 -3
- package/dist/channel/proxy-channel.d.ts.map +1 -1
- package/dist/channel/proxy-channel.js +119 -37
- package/dist/channel/proxy-channel.js.map +1 -1
- package/dist/common.d.ts +47 -19
- package/dist/common.d.ts.map +1 -1
- package/dist/common.js +13 -5
- package/dist/common.js.map +1 -1
- package/dist/mesh-schema.d.ts +79 -13
- package/dist/mesh-schema.d.ts.map +1 -1
- package/dist/mesh-schema.js +59 -10
- package/dist/mesh-schema.js.map +1 -1
- package/dist/mod.d.ts +2 -2
- package/dist/mod.d.ts.map +1 -1
- package/dist/mod.js +2 -2
- package/dist/mod.js.map +1 -1
- package/dist/node.d.ts +56 -23
- package/dist/node.d.ts.map +1 -1
- package/dist/node.js +323 -115
- package/dist/node.js.map +1 -1
- package/dist/node.test.d.ts +1 -1
- package/dist/node.test.d.ts.map +1 -1
- package/dist/node.test.js +489 -157
- package/dist/node.test.js.map +1 -1
- package/dist/utils.d.ts +4 -4
- package/dist/utils.d.ts.map +1 -1
- package/dist/utils.js +7 -1
- package/dist/utils.js.map +1 -1
- package/dist/websocket-edge.d.ts +56 -0
- package/dist/websocket-edge.d.ts.map +1 -0
- package/dist/websocket-edge.js +93 -0
- package/dist/websocket-edge.js.map +1 -0
- package/package.json +10 -6
- package/src/channel/direct-channel-internal.ts +356 -0
- package/src/channel/direct-channel.ts +234 -0
- package/src/channel/proxy-channel.ts +344 -234
- package/src/common.ts +24 -17
- package/src/mesh-schema.ts +73 -20
- package/src/mod.ts +2 -2
- package/src/node.test.ts +723 -190
- package/src/node.ts +482 -156
- package/src/utils.ts +13 -2
- package/src/websocket-edge.ts +191 -0
- package/dist/channel/message-channel.d.ts +0 -20
- package/dist/channel/message-channel.d.ts.map +0 -1
- package/dist/channel/message-channel.js +0 -183
- package/dist/channel/message-channel.js.map +0 -1
- package/dist/websocket-connection.d.ts +0 -51
- package/dist/websocket-connection.d.ts.map +0 -1
- package/dist/websocket-connection.js +0 -74
- package/dist/websocket-connection.js.map +0 -1
- package/dist/websocket-server.d.ts +0 -7
- package/dist/websocket-server.d.ts.map +0 -1
- package/dist/websocket-server.js +0 -24
- package/dist/websocket-server.js.map +0 -1
- package/src/channel/message-channel.ts +0 -354
- package/src/websocket-connection.ts +0 -158
- package/src/websocket-server.ts +0 -40
- package/tsconfig.json +0 -11
package/src/node.test.ts
CHANGED
|
@@ -1,6 +1,22 @@
|
|
|
1
|
-
import
|
|
2
|
-
|
|
3
|
-
import {
|
|
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
|
|
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.
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
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
|
-
|
|
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
|
-
|
|
43
|
-
sendSchema: Packet,
|
|
63
|
+
schema: Packet,
|
|
44
64
|
})
|
|
45
65
|
|
|
46
|
-
yield* nodeA.
|
|
47
|
-
|
|
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 ?? '
|
|
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:
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
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
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
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
|
-
|
|
118
|
-
|
|
163
|
+
const nodeYCode = Effect.gen(function* () {
|
|
164
|
+
const channelYToX = yield* createChannel(nodeY, nodeX.nodeName, { mode })
|
|
119
165
|
|
|
120
|
-
|
|
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
|
-
|
|
123
|
-
|
|
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
|
-
|
|
126
|
-
|
|
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
|
|
130
|
-
|
|
199
|
+
const [nodeNameX, nodeNameY] = nodeNames
|
|
200
|
+
const nodeX = yield* makeMeshNode(nodeNameX)
|
|
201
|
+
const nodeY = yield* makeMeshNode(nodeNameY)
|
|
131
202
|
|
|
132
|
-
|
|
133
|
-
|
|
203
|
+
yield* exchangeMessages({
|
|
204
|
+
nodeX,
|
|
205
|
+
nodeY,
|
|
206
|
+
channelType,
|
|
207
|
+
delays: { x: delayX, y: delayY, connect: connectDelay },
|
|
134
208
|
})
|
|
135
209
|
|
|
136
|
-
yield* Effect.
|
|
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, {
|
|
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
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
(
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
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
|
-
|
|
170
|
-
|
|
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
|
-
|
|
239
|
+
const nodeA = yield* makeMeshNode('A')
|
|
240
|
+
const nodeB = yield* makeMeshNode('B')
|
|
173
241
|
|
|
174
|
-
|
|
175
|
-
yield* connectNodes(nodeA, nodeB)
|
|
242
|
+
const { mode, connectNodes } = fromChannelType(channelType)
|
|
176
243
|
|
|
177
|
-
|
|
178
|
-
|
|
244
|
+
// TODO also optionally delay the edge
|
|
245
|
+
yield* connectNodes(nodeA, nodeB)
|
|
179
246
|
|
|
180
|
-
|
|
181
|
-
|
|
247
|
+
const waitForBToBeOffline =
|
|
248
|
+
waitForOfflineDelay === undefined ? undefined : yield* Deferred.make<void, never>()
|
|
182
249
|
|
|
183
|
-
|
|
184
|
-
|
|
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
|
-
|
|
187
|
-
|
|
188
|
-
|
|
255
|
+
console.log('nodeACode:waiting for B to be offline')
|
|
256
|
+
if (waitForBToBeOffline !== undefined) {
|
|
257
|
+
yield* waitForBToBeOffline
|
|
258
|
+
}
|
|
189
259
|
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
260
|
+
yield* channelAToB.send({ message: 'A2' })
|
|
261
|
+
expect(yield* getFirstMessage(channelAToB)).toEqual({ message: 'B2' })
|
|
262
|
+
})
|
|
193
263
|
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
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
|
-
|
|
200
|
-
|
|
201
|
-
|
|
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
|
-
|
|
204
|
-
|
|
205
|
-
|
|
275
|
+
console.log('nodeBCode:B node going offline')
|
|
276
|
+
if (waitForBToBeOffline !== undefined) {
|
|
277
|
+
yield* Deferred.succeed(waitForBToBeOffline, void 0)
|
|
278
|
+
}
|
|
206
279
|
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
280
|
+
if (sleepDelay !== undefined) {
|
|
281
|
+
yield* Effect.sleep(sleepDelay).pipe(Effect.withSpan(`B:sleep(${sleepDelay})`))
|
|
282
|
+
}
|
|
210
283
|
|
|
211
|
-
|
|
212
|
-
|
|
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
|
-
|
|
215
|
-
|
|
216
|
-
|
|
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
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
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', '
|
|
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
|
|
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
|
|
235
|
-
const
|
|
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(
|
|
375
|
+
yield* connectNodes(nodeX, nodeY)
|
|
240
376
|
|
|
241
|
-
const
|
|
377
|
+
const waitForEdgeReplacement = yield* Deferred.make<void>()
|
|
242
378
|
|
|
243
|
-
const
|
|
244
|
-
const
|
|
379
|
+
const nodeXCode = Effect.gen(function* () {
|
|
380
|
+
const channelXToY = yield* createChannel(nodeX, nodeLabel.y, { mode })
|
|
245
381
|
|
|
246
|
-
yield*
|
|
247
|
-
expect(yield* getFirstMessage(
|
|
382
|
+
yield* channelXToY.send({ message: `${nodeLabel.x}1` })
|
|
383
|
+
expect(yield* getFirstMessage(channelXToY)).toEqual({ message: `${nodeLabel.y}1` })
|
|
248
384
|
|
|
249
|
-
yield*
|
|
385
|
+
yield* waitForEdgeReplacement
|
|
250
386
|
|
|
251
|
-
yield*
|
|
252
|
-
expect(yield* getFirstMessage(
|
|
387
|
+
yield* channelXToY.send({ message: `${nodeLabel.x}2` })
|
|
388
|
+
expect(yield* getFirstMessage(channelXToY)).toEqual({ message: `${nodeLabel.y}2` })
|
|
253
389
|
})
|
|
254
390
|
|
|
255
|
-
const
|
|
256
|
-
const
|
|
391
|
+
const nodeYCode = Effect.gen(function* () {
|
|
392
|
+
const channelYToX = yield* createChannel(nodeY, nodeLabel.x, { mode })
|
|
257
393
|
|
|
258
|
-
yield*
|
|
259
|
-
expect(yield* getFirstMessage(
|
|
394
|
+
yield* channelYToX.send({ message: `${nodeLabel.y}1` })
|
|
395
|
+
expect(yield* getFirstMessage(channelYToX)).toEqual({ message: `${nodeLabel.x}1` })
|
|
260
396
|
|
|
261
|
-
// Switch out
|
|
262
|
-
yield*
|
|
263
|
-
yield*
|
|
264
|
-
yield* connectNodes(
|
|
265
|
-
yield* Deferred.succeed(
|
|
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*
|
|
268
|
-
expect(yield* getFirstMessage(
|
|
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([
|
|
272
|
-
}).pipe(
|
|
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
|
|
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(
|
|
323
|
-
|
|
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
|
|
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: '
|
|
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
|
|
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(
|
|
424
|
-
|
|
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
|
-
|
|
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
|
|
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('
|
|
821
|
+
expect(err._tag).toBe('EdgeAlreadyExistsError')
|
|
490
822
|
}).pipe(withCtx(test)),
|
|
491
823
|
)
|
|
492
824
|
|
|
493
|
-
|
|
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(
|
|
832
|
+
yield* connectNodesViaBroadcastChannel(nodeB, nodeC)
|
|
501
833
|
|
|
502
834
|
const nodeACode = Effect.gen(function* () {
|
|
503
|
-
const
|
|
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
|
|
510
|
-
yield*
|
|
511
|
-
|
|
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
|
|
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
|
-
(
|
|
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(
|
|
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),
|