@livestore/webmesh 0.3.0-dev.10 → 0.3.0-dev.12
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 +20 -1
- package/dist/.tsbuildinfo +1 -1
- package/dist/channel/message-channel copy.d.ts +9 -0
- package/dist/channel/message-channel copy.d.ts.map +1 -0
- package/dist/channel/message-channel copy.js +137 -0
- package/dist/channel/message-channel copy.js.map +1 -0
- package/dist/channel/message-channel-internal copy.d.ts +42 -0
- package/dist/channel/message-channel-internal copy.d.ts.map +1 -0
- package/dist/channel/message-channel-internal copy.js +239 -0
- package/dist/channel/message-channel-internal copy.js.map +1 -0
- 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 +128 -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 +7 -5
- package/dist/channel/proxy-channel.js.map +1 -1
- package/dist/common.d.ts +8 -4
- 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 +23 -1
- package/dist/mesh-schema.d.ts.map +1 -1
- package/dist/mesh-schema.js +21 -2
- package/dist/mesh-schema.js.map +1 -1
- package/dist/node.d.ts +12 -1
- package/dist/node.d.ts.map +1 -1
- package/dist/node.js +40 -9
- 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 +300 -147
- package/dist/node.test.js.map +1 -1
- package/dist/websocket-connection.d.ts +1 -2
- package/dist/websocket-connection.d.ts.map +1 -1
- package/dist/websocket-connection.js +5 -4
- package/dist/websocket-connection.js.map +1 -1
- package/package.json +3 -3
- package/src/channel/message-channel-internal.ts +356 -0
- package/src/channel/message-channel.ts +183 -311
- package/src/channel/proxy-channel.ts +238 -230
- package/src/common.ts +3 -1
- package/src/mesh-schema.ts +20 -2
- package/src/node.test.ts +426 -177
- package/src/node.ts +70 -12
- package/src/websocket-connection.ts +83 -79
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,302 @@ 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 })
|
|
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
|
+
})
|
|
103
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)
|
|
195
|
+
// console.log({ delayX, delayY, connectDelay, channelType, nodeNames })
|
|
116
196
|
|
|
117
|
-
const
|
|
118
|
-
const
|
|
197
|
+
const [nodeNameX, nodeNameY] = nodeNames
|
|
198
|
+
const nodeX = yield* makeMeshNode(nodeNameX)
|
|
199
|
+
const nodeY = yield* makeMeshNode(nodeNameY)
|
|
119
200
|
|
|
120
|
-
|
|
201
|
+
yield* exchangeMessages({
|
|
202
|
+
nodeX,
|
|
203
|
+
nodeY,
|
|
204
|
+
channelType,
|
|
205
|
+
delays: { x: delayX, y: delayY, connect: connectDelay },
|
|
206
|
+
})
|
|
207
|
+
}).pipe(
|
|
208
|
+
withCtx(test, {
|
|
209
|
+
skipOtel: true,
|
|
210
|
+
suffix: `delayX=${delayX} delayY=${delayY} connectDelay=${connectDelay} channelType=${channelType} nodeNames=${nodeNames}`,
|
|
211
|
+
}),
|
|
212
|
+
),
|
|
213
|
+
// { fastCheck: { numRuns: 20 } },
|
|
214
|
+
)
|
|
121
215
|
|
|
122
|
-
|
|
123
|
-
|
|
216
|
+
{
|
|
217
|
+
// const waitForOfflineDelay = undefined
|
|
218
|
+
// const sleepDelay = 0
|
|
219
|
+
// const channelType = 'messagechannel'
|
|
220
|
+
// Vitest.scopedLive(
|
|
221
|
+
// 'b reconnects',
|
|
222
|
+
// (test) =>
|
|
223
|
+
Vitest.scopedLive.prop(
|
|
224
|
+
'b reconnects',
|
|
225
|
+
[Delay, Delay, ChannelType],
|
|
226
|
+
([waitForOfflineDelay, sleepDelay, channelType], test) =>
|
|
227
|
+
Effect.gen(function* () {
|
|
228
|
+
// console.log({ waitForOfflineDelay, sleepDelay, channelType })
|
|
124
229
|
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
230
|
+
if (waitForOfflineDelay === undefined) {
|
|
231
|
+
// TODO we still need to fix this scenario but it shouldn't really be common in practice
|
|
232
|
+
return
|
|
233
|
+
}
|
|
128
234
|
|
|
129
|
-
|
|
130
|
-
const
|
|
235
|
+
const nodeA = yield* makeMeshNode('A')
|
|
236
|
+
const nodeB = yield* makeMeshNode('B')
|
|
131
237
|
|
|
132
|
-
|
|
133
|
-
expect(yield* getFirstMessage(channelBToA)).toEqual({ message: 'A1' })
|
|
134
|
-
})
|
|
238
|
+
const { mode, connectNodes } = fromChannelType(channelType)
|
|
135
239
|
|
|
136
|
-
|
|
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
|
-
)
|
|
144
|
-
}).pipe(
|
|
145
|
-
withCtx(test, { skipOtel: true, suffix: `delayA=${delayA} delayB=${delayB} channelType=${channelType}` }),
|
|
146
|
-
),
|
|
147
|
-
)
|
|
240
|
+
// TODO also optionally delay the connection
|
|
241
|
+
yield* connectNodes(nodeA, nodeB)
|
|
148
242
|
|
|
149
|
-
|
|
150
|
-
|
|
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')
|
|
243
|
+
const waitForBToBeOffline =
|
|
244
|
+
waitForOfflineDelay === undefined ? undefined : yield* Deferred.make<void, never>()
|
|
171
245
|
|
|
172
|
-
|
|
246
|
+
const nodeACode = Effect.gen(function* () {
|
|
247
|
+
const channelAToB = yield* createChannel(nodeA, 'B', { mode })
|
|
248
|
+
yield* channelAToB.send({ message: 'A1' })
|
|
249
|
+
expect(yield* getFirstMessage(channelAToB)).toEqual({ message: 'B1' })
|
|
173
250
|
|
|
174
|
-
|
|
175
|
-
|
|
251
|
+
console.log('nodeACode:waiting for B to be offline')
|
|
252
|
+
if (waitForBToBeOffline !== undefined) {
|
|
253
|
+
yield* waitForBToBeOffline
|
|
254
|
+
}
|
|
176
255
|
|
|
177
|
-
|
|
178
|
-
|
|
256
|
+
yield* channelAToB.send({ message: 'A2' })
|
|
257
|
+
expect(yield* getFirstMessage(channelAToB)).toEqual({ message: 'B2' })
|
|
258
|
+
})
|
|
179
259
|
|
|
180
|
-
|
|
181
|
-
|
|
260
|
+
// Simulating node b going offline and then coming back online
|
|
261
|
+
// This test also illustrates why we need a ack-message channel since otherwise
|
|
262
|
+
// sent messages might get lost
|
|
263
|
+
const nodeBCode = Effect.gen(function* () {
|
|
264
|
+
yield* Effect.gen(function* () {
|
|
265
|
+
const channelBToA = yield* createChannel(nodeB, 'A', { mode })
|
|
182
266
|
|
|
183
|
-
|
|
184
|
-
|
|
267
|
+
yield* channelBToA.send({ message: 'B1' })
|
|
268
|
+
expect(yield* getFirstMessage(channelBToA)).toEqual({ message: 'A1' })
|
|
269
|
+
}).pipe(Effect.scoped, Effect.withSpan('nodeBCode:part1'))
|
|
185
270
|
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
271
|
+
console.log('nodeBCode:B node going offline')
|
|
272
|
+
if (waitForBToBeOffline !== undefined) {
|
|
273
|
+
yield* Deferred.succeed(waitForBToBeOffline, void 0)
|
|
274
|
+
}
|
|
189
275
|
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
276
|
+
if (sleepDelay !== undefined) {
|
|
277
|
+
yield* Effect.sleep(sleepDelay).pipe(Effect.withSpan(`B:sleep(${sleepDelay})`))
|
|
278
|
+
}
|
|
193
279
|
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
const channelBToA = yield* createChannel(nodeB, 'A', { mode })
|
|
280
|
+
// Recreating the channel
|
|
281
|
+
yield* Effect.gen(function* () {
|
|
282
|
+
const channelBToA = yield* createChannel(nodeB, 'A', { mode })
|
|
198
283
|
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
284
|
+
yield* channelBToA.send({ message: 'B2' })
|
|
285
|
+
expect(yield* getFirstMessage(channelBToA)).toEqual({ message: 'A2' })
|
|
286
|
+
}).pipe(Effect.scoped, Effect.withSpan('nodeBCode:part2'))
|
|
287
|
+
})
|
|
202
288
|
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
289
|
+
yield* Effect.all([nodeACode, nodeBCode], { concurrency: 'unbounded' }).pipe(Effect.withSpan('test'))
|
|
290
|
+
}).pipe(
|
|
291
|
+
withCtx(test, {
|
|
292
|
+
skipOtel: true,
|
|
293
|
+
suffix: `waitForOfflineDelay=${waitForOfflineDelay} sleepDelay=${sleepDelay} channelType=${channelType}`,
|
|
294
|
+
}),
|
|
295
|
+
),
|
|
296
|
+
{ fastCheck: { numRuns: 20 } },
|
|
297
|
+
)
|
|
298
|
+
}
|
|
206
299
|
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
300
|
+
Vitest.scopedLive('reconnect with re-created node', (test) =>
|
|
301
|
+
Effect.gen(function* () {
|
|
302
|
+
const nodeBgen1Scope = yield* Scope.make()
|
|
210
303
|
|
|
211
|
-
|
|
212
|
-
|
|
304
|
+
const nodeA = yield* makeMeshNode('A')
|
|
305
|
+
const nodeBgen1 = yield* makeMeshNode('B').pipe(Scope.extend(nodeBgen1Scope))
|
|
306
|
+
|
|
307
|
+
yield* connectNodesViaMessageChannel(nodeA, nodeBgen1).pipe(Scope.extend(nodeBgen1Scope))
|
|
308
|
+
|
|
309
|
+
// yield* Effect.sleep(100)
|
|
310
|
+
|
|
311
|
+
const channelAToBOnce = yield* Effect.cached(createChannel(nodeA, 'B'))
|
|
312
|
+
const nodeACode = Effect.gen(function* () {
|
|
313
|
+
const channelAToB = yield* channelAToBOnce
|
|
314
|
+
yield* channelAToB.send({ message: 'A1' })
|
|
315
|
+
expect(yield* getFirstMessage(channelAToB)).toEqual({ message: 'B1' })
|
|
316
|
+
// expect(channelAToB.debugInfo.connectCounter).toBe(1)
|
|
317
|
+
})
|
|
213
318
|
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
319
|
+
const nodeBCode = (nodeB: MeshNode) =>
|
|
320
|
+
Effect.gen(function* () {
|
|
321
|
+
const channelBToA = yield* createChannel(nodeB, 'A')
|
|
322
|
+
|
|
323
|
+
yield* channelBToA.send({ message: 'B1' })
|
|
324
|
+
expect(yield* getFirstMessage(channelBToA)).toEqual({ message: 'A1' })
|
|
325
|
+
// expect(channelBToA.debugInfo.connectCounter).toBe(1)
|
|
217
326
|
})
|
|
218
327
|
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
)
|
|
328
|
+
yield* Effect.all([nodeACode, nodeBCode(nodeBgen1).pipe(Scope.extend(nodeBgen1Scope))], {
|
|
329
|
+
concurrency: 'unbounded',
|
|
330
|
+
}).pipe(Effect.withSpan('test1'))
|
|
331
|
+
|
|
332
|
+
yield* Scope.close(nodeBgen1Scope, Exit.void)
|
|
333
|
+
|
|
334
|
+
const nodeBgen2 = yield* makeMeshNode('B')
|
|
335
|
+
yield* connectNodesViaMessageChannel(nodeA, nodeBgen2, { replaceIfExists: true })
|
|
336
|
+
|
|
337
|
+
yield* Effect.all([nodeACode, nodeBCode(nodeBgen2)], { concurrency: 'unbounded' }).pipe(
|
|
338
|
+
Effect.withSpan('test2'),
|
|
339
|
+
)
|
|
340
|
+
}).pipe(withCtx(test)),
|
|
226
341
|
)
|
|
227
342
|
|
|
228
343
|
const ChannelTypeWithoutMessageChannelProxy = Schema.Literal('proxy', 'messagechannel')
|
|
229
344
|
Vitest.scopedLive.prop(
|
|
230
345
|
'replace connection while keeping the channel',
|
|
231
|
-
[ChannelTypeWithoutMessageChannelProxy],
|
|
232
|
-
([channelType], test) =>
|
|
346
|
+
[ChannelTypeWithoutMessageChannelProxy, NodeNames],
|
|
347
|
+
([channelType, nodeNames], test) =>
|
|
233
348
|
Effect.gen(function* () {
|
|
234
|
-
const
|
|
235
|
-
const
|
|
349
|
+
const [nodeNameX, nodeNameY] = nodeNames
|
|
350
|
+
const nodeX = yield* makeMeshNode(nodeNameX)
|
|
351
|
+
const nodeY = yield* makeMeshNode(nodeNameY)
|
|
352
|
+
const nodeLabel = { x: nodeX.nodeName, y: nodeY.nodeName }
|
|
236
353
|
|
|
237
354
|
const { mode, connectNodes } = fromChannelType(channelType)
|
|
238
355
|
|
|
239
|
-
yield* connectNodes(
|
|
356
|
+
yield* connectNodes(nodeX, nodeY)
|
|
240
357
|
|
|
241
358
|
const waitForConnectionReplacement = yield* Deferred.make<void>()
|
|
242
359
|
|
|
243
|
-
const
|
|
244
|
-
const
|
|
360
|
+
const nodeXCode = Effect.gen(function* () {
|
|
361
|
+
const channelXToY = yield* createChannel(nodeX, nodeLabel.y, { mode })
|
|
245
362
|
|
|
246
|
-
yield*
|
|
247
|
-
expect(yield* getFirstMessage(
|
|
363
|
+
yield* channelXToY.send({ message: `${nodeLabel.x}1` })
|
|
364
|
+
expect(yield* getFirstMessage(channelXToY)).toEqual({ message: `${nodeLabel.y}1` })
|
|
248
365
|
|
|
249
366
|
yield* waitForConnectionReplacement
|
|
250
367
|
|
|
251
|
-
yield*
|
|
252
|
-
expect(yield* getFirstMessage(
|
|
368
|
+
yield* channelXToY.send({ message: `${nodeLabel.x}2` })
|
|
369
|
+
expect(yield* getFirstMessage(channelXToY)).toEqual({ message: `${nodeLabel.y}2` })
|
|
253
370
|
})
|
|
254
371
|
|
|
255
|
-
const
|
|
256
|
-
const
|
|
372
|
+
const nodeYCode = Effect.gen(function* () {
|
|
373
|
+
const channelYToX = yield* createChannel(nodeY, nodeLabel.x, { mode })
|
|
257
374
|
|
|
258
|
-
yield*
|
|
259
|
-
expect(yield* getFirstMessage(
|
|
375
|
+
yield* channelYToX.send({ message: `${nodeLabel.y}1` })
|
|
376
|
+
expect(yield* getFirstMessage(channelYToX)).toEqual({ message: `${nodeLabel.x}1` })
|
|
260
377
|
|
|
261
378
|
// Switch out connection while keeping the channel
|
|
262
|
-
yield*
|
|
263
|
-
yield*
|
|
264
|
-
yield* connectNodes(
|
|
379
|
+
yield* nodeX.removeConnection(nodeLabel.y)
|
|
380
|
+
yield* nodeY.removeConnection(nodeLabel.x)
|
|
381
|
+
yield* connectNodes(nodeX, nodeY)
|
|
265
382
|
yield* Deferred.succeed(waitForConnectionReplacement, void 0)
|
|
266
383
|
|
|
267
|
-
yield*
|
|
268
|
-
expect(yield* getFirstMessage(
|
|
384
|
+
yield* channelYToX.send({ message: `${nodeLabel.y}2` })
|
|
385
|
+
expect(yield* getFirstMessage(channelYToX)).toEqual({ message: `${nodeLabel.x}2` })
|
|
269
386
|
})
|
|
270
387
|
|
|
271
|
-
yield* Effect.all([
|
|
272
|
-
}).pipe(
|
|
388
|
+
yield* Effect.all([nodeXCode, nodeYCode], { concurrency: 'unbounded' })
|
|
389
|
+
}).pipe(
|
|
390
|
+
withCtx(test, {
|
|
391
|
+
skipOtel: true,
|
|
392
|
+
suffix: `channelType=${channelType} nodeNames=${nodeNames}`,
|
|
393
|
+
}),
|
|
394
|
+
),
|
|
395
|
+
{ fastCheck: { numRuns: 10 } },
|
|
273
396
|
)
|
|
274
397
|
|
|
275
|
-
Vitest.describe
|
|
398
|
+
Vitest.describe('TODO improve latency', () => {
|
|
276
399
|
// TODO we need to improve latency when sending messages concurrently
|
|
277
400
|
Vitest.scopedLive.prop(
|
|
278
401
|
'concurrent messages',
|
|
@@ -319,12 +442,52 @@ Vitest.describe('webmesh node', { timeout: 1000 }, () => {
|
|
|
319
442
|
yield* Effect.all([nodeACode, nodeBCode, connectNodes(nodeA, nodeB).pipe(Effect.delay(100))], {
|
|
320
443
|
concurrency: 'unbounded',
|
|
321
444
|
})
|
|
322
|
-
}).pipe(
|
|
323
|
-
|
|
445
|
+
}).pipe(
|
|
446
|
+
withCtx(test, {
|
|
447
|
+
skipOtel: true,
|
|
448
|
+
suffix: `channelType=${channelType} count=${count}`,
|
|
449
|
+
timeout: testTimeout * 2,
|
|
450
|
+
}),
|
|
451
|
+
),
|
|
452
|
+
{ fastCheck: { numRuns: 10 } },
|
|
324
453
|
)
|
|
325
454
|
})
|
|
326
455
|
})
|
|
327
456
|
|
|
457
|
+
Vitest.describe('message channel specific tests', () => {
|
|
458
|
+
Vitest.scopedLive('differing initial connection counter', (test) =>
|
|
459
|
+
Effect.gen(function* () {
|
|
460
|
+
const nodeA = yield* makeMeshNode('A')
|
|
461
|
+
const nodeB = yield* makeMeshNode('B')
|
|
462
|
+
|
|
463
|
+
yield* connectNodesViaMessageChannel(nodeA, nodeB)
|
|
464
|
+
|
|
465
|
+
const messageCount = 3
|
|
466
|
+
|
|
467
|
+
const bFiber = yield* Effect.gen(function* () {
|
|
468
|
+
const channelBToA = yield* createChannel(nodeB, 'A')
|
|
469
|
+
yield* channelBToA.listen.pipe(
|
|
470
|
+
Stream.flatten(),
|
|
471
|
+
Stream.tap((msg) => channelBToA.send({ message: `resp:${msg.message}` })),
|
|
472
|
+
Stream.take(messageCount),
|
|
473
|
+
Stream.runDrain,
|
|
474
|
+
)
|
|
475
|
+
}).pipe(Effect.scoped, Effect.fork)
|
|
476
|
+
|
|
477
|
+
// yield* createChannel(nodeA, 'B').pipe(Effect.andThen(WebChannel.shutdown))
|
|
478
|
+
// // yield* createChannel(nodeA, 'B').pipe(Effect.andThen(WebChannel.shutdown))
|
|
479
|
+
// // yield* createChannel(nodeA, 'B').pipe(Effect.andThen(WebChannel.shutdown))
|
|
480
|
+
yield* Effect.gen(function* () {
|
|
481
|
+
const channelAToB = yield* createChannel(nodeA, 'B')
|
|
482
|
+
yield* channelAToB.send({ message: 'A' })
|
|
483
|
+
expect(yield* getFirstMessage(channelAToB)).toEqual({ message: 'resp:A' })
|
|
484
|
+
}).pipe(Effect.scoped, Effect.repeatN(messageCount))
|
|
485
|
+
|
|
486
|
+
yield* bFiber
|
|
487
|
+
}).pipe(withCtx(test)),
|
|
488
|
+
)
|
|
489
|
+
})
|
|
490
|
+
|
|
328
491
|
Vitest.scopedLive('manual debug test', (test) =>
|
|
329
492
|
Effect.gen(function* () {
|
|
330
493
|
const nodeA = yield* makeMeshNode('A')
|
|
@@ -382,6 +545,7 @@ Vitest.describe('webmesh node', { timeout: 1000 }, () => {
|
|
|
382
545
|
yield* channelAToC.send({ message: 'A1' })
|
|
383
546
|
expect(yield* getFirstMessage(channelAToC)).toEqual({ message: 'C1' })
|
|
384
547
|
expect(yield* getFirstMessage(channelAToC)).toEqual({ message: 'C2' })
|
|
548
|
+
expect(yield* getFirstMessage(channelAToC)).toEqual({ message: 'C3' })
|
|
385
549
|
})
|
|
386
550
|
|
|
387
551
|
const nodeCCode = Effect.gen(function* () {
|
|
@@ -420,9 +584,14 @@ Vitest.describe('webmesh node', { timeout: 1000 }, () => {
|
|
|
420
584
|
expect(yield* getFirstMessage(channelCToA)).toEqual({ message: 'A1' })
|
|
421
585
|
})
|
|
422
586
|
|
|
423
|
-
yield* Effect.all(
|
|
424
|
-
|
|
425
|
-
|
|
587
|
+
yield* Effect.all(
|
|
588
|
+
[
|
|
589
|
+
nodeACode,
|
|
590
|
+
nodeCCode,
|
|
591
|
+
connectNodes(nodeB, nodeC).pipe(Effect.delay(100), Effect.withSpan('connect-nodeB-nodeC-delay(100)')),
|
|
592
|
+
],
|
|
593
|
+
{ concurrency: 'unbounded' },
|
|
594
|
+
)
|
|
426
595
|
}).pipe(withCtx(test)),
|
|
427
596
|
)
|
|
428
597
|
|
|
@@ -473,6 +642,85 @@ Vitest.describe('webmesh node', { timeout: 1000 }, () => {
|
|
|
473
642
|
yield* Effect.all([nodeACode, nodeCCode], { concurrency: 'unbounded' })
|
|
474
643
|
}).pipe(withCtx(test)),
|
|
475
644
|
)
|
|
645
|
+
|
|
646
|
+
Vitest.scopedLive('reconnect with re-created node', (test) =>
|
|
647
|
+
Effect.gen(function* () {
|
|
648
|
+
const nodeCgen1Scope = yield* Scope.make()
|
|
649
|
+
|
|
650
|
+
const nodeA = yield* makeMeshNode('A')
|
|
651
|
+
const nodeB = yield* makeMeshNode('B')
|
|
652
|
+
const nodeCgen1 = yield* makeMeshNode('C').pipe(Scope.extend(nodeCgen1Scope))
|
|
653
|
+
|
|
654
|
+
yield* connectNodesViaMessageChannel(nodeA, nodeB)
|
|
655
|
+
yield* connectNodesViaMessageChannel(nodeB, nodeCgen1).pipe(Scope.extend(nodeCgen1Scope))
|
|
656
|
+
|
|
657
|
+
const nodeACode = Effect.gen(function* () {
|
|
658
|
+
const channelAToB = yield* createChannel(nodeA, 'C')
|
|
659
|
+
|
|
660
|
+
yield* channelAToB.send({ message: 'A1' })
|
|
661
|
+
expect(yield* getFirstMessage(channelAToB)).toEqual({ message: 'C1' })
|
|
662
|
+
})
|
|
663
|
+
|
|
664
|
+
const nodeCCode = (nodeB: MeshNode) =>
|
|
665
|
+
Effect.gen(function* () {
|
|
666
|
+
const channelBToA = yield* createChannel(nodeB, 'A')
|
|
667
|
+
|
|
668
|
+
yield* channelBToA.send({ message: 'C1' })
|
|
669
|
+
expect(yield* getFirstMessage(channelBToA)).toEqual({ message: 'A1' })
|
|
670
|
+
})
|
|
671
|
+
|
|
672
|
+
yield* Effect.all([nodeACode, nodeCCode(nodeCgen1)], { concurrency: 'unbounded' }).pipe(
|
|
673
|
+
Effect.withSpan('test1'),
|
|
674
|
+
Scope.extend(nodeCgen1Scope),
|
|
675
|
+
)
|
|
676
|
+
|
|
677
|
+
yield* Scope.close(nodeCgen1Scope, Exit.void)
|
|
678
|
+
|
|
679
|
+
const nodeCgen2 = yield* makeMeshNode('C')
|
|
680
|
+
yield* connectNodesViaMessageChannel(nodeB, nodeCgen2, { replaceIfExists: true })
|
|
681
|
+
|
|
682
|
+
yield* Effect.all([nodeACode, nodeCCode(nodeCgen2)], { concurrency: 'unbounded' }).pipe(
|
|
683
|
+
Effect.withSpan('test2'),
|
|
684
|
+
)
|
|
685
|
+
}).pipe(withCtx(test)),
|
|
686
|
+
)
|
|
687
|
+
})
|
|
688
|
+
|
|
689
|
+
/**
|
|
690
|
+
* A
|
|
691
|
+
* / \
|
|
692
|
+
* B C
|
|
693
|
+
* \ /
|
|
694
|
+
* D
|
|
695
|
+
*/
|
|
696
|
+
Vitest.describe('diamond topology', () => {
|
|
697
|
+
Vitest.scopedLive('should work', (test) =>
|
|
698
|
+
Effect.gen(function* () {
|
|
699
|
+
const nodeA = yield* makeMeshNode('A')
|
|
700
|
+
const nodeB = yield* makeMeshNode('B')
|
|
701
|
+
const nodeC = yield* makeMeshNode('C')
|
|
702
|
+
const nodeD = yield* makeMeshNode('D')
|
|
703
|
+
|
|
704
|
+
yield* connectNodesViaMessageChannel(nodeA, nodeB)
|
|
705
|
+
yield* connectNodesViaMessageChannel(nodeA, nodeC)
|
|
706
|
+
yield* connectNodesViaMessageChannel(nodeB, nodeD)
|
|
707
|
+
yield* connectNodesViaMessageChannel(nodeC, nodeD)
|
|
708
|
+
|
|
709
|
+
const nodeACode = Effect.gen(function* () {
|
|
710
|
+
const channelAToD = yield* createChannel(nodeA, 'D')
|
|
711
|
+
yield* channelAToD.send({ message: 'A1' })
|
|
712
|
+
expect(yield* getFirstMessage(channelAToD)).toEqual({ message: 'D1' })
|
|
713
|
+
})
|
|
714
|
+
|
|
715
|
+
const nodeDCode = Effect.gen(function* () {
|
|
716
|
+
const channelDToA = yield* createChannel(nodeD, 'A')
|
|
717
|
+
yield* channelDToA.send({ message: 'D1' })
|
|
718
|
+
expect(yield* getFirstMessage(channelDToA)).toEqual({ message: 'A1' })
|
|
719
|
+
})
|
|
720
|
+
|
|
721
|
+
yield* Effect.all([nodeACode, nodeDCode], { concurrency: 'unbounded' })
|
|
722
|
+
}).pipe(withCtx(test)),
|
|
723
|
+
)
|
|
476
724
|
})
|
|
477
725
|
|
|
478
726
|
Vitest.describe('mixture of messagechannel and proxy connections', () => {
|
|
@@ -491,42 +739,43 @@ Vitest.describe('webmesh node', { timeout: 1000 }, () => {
|
|
|
491
739
|
)
|
|
492
740
|
|
|
493
741
|
// TODO this currently fails but should work. probably needs some more guarding internally.
|
|
494
|
-
Vitest.scopedLive
|
|
742
|
+
Vitest.scopedLive('should work for messagechannels', (test) =>
|
|
495
743
|
Effect.gen(function* () {
|
|
496
744
|
const nodeA = yield* makeMeshNode('A')
|
|
497
745
|
const nodeB = yield* makeMeshNode('B')
|
|
746
|
+
const nodeC = yield* makeMeshNode('C')
|
|
498
747
|
|
|
499
748
|
yield* connectNodesViaMessageChannel(nodeB, nodeA)
|
|
500
|
-
yield* connectNodesViaBroadcastChannel(
|
|
749
|
+
yield* connectNodesViaBroadcastChannel(nodeB, nodeC)
|
|
501
750
|
|
|
502
751
|
const nodeACode = Effect.gen(function* () {
|
|
503
|
-
const
|
|
504
|
-
yield*
|
|
505
|
-
expect(yield* getFirstMessage(
|
|
752
|
+
const channelAToC = yield* createChannel(nodeA, 'C', { mode: 'proxy' })
|
|
753
|
+
yield* channelAToC.send({ message: 'A1' })
|
|
754
|
+
expect(yield* getFirstMessage(channelAToC)).toEqual({ message: 'C1' })
|
|
506
755
|
})
|
|
507
756
|
|
|
508
|
-
const
|
|
509
|
-
const
|
|
510
|
-
yield*
|
|
511
|
-
expect(yield* getFirstMessage(
|
|
757
|
+
const nodeCCode = Effect.gen(function* () {
|
|
758
|
+
const channelCToA = yield* createChannel(nodeC, 'A', { mode: 'proxy' })
|
|
759
|
+
yield* channelCToA.send({ message: 'C1' })
|
|
760
|
+
expect(yield* getFirstMessage(channelCToA)).toEqual({ message: 'A1' })
|
|
512
761
|
})
|
|
513
762
|
|
|
514
|
-
yield* Effect.all([nodeACode,
|
|
763
|
+
yield* Effect.all([nodeACode, nodeCCode], { concurrency: 'unbounded' })
|
|
515
764
|
}).pipe(withCtx(test)),
|
|
516
765
|
)
|
|
517
766
|
})
|
|
518
767
|
})
|
|
519
768
|
|
|
520
|
-
const
|
|
521
|
-
const isCi = envTruish(process.env.CI)
|
|
522
|
-
|
|
523
|
-
const otelLayer = isCi ? Layer.empty : OtelLiveHttp({ serviceName: 'webmesh-node-test', skipLogUrl: false })
|
|
769
|
+
const otelLayer = IS_CI ? Layer.empty : OtelLiveHttp({ serviceName: 'webmesh-node-test', skipLogUrl: false })
|
|
524
770
|
|
|
525
771
|
const withCtx =
|
|
526
|
-
(
|
|
772
|
+
(
|
|
773
|
+
testContext: Vitest.TaskContext,
|
|
774
|
+
{ suffix, skipOtel = false, timeout = testTimeout }: { suffix?: string; skipOtel?: boolean; timeout?: number } = {},
|
|
775
|
+
) =>
|
|
527
776
|
<A, E, R>(self: Effect.Effect<A, E, R>) =>
|
|
528
777
|
self.pipe(
|
|
529
|
-
Effect.timeout(
|
|
778
|
+
Effect.timeout(timeout),
|
|
530
779
|
Effect.provide(Logger.pretty),
|
|
531
780
|
Effect.scoped, // We need to scope the effect manually here because otherwise the span is not closed
|
|
532
781
|
Effect.withSpan(`${testContext.task.suite?.name}:${testContext.task.name}${suffix ? `:${suffix}` : ''}`),
|