@livestore/webmesh 0.0.0-snapshot-1d99fea7d2ce2c7a5d9ed0a3752f8a7bda6bc3db → 0.0.0-snapshot-7d3074f682f31cfc38b26ed2c4c2972ce1e9121e
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-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 +312 -146
- 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 +444 -174
- package/src/node.ts +70 -12
- package/src/websocket-connection.ts +83 -79
package/src/node.test.ts
CHANGED
|
@@ -1,5 +1,19 @@
|
|
|
1
|
+
import '@livestore/utils/node-vitest-polyfill'
|
|
2
|
+
|
|
1
3
|
import { IS_CI } from '@livestore/utils'
|
|
2
|
-
import {
|
|
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'
|
|
3
17
|
import { OtelLiveHttp } from '@livestore/utils/node'
|
|
4
18
|
import { Vitest } from '@livestore/utils/node-vitest'
|
|
5
19
|
import { expect } from 'vitest'
|
|
@@ -17,35 +31,47 @@ import { makeMeshNode } from './node.js'
|
|
|
17
31
|
|
|
18
32
|
const ExampleSchema = Schema.Struct({ message: Schema.String })
|
|
19
33
|
|
|
20
|
-
const connectNodesViaMessageChannel = (nodeA: MeshNode, nodeB: MeshNode) =>
|
|
34
|
+
const connectNodesViaMessageChannel = (nodeA: MeshNode, nodeB: MeshNode, options?: { replaceIfExists?: boolean }) =>
|
|
21
35
|
Effect.gen(function* () {
|
|
22
36
|
const mc = new MessageChannel()
|
|
23
37
|
const meshChannelAToB = yield* WebChannel.messagePortChannel({ port: mc.port1, schema: Packet })
|
|
24
38
|
const meshChannelBToA = yield* WebChannel.messagePortChannel({ port: mc.port2, schema: Packet })
|
|
25
39
|
|
|
26
|
-
yield* nodeA.addConnection({
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
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
|
+
})
|
|
30
50
|
}).pipe(Effect.withSpan(`connectNodesViaMessageChannel:${nodeA.nodeName}↔${nodeB.nodeName}`))
|
|
31
51
|
|
|
32
|
-
const connectNodesViaBroadcastChannel = (nodeA: MeshNode, nodeB: MeshNode) =>
|
|
52
|
+
const connectNodesViaBroadcastChannel = (nodeA: MeshNode, nodeB: MeshNode, options?: { replaceIfExists?: boolean }) =>
|
|
33
53
|
Effect.gen(function* () {
|
|
34
54
|
// Need to instantiate two different channels because they filter out messages they sent themselves
|
|
35
55
|
const broadcastWebChannelA = yield* WebChannel.broadcastChannelWithAck({
|
|
36
56
|
channelName: `${nodeA.nodeName}↔${nodeB.nodeName}`,
|
|
37
|
-
|
|
38
|
-
sendSchema: Packet,
|
|
57
|
+
schema: Packet,
|
|
39
58
|
})
|
|
40
59
|
|
|
41
60
|
const broadcastWebChannelB = yield* WebChannel.broadcastChannelWithAck({
|
|
42
61
|
channelName: `${nodeA.nodeName}↔${nodeB.nodeName}`,
|
|
43
|
-
|
|
44
|
-
sendSchema: Packet,
|
|
62
|
+
schema: Packet,
|
|
45
63
|
})
|
|
46
64
|
|
|
47
|
-
yield* nodeA.addConnection({
|
|
48
|
-
|
|
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
|
+
})
|
|
49
75
|
}).pipe(Effect.withSpan(`connectNodesViaBroadcastChannel:${nodeA.nodeName}↔${nodeB.nodeName}`))
|
|
50
76
|
|
|
51
77
|
const createChannel = (source: MeshNode, target: string, options?: Partial<Parameters<MeshNode['makeChannel']>[0]>) =>
|
|
@@ -74,208 +100,302 @@ const maybeDelay =
|
|
|
74
100
|
? effect
|
|
75
101
|
: Effect.sleep(delay).pipe(Effect.withSpan(`${label}:delay(${delay})`), Effect.andThen(effect))
|
|
76
102
|
|
|
77
|
-
const testTimeout = IS_CI ? 30_000 :
|
|
103
|
+
const testTimeout = IS_CI ? 30_000 : 1000
|
|
104
|
+
const propTestTimeout = IS_CI ? 60_000 : 20_000
|
|
78
105
|
|
|
79
106
|
// TODO also make work without `Vitest.scopedLive` (i.e. with `Vitest.scoped`)
|
|
80
107
|
// probably requires controlling the clocks
|
|
81
108
|
Vitest.describe('webmesh node', { timeout: testTimeout }, () => {
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
}
|
|
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 }
|
|
105
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 })
|
|
106
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) =>
|
|
107
190
|
Vitest.scopedLive.prop(
|
|
108
|
-
// Vitest.scopedLive.only(
|
|
109
191
|
'a / b connect at different times with different channel types',
|
|
110
|
-
[Delay, Delay, Delay, ChannelType],
|
|
111
|
-
([
|
|
112
|
-
// (test) =>
|
|
192
|
+
[Delay, Delay, Delay, ChannelType, NodeNames],
|
|
193
|
+
([delayX, delayY, connectDelay, channelType, nodeNames], test) =>
|
|
113
194
|
Effect.gen(function* () {
|
|
114
|
-
//
|
|
115
|
-
// const delayB = 10
|
|
116
|
-
// const connectDelay = 10
|
|
117
|
-
// const channelType = 'message.prefer'
|
|
118
|
-
// console.log('delayA', delayA, 'delayB', delayB, 'connectDelay', connectDelay, 'channelType', channelType)
|
|
195
|
+
// console.log({ delayX, delayY, connectDelay, channelType, nodeNames })
|
|
119
196
|
|
|
120
|
-
const
|
|
121
|
-
const
|
|
197
|
+
const [nodeNameX, nodeNameY] = nodeNames
|
|
198
|
+
const nodeX = yield* makeMeshNode(nodeNameX)
|
|
199
|
+
const nodeY = yield* makeMeshNode(nodeNameY)
|
|
122
200
|
|
|
123
|
-
|
|
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
|
+
)
|
|
124
215
|
|
|
125
|
-
|
|
126
|
-
|
|
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 })
|
|
127
229
|
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
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
|
+
}
|
|
131
234
|
|
|
132
|
-
|
|
133
|
-
const
|
|
235
|
+
const nodeA = yield* makeMeshNode('A')
|
|
236
|
+
const nodeB = yield* makeMeshNode('B')
|
|
134
237
|
|
|
135
|
-
|
|
136
|
-
expect(yield* getFirstMessage(channelBToA)).toEqual({ message: 'A1' })
|
|
137
|
-
})
|
|
238
|
+
const { mode, connectNodes } = fromChannelType(channelType)
|
|
138
239
|
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
connectNodes(nodeA, nodeB).pipe(maybeDelay(connectDelay, 'connectNodes')),
|
|
142
|
-
nodeACode.pipe(maybeDelay(delayA, 'nodeACode')),
|
|
143
|
-
nodeBCode.pipe(maybeDelay(delayB, 'nodeBCode')),
|
|
144
|
-
],
|
|
145
|
-
{ concurrency: 'unbounded' },
|
|
146
|
-
)
|
|
147
|
-
}).pipe(
|
|
148
|
-
withCtx(test, { skipOtel: true, suffix: `delayA=${delayA} delayB=${delayB} channelType=${channelType}` }),
|
|
149
|
-
),
|
|
150
|
-
)
|
|
240
|
+
// TODO also optionally delay the connection
|
|
241
|
+
yield* connectNodes(nodeA, nodeB)
|
|
151
242
|
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
// (test) =>
|
|
155
|
-
Vitest.scopedLive.prop(
|
|
156
|
-
'b reconnects',
|
|
157
|
-
[Delay, Delay, ChannelType],
|
|
158
|
-
([waitForOfflineDelay, sleepDelay, channelType], test) =>
|
|
159
|
-
Effect.gen(function* () {
|
|
160
|
-
// const waitForOfflineDelay = 0
|
|
161
|
-
// const sleepDelay = 10
|
|
162
|
-
// const channelType = 'proxy'
|
|
163
|
-
// console.log(
|
|
164
|
-
// 'waitForOfflineDelay',
|
|
165
|
-
// waitForOfflineDelay,
|
|
166
|
-
// 'sleepDelay',
|
|
167
|
-
// sleepDelay,
|
|
168
|
-
// 'channelType',
|
|
169
|
-
// channelType,
|
|
170
|
-
// )
|
|
171
|
-
|
|
172
|
-
const nodeA = yield* makeMeshNode('A')
|
|
173
|
-
const nodeB = yield* makeMeshNode('B')
|
|
243
|
+
const waitForBToBeOffline =
|
|
244
|
+
waitForOfflineDelay === undefined ? undefined : yield* Deferred.make<void, never>()
|
|
174
245
|
|
|
175
|
-
|
|
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' })
|
|
176
250
|
|
|
177
|
-
|
|
178
|
-
|
|
251
|
+
console.log('nodeACode:waiting for B to be offline')
|
|
252
|
+
if (waitForBToBeOffline !== undefined) {
|
|
253
|
+
yield* waitForBToBeOffline
|
|
254
|
+
}
|
|
179
255
|
|
|
180
|
-
|
|
181
|
-
|
|
256
|
+
yield* channelAToB.send({ message: 'A2' })
|
|
257
|
+
expect(yield* getFirstMessage(channelAToB)).toEqual({ message: 'B2' })
|
|
258
|
+
})
|
|
182
259
|
|
|
183
|
-
|
|
184
|
-
|
|
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 })
|
|
185
266
|
|
|
186
|
-
|
|
187
|
-
|
|
267
|
+
yield* channelBToA.send({ message: 'B1' })
|
|
268
|
+
expect(yield* getFirstMessage(channelBToA)).toEqual({ message: 'A1' })
|
|
269
|
+
}).pipe(Effect.scoped, Effect.withSpan('nodeBCode:part1'))
|
|
188
270
|
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
271
|
+
console.log('nodeBCode:B node going offline')
|
|
272
|
+
if (waitForBToBeOffline !== undefined) {
|
|
273
|
+
yield* Deferred.succeed(waitForBToBeOffline, void 0)
|
|
274
|
+
}
|
|
192
275
|
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
276
|
+
if (sleepDelay !== undefined) {
|
|
277
|
+
yield* Effect.sleep(sleepDelay).pipe(Effect.withSpan(`B:sleep(${sleepDelay})`))
|
|
278
|
+
}
|
|
196
279
|
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
const channelBToA = yield* createChannel(nodeB, 'A', { mode })
|
|
280
|
+
// Recreating the channel
|
|
281
|
+
yield* Effect.gen(function* () {
|
|
282
|
+
const channelBToA = yield* createChannel(nodeB, 'A', { mode })
|
|
201
283
|
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
284
|
+
yield* channelBToA.send({ message: 'B2' })
|
|
285
|
+
expect(yield* getFirstMessage(channelBToA)).toEqual({ message: 'A2' })
|
|
286
|
+
}).pipe(Effect.scoped, Effect.withSpan('nodeBCode:part2'))
|
|
287
|
+
})
|
|
205
288
|
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
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
|
+
}
|
|
209
299
|
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
300
|
+
Vitest.scopedLive('reconnect with re-created node', (test) =>
|
|
301
|
+
Effect.gen(function* () {
|
|
302
|
+
const nodeBgen1Scope = yield* Scope.make()
|
|
213
303
|
|
|
214
|
-
|
|
215
|
-
|
|
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')
|
|
216
322
|
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
323
|
+
yield* channelBToA.send({ message: 'B1' })
|
|
324
|
+
expect(yield* getFirstMessage(channelBToA)).toEqual({ message: 'A1' })
|
|
325
|
+
// expect(channelBToA.debugInfo.connectCounter).toBe(1)
|
|
220
326
|
})
|
|
221
327
|
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
)
|
|
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)),
|
|
229
341
|
)
|
|
230
342
|
|
|
231
343
|
const ChannelTypeWithoutMessageChannelProxy = Schema.Literal('proxy', 'messagechannel')
|
|
232
344
|
Vitest.scopedLive.prop(
|
|
233
345
|
'replace connection while keeping the channel',
|
|
234
|
-
[ChannelTypeWithoutMessageChannelProxy],
|
|
235
|
-
([channelType], test) =>
|
|
346
|
+
[ChannelTypeWithoutMessageChannelProxy, NodeNames],
|
|
347
|
+
([channelType, nodeNames], test) =>
|
|
236
348
|
Effect.gen(function* () {
|
|
237
|
-
const
|
|
238
|
-
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 }
|
|
239
353
|
|
|
240
354
|
const { mode, connectNodes } = fromChannelType(channelType)
|
|
241
355
|
|
|
242
|
-
yield* connectNodes(
|
|
356
|
+
yield* connectNodes(nodeX, nodeY)
|
|
243
357
|
|
|
244
358
|
const waitForConnectionReplacement = yield* Deferred.make<void>()
|
|
245
359
|
|
|
246
|
-
const
|
|
247
|
-
const
|
|
360
|
+
const nodeXCode = Effect.gen(function* () {
|
|
361
|
+
const channelXToY = yield* createChannel(nodeX, nodeLabel.y, { mode })
|
|
248
362
|
|
|
249
|
-
yield*
|
|
250
|
-
expect(yield* getFirstMessage(
|
|
363
|
+
yield* channelXToY.send({ message: `${nodeLabel.x}1` })
|
|
364
|
+
expect(yield* getFirstMessage(channelXToY)).toEqual({ message: `${nodeLabel.y}1` })
|
|
251
365
|
|
|
252
366
|
yield* waitForConnectionReplacement
|
|
253
367
|
|
|
254
|
-
yield*
|
|
255
|
-
expect(yield* getFirstMessage(
|
|
368
|
+
yield* channelXToY.send({ message: `${nodeLabel.x}2` })
|
|
369
|
+
expect(yield* getFirstMessage(channelXToY)).toEqual({ message: `${nodeLabel.y}2` })
|
|
256
370
|
})
|
|
257
371
|
|
|
258
|
-
const
|
|
259
|
-
const
|
|
372
|
+
const nodeYCode = Effect.gen(function* () {
|
|
373
|
+
const channelYToX = yield* createChannel(nodeY, nodeLabel.x, { mode })
|
|
260
374
|
|
|
261
|
-
yield*
|
|
262
|
-
expect(yield* getFirstMessage(
|
|
375
|
+
yield* channelYToX.send({ message: `${nodeLabel.y}1` })
|
|
376
|
+
expect(yield* getFirstMessage(channelYToX)).toEqual({ message: `${nodeLabel.x}1` })
|
|
263
377
|
|
|
264
378
|
// Switch out connection while keeping the channel
|
|
265
|
-
yield*
|
|
266
|
-
yield*
|
|
267
|
-
yield* connectNodes(
|
|
379
|
+
yield* nodeX.removeConnection(nodeLabel.y)
|
|
380
|
+
yield* nodeY.removeConnection(nodeLabel.x)
|
|
381
|
+
yield* connectNodes(nodeX, nodeY)
|
|
268
382
|
yield* Deferred.succeed(waitForConnectionReplacement, void 0)
|
|
269
383
|
|
|
270
|
-
yield*
|
|
271
|
-
expect(yield* getFirstMessage(
|
|
384
|
+
yield* channelYToX.send({ message: `${nodeLabel.y}2` })
|
|
385
|
+
expect(yield* getFirstMessage(channelYToX)).toEqual({ message: `${nodeLabel.x}2` })
|
|
272
386
|
})
|
|
273
387
|
|
|
274
|
-
yield* Effect.all([
|
|
275
|
-
}).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 } },
|
|
276
396
|
)
|
|
277
397
|
|
|
278
|
-
Vitest.describe
|
|
398
|
+
Vitest.describe('TODO improve latency', () => {
|
|
279
399
|
// TODO we need to improve latency when sending messages concurrently
|
|
280
400
|
Vitest.scopedLive.prop(
|
|
281
401
|
'concurrent messages',
|
|
@@ -322,11 +442,52 @@ Vitest.describe('webmesh node', { timeout: testTimeout }, () => {
|
|
|
322
442
|
yield* Effect.all([nodeACode, nodeBCode, connectNodes(nodeA, nodeB).pipe(Effect.delay(100))], {
|
|
323
443
|
concurrency: 'unbounded',
|
|
324
444
|
})
|
|
325
|
-
}).pipe(
|
|
445
|
+
}).pipe(
|
|
446
|
+
withCtx(test, {
|
|
447
|
+
skipOtel: true,
|
|
448
|
+
suffix: `channelType=${channelType} count=${count}`,
|
|
449
|
+
timeout: testTimeout * 2,
|
|
450
|
+
}),
|
|
451
|
+
),
|
|
452
|
+
{ fastCheck: { numRuns: 10 } },
|
|
326
453
|
)
|
|
327
454
|
})
|
|
328
455
|
})
|
|
329
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
|
+
|
|
330
491
|
Vitest.scopedLive('manual debug test', (test) =>
|
|
331
492
|
Effect.gen(function* () {
|
|
332
493
|
const nodeA = yield* makeMeshNode('A')
|
|
@@ -384,6 +545,7 @@ Vitest.describe('webmesh node', { timeout: testTimeout }, () => {
|
|
|
384
545
|
yield* channelAToC.send({ message: 'A1' })
|
|
385
546
|
expect(yield* getFirstMessage(channelAToC)).toEqual({ message: 'C1' })
|
|
386
547
|
expect(yield* getFirstMessage(channelAToC)).toEqual({ message: 'C2' })
|
|
548
|
+
expect(yield* getFirstMessage(channelAToC)).toEqual({ message: 'C3' })
|
|
387
549
|
})
|
|
388
550
|
|
|
389
551
|
const nodeCCode = Effect.gen(function* () {
|
|
@@ -422,9 +584,14 @@ Vitest.describe('webmesh node', { timeout: testTimeout }, () => {
|
|
|
422
584
|
expect(yield* getFirstMessage(channelCToA)).toEqual({ message: 'A1' })
|
|
423
585
|
})
|
|
424
586
|
|
|
425
|
-
yield* Effect.all(
|
|
426
|
-
|
|
427
|
-
|
|
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
|
+
)
|
|
428
595
|
}).pipe(withCtx(test)),
|
|
429
596
|
)
|
|
430
597
|
|
|
@@ -453,7 +620,7 @@ Vitest.describe('webmesh node', { timeout: testTimeout }, () => {
|
|
|
453
620
|
}).pipe(withCtx(test)),
|
|
454
621
|
)
|
|
455
622
|
|
|
456
|
-
Vitest.scopedLive('should fail', (test) =>
|
|
623
|
+
Vitest.scopedLive('should fail with timeout due to missing connection', (test) =>
|
|
457
624
|
Effect.gen(function* () {
|
|
458
625
|
const nodeA = yield* makeMeshNode('A')
|
|
459
626
|
const nodeB = yield* makeMeshNode('B')
|
|
@@ -475,6 +642,106 @@ Vitest.describe('webmesh node', { timeout: testTimeout }, () => {
|
|
|
475
642
|
yield* Effect.all([nodeACode, nodeCCode], { concurrency: 'unbounded' })
|
|
476
643
|
}).pipe(withCtx(test)),
|
|
477
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
|
+
)
|
|
478
745
|
})
|
|
479
746
|
|
|
480
747
|
Vitest.describe('mixture of messagechannel and proxy connections', () => {
|
|
@@ -492,28 +759,28 @@ Vitest.describe('webmesh node', { timeout: testTimeout }, () => {
|
|
|
492
759
|
}).pipe(withCtx(test)),
|
|
493
760
|
)
|
|
494
761
|
|
|
495
|
-
|
|
496
|
-
Vitest.scopedLive.skip('should work for messagechannels', (test) =>
|
|
762
|
+
Vitest.scopedLive('should work for messagechannels', (test) =>
|
|
497
763
|
Effect.gen(function* () {
|
|
498
764
|
const nodeA = yield* makeMeshNode('A')
|
|
499
765
|
const nodeB = yield* makeMeshNode('B')
|
|
766
|
+
const nodeC = yield* makeMeshNode('C')
|
|
500
767
|
|
|
501
768
|
yield* connectNodesViaMessageChannel(nodeB, nodeA)
|
|
502
|
-
yield* connectNodesViaBroadcastChannel(
|
|
769
|
+
yield* connectNodesViaBroadcastChannel(nodeB, nodeC)
|
|
503
770
|
|
|
504
771
|
const nodeACode = Effect.gen(function* () {
|
|
505
|
-
const
|
|
506
|
-
yield*
|
|
507
|
-
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' })
|
|
508
775
|
})
|
|
509
776
|
|
|
510
|
-
const
|
|
511
|
-
const
|
|
512
|
-
yield*
|
|
513
|
-
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' })
|
|
514
781
|
})
|
|
515
782
|
|
|
516
|
-
yield* Effect.all([nodeACode,
|
|
783
|
+
yield* Effect.all([nodeACode, nodeCCode], { concurrency: 'unbounded' })
|
|
517
784
|
}).pipe(withCtx(test)),
|
|
518
785
|
)
|
|
519
786
|
})
|
|
@@ -522,10 +789,13 @@ Vitest.describe('webmesh node', { timeout: testTimeout }, () => {
|
|
|
522
789
|
const otelLayer = IS_CI ? Layer.empty : OtelLiveHttp({ serviceName: 'webmesh-node-test', skipLogUrl: false })
|
|
523
790
|
|
|
524
791
|
const withCtx =
|
|
525
|
-
(
|
|
792
|
+
(
|
|
793
|
+
testContext: Vitest.TaskContext,
|
|
794
|
+
{ suffix, skipOtel = false, timeout = testTimeout }: { suffix?: string; skipOtel?: boolean; timeout?: number } = {},
|
|
795
|
+
) =>
|
|
526
796
|
<A, E, R>(self: Effect.Effect<A, E, R>) =>
|
|
527
797
|
self.pipe(
|
|
528
|
-
Effect.timeout(
|
|
798
|
+
Effect.timeout(timeout),
|
|
529
799
|
Effect.provide(Logger.pretty),
|
|
530
800
|
Effect.scoped, // We need to scope the effect manually here because otherwise the span is not closed
|
|
531
801
|
Effect.withSpan(`${testContext.task.suite?.name}:${testContext.task.name}${suffix ? `:${suffix}` : ''}`),
|