@livestore/webmesh 0.0.0-snapshot-1d99fea7d2ce2c7a5d9ed0a3752f8a7bda6bc3db
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 +5 -0
- package/dist/.tsbuildinfo +1 -0
- package/dist/channel/message-channel.d.ts +20 -0
- package/dist/channel/message-channel.d.ts.map +1 -0
- package/dist/channel/message-channel.js +183 -0
- package/dist/channel/message-channel.js.map +1 -0
- package/dist/channel/proxy-channel.d.ts +19 -0
- package/dist/channel/proxy-channel.d.ts.map +1 -0
- package/dist/channel/proxy-channel.js +179 -0
- package/dist/channel/proxy-channel.js.map +1 -0
- package/dist/common.d.ts +83 -0
- package/dist/common.d.ts.map +1 -0
- package/dist/common.js +13 -0
- package/dist/common.js.map +1 -0
- package/dist/mesh-schema.d.ts +104 -0
- package/dist/mesh-schema.d.ts.map +1 -0
- package/dist/mesh-schema.js +77 -0
- package/dist/mesh-schema.js.map +1 -0
- package/dist/mod.d.ts +5 -0
- package/dist/mod.d.ts.map +1 -0
- package/dist/mod.js +5 -0
- package/dist/mod.js.map +1 -0
- package/dist/node.d.ts +65 -0
- package/dist/node.d.ts.map +1 -0
- package/dist/node.js +216 -0
- package/dist/node.js.map +1 -0
- package/dist/node.test.d.ts +2 -0
- package/dist/node.test.d.ts.map +1 -0
- package/dist/node.test.js +351 -0
- package/dist/node.test.js.map +1 -0
- package/dist/utils.d.ts +19 -0
- package/dist/utils.d.ts.map +1 -0
- package/dist/utils.js +41 -0
- package/dist/utils.js.map +1 -0
- package/dist/websocket-connection.d.ts +51 -0
- package/dist/websocket-connection.d.ts.map +1 -0
- package/dist/websocket-connection.js +74 -0
- package/dist/websocket-connection.js.map +1 -0
- package/dist/websocket-server.d.ts +7 -0
- package/dist/websocket-server.d.ts.map +1 -0
- package/dist/websocket-server.js +24 -0
- package/dist/websocket-server.js.map +1 -0
- package/package.json +32 -0
- package/src/channel/message-channel.ts +354 -0
- package/src/channel/proxy-channel.ts +332 -0
- package/src/common.ts +36 -0
- package/src/mesh-schema.ts +94 -0
- package/src/mod.ts +4 -0
- package/src/node.test.ts +533 -0
- package/src/node.ts +408 -0
- package/src/utils.ts +47 -0
- package/src/websocket-connection.ts +158 -0
- package/src/websocket-server.ts +40 -0
- package/tsconfig.json +11 -0
package/src/node.test.ts
ADDED
|
@@ -0,0 +1,533 @@
|
|
|
1
|
+
import { IS_CI } from '@livestore/utils'
|
|
2
|
+
import { Chunk, Deferred, Effect, identity, Layer, Logger, Schema, Stream, WebChannel } from '@livestore/utils/effect'
|
|
3
|
+
import { OtelLiveHttp } from '@livestore/utils/node'
|
|
4
|
+
import { Vitest } from '@livestore/utils/node-vitest'
|
|
5
|
+
import { expect } from 'vitest'
|
|
6
|
+
|
|
7
|
+
import { Packet } from './mesh-schema.js'
|
|
8
|
+
import type { MeshNode } from './node.js'
|
|
9
|
+
import { makeMeshNode } from './node.js'
|
|
10
|
+
|
|
11
|
+
// TODO test cases where in-between node only comes online later
|
|
12
|
+
// TODO test cases where other side tries to reconnect
|
|
13
|
+
// TODO test combination of connection types (message, proxy)
|
|
14
|
+
// TODO test "diamond shape" topology (A <> B1, A <> B2, B1 <> C, B2 <> C)
|
|
15
|
+
// TODO test cases where multiple entities try to claim to be the same channel end (e.g. A,B,B)
|
|
16
|
+
// TODO write tests with worker threads
|
|
17
|
+
|
|
18
|
+
const ExampleSchema = Schema.Struct({ message: Schema.String })
|
|
19
|
+
|
|
20
|
+
const connectNodesViaMessageChannel = (nodeA: MeshNode, nodeB: MeshNode) =>
|
|
21
|
+
Effect.gen(function* () {
|
|
22
|
+
const mc = new MessageChannel()
|
|
23
|
+
const meshChannelAToB = yield* WebChannel.messagePortChannel({ port: mc.port1, schema: Packet })
|
|
24
|
+
const meshChannelBToA = yield* WebChannel.messagePortChannel({ port: mc.port2, schema: Packet })
|
|
25
|
+
|
|
26
|
+
yield* nodeA.addConnection({ target: nodeB.nodeName, connectionChannel: meshChannelAToB })
|
|
27
|
+
yield* nodeB.addConnection({ target: nodeA.nodeName, connectionChannel: meshChannelBToA })
|
|
28
|
+
|
|
29
|
+
return mc
|
|
30
|
+
}).pipe(Effect.withSpan(`connectNodesViaMessageChannel:${nodeA.nodeName}↔${nodeB.nodeName}`))
|
|
31
|
+
|
|
32
|
+
const connectNodesViaBroadcastChannel = (nodeA: MeshNode, nodeB: MeshNode) =>
|
|
33
|
+
Effect.gen(function* () {
|
|
34
|
+
// Need to instantiate two different channels because they filter out messages they sent themselves
|
|
35
|
+
const broadcastWebChannelA = yield* WebChannel.broadcastChannelWithAck({
|
|
36
|
+
channelName: `${nodeA.nodeName}↔${nodeB.nodeName}`,
|
|
37
|
+
listenSchema: Packet,
|
|
38
|
+
sendSchema: Packet,
|
|
39
|
+
})
|
|
40
|
+
|
|
41
|
+
const broadcastWebChannelB = yield* WebChannel.broadcastChannelWithAck({
|
|
42
|
+
channelName: `${nodeA.nodeName}↔${nodeB.nodeName}`,
|
|
43
|
+
listenSchema: Packet,
|
|
44
|
+
sendSchema: Packet,
|
|
45
|
+
})
|
|
46
|
+
|
|
47
|
+
yield* nodeA.addConnection({ target: nodeB.nodeName, connectionChannel: broadcastWebChannelA })
|
|
48
|
+
yield* nodeB.addConnection({ target: nodeA.nodeName, connectionChannel: broadcastWebChannelB })
|
|
49
|
+
}).pipe(Effect.withSpan(`connectNodesViaBroadcastChannel:${nodeA.nodeName}↔${nodeB.nodeName}`))
|
|
50
|
+
|
|
51
|
+
const createChannel = (source: MeshNode, target: string, options?: Partial<Parameters<MeshNode['makeChannel']>[0]>) =>
|
|
52
|
+
source.makeChannel({
|
|
53
|
+
target,
|
|
54
|
+
channelName: options?.channelName ?? 'test',
|
|
55
|
+
schema: ExampleSchema,
|
|
56
|
+
// transferables: options?.transferables ?? 'prefer',
|
|
57
|
+
mode: options?.mode ?? 'messagechannel',
|
|
58
|
+
timeout: options?.timeout ?? 200,
|
|
59
|
+
})
|
|
60
|
+
|
|
61
|
+
const getFirstMessage = <T1, T2>(channel: WebChannel.WebChannel<T1, T2>) =>
|
|
62
|
+
channel.listen.pipe(
|
|
63
|
+
Stream.flatten(),
|
|
64
|
+
Stream.take(1),
|
|
65
|
+
Stream.runCollect,
|
|
66
|
+
Effect.map(([message]) => message),
|
|
67
|
+
)
|
|
68
|
+
|
|
69
|
+
// NOTE we distinguish between undefined and 0 delays as it changes the fiber execution
|
|
70
|
+
const maybeDelay =
|
|
71
|
+
(delay: number | undefined, label: string) =>
|
|
72
|
+
<A, E, R>(effect: Effect.Effect<A, E, R>) =>
|
|
73
|
+
delay === undefined
|
|
74
|
+
? effect
|
|
75
|
+
: Effect.sleep(delay).pipe(Effect.withSpan(`${label}:delay(${delay})`), Effect.andThen(effect))
|
|
76
|
+
|
|
77
|
+
const testTimeout = IS_CI ? 30_000 : 500
|
|
78
|
+
|
|
79
|
+
// TODO also make work without `Vitest.scopedLive` (i.e. with `Vitest.scoped`)
|
|
80
|
+
// probably requires controlling the clocks
|
|
81
|
+
Vitest.describe('webmesh node', { timeout: testTimeout }, () => {
|
|
82
|
+
Vitest.describe('A <> B', () => {
|
|
83
|
+
Vitest.describe('prop tests', () => {
|
|
84
|
+
const Delay = Schema.UndefinedOr(Schema.Literal(0, 1, 10, 50))
|
|
85
|
+
// NOTE for message channels, we test both with and without transferables (i.e. proxying)
|
|
86
|
+
const ChannelType = Schema.Literal('messagechannel', 'messagechannel.proxy', 'proxy')
|
|
87
|
+
|
|
88
|
+
const fromChannelType = (
|
|
89
|
+
channelType: typeof ChannelType.Type,
|
|
90
|
+
): {
|
|
91
|
+
mode: 'messagechannel' | 'proxy'
|
|
92
|
+
connectNodes: typeof connectNodesViaMessageChannel | typeof connectNodesViaBroadcastChannel
|
|
93
|
+
} => {
|
|
94
|
+
switch (channelType) {
|
|
95
|
+
case 'proxy': {
|
|
96
|
+
return { mode: 'proxy', connectNodes: connectNodesViaBroadcastChannel }
|
|
97
|
+
}
|
|
98
|
+
case 'messagechannel': {
|
|
99
|
+
return { mode: 'messagechannel', connectNodes: connectNodesViaMessageChannel }
|
|
100
|
+
}
|
|
101
|
+
case 'messagechannel.proxy': {
|
|
102
|
+
return { mode: 'proxy', connectNodes: connectNodesViaMessageChannel }
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
Vitest.scopedLive.prop(
|
|
108
|
+
// Vitest.scopedLive.only(
|
|
109
|
+
'a / b connect at different times with different channel types',
|
|
110
|
+
[Delay, Delay, Delay, ChannelType],
|
|
111
|
+
([delayA, delayB, connectDelay, channelType], test) =>
|
|
112
|
+
// (test) =>
|
|
113
|
+
Effect.gen(function* () {
|
|
114
|
+
// const delayA = 1
|
|
115
|
+
// const delayB = 10
|
|
116
|
+
// const connectDelay = 10
|
|
117
|
+
// const channelType = 'message.prefer'
|
|
118
|
+
// console.log('delayA', delayA, 'delayB', delayB, 'connectDelay', connectDelay, 'channelType', channelType)
|
|
119
|
+
|
|
120
|
+
const nodeA = yield* makeMeshNode('A')
|
|
121
|
+
const nodeB = yield* makeMeshNode('B')
|
|
122
|
+
|
|
123
|
+
const { mode, connectNodes } = fromChannelType(channelType)
|
|
124
|
+
|
|
125
|
+
const nodeACode = Effect.gen(function* () {
|
|
126
|
+
const channelAToB = yield* createChannel(nodeA, 'B', { mode })
|
|
127
|
+
|
|
128
|
+
yield* channelAToB.send({ message: 'A1' })
|
|
129
|
+
expect(yield* getFirstMessage(channelAToB)).toEqual({ message: 'A2' })
|
|
130
|
+
})
|
|
131
|
+
|
|
132
|
+
const nodeBCode = Effect.gen(function* () {
|
|
133
|
+
const channelBToA = yield* createChannel(nodeB, 'A', { mode })
|
|
134
|
+
|
|
135
|
+
yield* channelBToA.send({ message: 'A2' })
|
|
136
|
+
expect(yield* getFirstMessage(channelBToA)).toEqual({ message: 'A1' })
|
|
137
|
+
})
|
|
138
|
+
|
|
139
|
+
yield* Effect.all(
|
|
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
|
+
)
|
|
151
|
+
|
|
152
|
+
// Vitest.scopedLive.only(
|
|
153
|
+
// 'reconnects',
|
|
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')
|
|
174
|
+
|
|
175
|
+
const { mode, connectNodes } = fromChannelType(channelType)
|
|
176
|
+
|
|
177
|
+
// TODO also optionally delay the connection
|
|
178
|
+
yield* connectNodes(nodeA, nodeB)
|
|
179
|
+
|
|
180
|
+
const waitForBToBeOffline =
|
|
181
|
+
waitForOfflineDelay === undefined ? undefined : yield* Deferred.make<void, never>()
|
|
182
|
+
|
|
183
|
+
const nodeACode = Effect.gen(function* () {
|
|
184
|
+
const channelAToB = yield* createChannel(nodeA, 'B', { mode })
|
|
185
|
+
|
|
186
|
+
yield* channelAToB.send({ message: 'A1' })
|
|
187
|
+
expect(yield* getFirstMessage(channelAToB)).toEqual({ message: 'B1' })
|
|
188
|
+
|
|
189
|
+
if (waitForBToBeOffline !== undefined) {
|
|
190
|
+
yield* waitForBToBeOffline
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
yield* channelAToB.send({ message: 'A2' })
|
|
194
|
+
expect(yield* getFirstMessage(channelAToB)).toEqual({ message: 'B2' })
|
|
195
|
+
})
|
|
196
|
+
|
|
197
|
+
// Simulating node b going offline and then coming back online
|
|
198
|
+
const nodeBCode = Effect.gen(function* () {
|
|
199
|
+
yield* Effect.gen(function* () {
|
|
200
|
+
const channelBToA = yield* createChannel(nodeB, 'A', { mode })
|
|
201
|
+
|
|
202
|
+
yield* channelBToA.send({ message: 'B1' })
|
|
203
|
+
expect(yield* getFirstMessage(channelBToA)).toEqual({ message: 'A1' })
|
|
204
|
+
}).pipe(Effect.scoped)
|
|
205
|
+
|
|
206
|
+
if (waitForBToBeOffline !== undefined) {
|
|
207
|
+
yield* Deferred.succeed(waitForBToBeOffline, void 0)
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
if (sleepDelay !== undefined) {
|
|
211
|
+
yield* Effect.sleep(sleepDelay).pipe(Effect.withSpan(`B:sleep(${sleepDelay})`))
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
yield* Effect.gen(function* () {
|
|
215
|
+
const channelBToA = yield* createChannel(nodeB, 'A', { mode })
|
|
216
|
+
|
|
217
|
+
yield* channelBToA.send({ message: 'B2' })
|
|
218
|
+
expect(yield* getFirstMessage(channelBToA)).toEqual({ message: 'A2' })
|
|
219
|
+
}).pipe(Effect.scoped)
|
|
220
|
+
})
|
|
221
|
+
|
|
222
|
+
yield* Effect.all([nodeACode, nodeBCode], { concurrency: 'unbounded' })
|
|
223
|
+
}).pipe(
|
|
224
|
+
withCtx(test, {
|
|
225
|
+
skipOtel: true,
|
|
226
|
+
suffix: `waitForOfflineDelay=${waitForOfflineDelay} sleepDelay=${sleepDelay} channelType=${channelType}`,
|
|
227
|
+
}),
|
|
228
|
+
),
|
|
229
|
+
)
|
|
230
|
+
|
|
231
|
+
const ChannelTypeWithoutMessageChannelProxy = Schema.Literal('proxy', 'messagechannel')
|
|
232
|
+
Vitest.scopedLive.prop(
|
|
233
|
+
'replace connection while keeping the channel',
|
|
234
|
+
[ChannelTypeWithoutMessageChannelProxy],
|
|
235
|
+
([channelType], test) =>
|
|
236
|
+
Effect.gen(function* () {
|
|
237
|
+
const nodeA = yield* makeMeshNode('A')
|
|
238
|
+
const nodeB = yield* makeMeshNode('B')
|
|
239
|
+
|
|
240
|
+
const { mode, connectNodes } = fromChannelType(channelType)
|
|
241
|
+
|
|
242
|
+
yield* connectNodes(nodeA, nodeB)
|
|
243
|
+
|
|
244
|
+
const waitForConnectionReplacement = yield* Deferred.make<void>()
|
|
245
|
+
|
|
246
|
+
const nodeACode = Effect.gen(function* () {
|
|
247
|
+
const channelAToB = yield* createChannel(nodeA, 'B', { mode })
|
|
248
|
+
|
|
249
|
+
yield* channelAToB.send({ message: 'A1' })
|
|
250
|
+
expect(yield* getFirstMessage(channelAToB)).toEqual({ message: 'B1' })
|
|
251
|
+
|
|
252
|
+
yield* waitForConnectionReplacement
|
|
253
|
+
|
|
254
|
+
yield* channelAToB.send({ message: 'A2' })
|
|
255
|
+
expect(yield* getFirstMessage(channelAToB)).toEqual({ message: 'B2' })
|
|
256
|
+
})
|
|
257
|
+
|
|
258
|
+
const nodeBCode = Effect.gen(function* () {
|
|
259
|
+
const channelBToA = yield* createChannel(nodeB, 'A', { mode })
|
|
260
|
+
|
|
261
|
+
yield* channelBToA.send({ message: 'B1' })
|
|
262
|
+
expect(yield* getFirstMessage(channelBToA)).toEqual({ message: 'A1' })
|
|
263
|
+
|
|
264
|
+
// Switch out connection while keeping the channel
|
|
265
|
+
yield* nodeA.removeConnection('B')
|
|
266
|
+
yield* nodeB.removeConnection('A')
|
|
267
|
+
yield* connectNodes(nodeA, nodeB)
|
|
268
|
+
yield* Deferred.succeed(waitForConnectionReplacement, void 0)
|
|
269
|
+
|
|
270
|
+
yield* channelBToA.send({ message: 'B2' })
|
|
271
|
+
expect(yield* getFirstMessage(channelBToA)).toEqual({ message: 'A2' })
|
|
272
|
+
})
|
|
273
|
+
|
|
274
|
+
yield* Effect.all([nodeACode, nodeBCode], { concurrency: 'unbounded' })
|
|
275
|
+
}).pipe(withCtx(test, { skipOtel: true, suffix: `channelType=${channelType}` })),
|
|
276
|
+
)
|
|
277
|
+
|
|
278
|
+
Vitest.describe.todo('TODO improve latency', () => {
|
|
279
|
+
// TODO we need to improve latency when sending messages concurrently
|
|
280
|
+
Vitest.scopedLive.prop(
|
|
281
|
+
'concurrent messages',
|
|
282
|
+
[ChannelType, Schema.Int.pipe(Schema.between(1, 50))],
|
|
283
|
+
([channelType, count], test) =>
|
|
284
|
+
Effect.gen(function* () {
|
|
285
|
+
const nodeA = yield* makeMeshNode('A')
|
|
286
|
+
const nodeB = yield* makeMeshNode('B')
|
|
287
|
+
|
|
288
|
+
const { mode, connectNodes } = fromChannelType(channelType)
|
|
289
|
+
console.log('channelType', channelType, 'mode', mode)
|
|
290
|
+
|
|
291
|
+
const nodeACode = Effect.gen(function* () {
|
|
292
|
+
const channelAToB = yield* createChannel(nodeA, 'B', { mode })
|
|
293
|
+
|
|
294
|
+
// send 10 times A1
|
|
295
|
+
yield* Effect.forEach(
|
|
296
|
+
Chunk.makeBy(count, (i) => ({ message: `A${i}` })),
|
|
297
|
+
channelAToB.send,
|
|
298
|
+
{ concurrency: 'unbounded' },
|
|
299
|
+
)
|
|
300
|
+
|
|
301
|
+
expect(yield* channelAToB.listen.pipe(Stream.flatten(), Stream.take(count), Stream.runCollect)).toEqual(
|
|
302
|
+
Chunk.makeBy(count, (i) => ({ message: `B${i}` })),
|
|
303
|
+
)
|
|
304
|
+
// expect(yield* getFirstMessage(channelAToB)).toEqual({ message: 'A2' })
|
|
305
|
+
})
|
|
306
|
+
|
|
307
|
+
const nodeBCode = Effect.gen(function* () {
|
|
308
|
+
const channelBToA = yield* createChannel(nodeB, 'A', { mode })
|
|
309
|
+
|
|
310
|
+
// send 10 times B1
|
|
311
|
+
yield* Effect.forEach(
|
|
312
|
+
Chunk.makeBy(count, (i) => ({ message: `B${i}` })),
|
|
313
|
+
channelBToA.send,
|
|
314
|
+
{ concurrency: 'unbounded' },
|
|
315
|
+
)
|
|
316
|
+
|
|
317
|
+
expect(yield* channelBToA.listen.pipe(Stream.flatten(), Stream.take(count), Stream.runCollect)).toEqual(
|
|
318
|
+
Chunk.makeBy(count, (i) => ({ message: `A${i}` })),
|
|
319
|
+
)
|
|
320
|
+
})
|
|
321
|
+
|
|
322
|
+
yield* Effect.all([nodeACode, nodeBCode, connectNodes(nodeA, nodeB).pipe(Effect.delay(100))], {
|
|
323
|
+
concurrency: 'unbounded',
|
|
324
|
+
})
|
|
325
|
+
}).pipe(withCtx(test, { skipOtel: false, suffix: `channelType=${channelType} count=${count}` })),
|
|
326
|
+
)
|
|
327
|
+
})
|
|
328
|
+
})
|
|
329
|
+
|
|
330
|
+
Vitest.scopedLive('manual debug test', (test) =>
|
|
331
|
+
Effect.gen(function* () {
|
|
332
|
+
const nodeA = yield* makeMeshNode('A')
|
|
333
|
+
const nodeB = yield* makeMeshNode('B')
|
|
334
|
+
|
|
335
|
+
// const connectNodes = connectNodesViaBroadcastChannel
|
|
336
|
+
const connectNodes = connectNodesViaMessageChannel
|
|
337
|
+
|
|
338
|
+
const nodeACode = Effect.gen(function* () {
|
|
339
|
+
const channelAToB = yield* createChannel(nodeA, 'B')
|
|
340
|
+
|
|
341
|
+
yield* channelAToB.send({ message: 'A1' })
|
|
342
|
+
expect(yield* getFirstMessage(channelAToB)).toEqual({ message: 'A2' })
|
|
343
|
+
})
|
|
344
|
+
|
|
345
|
+
const nodeBCode = Effect.gen(function* () {
|
|
346
|
+
const channelBToA = yield* createChannel(nodeB, 'A')
|
|
347
|
+
|
|
348
|
+
yield* channelBToA.send({ message: 'A2' })
|
|
349
|
+
expect(yield* getFirstMessage(channelBToA)).toEqual({ message: 'A1' })
|
|
350
|
+
})
|
|
351
|
+
|
|
352
|
+
yield* Effect.all([nodeACode, nodeBCode, connectNodes(nodeA, nodeB).pipe(Effect.delay(100))], {
|
|
353
|
+
concurrency: 'unbounded',
|
|
354
|
+
})
|
|
355
|
+
}).pipe(withCtx(test)),
|
|
356
|
+
)
|
|
357
|
+
|
|
358
|
+
Vitest.scopedLive('broadcast connection with message channel', (test) =>
|
|
359
|
+
Effect.gen(function* () {
|
|
360
|
+
const nodeA = yield* makeMeshNode('A')
|
|
361
|
+
const nodeB = yield* makeMeshNode('B')
|
|
362
|
+
|
|
363
|
+
yield* connectNodesViaBroadcastChannel(nodeA, nodeB)
|
|
364
|
+
|
|
365
|
+
const err = yield* createChannel(nodeA, 'B', { mode: 'messagechannel' }).pipe(Effect.timeout(200), Effect.flip)
|
|
366
|
+
expect(err._tag).toBe('TimeoutException')
|
|
367
|
+
}).pipe(withCtx(test)),
|
|
368
|
+
)
|
|
369
|
+
})
|
|
370
|
+
|
|
371
|
+
Vitest.describe('A <> B <> C', () => {
|
|
372
|
+
Vitest.scopedLive('should work', (test) =>
|
|
373
|
+
Effect.gen(function* () {
|
|
374
|
+
const nodeA = yield* makeMeshNode('A')
|
|
375
|
+
const nodeB = yield* makeMeshNode('B')
|
|
376
|
+
const nodeC = yield* makeMeshNode('C')
|
|
377
|
+
|
|
378
|
+
yield* connectNodesViaMessageChannel(nodeA, nodeB)
|
|
379
|
+
yield* connectNodesViaMessageChannel(nodeB, nodeC)
|
|
380
|
+
|
|
381
|
+
const nodeACode = Effect.gen(function* () {
|
|
382
|
+
const channelAToC = yield* createChannel(nodeA, 'C')
|
|
383
|
+
|
|
384
|
+
yield* channelAToC.send({ message: 'A1' })
|
|
385
|
+
expect(yield* getFirstMessage(channelAToC)).toEqual({ message: 'C1' })
|
|
386
|
+
expect(yield* getFirstMessage(channelAToC)).toEqual({ message: 'C2' })
|
|
387
|
+
})
|
|
388
|
+
|
|
389
|
+
const nodeCCode = Effect.gen(function* () {
|
|
390
|
+
const channelCToA = yield* createChannel(nodeC, 'A')
|
|
391
|
+
yield* channelCToA.send({ message: 'C1' })
|
|
392
|
+
yield* channelCToA.send({ message: 'C2' })
|
|
393
|
+
yield* channelCToA.send({ message: 'C3' })
|
|
394
|
+
expect(yield* getFirstMessage(channelCToA)).toEqual({ message: 'A1' })
|
|
395
|
+
})
|
|
396
|
+
|
|
397
|
+
yield* Effect.all([nodeACode, nodeCCode], { concurrency: 'unbounded' })
|
|
398
|
+
}).pipe(withCtx(test)),
|
|
399
|
+
)
|
|
400
|
+
|
|
401
|
+
Vitest.scopedLive('should work - delayed connection', (test) =>
|
|
402
|
+
Effect.gen(function* () {
|
|
403
|
+
const nodeA = yield* makeMeshNode('A')
|
|
404
|
+
const nodeB = yield* makeMeshNode('B')
|
|
405
|
+
const nodeC = yield* makeMeshNode('C')
|
|
406
|
+
|
|
407
|
+
const connectNodes = connectNodesViaMessageChannel
|
|
408
|
+
// const connectNodes = connectNodesViaBroadcastChannel
|
|
409
|
+
yield* connectNodes(nodeA, nodeB)
|
|
410
|
+
// yield* connectNodes(nodeB, nodeC)
|
|
411
|
+
|
|
412
|
+
const nodeACode = Effect.gen(function* () {
|
|
413
|
+
const channelAToC = yield* createChannel(nodeA, 'C')
|
|
414
|
+
|
|
415
|
+
yield* channelAToC.send({ message: 'A1' })
|
|
416
|
+
expect(yield* getFirstMessage(channelAToC)).toEqual({ message: 'C1' })
|
|
417
|
+
})
|
|
418
|
+
|
|
419
|
+
const nodeCCode = Effect.gen(function* () {
|
|
420
|
+
const channelCToA = yield* createChannel(nodeC, 'A')
|
|
421
|
+
yield* channelCToA.send({ message: 'C1' })
|
|
422
|
+
expect(yield* getFirstMessage(channelCToA)).toEqual({ message: 'A1' })
|
|
423
|
+
})
|
|
424
|
+
|
|
425
|
+
yield* Effect.all([nodeACode, nodeCCode, connectNodes(nodeB, nodeC).pipe(Effect.delay(100))], {
|
|
426
|
+
concurrency: 'unbounded',
|
|
427
|
+
})
|
|
428
|
+
}).pipe(withCtx(test)),
|
|
429
|
+
)
|
|
430
|
+
|
|
431
|
+
Vitest.scopedLive('proxy channel', (test) =>
|
|
432
|
+
Effect.gen(function* () {
|
|
433
|
+
const nodeA = yield* makeMeshNode('A')
|
|
434
|
+
const nodeB = yield* makeMeshNode('B')
|
|
435
|
+
const nodeC = yield* makeMeshNode('C')
|
|
436
|
+
|
|
437
|
+
yield* connectNodesViaBroadcastChannel(nodeA, nodeB)
|
|
438
|
+
yield* connectNodesViaBroadcastChannel(nodeB, nodeC)
|
|
439
|
+
|
|
440
|
+
const nodeACode = Effect.gen(function* () {
|
|
441
|
+
const channelAToC = yield* createChannel(nodeA, 'C', { mode: 'proxy' })
|
|
442
|
+
yield* channelAToC.send({ message: 'A1' })
|
|
443
|
+
expect(yield* getFirstMessage(channelAToC)).toEqual({ message: 'hello from nodeC' })
|
|
444
|
+
})
|
|
445
|
+
|
|
446
|
+
const nodeCCode = Effect.gen(function* () {
|
|
447
|
+
const channelCToA = yield* createChannel(nodeC, 'A', { mode: 'proxy' })
|
|
448
|
+
yield* channelCToA.send({ message: 'hello from nodeC' })
|
|
449
|
+
expect(yield* getFirstMessage(channelCToA)).toEqual({ message: 'A1' })
|
|
450
|
+
})
|
|
451
|
+
|
|
452
|
+
yield* Effect.all([nodeACode, nodeCCode], { concurrency: 'unbounded' })
|
|
453
|
+
}).pipe(withCtx(test)),
|
|
454
|
+
)
|
|
455
|
+
|
|
456
|
+
Vitest.scopedLive('should fail', (test) =>
|
|
457
|
+
Effect.gen(function* () {
|
|
458
|
+
const nodeA = yield* makeMeshNode('A')
|
|
459
|
+
const nodeB = yield* makeMeshNode('B')
|
|
460
|
+
const nodeC = yield* makeMeshNode('C')
|
|
461
|
+
|
|
462
|
+
yield* connectNodesViaMessageChannel(nodeA, nodeB)
|
|
463
|
+
// We're not connecting nodeB and nodeC, so this should fail
|
|
464
|
+
|
|
465
|
+
const nodeACode = Effect.gen(function* () {
|
|
466
|
+
const err = yield* createChannel(nodeA, 'C').pipe(Effect.timeout(200), Effect.flip)
|
|
467
|
+
expect(err._tag).toBe('TimeoutException')
|
|
468
|
+
})
|
|
469
|
+
|
|
470
|
+
const nodeCCode = Effect.gen(function* () {
|
|
471
|
+
const err = yield* createChannel(nodeC, 'A').pipe(Effect.timeout(200), Effect.flip)
|
|
472
|
+
expect(err._tag).toBe('TimeoutException')
|
|
473
|
+
})
|
|
474
|
+
|
|
475
|
+
yield* Effect.all([nodeACode, nodeCCode], { concurrency: 'unbounded' })
|
|
476
|
+
}).pipe(withCtx(test)),
|
|
477
|
+
)
|
|
478
|
+
})
|
|
479
|
+
|
|
480
|
+
Vitest.describe('mixture of messagechannel and proxy connections', () => {
|
|
481
|
+
// TODO test case to better guard against case where side A tries to create a proxy channel to B
|
|
482
|
+
// and side B tries to create a messagechannel to A
|
|
483
|
+
Vitest.scopedLive('should work for proxy channels', (test) =>
|
|
484
|
+
Effect.gen(function* () {
|
|
485
|
+
const nodeA = yield* makeMeshNode('A')
|
|
486
|
+
const nodeB = yield* makeMeshNode('B')
|
|
487
|
+
|
|
488
|
+
yield* connectNodesViaMessageChannel(nodeB, nodeA)
|
|
489
|
+
const err = yield* connectNodesViaBroadcastChannel(nodeA, nodeB).pipe(Effect.flip)
|
|
490
|
+
|
|
491
|
+
expect(err._tag).toBe('ConnectionAlreadyExistsError')
|
|
492
|
+
}).pipe(withCtx(test)),
|
|
493
|
+
)
|
|
494
|
+
|
|
495
|
+
// TODO this currently fails but should work. probably needs some more guarding internally.
|
|
496
|
+
Vitest.scopedLive.skip('should work for messagechannels', (test) =>
|
|
497
|
+
Effect.gen(function* () {
|
|
498
|
+
const nodeA = yield* makeMeshNode('A')
|
|
499
|
+
const nodeB = yield* makeMeshNode('B')
|
|
500
|
+
|
|
501
|
+
yield* connectNodesViaMessageChannel(nodeB, nodeA)
|
|
502
|
+
yield* connectNodesViaBroadcastChannel(nodeA, nodeB)
|
|
503
|
+
|
|
504
|
+
const nodeACode = Effect.gen(function* () {
|
|
505
|
+
const channelAToB = yield* createChannel(nodeA, 'B', { mode: 'messagechannel' })
|
|
506
|
+
yield* channelAToB.send({ message: 'A1' })
|
|
507
|
+
expect(yield* getFirstMessage(channelAToB)).toEqual({ message: 'B1' })
|
|
508
|
+
})
|
|
509
|
+
|
|
510
|
+
const nodeBCode = Effect.gen(function* () {
|
|
511
|
+
const channelBToA = yield* createChannel(nodeB, 'A', { mode: 'messagechannel' })
|
|
512
|
+
yield* channelBToA.send({ message: 'B1' })
|
|
513
|
+
expect(yield* getFirstMessage(channelBToA)).toEqual({ message: 'A1' })
|
|
514
|
+
})
|
|
515
|
+
|
|
516
|
+
yield* Effect.all([nodeACode, nodeBCode], { concurrency: 'unbounded' })
|
|
517
|
+
}).pipe(withCtx(test)),
|
|
518
|
+
)
|
|
519
|
+
})
|
|
520
|
+
})
|
|
521
|
+
|
|
522
|
+
const otelLayer = IS_CI ? Layer.empty : OtelLiveHttp({ serviceName: 'webmesh-node-test', skipLogUrl: false })
|
|
523
|
+
|
|
524
|
+
const withCtx =
|
|
525
|
+
(testContext: Vitest.TaskContext, { suffix, skipOtel = false }: { suffix?: string; skipOtel?: boolean } = {}) =>
|
|
526
|
+
<A, E, R>(self: Effect.Effect<A, E, R>) =>
|
|
527
|
+
self.pipe(
|
|
528
|
+
Effect.timeout(testTimeout),
|
|
529
|
+
Effect.provide(Logger.pretty),
|
|
530
|
+
Effect.scoped, // We need to scope the effect manually here because otherwise the span is not closed
|
|
531
|
+
Effect.withSpan(`${testContext.task.suite?.name}:${testContext.task.name}${suffix ? `:${suffix}` : ''}`),
|
|
532
|
+
skipOtel ? identity : Effect.provide(otelLayer),
|
|
533
|
+
)
|