@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.
- package/README.md +26 -0
- package/dist/.tsbuildinfo +1 -1
- package/dist/channel/message-channel-internal.d.ts +26 -0
- package/dist/channel/message-channel-internal.d.ts.map +1 -0
- package/dist/channel/message-channel-internal.js +217 -0
- package/dist/channel/message-channel-internal.js.map +1 -0
- package/dist/channel/message-channel.d.ts +21 -19
- package/dist/channel/message-channel.d.ts.map +1 -1
- package/dist/channel/message-channel.js +132 -162
- package/dist/channel/message-channel.js.map +1 -1
- package/dist/channel/proxy-channel.d.ts +2 -2
- package/dist/channel/proxy-channel.d.ts.map +1 -1
- package/dist/channel/proxy-channel.js +30 -11
- package/dist/channel/proxy-channel.js.map +1 -1
- package/dist/common.d.ts +32 -5
- package/dist/common.d.ts.map +1 -1
- package/dist/common.js +2 -1
- package/dist/common.js.map +1 -1
- package/dist/mesh-schema.d.ts +68 -2
- package/dist/mesh-schema.d.ts.map +1 -1
- package/dist/mesh-schema.js +53 -4
- package/dist/mesh-schema.js.map +1 -1
- package/dist/node.d.ts +31 -9
- package/dist/node.d.ts.map +1 -1
- package/dist/node.js +225 -49
- 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 +384 -149
- package/dist/node.test.js.map +1 -1
- package/dist/websocket-connection.d.ts +5 -6
- package/dist/websocket-connection.d.ts.map +1 -1
- package/dist/websocket-connection.js +21 -26
- package/dist/websocket-connection.js.map +1 -1
- package/dist/websocket-server.d.ts.map +1 -1
- package/dist/websocket-server.js +17 -3
- package/dist/websocket-server.js.map +1 -1
- package/package.json +7 -6
- package/src/channel/message-channel-internal.ts +356 -0
- package/src/channel/message-channel.ts +190 -310
- package/src/channel/proxy-channel.ts +257 -229
- package/src/common.ts +4 -2
- package/src/mesh-schema.ts +60 -4
- package/src/node.test.ts +544 -179
- package/src/node.ts +363 -69
- package/src/websocket-connection.ts +96 -95
- package/src/websocket-server.ts +20 -3
- package/tmp/pack.tgz +0 -0
package/src/node.test.ts
CHANGED
|
@@ -1,4 +1,19 @@
|
|
|
1
|
-
import
|
|
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({
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
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
|
-
|
|
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
|
-
|
|
43
|
-
sendSchema: Packet,
|
|
62
|
+
schema: Packet,
|
|
44
63
|
})
|
|
45
64
|
|
|
46
|
-
yield* nodeA.addConnection({
|
|
47
|
-
|
|
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:
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
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
|
-
([
|
|
109
|
-
// (test) =>
|
|
192
|
+
[Delay, Delay, Delay, ChannelType, NodeNames],
|
|
193
|
+
([delayX, delayY, connectDelay, channelType, nodeNames], test) =>
|
|
110
194
|
Effect.gen(function* () {
|
|
111
|
-
//
|
|
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
|
|
197
|
+
const [nodeNameX, nodeNameY] = nodeNames
|
|
198
|
+
const nodeX = yield* makeMeshNode(nodeNameX)
|
|
199
|
+
const nodeY = yield* makeMeshNode(nodeNameY)
|
|
121
200
|
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
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.
|
|
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, {
|
|
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
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
(
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
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
|
-
|
|
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
|
-
|
|
175
|
-
|
|
237
|
+
const nodeA = yield* makeMeshNode('A')
|
|
238
|
+
const nodeB = yield* makeMeshNode('B')
|
|
176
239
|
|
|
177
|
-
|
|
178
|
-
waitForOfflineDelay === undefined ? undefined : yield* Deferred.make<void, never>()
|
|
240
|
+
const { mode, connectNodes } = fromChannelType(channelType)
|
|
179
241
|
|
|
180
|
-
|
|
181
|
-
|
|
242
|
+
// TODO also optionally delay the connection
|
|
243
|
+
yield* connectNodes(nodeA, nodeB)
|
|
182
244
|
|
|
183
|
-
|
|
184
|
-
|
|
245
|
+
const waitForBToBeOffline =
|
|
246
|
+
waitForOfflineDelay === undefined ? undefined : yield* Deferred.make<void, never>()
|
|
185
247
|
|
|
186
|
-
|
|
187
|
-
yield*
|
|
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
|
-
|
|
191
|
-
|
|
192
|
-
|
|
253
|
+
console.log('nodeACode:waiting for B to be offline')
|
|
254
|
+
if (waitForBToBeOffline !== undefined) {
|
|
255
|
+
yield* waitForBToBeOffline
|
|
256
|
+
}
|
|
193
257
|
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
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
|
-
|
|
200
|
-
|
|
201
|
-
|
|
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
|
-
|
|
204
|
-
|
|
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
|
-
|
|
208
|
-
|
|
209
|
-
|
|
273
|
+
console.log('nodeBCode:B node going offline')
|
|
274
|
+
if (waitForBToBeOffline !== undefined) {
|
|
275
|
+
yield* Deferred.succeed(waitForBToBeOffline, void 0)
|
|
276
|
+
}
|
|
210
277
|
|
|
211
|
-
|
|
212
|
-
|
|
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
|
-
|
|
215
|
-
|
|
216
|
-
|
|
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
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
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
|
|
235
|
-
const
|
|
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(
|
|
373
|
+
yield* connectNodes(nodeX, nodeY)
|
|
240
374
|
|
|
241
375
|
const waitForConnectionReplacement = yield* Deferred.make<void>()
|
|
242
376
|
|
|
243
|
-
const
|
|
244
|
-
const
|
|
377
|
+
const nodeXCode = Effect.gen(function* () {
|
|
378
|
+
const channelXToY = yield* createChannel(nodeX, nodeLabel.y, { mode })
|
|
245
379
|
|
|
246
|
-
yield*
|
|
247
|
-
expect(yield* getFirstMessage(
|
|
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*
|
|
252
|
-
expect(yield* getFirstMessage(
|
|
385
|
+
yield* channelXToY.send({ message: `${nodeLabel.x}2` })
|
|
386
|
+
expect(yield* getFirstMessage(channelXToY)).toEqual({ message: `${nodeLabel.y}2` })
|
|
253
387
|
})
|
|
254
388
|
|
|
255
|
-
const
|
|
256
|
-
const
|
|
389
|
+
const nodeYCode = Effect.gen(function* () {
|
|
390
|
+
const channelYToX = yield* createChannel(nodeY, nodeLabel.x, { mode })
|
|
257
391
|
|
|
258
|
-
yield*
|
|
259
|
-
expect(yield* getFirstMessage(
|
|
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*
|
|
263
|
-
yield*
|
|
264
|
-
yield* connectNodes(
|
|
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*
|
|
268
|
-
expect(yield* getFirstMessage(
|
|
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([
|
|
272
|
-
}).pipe(
|
|
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
|
|
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(
|
|
323
|
-
|
|
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(
|
|
424
|
-
|
|
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
|
-
|
|
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(
|
|
830
|
+
yield* connectNodesViaBroadcastChannel(nodeB, nodeC)
|
|
501
831
|
|
|
502
832
|
const nodeACode = Effect.gen(function* () {
|
|
503
|
-
const
|
|
504
|
-
yield*
|
|
505
|
-
expect(yield* getFirstMessage(
|
|
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
|
|
509
|
-
const
|
|
510
|
-
yield*
|
|
511
|
-
expect(yield* getFirstMessage(
|
|
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,
|
|
844
|
+
yield* Effect.all([nodeACode, nodeCCode], { concurrency: 'unbounded' })
|
|
515
845
|
}).pipe(withCtx(test)),
|
|
516
846
|
)
|
|
517
847
|
})
|
|
518
|
-
})
|
|
519
848
|
|
|
520
|
-
|
|
521
|
-
|
|
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 =
|
|
885
|
+
const otelLayer = IS_CI ? Layer.empty : OtelLiveHttp({ serviceName: 'webmesh-node-test', skipLogUrl: false })
|
|
524
886
|
|
|
525
887
|
const withCtx =
|
|
526
|
-
(
|
|
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(
|
|
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}` : ''}`),
|