@livestore/webmesh 0.3.0-dev.11 → 0.3.0-dev.13
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 +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 +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 +315 -149
- 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 +190 -310
- 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 +448 -179
- 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
|
-
|
|
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 }
|
|
129
|
+
}
|
|
130
|
+
case 'messagechannel.proxy': {
|
|
131
|
+
return { mode: 'proxy', connectNodes: connectNodesViaMessageChannel }
|
|
102
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)
|
|
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
|
+
})
|
|
318
|
+
|
|
319
|
+
const nodeBCode = (nodeB: MeshNode) =>
|
|
320
|
+
Effect.gen(function* () {
|
|
321
|
+
const channelBToA = yield* createChannel(nodeB, 'A')
|
|
213
322
|
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
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
|
|
|
@@ -451,7 +620,7 @@ Vitest.describe('webmesh node', { timeout: 1000 }, () => {
|
|
|
451
620
|
}).pipe(withCtx(test)),
|
|
452
621
|
)
|
|
453
622
|
|
|
454
|
-
Vitest.scopedLive('should fail', (test) =>
|
|
623
|
+
Vitest.scopedLive('should fail with timeout due to missing connection', (test) =>
|
|
455
624
|
Effect.gen(function* () {
|
|
456
625
|
const nodeA = yield* makeMeshNode('A')
|
|
457
626
|
const nodeB = yield* makeMeshNode('B')
|
|
@@ -473,6 +642,106 @@ 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('should fail with timeout due no transferable', (test) =>
|
|
647
|
+
Effect.gen(function* () {
|
|
648
|
+
const nodeA = yield* makeMeshNode('A')
|
|
649
|
+
const nodeB = yield* makeMeshNode('B')
|
|
650
|
+
|
|
651
|
+
yield* connectNodesViaBroadcastChannel(nodeA, nodeB)
|
|
652
|
+
|
|
653
|
+
const nodeACode = Effect.gen(function* () {
|
|
654
|
+
const err = yield* createChannel(nodeA, 'B').pipe(Effect.timeout(200), Effect.flip)
|
|
655
|
+
expect(err._tag).toBe('TimeoutException')
|
|
656
|
+
})
|
|
657
|
+
|
|
658
|
+
const nodeBCode = Effect.gen(function* () {
|
|
659
|
+
const err = yield* createChannel(nodeB, 'A').pipe(Effect.timeout(200), Effect.flip)
|
|
660
|
+
expect(err._tag).toBe('TimeoutException')
|
|
661
|
+
})
|
|
662
|
+
|
|
663
|
+
yield* Effect.all([nodeACode, nodeBCode], { concurrency: 'unbounded' })
|
|
664
|
+
}).pipe(withCtx(test)),
|
|
665
|
+
)
|
|
666
|
+
|
|
667
|
+
Vitest.scopedLive('reconnect with re-created node', (test) =>
|
|
668
|
+
Effect.gen(function* () {
|
|
669
|
+
const nodeCgen1Scope = yield* Scope.make()
|
|
670
|
+
|
|
671
|
+
const nodeA = yield* makeMeshNode('A')
|
|
672
|
+
const nodeB = yield* makeMeshNode('B')
|
|
673
|
+
const nodeCgen1 = yield* makeMeshNode('C').pipe(Scope.extend(nodeCgen1Scope))
|
|
674
|
+
|
|
675
|
+
yield* connectNodesViaMessageChannel(nodeA, nodeB)
|
|
676
|
+
yield* connectNodesViaMessageChannel(nodeB, nodeCgen1).pipe(Scope.extend(nodeCgen1Scope))
|
|
677
|
+
|
|
678
|
+
const nodeACode = Effect.gen(function* () {
|
|
679
|
+
const channelAToB = yield* createChannel(nodeA, 'C')
|
|
680
|
+
|
|
681
|
+
yield* channelAToB.send({ message: 'A1' })
|
|
682
|
+
expect(yield* getFirstMessage(channelAToB)).toEqual({ message: 'C1' })
|
|
683
|
+
})
|
|
684
|
+
|
|
685
|
+
const nodeCCode = (nodeB: MeshNode) =>
|
|
686
|
+
Effect.gen(function* () {
|
|
687
|
+
const channelBToA = yield* createChannel(nodeB, 'A')
|
|
688
|
+
|
|
689
|
+
yield* channelBToA.send({ message: 'C1' })
|
|
690
|
+
expect(yield* getFirstMessage(channelBToA)).toEqual({ message: 'A1' })
|
|
691
|
+
})
|
|
692
|
+
|
|
693
|
+
yield* Effect.all([nodeACode, nodeCCode(nodeCgen1)], { concurrency: 'unbounded' }).pipe(
|
|
694
|
+
Effect.withSpan('test1'),
|
|
695
|
+
Scope.extend(nodeCgen1Scope),
|
|
696
|
+
)
|
|
697
|
+
|
|
698
|
+
yield* Scope.close(nodeCgen1Scope, Exit.void)
|
|
699
|
+
|
|
700
|
+
const nodeCgen2 = yield* makeMeshNode('C')
|
|
701
|
+
yield* connectNodesViaMessageChannel(nodeB, nodeCgen2, { replaceIfExists: true })
|
|
702
|
+
|
|
703
|
+
yield* Effect.all([nodeACode, nodeCCode(nodeCgen2)], { concurrency: 'unbounded' }).pipe(
|
|
704
|
+
Effect.withSpan('test2'),
|
|
705
|
+
)
|
|
706
|
+
}).pipe(withCtx(test)),
|
|
707
|
+
)
|
|
708
|
+
})
|
|
709
|
+
|
|
710
|
+
/**
|
|
711
|
+
* A
|
|
712
|
+
* / \
|
|
713
|
+
* B C
|
|
714
|
+
* \ /
|
|
715
|
+
* D
|
|
716
|
+
*/
|
|
717
|
+
Vitest.describe('diamond topology', () => {
|
|
718
|
+
Vitest.scopedLive('should work', (test) =>
|
|
719
|
+
Effect.gen(function* () {
|
|
720
|
+
const nodeA = yield* makeMeshNode('A')
|
|
721
|
+
const nodeB = yield* makeMeshNode('B')
|
|
722
|
+
const nodeC = yield* makeMeshNode('C')
|
|
723
|
+
const nodeD = yield* makeMeshNode('D')
|
|
724
|
+
|
|
725
|
+
yield* connectNodesViaMessageChannel(nodeA, nodeB)
|
|
726
|
+
yield* connectNodesViaMessageChannel(nodeA, nodeC)
|
|
727
|
+
yield* connectNodesViaMessageChannel(nodeB, nodeD)
|
|
728
|
+
yield* connectNodesViaMessageChannel(nodeC, nodeD)
|
|
729
|
+
|
|
730
|
+
const nodeACode = Effect.gen(function* () {
|
|
731
|
+
const channelAToD = yield* createChannel(nodeA, 'D')
|
|
732
|
+
yield* channelAToD.send({ message: 'A1' })
|
|
733
|
+
expect(yield* getFirstMessage(channelAToD)).toEqual({ message: 'D1' })
|
|
734
|
+
})
|
|
735
|
+
|
|
736
|
+
const nodeDCode = Effect.gen(function* () {
|
|
737
|
+
const channelDToA = yield* createChannel(nodeD, 'A')
|
|
738
|
+
yield* channelDToA.send({ message: 'D1' })
|
|
739
|
+
expect(yield* getFirstMessage(channelDToA)).toEqual({ message: 'A1' })
|
|
740
|
+
})
|
|
741
|
+
|
|
742
|
+
yield* Effect.all([nodeACode, nodeDCode], { concurrency: 'unbounded' })
|
|
743
|
+
}).pipe(withCtx(test)),
|
|
744
|
+
)
|
|
476
745
|
})
|
|
477
746
|
|
|
478
747
|
Vitest.describe('mixture of messagechannel and proxy connections', () => {
|
|
@@ -490,43 +759,43 @@ Vitest.describe('webmesh node', { timeout: 1000 }, () => {
|
|
|
490
759
|
}).pipe(withCtx(test)),
|
|
491
760
|
)
|
|
492
761
|
|
|
493
|
-
|
|
494
|
-
Vitest.scopedLive.skip('should work for messagechannels', (test) =>
|
|
762
|
+
Vitest.scopedLive('should work for messagechannels', (test) =>
|
|
495
763
|
Effect.gen(function* () {
|
|
496
764
|
const nodeA = yield* makeMeshNode('A')
|
|
497
765
|
const nodeB = yield* makeMeshNode('B')
|
|
766
|
+
const nodeC = yield* makeMeshNode('C')
|
|
498
767
|
|
|
499
768
|
yield* connectNodesViaMessageChannel(nodeB, nodeA)
|
|
500
|
-
yield* connectNodesViaBroadcastChannel(
|
|
769
|
+
yield* connectNodesViaBroadcastChannel(nodeB, nodeC)
|
|
501
770
|
|
|
502
771
|
const nodeACode = Effect.gen(function* () {
|
|
503
|
-
const
|
|
504
|
-
yield*
|
|
505
|
-
expect(yield* getFirstMessage(
|
|
772
|
+
const channelAToC = yield* createChannel(nodeA, 'C', { mode: 'proxy' })
|
|
773
|
+
yield* channelAToC.send({ message: 'A1' })
|
|
774
|
+
expect(yield* getFirstMessage(channelAToC)).toEqual({ message: 'C1' })
|
|
506
775
|
})
|
|
507
776
|
|
|
508
|
-
const
|
|
509
|
-
const
|
|
510
|
-
yield*
|
|
511
|
-
expect(yield* getFirstMessage(
|
|
777
|
+
const nodeCCode = Effect.gen(function* () {
|
|
778
|
+
const channelCToA = yield* createChannel(nodeC, 'A', { mode: 'proxy' })
|
|
779
|
+
yield* channelCToA.send({ message: 'C1' })
|
|
780
|
+
expect(yield* getFirstMessage(channelCToA)).toEqual({ message: 'A1' })
|
|
512
781
|
})
|
|
513
782
|
|
|
514
|
-
yield* Effect.all([nodeACode,
|
|
783
|
+
yield* Effect.all([nodeACode, nodeCCode], { concurrency: 'unbounded' })
|
|
515
784
|
}).pipe(withCtx(test)),
|
|
516
785
|
)
|
|
517
786
|
})
|
|
518
787
|
})
|
|
519
788
|
|
|
520
|
-
const
|
|
521
|
-
const isCi = envTruish(process.env.CI)
|
|
522
|
-
|
|
523
|
-
const otelLayer = isCi ? Layer.empty : OtelLiveHttp({ serviceName: 'webmesh-node-test', skipLogUrl: false })
|
|
789
|
+
const otelLayer = IS_CI ? Layer.empty : OtelLiveHttp({ serviceName: 'webmesh-node-test', skipLogUrl: false })
|
|
524
790
|
|
|
525
791
|
const withCtx =
|
|
526
|
-
(
|
|
792
|
+
(
|
|
793
|
+
testContext: Vitest.TaskContext,
|
|
794
|
+
{ suffix, skipOtel = false, timeout = testTimeout }: { suffix?: string; skipOtel?: boolean; timeout?: number } = {},
|
|
795
|
+
) =>
|
|
527
796
|
<A, E, R>(self: Effect.Effect<A, E, R>) =>
|
|
528
797
|
self.pipe(
|
|
529
|
-
Effect.timeout(
|
|
798
|
+
Effect.timeout(timeout),
|
|
530
799
|
Effect.provide(Logger.pretty),
|
|
531
800
|
Effect.scoped, // We need to scope the effect manually here because otherwise the span is not closed
|
|
532
801
|
Effect.withSpan(`${testContext.task.suite?.name}:${testContext.task.name}${suffix ? `:${suffix}` : ''}`),
|