@livestore/webmesh 0.0.0-snapshot-1d99fea7d2ce2c7a5d9ed0a3752f8a7bda6bc3db → 0.0.0-snapshot-aed277ba0960f72b8d464508961ab4aec1881230
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 +19 -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 +202 -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 +125 -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 +39 -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 +256 -124
- 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 +337 -0
- package/src/channel/message-channel.ts +177 -308
- 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 +367 -150
- package/src/node.ts +68 -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,16 +100,21 @@ 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
109
|
Vitest.describe('A <> B', () => {
|
|
83
|
-
Vitest.describe('prop tests', () => {
|
|
110
|
+
Vitest.describe('prop tests', { timeout: propTestTimeout }, () => {
|
|
84
111
|
const Delay = Schema.UndefinedOr(Schema.Literal(0, 1, 10, 50))
|
|
85
112
|
// NOTE for message channels, we test both with and without transferables (i.e. proxying)
|
|
86
113
|
const ChannelType = Schema.Literal('messagechannel', 'messagechannel.proxy', 'proxy')
|
|
114
|
+
const NodeNames = Schema.Union(
|
|
115
|
+
Schema.Tuple(Schema.Literal('A'), Schema.Literal('B')),
|
|
116
|
+
Schema.Tuple(Schema.Literal('B'), Schema.Literal('A')),
|
|
117
|
+
)
|
|
87
118
|
|
|
88
119
|
const fromChannelType = (
|
|
89
120
|
channelType: typeof ChannelType.Type,
|
|
@@ -104,178 +135,268 @@ Vitest.describe('webmesh node', { timeout: testTimeout }, () => {
|
|
|
104
135
|
}
|
|
105
136
|
}
|
|
106
137
|
|
|
138
|
+
const exchangeMessages = ({
|
|
139
|
+
nodeX,
|
|
140
|
+
nodeY,
|
|
141
|
+
channelType,
|
|
142
|
+
// numberOfMessages = 1,
|
|
143
|
+
delays,
|
|
144
|
+
}: {
|
|
145
|
+
nodeX: MeshNode
|
|
146
|
+
nodeY: MeshNode
|
|
147
|
+
channelType: 'messagechannel' | 'proxy' | 'messagechannel.proxy'
|
|
148
|
+
numberOfMessages?: number
|
|
149
|
+
delays?: { x?: number; y?: number; connect?: number }
|
|
150
|
+
}) =>
|
|
151
|
+
Effect.gen(function* () {
|
|
152
|
+
const nodeLabel = { x: nodeX.nodeName, y: nodeY.nodeName }
|
|
153
|
+
const { mode, connectNodes } = fromChannelType(channelType)
|
|
154
|
+
|
|
155
|
+
const nodeXCode = Effect.gen(function* () {
|
|
156
|
+
const channelXToY = yield* createChannel(nodeX, nodeY.nodeName, { mode })
|
|
157
|
+
|
|
158
|
+
yield* channelXToY.send({ message: `${nodeLabel.x}1` })
|
|
159
|
+
// console.log('channelXToY', channelXToY.debugInfo)
|
|
160
|
+
expect(yield* getFirstMessage(channelXToY)).toEqual({ message: `${nodeLabel.y}1` })
|
|
161
|
+
// expect(channelXToY.debugInfo.connectCounter).toBe(1)
|
|
162
|
+
})
|
|
163
|
+
|
|
164
|
+
const nodeYCode = Effect.gen(function* () {
|
|
165
|
+
const channelYToX = yield* createChannel(nodeY, nodeX.nodeName, { mode })
|
|
166
|
+
|
|
167
|
+
yield* channelYToX.send({ message: `${nodeLabel.y}1` })
|
|
168
|
+
// console.log('channelYToX', channelYToX.debugInfo)
|
|
169
|
+
expect(yield* getFirstMessage(channelYToX)).toEqual({ message: `${nodeLabel.x}1` })
|
|
170
|
+
// expect(channelYToX.debugInfo.connectCounter).toBe(1)
|
|
171
|
+
})
|
|
172
|
+
|
|
173
|
+
yield* Effect.all(
|
|
174
|
+
[
|
|
175
|
+
connectNodes(nodeX, nodeY).pipe(maybeDelay(delays?.connect, 'connectNodes')),
|
|
176
|
+
nodeXCode.pipe(maybeDelay(delays?.x, `node${nodeLabel.x}Code`)),
|
|
177
|
+
nodeYCode.pipe(maybeDelay(delays?.y, `node${nodeLabel.y}Code`)),
|
|
178
|
+
],
|
|
179
|
+
{ concurrency: 'unbounded' },
|
|
180
|
+
).pipe(Effect.withSpan(`exchangeMessages(${nodeLabel.x}↔${nodeLabel.y})`))
|
|
181
|
+
})
|
|
182
|
+
|
|
183
|
+
// const delayX = 40
|
|
184
|
+
// const delayY = undefined
|
|
185
|
+
// const connectDelay = undefined
|
|
186
|
+
// const channelType = 'messagechannel'
|
|
187
|
+
// const nodeNames = ['B', 'A'] as const
|
|
188
|
+
// Vitest.scopedLive(
|
|
189
|
+
// 'a / b connect at different times with different channel types',
|
|
190
|
+
// (test) =>
|
|
107
191
|
Vitest.scopedLive.prop(
|
|
108
|
-
// Vitest.scopedLive.only(
|
|
109
192
|
'a / b connect at different times with different channel types',
|
|
110
|
-
[Delay, Delay, Delay, ChannelType],
|
|
111
|
-
([
|
|
112
|
-
// (test) =>
|
|
193
|
+
[Delay, Delay, Delay, ChannelType, NodeNames],
|
|
194
|
+
([delayX, delayY, connectDelay, channelType, nodeNames], test) =>
|
|
113
195
|
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)
|
|
196
|
+
// console.log({ delayX, delayY, connectDelay, channelType, nodeNames })
|
|
119
197
|
|
|
120
|
-
const
|
|
121
|
-
const
|
|
198
|
+
const [nodeNameX, nodeNameY] = nodeNames
|
|
199
|
+
const nodeX = yield* makeMeshNode(nodeNameX)
|
|
200
|
+
const nodeY = yield* makeMeshNode(nodeNameY)
|
|
122
201
|
|
|
123
|
-
|
|
202
|
+
yield* exchangeMessages({
|
|
203
|
+
nodeX,
|
|
204
|
+
nodeY,
|
|
205
|
+
channelType,
|
|
206
|
+
delays: { x: delayX, y: delayY, connect: connectDelay },
|
|
207
|
+
})
|
|
208
|
+
}).pipe(
|
|
209
|
+
withCtx(test, {
|
|
210
|
+
skipOtel: true,
|
|
211
|
+
suffix: `delayX=${delayX} delayY=${delayY} connectDelay=${connectDelay} channelType=${channelType} nodeNames=${nodeNames}`,
|
|
212
|
+
}),
|
|
213
|
+
),
|
|
214
|
+
// { fastCheck: { numRuns: 20 } },
|
|
215
|
+
)
|
|
124
216
|
|
|
125
|
-
|
|
126
|
-
|
|
217
|
+
{
|
|
218
|
+
// const waitForOfflineDelay = undefined
|
|
219
|
+
// const sleepDelay = 0
|
|
220
|
+
// const channelType = 'messagechannel'
|
|
221
|
+
// Vitest.scopedLive(
|
|
222
|
+
// 'b reconnects',
|
|
223
|
+
// (test) =>
|
|
224
|
+
Vitest.scopedLive.prop(
|
|
225
|
+
'b reconnects',
|
|
226
|
+
[Delay, Delay, ChannelType],
|
|
227
|
+
([waitForOfflineDelay, sleepDelay, channelType], test) =>
|
|
228
|
+
Effect.gen(function* () {
|
|
229
|
+
// console.log({ waitForOfflineDelay, sleepDelay, channelType })
|
|
127
230
|
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
231
|
+
if (waitForOfflineDelay === undefined) {
|
|
232
|
+
// TODO we still need to fix this scenario but it shouldn't really be common in practice
|
|
233
|
+
return
|
|
234
|
+
}
|
|
131
235
|
|
|
132
|
-
|
|
133
|
-
const
|
|
236
|
+
const nodeA = yield* makeMeshNode('A')
|
|
237
|
+
const nodeB = yield* makeMeshNode('B')
|
|
134
238
|
|
|
135
|
-
|
|
136
|
-
expect(yield* getFirstMessage(channelBToA)).toEqual({ message: 'A1' })
|
|
137
|
-
})
|
|
239
|
+
const { mode, connectNodes } = fromChannelType(channelType)
|
|
138
240
|
|
|
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
|
-
)
|
|
241
|
+
// TODO also optionally delay the connection
|
|
242
|
+
yield* connectNodes(nodeA, nodeB)
|
|
151
243
|
|
|
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')
|
|
244
|
+
const waitForBToBeOffline =
|
|
245
|
+
waitForOfflineDelay === undefined ? undefined : yield* Deferred.make<void, never>()
|
|
174
246
|
|
|
175
|
-
|
|
247
|
+
const nodeACode = Effect.gen(function* () {
|
|
248
|
+
const channelAToB = yield* createChannel(nodeA, 'B', { mode })
|
|
249
|
+
yield* channelAToB.send({ message: 'A1' })
|
|
250
|
+
expect(yield* getFirstMessage(channelAToB)).toEqual({ message: 'B1' })
|
|
176
251
|
|
|
177
|
-
|
|
178
|
-
|
|
252
|
+
console.log('nodeACode:waiting for B to be offline')
|
|
253
|
+
if (waitForBToBeOffline !== undefined) {
|
|
254
|
+
yield* waitForBToBeOffline
|
|
255
|
+
}
|
|
179
256
|
|
|
180
|
-
|
|
181
|
-
|
|
257
|
+
yield* channelAToB.send({ message: 'A2' })
|
|
258
|
+
expect(yield* getFirstMessage(channelAToB)).toEqual({ message: 'B2' })
|
|
259
|
+
})
|
|
182
260
|
|
|
183
|
-
|
|
184
|
-
|
|
261
|
+
// Simulating node b going offline and then coming back online
|
|
262
|
+
// This test also illustrates why we need a ack-message channel since otherwise
|
|
263
|
+
// sent messages might get lost
|
|
264
|
+
const nodeBCode = Effect.gen(function* () {
|
|
265
|
+
yield* Effect.gen(function* () {
|
|
266
|
+
const channelBToA = yield* createChannel(nodeB, 'A', { mode })
|
|
185
267
|
|
|
186
|
-
|
|
187
|
-
|
|
268
|
+
yield* channelBToA.send({ message: 'B1' })
|
|
269
|
+
expect(yield* getFirstMessage(channelBToA)).toEqual({ message: 'A1' })
|
|
270
|
+
}).pipe(Effect.scoped, Effect.withSpan('nodeBCode:part1'))
|
|
188
271
|
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
272
|
+
console.log('nodeBCode:B node going offline')
|
|
273
|
+
if (waitForBToBeOffline !== undefined) {
|
|
274
|
+
yield* Deferred.succeed(waitForBToBeOffline, void 0)
|
|
275
|
+
}
|
|
192
276
|
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
277
|
+
if (sleepDelay !== undefined) {
|
|
278
|
+
yield* Effect.sleep(sleepDelay).pipe(Effect.withSpan(`B:sleep(${sleepDelay})`))
|
|
279
|
+
}
|
|
196
280
|
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
const channelBToA = yield* createChannel(nodeB, 'A', { mode })
|
|
281
|
+
// Recreating the channel
|
|
282
|
+
yield* Effect.gen(function* () {
|
|
283
|
+
const channelBToA = yield* createChannel(nodeB, 'A', { mode })
|
|
201
284
|
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
285
|
+
yield* channelBToA.send({ message: 'B2' })
|
|
286
|
+
expect(yield* getFirstMessage(channelBToA)).toEqual({ message: 'A2' })
|
|
287
|
+
}).pipe(Effect.scoped, Effect.withSpan('nodeBCode:part2'))
|
|
288
|
+
})
|
|
205
289
|
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
290
|
+
yield* Effect.all([nodeACode, nodeBCode], { concurrency: 'unbounded' }).pipe(Effect.withSpan('test'))
|
|
291
|
+
}).pipe(
|
|
292
|
+
withCtx(test, {
|
|
293
|
+
skipOtel: true,
|
|
294
|
+
suffix: `waitForOfflineDelay=${waitForOfflineDelay} sleepDelay=${sleepDelay} channelType=${channelType}`,
|
|
295
|
+
}),
|
|
296
|
+
),
|
|
297
|
+
{ fastCheck: { numRuns: 20 } },
|
|
298
|
+
)
|
|
299
|
+
}
|
|
209
300
|
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
301
|
+
Vitest.scopedLive('reconnect with re-created node', (test) =>
|
|
302
|
+
Effect.gen(function* () {
|
|
303
|
+
const nodeBgen1Scope = yield* Scope.make()
|
|
213
304
|
|
|
214
|
-
|
|
215
|
-
|
|
305
|
+
const nodeA = yield* makeMeshNode('A')
|
|
306
|
+
const nodeBgen1 = yield* makeMeshNode('B').pipe(Scope.extend(nodeBgen1Scope))
|
|
216
307
|
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
308
|
+
yield* connectNodesViaMessageChannel(nodeA, nodeBgen1).pipe(Scope.extend(nodeBgen1Scope))
|
|
309
|
+
|
|
310
|
+
// yield* Effect.sleep(100)
|
|
311
|
+
|
|
312
|
+
const channelAToBOnce = yield* Effect.cached(createChannel(nodeA, 'B'))
|
|
313
|
+
const nodeACode = Effect.gen(function* () {
|
|
314
|
+
const channelAToB = yield* channelAToBOnce
|
|
315
|
+
yield* channelAToB.send({ message: 'A1' })
|
|
316
|
+
expect(yield* getFirstMessage(channelAToB)).toEqual({ message: 'B1' })
|
|
317
|
+
// expect(channelAToB.debugInfo.connectCounter).toBe(1)
|
|
318
|
+
})
|
|
319
|
+
|
|
320
|
+
const nodeBCode = (nodeB: MeshNode) =>
|
|
321
|
+
Effect.gen(function* () {
|
|
322
|
+
const channelBToA = yield* createChannel(nodeB, 'A')
|
|
323
|
+
|
|
324
|
+
yield* channelBToA.send({ message: 'B1' })
|
|
325
|
+
expect(yield* getFirstMessage(channelBToA)).toEqual({ message: 'A1' })
|
|
326
|
+
// expect(channelBToA.debugInfo.connectCounter).toBe(1)
|
|
220
327
|
})
|
|
221
328
|
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
)
|
|
329
|
+
yield* Effect.all([nodeACode, nodeBCode(nodeBgen1).pipe(Scope.extend(nodeBgen1Scope))], {
|
|
330
|
+
concurrency: 'unbounded',
|
|
331
|
+
}).pipe(Effect.withSpan('test1'))
|
|
332
|
+
|
|
333
|
+
yield* Scope.close(nodeBgen1Scope, Exit.void)
|
|
334
|
+
|
|
335
|
+
const nodeBgen2 = yield* makeMeshNode('B')
|
|
336
|
+
yield* connectNodesViaMessageChannel(nodeA, nodeBgen2, { replaceIfExists: true })
|
|
337
|
+
|
|
338
|
+
yield* Effect.all([nodeACode, nodeBCode(nodeBgen2)], { concurrency: 'unbounded' }).pipe(
|
|
339
|
+
Effect.withSpan('test2'),
|
|
340
|
+
)
|
|
341
|
+
}).pipe(withCtx(test)),
|
|
229
342
|
)
|
|
230
343
|
|
|
231
344
|
const ChannelTypeWithoutMessageChannelProxy = Schema.Literal('proxy', 'messagechannel')
|
|
232
345
|
Vitest.scopedLive.prop(
|
|
233
346
|
'replace connection while keeping the channel',
|
|
234
|
-
[ChannelTypeWithoutMessageChannelProxy],
|
|
235
|
-
([channelType], test) =>
|
|
347
|
+
[ChannelTypeWithoutMessageChannelProxy, NodeNames],
|
|
348
|
+
([channelType, nodeNames], test) =>
|
|
236
349
|
Effect.gen(function* () {
|
|
237
|
-
const
|
|
238
|
-
const
|
|
350
|
+
const [nodeNameX, nodeNameY] = nodeNames
|
|
351
|
+
const nodeX = yield* makeMeshNode(nodeNameX)
|
|
352
|
+
const nodeY = yield* makeMeshNode(nodeNameY)
|
|
353
|
+
const nodeLabel = { x: nodeX.nodeName, y: nodeY.nodeName }
|
|
239
354
|
|
|
240
355
|
const { mode, connectNodes } = fromChannelType(channelType)
|
|
241
356
|
|
|
242
|
-
yield* connectNodes(
|
|
357
|
+
yield* connectNodes(nodeX, nodeY)
|
|
243
358
|
|
|
244
359
|
const waitForConnectionReplacement = yield* Deferred.make<void>()
|
|
245
360
|
|
|
246
|
-
const
|
|
247
|
-
const
|
|
361
|
+
const nodeXCode = Effect.gen(function* () {
|
|
362
|
+
const channelXToY = yield* createChannel(nodeX, nodeLabel.y, { mode })
|
|
248
363
|
|
|
249
|
-
yield*
|
|
250
|
-
expect(yield* getFirstMessage(
|
|
364
|
+
yield* channelXToY.send({ message: `${nodeLabel.x}1` })
|
|
365
|
+
expect(yield* getFirstMessage(channelXToY)).toEqual({ message: `${nodeLabel.y}1` })
|
|
251
366
|
|
|
252
367
|
yield* waitForConnectionReplacement
|
|
253
368
|
|
|
254
|
-
yield*
|
|
255
|
-
expect(yield* getFirstMessage(
|
|
369
|
+
yield* channelXToY.send({ message: `${nodeLabel.x}2` })
|
|
370
|
+
expect(yield* getFirstMessage(channelXToY)).toEqual({ message: `${nodeLabel.y}2` })
|
|
256
371
|
})
|
|
257
372
|
|
|
258
|
-
const
|
|
259
|
-
const
|
|
373
|
+
const nodeYCode = Effect.gen(function* () {
|
|
374
|
+
const channelYToX = yield* createChannel(nodeY, nodeLabel.x, { mode })
|
|
260
375
|
|
|
261
|
-
yield*
|
|
262
|
-
expect(yield* getFirstMessage(
|
|
376
|
+
yield* channelYToX.send({ message: `${nodeLabel.y}1` })
|
|
377
|
+
expect(yield* getFirstMessage(channelYToX)).toEqual({ message: `${nodeLabel.x}1` })
|
|
263
378
|
|
|
264
379
|
// Switch out connection while keeping the channel
|
|
265
|
-
yield*
|
|
266
|
-
yield*
|
|
267
|
-
yield* connectNodes(
|
|
380
|
+
yield* nodeX.removeConnection(nodeLabel.y)
|
|
381
|
+
yield* nodeY.removeConnection(nodeLabel.x)
|
|
382
|
+
yield* connectNodes(nodeX, nodeY)
|
|
268
383
|
yield* Deferred.succeed(waitForConnectionReplacement, void 0)
|
|
269
384
|
|
|
270
|
-
yield*
|
|
271
|
-
expect(yield* getFirstMessage(
|
|
385
|
+
yield* channelYToX.send({ message: `${nodeLabel.y}2` })
|
|
386
|
+
expect(yield* getFirstMessage(channelYToX)).toEqual({ message: `${nodeLabel.x}2` })
|
|
272
387
|
})
|
|
273
388
|
|
|
274
|
-
yield* Effect.all([
|
|
275
|
-
}).pipe(
|
|
389
|
+
yield* Effect.all([nodeXCode, nodeYCode], { concurrency: 'unbounded' })
|
|
390
|
+
}).pipe(
|
|
391
|
+
withCtx(test, {
|
|
392
|
+
skipOtel: true,
|
|
393
|
+
suffix: `channelType=${channelType} nodeNames=${nodeNames}`,
|
|
394
|
+
}),
|
|
395
|
+
),
|
|
396
|
+
{ fastCheck: { numRuns: 10 } },
|
|
276
397
|
)
|
|
277
398
|
|
|
278
|
-
Vitest.describe
|
|
399
|
+
Vitest.describe('TODO improve latency', () => {
|
|
279
400
|
// TODO we need to improve latency when sending messages concurrently
|
|
280
401
|
Vitest.scopedLive.prop(
|
|
281
402
|
'concurrent messages',
|
|
@@ -322,7 +443,14 @@ Vitest.describe('webmesh node', { timeout: testTimeout }, () => {
|
|
|
322
443
|
yield* Effect.all([nodeACode, nodeBCode, connectNodes(nodeA, nodeB).pipe(Effect.delay(100))], {
|
|
323
444
|
concurrency: 'unbounded',
|
|
324
445
|
})
|
|
325
|
-
}).pipe(
|
|
446
|
+
}).pipe(
|
|
447
|
+
withCtx(test, {
|
|
448
|
+
skipOtel: true,
|
|
449
|
+
suffix: `channelType=${channelType} count=${count}`,
|
|
450
|
+
timeout: testTimeout * 2,
|
|
451
|
+
}),
|
|
452
|
+
),
|
|
453
|
+
{ fastCheck: { numRuns: 10 } },
|
|
326
454
|
)
|
|
327
455
|
})
|
|
328
456
|
})
|
|
@@ -384,6 +512,7 @@ Vitest.describe('webmesh node', { timeout: testTimeout }, () => {
|
|
|
384
512
|
yield* channelAToC.send({ message: 'A1' })
|
|
385
513
|
expect(yield* getFirstMessage(channelAToC)).toEqual({ message: 'C1' })
|
|
386
514
|
expect(yield* getFirstMessage(channelAToC)).toEqual({ message: 'C2' })
|
|
515
|
+
expect(yield* getFirstMessage(channelAToC)).toEqual({ message: 'C3' })
|
|
387
516
|
})
|
|
388
517
|
|
|
389
518
|
const nodeCCode = Effect.gen(function* () {
|
|
@@ -422,9 +551,14 @@ Vitest.describe('webmesh node', { timeout: testTimeout }, () => {
|
|
|
422
551
|
expect(yield* getFirstMessage(channelCToA)).toEqual({ message: 'A1' })
|
|
423
552
|
})
|
|
424
553
|
|
|
425
|
-
yield* Effect.all(
|
|
426
|
-
|
|
427
|
-
|
|
554
|
+
yield* Effect.all(
|
|
555
|
+
[
|
|
556
|
+
nodeACode,
|
|
557
|
+
nodeCCode,
|
|
558
|
+
connectNodes(nodeB, nodeC).pipe(Effect.delay(100), Effect.withSpan('connect-nodeB-nodeC-delay(100)')),
|
|
559
|
+
],
|
|
560
|
+
{ concurrency: 'unbounded' },
|
|
561
|
+
)
|
|
428
562
|
}).pipe(withCtx(test)),
|
|
429
563
|
)
|
|
430
564
|
|
|
@@ -475,6 +609,85 @@ Vitest.describe('webmesh node', { timeout: testTimeout }, () => {
|
|
|
475
609
|
yield* Effect.all([nodeACode, nodeCCode], { concurrency: 'unbounded' })
|
|
476
610
|
}).pipe(withCtx(test)),
|
|
477
611
|
)
|
|
612
|
+
|
|
613
|
+
Vitest.scopedLive('reconnect with re-created node', (test) =>
|
|
614
|
+
Effect.gen(function* () {
|
|
615
|
+
const nodeCgen1Scope = yield* Scope.make()
|
|
616
|
+
|
|
617
|
+
const nodeA = yield* makeMeshNode('A')
|
|
618
|
+
const nodeB = yield* makeMeshNode('B')
|
|
619
|
+
const nodeCgen1 = yield* makeMeshNode('C').pipe(Scope.extend(nodeCgen1Scope))
|
|
620
|
+
|
|
621
|
+
yield* connectNodesViaMessageChannel(nodeA, nodeB)
|
|
622
|
+
yield* connectNodesViaMessageChannel(nodeB, nodeCgen1).pipe(Scope.extend(nodeCgen1Scope))
|
|
623
|
+
|
|
624
|
+
const nodeACode = Effect.gen(function* () {
|
|
625
|
+
const channelAToB = yield* createChannel(nodeA, 'C')
|
|
626
|
+
|
|
627
|
+
yield* channelAToB.send({ message: 'A1' })
|
|
628
|
+
expect(yield* getFirstMessage(channelAToB)).toEqual({ message: 'C1' })
|
|
629
|
+
})
|
|
630
|
+
|
|
631
|
+
const nodeCCode = (nodeB: MeshNode) =>
|
|
632
|
+
Effect.gen(function* () {
|
|
633
|
+
const channelBToA = yield* createChannel(nodeB, 'A')
|
|
634
|
+
|
|
635
|
+
yield* channelBToA.send({ message: 'C1' })
|
|
636
|
+
expect(yield* getFirstMessage(channelBToA)).toEqual({ message: 'A1' })
|
|
637
|
+
})
|
|
638
|
+
|
|
639
|
+
yield* Effect.all([nodeACode, nodeCCode(nodeCgen1)], { concurrency: 'unbounded' }).pipe(
|
|
640
|
+
Effect.withSpan('test1'),
|
|
641
|
+
Scope.extend(nodeCgen1Scope),
|
|
642
|
+
)
|
|
643
|
+
|
|
644
|
+
yield* Scope.close(nodeCgen1Scope, Exit.void)
|
|
645
|
+
|
|
646
|
+
const nodeCgen2 = yield* makeMeshNode('C')
|
|
647
|
+
yield* connectNodesViaMessageChannel(nodeB, nodeCgen2, { replaceIfExists: true })
|
|
648
|
+
|
|
649
|
+
yield* Effect.all([nodeACode, nodeCCode(nodeCgen2)], { concurrency: 'unbounded' }).pipe(
|
|
650
|
+
Effect.withSpan('test2'),
|
|
651
|
+
)
|
|
652
|
+
}).pipe(withCtx(test)),
|
|
653
|
+
)
|
|
654
|
+
})
|
|
655
|
+
|
|
656
|
+
/**
|
|
657
|
+
* A
|
|
658
|
+
* / \
|
|
659
|
+
* B C
|
|
660
|
+
* \ /
|
|
661
|
+
* D
|
|
662
|
+
*/
|
|
663
|
+
Vitest.describe('diamond topology', () => {
|
|
664
|
+
Vitest.scopedLive('should work', (test) =>
|
|
665
|
+
Effect.gen(function* () {
|
|
666
|
+
const nodeA = yield* makeMeshNode('A')
|
|
667
|
+
const nodeB = yield* makeMeshNode('B')
|
|
668
|
+
const nodeC = yield* makeMeshNode('C')
|
|
669
|
+
const nodeD = yield* makeMeshNode('D')
|
|
670
|
+
|
|
671
|
+
yield* connectNodesViaMessageChannel(nodeA, nodeB)
|
|
672
|
+
yield* connectNodesViaMessageChannel(nodeA, nodeC)
|
|
673
|
+
yield* connectNodesViaMessageChannel(nodeB, nodeD)
|
|
674
|
+
yield* connectNodesViaMessageChannel(nodeC, nodeD)
|
|
675
|
+
|
|
676
|
+
const nodeACode = Effect.gen(function* () {
|
|
677
|
+
const channelAToD = yield* createChannel(nodeA, 'D')
|
|
678
|
+
yield* channelAToD.send({ message: 'A1' })
|
|
679
|
+
expect(yield* getFirstMessage(channelAToD)).toEqual({ message: 'D1' })
|
|
680
|
+
})
|
|
681
|
+
|
|
682
|
+
const nodeDCode = Effect.gen(function* () {
|
|
683
|
+
const channelDToA = yield* createChannel(nodeD, 'A')
|
|
684
|
+
yield* channelDToA.send({ message: 'D1' })
|
|
685
|
+
expect(yield* getFirstMessage(channelDToA)).toEqual({ message: 'A1' })
|
|
686
|
+
})
|
|
687
|
+
|
|
688
|
+
yield* Effect.all([nodeACode, nodeDCode], { concurrency: 'unbounded' })
|
|
689
|
+
}).pipe(withCtx(test)),
|
|
690
|
+
)
|
|
478
691
|
})
|
|
479
692
|
|
|
480
693
|
Vitest.describe('mixture of messagechannel and proxy connections', () => {
|
|
@@ -493,27 +706,28 @@ Vitest.describe('webmesh node', { timeout: testTimeout }, () => {
|
|
|
493
706
|
)
|
|
494
707
|
|
|
495
708
|
// TODO this currently fails but should work. probably needs some more guarding internally.
|
|
496
|
-
Vitest.scopedLive
|
|
709
|
+
Vitest.scopedLive('should work for messagechannels', (test) =>
|
|
497
710
|
Effect.gen(function* () {
|
|
498
711
|
const nodeA = yield* makeMeshNode('A')
|
|
499
712
|
const nodeB = yield* makeMeshNode('B')
|
|
713
|
+
const nodeC = yield* makeMeshNode('C')
|
|
500
714
|
|
|
501
715
|
yield* connectNodesViaMessageChannel(nodeB, nodeA)
|
|
502
|
-
yield* connectNodesViaBroadcastChannel(
|
|
716
|
+
yield* connectNodesViaBroadcastChannel(nodeB, nodeC)
|
|
503
717
|
|
|
504
718
|
const nodeACode = Effect.gen(function* () {
|
|
505
|
-
const
|
|
506
|
-
yield*
|
|
507
|
-
expect(yield* getFirstMessage(
|
|
719
|
+
const channelAToC = yield* createChannel(nodeA, 'C', { mode: 'proxy' })
|
|
720
|
+
yield* channelAToC.send({ message: 'A1' })
|
|
721
|
+
expect(yield* getFirstMessage(channelAToC)).toEqual({ message: 'C1' })
|
|
508
722
|
})
|
|
509
723
|
|
|
510
|
-
const
|
|
511
|
-
const
|
|
512
|
-
yield*
|
|
513
|
-
expect(yield* getFirstMessage(
|
|
724
|
+
const nodeCCode = Effect.gen(function* () {
|
|
725
|
+
const channelCToA = yield* createChannel(nodeC, 'A', { mode: 'proxy' })
|
|
726
|
+
yield* channelCToA.send({ message: 'C1' })
|
|
727
|
+
expect(yield* getFirstMessage(channelCToA)).toEqual({ message: 'A1' })
|
|
514
728
|
})
|
|
515
729
|
|
|
516
|
-
yield* Effect.all([nodeACode,
|
|
730
|
+
yield* Effect.all([nodeACode, nodeCCode], { concurrency: 'unbounded' })
|
|
517
731
|
}).pipe(withCtx(test)),
|
|
518
732
|
)
|
|
519
733
|
})
|
|
@@ -522,10 +736,13 @@ Vitest.describe('webmesh node', { timeout: testTimeout }, () => {
|
|
|
522
736
|
const otelLayer = IS_CI ? Layer.empty : OtelLiveHttp({ serviceName: 'webmesh-node-test', skipLogUrl: false })
|
|
523
737
|
|
|
524
738
|
const withCtx =
|
|
525
|
-
(
|
|
739
|
+
(
|
|
740
|
+
testContext: Vitest.TaskContext,
|
|
741
|
+
{ suffix, skipOtel = false, timeout = testTimeout }: { suffix?: string; skipOtel?: boolean; timeout?: number } = {},
|
|
742
|
+
) =>
|
|
526
743
|
<A, E, R>(self: Effect.Effect<A, E, R>) =>
|
|
527
744
|
self.pipe(
|
|
528
|
-
Effect.timeout(
|
|
745
|
+
Effect.timeout(timeout),
|
|
529
746
|
Effect.provide(Logger.pretty),
|
|
530
747
|
Effect.scoped, // We need to scope the effect manually here because otherwise the span is not closed
|
|
531
748
|
Effect.withSpan(`${testContext.task.suite?.name}:${testContext.task.name}${suffix ? `:${suffix}` : ''}`),
|