@livestore/webmesh 0.3.0-dev.4 → 0.3.0-dev.41
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 +42 -0
- package/dist/.tsbuildinfo +1 -1
- package/dist/channel/direct-channel-internal.d.ts +26 -0
- package/dist/channel/direct-channel-internal.d.ts.map +1 -0
- package/dist/channel/direct-channel-internal.js +217 -0
- package/dist/channel/direct-channel-internal.js.map +1 -0
- package/dist/channel/direct-channel.d.ts +22 -0
- package/dist/channel/direct-channel.d.ts.map +1 -0
- package/dist/channel/direct-channel.js +153 -0
- package/dist/channel/direct-channel.js.map +1 -0
- package/dist/channel/proxy-channel.d.ts +3 -3
- package/dist/channel/proxy-channel.d.ts.map +1 -1
- package/dist/channel/proxy-channel.js +119 -37
- package/dist/channel/proxy-channel.js.map +1 -1
- package/dist/common.d.ts +47 -19
- package/dist/common.d.ts.map +1 -1
- package/dist/common.js +13 -5
- package/dist/common.js.map +1 -1
- package/dist/mesh-schema.d.ts +79 -13
- package/dist/mesh-schema.d.ts.map +1 -1
- package/dist/mesh-schema.js +59 -10
- package/dist/mesh-schema.js.map +1 -1
- package/dist/mod.d.ts +2 -2
- package/dist/mod.d.ts.map +1 -1
- package/dist/mod.js +2 -2
- package/dist/mod.js.map +1 -1
- package/dist/node.d.ts +51 -24
- package/dist/node.d.ts.map +1 -1
- package/dist/node.js +322 -111
- 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 +489 -157
- package/dist/node.test.js.map +1 -1
- package/dist/utils.d.ts +4 -4
- package/dist/utils.d.ts.map +1 -1
- package/dist/utils.js +7 -1
- package/dist/utils.js.map +1 -1
- package/dist/websocket-edge.d.ts +53 -0
- package/dist/websocket-edge.d.ts.map +1 -0
- package/dist/websocket-edge.js +89 -0
- package/dist/websocket-edge.js.map +1 -0
- package/package.json +10 -6
- package/src/channel/direct-channel-internal.ts +356 -0
- package/src/channel/direct-channel.ts +234 -0
- package/src/channel/proxy-channel.ts +344 -234
- package/src/common.ts +24 -17
- package/src/mesh-schema.ts +73 -20
- package/src/mod.ts +2 -2
- package/src/node.test.ts +723 -190
- package/src/node.ts +497 -152
- package/src/utils.ts +13 -2
- package/src/websocket-edge.ts +183 -0
- package/dist/channel/message-channel.d.ts +0 -20
- package/dist/channel/message-channel.d.ts.map +0 -1
- package/dist/channel/message-channel.js +0 -183
- package/dist/channel/message-channel.js.map +0 -1
- package/dist/websocket-connection.d.ts +0 -51
- package/dist/websocket-connection.d.ts.map +0 -1
- package/dist/websocket-connection.js +0 -74
- package/dist/websocket-connection.js.map +0 -1
- package/dist/websocket-server.d.ts +0 -7
- package/dist/websocket-server.d.ts.map +0 -1
- package/dist/websocket-server.js +0 -24
- package/dist/websocket-server.js.map +0 -1
- package/src/channel/message-channel.ts +0 -354
- package/src/websocket-connection.ts +0 -158
- package/src/websocket-server.ts +0 -40
- package/tsconfig.json +0 -11
package/dist/node.test.js
CHANGED
|
@@ -1,45 +1,60 @@
|
|
|
1
|
-
import
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
1
|
+
import '@livestore/utils-dev/node-vitest-polyfill';
|
|
2
|
+
import { IS_CI } from '@livestore/utils';
|
|
3
|
+
import { Chunk, Deferred, Effect, Exit, identity, Layer, Logger, LogLevel, Schema, Scope, Stream, WebChannel, } from '@livestore/utils/effect';
|
|
4
|
+
import { OtelLiveHttp } from '@livestore/utils-dev/node';
|
|
5
|
+
import { Vitest } from '@livestore/utils-dev/node-vitest';
|
|
4
6
|
import { expect } from 'vitest';
|
|
5
7
|
import { Packet } from './mesh-schema.js';
|
|
6
8
|
import { makeMeshNode } from './node.js';
|
|
7
9
|
// TODO test cases where in-between node only comes online later
|
|
8
10
|
// TODO test cases where other side tries to reconnect
|
|
9
|
-
// TODO test combination of
|
|
11
|
+
// TODO test combination of channel types (message, proxy)
|
|
10
12
|
// TODO test "diamond shape" topology (A <> B1, A <> B2, B1 <> C, B2 <> C)
|
|
11
13
|
// TODO test cases where multiple entities try to claim to be the same channel end (e.g. A,B,B)
|
|
12
14
|
// TODO write tests with worker threads
|
|
13
15
|
const ExampleSchema = Schema.Struct({ message: Schema.String });
|
|
14
|
-
const connectNodesViaMessageChannel = (nodeA, nodeB) => Effect.gen(function* () {
|
|
16
|
+
const connectNodesViaMessageChannel = (nodeA, nodeB, options) => Effect.gen(function* () {
|
|
15
17
|
const mc = new MessageChannel();
|
|
16
18
|
const meshChannelAToB = yield* WebChannel.messagePortChannel({ port: mc.port1, schema: Packet });
|
|
17
19
|
const meshChannelBToA = yield* WebChannel.messagePortChannel({ port: mc.port2, schema: Packet });
|
|
18
|
-
yield* nodeA.
|
|
19
|
-
|
|
20
|
-
|
|
20
|
+
yield* nodeA.addEdge({
|
|
21
|
+
target: nodeB.nodeName,
|
|
22
|
+
edgeChannel: meshChannelAToB,
|
|
23
|
+
replaceIfExists: options?.replaceIfExists,
|
|
24
|
+
});
|
|
25
|
+
yield* nodeB.addEdge({
|
|
26
|
+
target: nodeA.nodeName,
|
|
27
|
+
edgeChannel: meshChannelBToA,
|
|
28
|
+
replaceIfExists: options?.replaceIfExists,
|
|
29
|
+
});
|
|
21
30
|
}).pipe(Effect.withSpan(`connectNodesViaMessageChannel:${nodeA.nodeName}↔${nodeB.nodeName}`));
|
|
22
|
-
const connectNodesViaBroadcastChannel = (nodeA, nodeB) => Effect.gen(function* () {
|
|
31
|
+
const connectNodesViaBroadcastChannel = (nodeA, nodeB, options) => Effect.gen(function* () {
|
|
23
32
|
// Need to instantiate two different channels because they filter out messages they sent themselves
|
|
24
33
|
const broadcastWebChannelA = yield* WebChannel.broadcastChannelWithAck({
|
|
25
34
|
channelName: `${nodeA.nodeName}↔${nodeB.nodeName}`,
|
|
26
|
-
|
|
27
|
-
sendSchema: Packet,
|
|
35
|
+
schema: Packet,
|
|
28
36
|
});
|
|
29
37
|
const broadcastWebChannelB = yield* WebChannel.broadcastChannelWithAck({
|
|
30
38
|
channelName: `${nodeA.nodeName}↔${nodeB.nodeName}`,
|
|
31
|
-
|
|
32
|
-
|
|
39
|
+
schema: Packet,
|
|
40
|
+
});
|
|
41
|
+
yield* nodeA.addEdge({
|
|
42
|
+
target: nodeB.nodeName,
|
|
43
|
+
edgeChannel: broadcastWebChannelA,
|
|
44
|
+
replaceIfExists: options?.replaceIfExists,
|
|
45
|
+
});
|
|
46
|
+
yield* nodeB.addEdge({
|
|
47
|
+
target: nodeA.nodeName,
|
|
48
|
+
edgeChannel: broadcastWebChannelB,
|
|
49
|
+
replaceIfExists: options?.replaceIfExists,
|
|
33
50
|
});
|
|
34
|
-
yield* nodeA.addConnection({ target: nodeB.nodeName, connectionChannel: broadcastWebChannelA });
|
|
35
|
-
yield* nodeB.addConnection({ target: nodeA.nodeName, connectionChannel: broadcastWebChannelB });
|
|
36
51
|
}).pipe(Effect.withSpan(`connectNodesViaBroadcastChannel:${nodeA.nodeName}↔${nodeB.nodeName}`));
|
|
37
52
|
const createChannel = (source, target, options) => source.makeChannel({
|
|
38
53
|
target,
|
|
39
54
|
channelName: options?.channelName ?? 'test',
|
|
40
55
|
schema: ExampleSchema,
|
|
41
56
|
// transferables: options?.transferables ?? 'prefer',
|
|
42
|
-
mode: options?.mode ?? '
|
|
57
|
+
mode: options?.mode ?? 'direct',
|
|
43
58
|
timeout: options?.timeout ?? 200,
|
|
44
59
|
});
|
|
45
60
|
const getFirstMessage = (channel) => channel.listen.pipe(Stream.flatten(), Stream.take(1), Stream.runCollect, Effect.map(([message]) => message));
|
|
@@ -47,141 +62,215 @@ const getFirstMessage = (channel) => channel.listen.pipe(Stream.flatten(), Strea
|
|
|
47
62
|
const maybeDelay = (delay, label) => (effect) => delay === undefined
|
|
48
63
|
? effect
|
|
49
64
|
: Effect.sleep(delay).pipe(Effect.withSpan(`${label}:delay(${delay})`), Effect.andThen(effect));
|
|
65
|
+
const testTimeout = IS_CI ? 30_000 : 1000;
|
|
66
|
+
const propTestTimeout = IS_CI ? 60_000 : 20_000;
|
|
50
67
|
// TODO also make work without `Vitest.scopedLive` (i.e. with `Vitest.scoped`)
|
|
51
68
|
// probably requires controlling the clocks
|
|
52
|
-
Vitest.describe('webmesh node', { timeout:
|
|
69
|
+
Vitest.describe('webmesh node', { timeout: testTimeout }, () => {
|
|
70
|
+
const Delay = Schema.UndefinedOr(Schema.Literal(0, 1, 10, 50));
|
|
71
|
+
// NOTE for message channels, we test both with and without transferables (i.e. proxying)
|
|
72
|
+
const ChannelType = Schema.Literal('direct', 'proxy(via-messagechannel-edge)', 'proxy');
|
|
73
|
+
const NodeNames = Schema.Union(Schema.Tuple(Schema.Literal('A'), Schema.Literal('B')), Schema.Tuple(Schema.Literal('B'), Schema.Literal('A')));
|
|
74
|
+
const fromChannelType = (channelType) => {
|
|
75
|
+
switch (channelType) {
|
|
76
|
+
case 'proxy': {
|
|
77
|
+
return { mode: 'proxy', connectNodes: connectNodesViaBroadcastChannel };
|
|
78
|
+
}
|
|
79
|
+
case 'direct': {
|
|
80
|
+
return { mode: 'direct', connectNodes: connectNodesViaMessageChannel };
|
|
81
|
+
}
|
|
82
|
+
case 'proxy(via-messagechannel-edge)': {
|
|
83
|
+
return { mode: 'proxy', connectNodes: connectNodesViaMessageChannel };
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
};
|
|
87
|
+
const exchangeMessages = ({ nodeX, nodeY, channelType,
|
|
88
|
+
// numberOfMessages = 1,
|
|
89
|
+
delays, }) => Effect.gen(function* () {
|
|
90
|
+
const nodeLabel = { x: nodeX.nodeName, y: nodeY.nodeName };
|
|
91
|
+
const { mode, connectNodes } = fromChannelType(channelType);
|
|
92
|
+
const nodeXCode = Effect.gen(function* () {
|
|
93
|
+
const channelXToY = yield* createChannel(nodeX, nodeY.nodeName, { mode });
|
|
94
|
+
yield* channelXToY.send({ message: `${nodeLabel.x}1` });
|
|
95
|
+
// console.log('channelXToY', channelXToY.debugInfo)
|
|
96
|
+
expect(yield* getFirstMessage(channelXToY)).toEqual({ message: `${nodeLabel.y}1` });
|
|
97
|
+
// expect(channelXToY.debugInfo.connectCounter).toBe(1)
|
|
98
|
+
});
|
|
99
|
+
const nodeYCode = Effect.gen(function* () {
|
|
100
|
+
const channelYToX = yield* createChannel(nodeY, nodeX.nodeName, { mode });
|
|
101
|
+
yield* channelYToX.send({ message: `${nodeLabel.y}1` });
|
|
102
|
+
// console.log('channelYToX', channelYToX.debugInfo)
|
|
103
|
+
expect(yield* getFirstMessage(channelYToX)).toEqual({ message: `${nodeLabel.x}1` });
|
|
104
|
+
// expect(channelYToX.debugInfo.connectCounter).toBe(1)
|
|
105
|
+
});
|
|
106
|
+
yield* Effect.all([
|
|
107
|
+
connectNodes(nodeX, nodeY).pipe(maybeDelay(delays?.connect, 'connectNodes')),
|
|
108
|
+
nodeXCode.pipe(maybeDelay(delays?.x, `node${nodeLabel.x}Code`)),
|
|
109
|
+
nodeYCode.pipe(maybeDelay(delays?.y, `node${nodeLabel.y}Code`)),
|
|
110
|
+
], { concurrency: 'unbounded' }).pipe(Effect.withSpan(`exchangeMessages(${nodeLabel.x}↔${nodeLabel.y})`));
|
|
111
|
+
});
|
|
53
112
|
Vitest.describe('A <> B', () => {
|
|
54
|
-
Vitest.describe('prop tests', { timeout:
|
|
55
|
-
const
|
|
56
|
-
//
|
|
57
|
-
const
|
|
58
|
-
const
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
}
|
|
63
|
-
case 'messagechannel': {
|
|
64
|
-
return { mode: 'messagechannel', connectNodes: connectNodesViaMessageChannel };
|
|
65
|
-
}
|
|
66
|
-
case 'messagechannel.proxy': {
|
|
67
|
-
return { mode: 'proxy', connectNodes: connectNodesViaMessageChannel };
|
|
68
|
-
}
|
|
69
|
-
}
|
|
70
|
-
};
|
|
71
|
-
Vitest.scopedLive.prop(
|
|
72
|
-
// Vitest.scopedLive.only(
|
|
73
|
-
'a / b connect at different times with different channel types', [Delay, Delay, Delay, ChannelType], ([delayA, delayB, connectDelay, channelType], test) =>
|
|
74
|
-
// (test) =>
|
|
75
|
-
Effect.gen(function* () {
|
|
76
|
-
// const delayA = 1
|
|
77
|
-
// const delayB = 10
|
|
78
|
-
// const connectDelay = 10
|
|
79
|
-
// const channelType = 'message.prefer'
|
|
80
|
-
// console.log('delayA', delayA, 'delayB', delayB, 'connectDelay', connectDelay, 'channelType', channelType)
|
|
81
|
-
const nodeA = yield* makeMeshNode('A');
|
|
82
|
-
const nodeB = yield* makeMeshNode('B');
|
|
83
|
-
const { mode, connectNodes } = fromChannelType(channelType);
|
|
84
|
-
const nodeACode = Effect.gen(function* () {
|
|
85
|
-
const channelAToB = yield* createChannel(nodeA, 'B', { mode });
|
|
86
|
-
yield* channelAToB.send({ message: 'A1' });
|
|
87
|
-
expect(yield* getFirstMessage(channelAToB)).toEqual({ message: 'A2' });
|
|
88
|
-
});
|
|
89
|
-
const nodeBCode = Effect.gen(function* () {
|
|
90
|
-
const channelBToA = yield* createChannel(nodeB, 'A', { mode });
|
|
91
|
-
yield* channelBToA.send({ message: 'A2' });
|
|
92
|
-
expect(yield* getFirstMessage(channelBToA)).toEqual({ message: 'A1' });
|
|
93
|
-
});
|
|
94
|
-
yield* Effect.all([
|
|
95
|
-
connectNodes(nodeA, nodeB).pipe(maybeDelay(connectDelay, 'connectNodes')),
|
|
96
|
-
nodeACode.pipe(maybeDelay(delayA, 'nodeACode')),
|
|
97
|
-
nodeBCode.pipe(maybeDelay(delayB, 'nodeBCode')),
|
|
98
|
-
], { concurrency: 'unbounded' });
|
|
99
|
-
}).pipe(withCtx(test, { skipOtel: true, suffix: `delayA=${delayA} delayB=${delayB} channelType=${channelType}` })));
|
|
100
|
-
// Vitest.scopedLive.only(
|
|
101
|
-
// 'reconnects',
|
|
113
|
+
Vitest.describe('prop tests', { timeout: propTestTimeout }, () => {
|
|
114
|
+
// const delayX = 40
|
|
115
|
+
// const delayY = undefined
|
|
116
|
+
// const connectDelay = undefined
|
|
117
|
+
// const channelType = 'direct'
|
|
118
|
+
// const nodeNames = ['B', 'A'] as const
|
|
119
|
+
// Vitest.scopedLive(
|
|
120
|
+
// 'a / b connect at different times with different channel types',
|
|
102
121
|
// (test) =>
|
|
103
|
-
Vitest.scopedLive.prop('b
|
|
104
|
-
//
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
// channelType,
|
|
114
|
-
// )
|
|
115
|
-
const nodeA = yield* makeMeshNode('A');
|
|
116
|
-
const nodeB = yield* makeMeshNode('B');
|
|
117
|
-
const { mode, connectNodes } = fromChannelType(channelType);
|
|
118
|
-
// TODO also optionally delay the connection
|
|
119
|
-
yield* connectNodes(nodeA, nodeB);
|
|
120
|
-
const waitForBToBeOffline = waitForOfflineDelay === undefined ? undefined : yield* Deferred.make();
|
|
121
|
-
const nodeACode = Effect.gen(function* () {
|
|
122
|
-
const channelAToB = yield* createChannel(nodeA, 'B', { mode });
|
|
123
|
-
yield* channelAToB.send({ message: 'A1' });
|
|
124
|
-
expect(yield* getFirstMessage(channelAToB)).toEqual({ message: 'B1' });
|
|
125
|
-
if (waitForBToBeOffline !== undefined) {
|
|
126
|
-
yield* waitForBToBeOffline;
|
|
127
|
-
}
|
|
128
|
-
yield* channelAToB.send({ message: 'A2' });
|
|
129
|
-
expect(yield* getFirstMessage(channelAToB)).toEqual({ message: 'B2' });
|
|
130
|
-
});
|
|
131
|
-
// Simulating node b going offline and then coming back online
|
|
132
|
-
const nodeBCode = Effect.gen(function* () {
|
|
133
|
-
yield* Effect.gen(function* () {
|
|
134
|
-
const channelBToA = yield* createChannel(nodeB, 'A', { mode });
|
|
135
|
-
yield* channelBToA.send({ message: 'B1' });
|
|
136
|
-
expect(yield* getFirstMessage(channelBToA)).toEqual({ message: 'A1' });
|
|
137
|
-
}).pipe(Effect.scoped);
|
|
138
|
-
if (waitForBToBeOffline !== undefined) {
|
|
139
|
-
yield* Deferred.succeed(waitForBToBeOffline, void 0);
|
|
140
|
-
}
|
|
141
|
-
if (sleepDelay !== undefined) {
|
|
142
|
-
yield* Effect.sleep(sleepDelay).pipe(Effect.withSpan(`B:sleep(${sleepDelay})`));
|
|
143
|
-
}
|
|
144
|
-
yield* Effect.gen(function* () {
|
|
145
|
-
const channelBToA = yield* createChannel(nodeB, 'A', { mode });
|
|
146
|
-
yield* channelBToA.send({ message: 'B2' });
|
|
147
|
-
expect(yield* getFirstMessage(channelBToA)).toEqual({ message: 'A2' });
|
|
148
|
-
}).pipe(Effect.scoped);
|
|
122
|
+
Vitest.scopedLive.prop('a / b connect at different times with different channel types', [Delay, Delay, Delay, ChannelType, NodeNames], ([delayX, delayY, connectDelay, channelType, nodeNames], test) => Effect.gen(function* () {
|
|
123
|
+
// console.log({ delayX, delayY, connectDelay, channelType, nodeNames })
|
|
124
|
+
const [nodeNameX, nodeNameY] = nodeNames;
|
|
125
|
+
const nodeX = yield* makeMeshNode(nodeNameX);
|
|
126
|
+
const nodeY = yield* makeMeshNode(nodeNameY);
|
|
127
|
+
yield* exchangeMessages({
|
|
128
|
+
nodeX,
|
|
129
|
+
nodeY,
|
|
130
|
+
channelType,
|
|
131
|
+
delays: { x: delayX, y: delayY, connect: connectDelay },
|
|
149
132
|
});
|
|
150
|
-
yield* Effect.
|
|
133
|
+
yield* Effect.promise(() => nodeX.debug.requestTopology(100));
|
|
151
134
|
}).pipe(withCtx(test, {
|
|
152
135
|
skipOtel: true,
|
|
153
|
-
suffix: `
|
|
136
|
+
suffix: `delayX=${delayX} delayY=${delayY} connectDelay=${connectDelay} channelType=${channelType} nodeNames=${nodeNames}`,
|
|
154
137
|
})));
|
|
155
|
-
|
|
156
|
-
|
|
138
|
+
{
|
|
139
|
+
// const waitForOfflineDelay = undefined
|
|
140
|
+
// const sleepDelay = 0
|
|
141
|
+
// const channelType = 'direct'
|
|
142
|
+
// Vitest.scopedLive(
|
|
143
|
+
// 'b reconnects',
|
|
144
|
+
// (test) =>
|
|
145
|
+
Vitest.scopedLive.prop('b reconnects', [Delay, Delay, ChannelType], ([waitForOfflineDelay, sleepDelay, channelType], test) => Effect.gen(function* () {
|
|
146
|
+
// console.log({ waitForOfflineDelay, sleepDelay, channelType })
|
|
147
|
+
if (waitForOfflineDelay === undefined) {
|
|
148
|
+
// TODO we still need to fix this scenario but it shouldn't really be common in practice
|
|
149
|
+
return;
|
|
150
|
+
}
|
|
151
|
+
const nodeA = yield* makeMeshNode('A');
|
|
152
|
+
const nodeB = yield* makeMeshNode('B');
|
|
153
|
+
const { mode, connectNodes } = fromChannelType(channelType);
|
|
154
|
+
// TODO also optionally delay the edge
|
|
155
|
+
yield* connectNodes(nodeA, nodeB);
|
|
156
|
+
const waitForBToBeOffline = waitForOfflineDelay === undefined ? undefined : yield* Deferred.make();
|
|
157
|
+
const nodeACode = Effect.gen(function* () {
|
|
158
|
+
const channelAToB = yield* createChannel(nodeA, 'B', { mode });
|
|
159
|
+
yield* channelAToB.send({ message: 'A1' });
|
|
160
|
+
expect(yield* getFirstMessage(channelAToB)).toEqual({ message: 'B1' });
|
|
161
|
+
console.log('nodeACode:waiting for B to be offline');
|
|
162
|
+
if (waitForBToBeOffline !== undefined) {
|
|
163
|
+
yield* waitForBToBeOffline;
|
|
164
|
+
}
|
|
165
|
+
yield* channelAToB.send({ message: 'A2' });
|
|
166
|
+
expect(yield* getFirstMessage(channelAToB)).toEqual({ message: 'B2' });
|
|
167
|
+
});
|
|
168
|
+
// Simulating node b going offline and then coming back online
|
|
169
|
+
// This test also illustrates why we need a ack-message channel since otherwise
|
|
170
|
+
// sent messages might get lost
|
|
171
|
+
const nodeBCode = Effect.gen(function* () {
|
|
172
|
+
yield* Effect.gen(function* () {
|
|
173
|
+
const channelBToA = yield* createChannel(nodeB, 'A', { mode });
|
|
174
|
+
yield* channelBToA.send({ message: 'B1' });
|
|
175
|
+
expect(yield* getFirstMessage(channelBToA)).toEqual({ message: 'A1' });
|
|
176
|
+
}).pipe(Effect.scoped, Effect.withSpan('nodeBCode:part1'));
|
|
177
|
+
console.log('nodeBCode:B node going offline');
|
|
178
|
+
if (waitForBToBeOffline !== undefined) {
|
|
179
|
+
yield* Deferred.succeed(waitForBToBeOffline, void 0);
|
|
180
|
+
}
|
|
181
|
+
if (sleepDelay !== undefined) {
|
|
182
|
+
yield* Effect.sleep(sleepDelay).pipe(Effect.withSpan(`B:sleep(${sleepDelay})`));
|
|
183
|
+
}
|
|
184
|
+
// Recreating the channel
|
|
185
|
+
yield* Effect.gen(function* () {
|
|
186
|
+
const channelBToA = yield* createChannel(nodeB, 'A', { mode });
|
|
187
|
+
yield* channelBToA.send({ message: 'B2' });
|
|
188
|
+
expect(yield* getFirstMessage(channelBToA)).toEqual({ message: 'A2' });
|
|
189
|
+
}).pipe(Effect.scoped, Effect.withSpan('nodeBCode:part2'));
|
|
190
|
+
});
|
|
191
|
+
yield* Effect.all([nodeACode, nodeBCode], { concurrency: 'unbounded' }).pipe(Effect.withSpan('test'));
|
|
192
|
+
}).pipe(withCtx(test, {
|
|
193
|
+
skipOtel: true,
|
|
194
|
+
suffix: `waitForOfflineDelay=${waitForOfflineDelay} sleepDelay=${sleepDelay} channelType=${channelType}`,
|
|
195
|
+
})), { fastCheck: { numRuns: 20 } });
|
|
196
|
+
}
|
|
197
|
+
Vitest.scopedLive('reconnect with re-created node', (test) => Effect.gen(function* () {
|
|
198
|
+
const nodeBgen1Scope = yield* Scope.make();
|
|
157
199
|
const nodeA = yield* makeMeshNode('A');
|
|
158
|
-
const
|
|
159
|
-
|
|
160
|
-
yield*
|
|
161
|
-
const
|
|
200
|
+
const nodeBgen1 = yield* makeMeshNode('B').pipe(Scope.extend(nodeBgen1Scope));
|
|
201
|
+
yield* connectNodesViaMessageChannel(nodeA, nodeBgen1).pipe(Scope.extend(nodeBgen1Scope));
|
|
202
|
+
// yield* Effect.sleep(100)
|
|
203
|
+
const channelAToBOnce = yield* Effect.cached(createChannel(nodeA, 'B'));
|
|
162
204
|
const nodeACode = Effect.gen(function* () {
|
|
163
|
-
const channelAToB = yield*
|
|
205
|
+
const channelAToB = yield* channelAToBOnce;
|
|
164
206
|
yield* channelAToB.send({ message: 'A1' });
|
|
165
207
|
expect(yield* getFirstMessage(channelAToB)).toEqual({ message: 'B1' });
|
|
166
|
-
|
|
167
|
-
yield* channelAToB.send({ message: 'A2' });
|
|
168
|
-
expect(yield* getFirstMessage(channelAToB)).toEqual({ message: 'B2' });
|
|
208
|
+
// expect(channelAToB.debugInfo.connectCounter).toBe(1)
|
|
169
209
|
});
|
|
170
|
-
const nodeBCode = Effect.gen(function* () {
|
|
171
|
-
const channelBToA = yield* createChannel(nodeB, 'A'
|
|
210
|
+
const nodeBCode = (nodeB) => Effect.gen(function* () {
|
|
211
|
+
const channelBToA = yield* createChannel(nodeB, 'A');
|
|
172
212
|
yield* channelBToA.send({ message: 'B1' });
|
|
173
213
|
expect(yield* getFirstMessage(channelBToA)).toEqual({ message: 'A1' });
|
|
174
|
-
//
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
214
|
+
// expect(channelBToA.debugInfo.connectCounter).toBe(1)
|
|
215
|
+
});
|
|
216
|
+
yield* Effect.all([nodeACode, nodeBCode(nodeBgen1).pipe(Scope.extend(nodeBgen1Scope))], {
|
|
217
|
+
concurrency: 'unbounded',
|
|
218
|
+
}).pipe(Effect.withSpan('test1'));
|
|
219
|
+
yield* Scope.close(nodeBgen1Scope, Exit.void);
|
|
220
|
+
const nodeBgen2 = yield* makeMeshNode('B');
|
|
221
|
+
yield* connectNodesViaMessageChannel(nodeA, nodeBgen2, { replaceIfExists: true });
|
|
222
|
+
yield* Effect.all([nodeACode, nodeBCode(nodeBgen2)], { concurrency: 'unbounded' }).pipe(Effect.withSpan('test2'));
|
|
223
|
+
}).pipe(withCtx(test)));
|
|
224
|
+
const ChannelTypeWithoutMessageChannelProxy = Schema.Literal('proxy', 'direct');
|
|
225
|
+
// TODO there seems to be a flaky case here which gets hit sometimes (e.g. 2025-02-28-17:11)
|
|
226
|
+
// Log output:
|
|
227
|
+
// test: { seed: -964670352, path: "1", endOnFailure: true }
|
|
228
|
+
// test: Counterexample: ["direct",["A","B"]]
|
|
229
|
+
// test: Shrunk 0 time(s)
|
|
230
|
+
// test: Got AssertionError: expected { _tag: 'MessageChannelPing' } to deeply equal { message: 'A1' }
|
|
231
|
+
// test: at next (/Users/schickling/Code/overtone/submodules/livestore/packages/@livestore/webmesh/src/node.test.ts:376:59)
|
|
232
|
+
// test: at prop tests:replace edge while keeping the channel:channelType=direct nodeNames=A,B (/Users/schickling/Code/overtone/submodules/livestore/packages/@livestore/webmesh/src/node.test.ts:801:14)
|
|
233
|
+
// test: Hint: Enable verbose mode in order to have the list of all failing values encountered during the run
|
|
234
|
+
// test: ✓ webmesh node > A <> B > prop tests > TODO improve latency > concurrent messages 2110ms
|
|
235
|
+
// test: ⎯⎯⎯⎯⎯⎯⎯ Failed Tests 1 ⎯⎯⎯⎯⎯⎯⎯
|
|
236
|
+
// test: FAIL src/node.test.ts > webmesh node > A <> B > prop tests > replace edge while keeping the channel
|
|
237
|
+
// test: Error: Property failed after 2 tests
|
|
238
|
+
// test: { seed: -964670352, path: "1", endOnFailure: true }
|
|
239
|
+
// test: Counterexample: ["direct",["A","B"]]
|
|
240
|
+
Vitest.scopedLive.prop('replace edge while keeping the channel', [ChannelTypeWithoutMessageChannelProxy, NodeNames], ([channelType, nodeNames], test) => Effect.gen(function* () {
|
|
241
|
+
const [nodeNameX, nodeNameY] = nodeNames;
|
|
242
|
+
const nodeX = yield* makeMeshNode(nodeNameX);
|
|
243
|
+
const nodeY = yield* makeMeshNode(nodeNameY);
|
|
244
|
+
const nodeLabel = { x: nodeX.nodeName, y: nodeY.nodeName };
|
|
245
|
+
const { mode, connectNodes } = fromChannelType(channelType);
|
|
246
|
+
yield* connectNodes(nodeX, nodeY);
|
|
247
|
+
const waitForEdgeReplacement = yield* Deferred.make();
|
|
248
|
+
const nodeXCode = Effect.gen(function* () {
|
|
249
|
+
const channelXToY = yield* createChannel(nodeX, nodeLabel.y, { mode });
|
|
250
|
+
yield* channelXToY.send({ message: `${nodeLabel.x}1` });
|
|
251
|
+
expect(yield* getFirstMessage(channelXToY)).toEqual({ message: `${nodeLabel.y}1` });
|
|
252
|
+
yield* waitForEdgeReplacement;
|
|
253
|
+
yield* channelXToY.send({ message: `${nodeLabel.x}2` });
|
|
254
|
+
expect(yield* getFirstMessage(channelXToY)).toEqual({ message: `${nodeLabel.y}2` });
|
|
255
|
+
});
|
|
256
|
+
const nodeYCode = Effect.gen(function* () {
|
|
257
|
+
const channelYToX = yield* createChannel(nodeY, nodeLabel.x, { mode });
|
|
258
|
+
yield* channelYToX.send({ message: `${nodeLabel.y}1` });
|
|
259
|
+
expect(yield* getFirstMessage(channelYToX)).toEqual({ message: `${nodeLabel.x}1` });
|
|
260
|
+
// Switch out edge while keeping the channel
|
|
261
|
+
yield* nodeX.removeEdge(nodeLabel.y);
|
|
262
|
+
yield* nodeY.removeEdge(nodeLabel.x);
|
|
263
|
+
yield* connectNodes(nodeX, nodeY);
|
|
264
|
+
yield* Deferred.succeed(waitForEdgeReplacement, void 0);
|
|
265
|
+
yield* channelYToX.send({ message: `${nodeLabel.y}2` });
|
|
266
|
+
expect(yield* getFirstMessage(channelYToX)).toEqual({ message: `${nodeLabel.x}2` });
|
|
181
267
|
});
|
|
182
|
-
yield* Effect.all([
|
|
183
|
-
}).pipe(withCtx(test, {
|
|
184
|
-
|
|
268
|
+
yield* Effect.all([nodeXCode, nodeYCode], { concurrency: 'unbounded' });
|
|
269
|
+
}).pipe(withCtx(test, {
|
|
270
|
+
skipOtel: true,
|
|
271
|
+
suffix: `channelType=${channelType} nodeNames=${nodeNames}`,
|
|
272
|
+
})), { fastCheck: { numRuns: 10 } });
|
|
273
|
+
Vitest.describe('TODO improve latency', () => {
|
|
185
274
|
// TODO we need to improve latency when sending messages concurrently
|
|
186
275
|
Vitest.scopedLive.prop('concurrent messages', [ChannelType, Schema.Int.pipe(Schema.between(1, 50))], ([channelType, count], test) => Effect.gen(function* () {
|
|
187
276
|
const nodeA = yield* makeMeshNode('A');
|
|
@@ -204,9 +293,34 @@ Vitest.describe('webmesh node', { timeout: 1000 }, () => {
|
|
|
204
293
|
yield* Effect.all([nodeACode, nodeBCode, connectNodes(nodeA, nodeB).pipe(Effect.delay(100))], {
|
|
205
294
|
concurrency: 'unbounded',
|
|
206
295
|
});
|
|
207
|
-
}).pipe(withCtx(test, {
|
|
296
|
+
}).pipe(withCtx(test, {
|
|
297
|
+
skipOtel: true,
|
|
298
|
+
suffix: `channelType=${channelType} count=${count}`,
|
|
299
|
+
timeout: testTimeout * 2,
|
|
300
|
+
})), { fastCheck: { numRuns: 10 } });
|
|
208
301
|
});
|
|
209
302
|
});
|
|
303
|
+
Vitest.describe('message channel specific tests', () => {
|
|
304
|
+
Vitest.scopedLive('differing initial edge counter', (test) => Effect.gen(function* () {
|
|
305
|
+
const nodeA = yield* makeMeshNode('A');
|
|
306
|
+
const nodeB = yield* makeMeshNode('B');
|
|
307
|
+
yield* connectNodesViaMessageChannel(nodeA, nodeB);
|
|
308
|
+
const messageCount = 3;
|
|
309
|
+
const bFiber = yield* Effect.gen(function* () {
|
|
310
|
+
const channelBToA = yield* createChannel(nodeB, 'A');
|
|
311
|
+
yield* channelBToA.listen.pipe(Stream.flatten(), Stream.tap((msg) => channelBToA.send({ message: `resp:${msg.message}` })), Stream.take(messageCount), Stream.runDrain);
|
|
312
|
+
}).pipe(Effect.scoped, Effect.fork);
|
|
313
|
+
// yield* createChannel(nodeA, 'B').pipe(Effect.andThen(WebChannel.shutdown))
|
|
314
|
+
// // yield* createChannel(nodeA, 'B').pipe(Effect.andThen(WebChannel.shutdown))
|
|
315
|
+
// // yield* createChannel(nodeA, 'B').pipe(Effect.andThen(WebChannel.shutdown))
|
|
316
|
+
yield* Effect.gen(function* () {
|
|
317
|
+
const channelAToB = yield* createChannel(nodeA, 'B');
|
|
318
|
+
yield* channelAToB.send({ message: 'A' });
|
|
319
|
+
expect(yield* getFirstMessage(channelAToB)).toEqual({ message: 'resp:A' });
|
|
320
|
+
}).pipe(Effect.scoped, Effect.repeatN(messageCount));
|
|
321
|
+
yield* bFiber;
|
|
322
|
+
}).pipe(withCtx(test)));
|
|
323
|
+
});
|
|
210
324
|
Vitest.scopedLive('manual debug test', (test) => Effect.gen(function* () {
|
|
211
325
|
const nodeA = yield* makeMeshNode('A');
|
|
212
326
|
const nodeB = yield* makeMeshNode('B');
|
|
@@ -226,11 +340,11 @@ Vitest.describe('webmesh node', { timeout: 1000 }, () => {
|
|
|
226
340
|
concurrency: 'unbounded',
|
|
227
341
|
});
|
|
228
342
|
}).pipe(withCtx(test)));
|
|
229
|
-
Vitest.scopedLive('broadcast
|
|
343
|
+
Vitest.scopedLive('broadcast edge with message channel', (test) => Effect.gen(function* () {
|
|
230
344
|
const nodeA = yield* makeMeshNode('A');
|
|
231
345
|
const nodeB = yield* makeMeshNode('B');
|
|
232
346
|
yield* connectNodesViaBroadcastChannel(nodeA, nodeB);
|
|
233
|
-
const err = yield* createChannel(nodeA, 'B', { mode: '
|
|
347
|
+
const err = yield* createChannel(nodeA, 'B', { mode: 'direct' }).pipe(Effect.timeout(200), Effect.flip);
|
|
234
348
|
expect(err._tag).toBe('TimeoutException');
|
|
235
349
|
}).pipe(withCtx(test)));
|
|
236
350
|
});
|
|
@@ -246,6 +360,7 @@ Vitest.describe('webmesh node', { timeout: 1000 }, () => {
|
|
|
246
360
|
yield* channelAToC.send({ message: 'A1' });
|
|
247
361
|
expect(yield* getFirstMessage(channelAToC)).toEqual({ message: 'C1' });
|
|
248
362
|
expect(yield* getFirstMessage(channelAToC)).toEqual({ message: 'C2' });
|
|
363
|
+
expect(yield* getFirstMessage(channelAToC)).toEqual({ message: 'C3' });
|
|
249
364
|
});
|
|
250
365
|
const nodeCCode = Effect.gen(function* () {
|
|
251
366
|
const channelCToA = yield* createChannel(nodeC, 'A');
|
|
@@ -256,7 +371,7 @@ Vitest.describe('webmesh node', { timeout: 1000 }, () => {
|
|
|
256
371
|
});
|
|
257
372
|
yield* Effect.all([nodeACode, nodeCCode], { concurrency: 'unbounded' });
|
|
258
373
|
}).pipe(withCtx(test)));
|
|
259
|
-
Vitest.scopedLive('should work - delayed
|
|
374
|
+
Vitest.scopedLive('should work - delayed edge', (test) => Effect.gen(function* () {
|
|
260
375
|
const nodeA = yield* makeMeshNode('A');
|
|
261
376
|
const nodeB = yield* makeMeshNode('B');
|
|
262
377
|
const nodeC = yield* makeMeshNode('C');
|
|
@@ -274,9 +389,11 @@ Vitest.describe('webmesh node', { timeout: 1000 }, () => {
|
|
|
274
389
|
yield* channelCToA.send({ message: 'C1' });
|
|
275
390
|
expect(yield* getFirstMessage(channelCToA)).toEqual({ message: 'A1' });
|
|
276
391
|
});
|
|
277
|
-
yield* Effect.all([
|
|
278
|
-
|
|
279
|
-
|
|
392
|
+
yield* Effect.all([
|
|
393
|
+
nodeACode,
|
|
394
|
+
nodeCCode,
|
|
395
|
+
connectNodes(nodeB, nodeC).pipe(Effect.delay(100), Effect.withSpan('connect-nodeB-nodeC-delay(100)')),
|
|
396
|
+
], { concurrency: 'unbounded' });
|
|
280
397
|
}).pipe(withCtx(test)));
|
|
281
398
|
Vitest.scopedLive('proxy channel', (test) => Effect.gen(function* () {
|
|
282
399
|
const nodeA = yield* makeMeshNode('A');
|
|
@@ -296,7 +413,7 @@ Vitest.describe('webmesh node', { timeout: 1000 }, () => {
|
|
|
296
413
|
});
|
|
297
414
|
yield* Effect.all([nodeACode, nodeCCode], { concurrency: 'unbounded' });
|
|
298
415
|
}).pipe(withCtx(test)));
|
|
299
|
-
Vitest.scopedLive('should fail', (test) => Effect.gen(function* () {
|
|
416
|
+
Vitest.scopedLive('should fail with timeout due to missing edge', (test) => Effect.gen(function* () {
|
|
300
417
|
const nodeA = yield* makeMeshNode('A');
|
|
301
418
|
const nodeB = yield* makeMeshNode('B');
|
|
302
419
|
const nodeC = yield* makeMeshNode('C');
|
|
@@ -312,40 +429,255 @@ Vitest.describe('webmesh node', { timeout: 1000 }, () => {
|
|
|
312
429
|
});
|
|
313
430
|
yield* Effect.all([nodeACode, nodeCCode], { concurrency: 'unbounded' });
|
|
314
431
|
}).pipe(withCtx(test)));
|
|
432
|
+
Vitest.scopedLive('should fail with timeout due no transferable', (test) => Effect.gen(function* () {
|
|
433
|
+
const nodeA = yield* makeMeshNode('A');
|
|
434
|
+
const nodeB = yield* makeMeshNode('B');
|
|
435
|
+
yield* connectNodesViaBroadcastChannel(nodeA, nodeB);
|
|
436
|
+
const nodeACode = Effect.gen(function* () {
|
|
437
|
+
const err = yield* createChannel(nodeA, 'B').pipe(Effect.timeout(200), Effect.flip);
|
|
438
|
+
expect(err._tag).toBe('TimeoutException');
|
|
439
|
+
});
|
|
440
|
+
const nodeBCode = Effect.gen(function* () {
|
|
441
|
+
const err = yield* createChannel(nodeB, 'A').pipe(Effect.timeout(200), Effect.flip);
|
|
442
|
+
expect(err._tag).toBe('TimeoutException');
|
|
443
|
+
});
|
|
444
|
+
yield* Effect.all([nodeACode, nodeBCode], { concurrency: 'unbounded' });
|
|
445
|
+
}).pipe(withCtx(test)));
|
|
446
|
+
Vitest.scopedLive('reconnect with re-created node', (test) => Effect.gen(function* () {
|
|
447
|
+
const nodeCgen1Scope = yield* Scope.make();
|
|
448
|
+
const nodeA = yield* makeMeshNode('A');
|
|
449
|
+
const nodeB = yield* makeMeshNode('B');
|
|
450
|
+
const nodeCgen1 = yield* makeMeshNode('C').pipe(Scope.extend(nodeCgen1Scope));
|
|
451
|
+
yield* connectNodesViaMessageChannel(nodeA, nodeB);
|
|
452
|
+
yield* connectNodesViaMessageChannel(nodeB, nodeCgen1).pipe(Scope.extend(nodeCgen1Scope));
|
|
453
|
+
const nodeACode = Effect.gen(function* () {
|
|
454
|
+
const channelAToB = yield* createChannel(nodeA, 'C');
|
|
455
|
+
yield* channelAToB.send({ message: 'A1' });
|
|
456
|
+
expect(yield* getFirstMessage(channelAToB)).toEqual({ message: 'C1' });
|
|
457
|
+
});
|
|
458
|
+
const nodeCCode = (nodeB) => Effect.gen(function* () {
|
|
459
|
+
const channelBToA = yield* createChannel(nodeB, 'A');
|
|
460
|
+
yield* channelBToA.send({ message: 'C1' });
|
|
461
|
+
expect(yield* getFirstMessage(channelBToA)).toEqual({ message: 'A1' });
|
|
462
|
+
});
|
|
463
|
+
yield* Effect.all([nodeACode, nodeCCode(nodeCgen1)], { concurrency: 'unbounded' }).pipe(Effect.withSpan('test1'), Scope.extend(nodeCgen1Scope));
|
|
464
|
+
yield* Scope.close(nodeCgen1Scope, Exit.void);
|
|
465
|
+
const nodeCgen2 = yield* makeMeshNode('C');
|
|
466
|
+
yield* connectNodesViaMessageChannel(nodeB, nodeCgen2, { replaceIfExists: true });
|
|
467
|
+
yield* Effect.all([nodeACode, nodeCCode(nodeCgen2)], { concurrency: 'unbounded' }).pipe(Effect.withSpan('test2'));
|
|
468
|
+
}).pipe(withCtx(test)));
|
|
469
|
+
});
|
|
470
|
+
/**
|
|
471
|
+
* A
|
|
472
|
+
* / \
|
|
473
|
+
* B C
|
|
474
|
+
* \ /
|
|
475
|
+
* D
|
|
476
|
+
*/
|
|
477
|
+
Vitest.describe('diamond topology', () => {
|
|
478
|
+
Vitest.scopedLive('should work', (test) => Effect.gen(function* () {
|
|
479
|
+
const nodeA = yield* makeMeshNode('A');
|
|
480
|
+
const nodeB = yield* makeMeshNode('B');
|
|
481
|
+
const nodeC = yield* makeMeshNode('C');
|
|
482
|
+
const nodeD = yield* makeMeshNode('D');
|
|
483
|
+
yield* connectNodesViaMessageChannel(nodeA, nodeB);
|
|
484
|
+
yield* connectNodesViaMessageChannel(nodeA, nodeC);
|
|
485
|
+
yield* connectNodesViaMessageChannel(nodeB, nodeD);
|
|
486
|
+
yield* connectNodesViaMessageChannel(nodeC, nodeD);
|
|
487
|
+
const nodeACode = Effect.gen(function* () {
|
|
488
|
+
const channelAToD = yield* createChannel(nodeA, 'D');
|
|
489
|
+
yield* channelAToD.send({ message: 'A1' });
|
|
490
|
+
expect(yield* getFirstMessage(channelAToD)).toEqual({ message: 'D1' });
|
|
491
|
+
});
|
|
492
|
+
const nodeDCode = Effect.gen(function* () {
|
|
493
|
+
const channelDToA = yield* createChannel(nodeD, 'A');
|
|
494
|
+
yield* channelDToA.send({ message: 'D1' });
|
|
495
|
+
expect(yield* getFirstMessage(channelDToA)).toEqual({ message: 'A1' });
|
|
496
|
+
});
|
|
497
|
+
yield* Effect.all([nodeACode, nodeDCode], { concurrency: 'unbounded' });
|
|
498
|
+
}).pipe(withCtx(test)));
|
|
499
|
+
});
|
|
500
|
+
/**
|
|
501
|
+
* A E
|
|
502
|
+
* \ /
|
|
503
|
+
* C---D
|
|
504
|
+
* / \
|
|
505
|
+
* B F
|
|
506
|
+
*
|
|
507
|
+
* Topology: Butterfly topology with two connected hubs (C-D) each serving multiple nodes
|
|
508
|
+
*/
|
|
509
|
+
Vitest.describe('butterfly topology', () => {
|
|
510
|
+
Vitest.scopedLive('should work', (test) => Effect.gen(function* () {
|
|
511
|
+
const nodeA = yield* makeMeshNode('A');
|
|
512
|
+
const nodeB = yield* makeMeshNode('B');
|
|
513
|
+
const nodeC = yield* makeMeshNode('C');
|
|
514
|
+
const nodeD = yield* makeMeshNode('D');
|
|
515
|
+
const nodeE = yield* makeMeshNode('E');
|
|
516
|
+
const nodeF = yield* makeMeshNode('F');
|
|
517
|
+
yield* connectNodesViaMessageChannel(nodeA, nodeC);
|
|
518
|
+
yield* connectNodesViaMessageChannel(nodeB, nodeC);
|
|
519
|
+
yield* connectNodesViaMessageChannel(nodeC, nodeD);
|
|
520
|
+
yield* connectNodesViaMessageChannel(nodeD, nodeE);
|
|
521
|
+
yield* connectNodesViaMessageChannel(nodeD, nodeF);
|
|
522
|
+
yield* Effect.promise(() => nodeA.debug.requestTopology(100));
|
|
523
|
+
const nodeACode = Effect.gen(function* () {
|
|
524
|
+
const channelAToE = yield* createChannel(nodeA, 'E');
|
|
525
|
+
yield* channelAToE.send({ message: 'A1' });
|
|
526
|
+
expect(yield* getFirstMessage(channelAToE)).toEqual({ message: 'E1' });
|
|
527
|
+
});
|
|
528
|
+
const nodeECode = Effect.gen(function* () {
|
|
529
|
+
const channelEToA = yield* createChannel(nodeE, 'A');
|
|
530
|
+
yield* channelEToA.send({ message: 'E1' });
|
|
531
|
+
expect(yield* getFirstMessage(channelEToA)).toEqual({ message: 'A1' });
|
|
532
|
+
});
|
|
533
|
+
yield* Effect.all([nodeACode, nodeECode], { concurrency: 'unbounded' });
|
|
534
|
+
}).pipe(withCtx(test)));
|
|
315
535
|
});
|
|
316
|
-
Vitest.describe('mixture of
|
|
536
|
+
Vitest.describe('mixture of direct and proxy edge connections', () => {
|
|
317
537
|
// TODO test case to better guard against case where side A tries to create a proxy channel to B
|
|
318
|
-
// and side B tries to create a
|
|
538
|
+
// and side B tries to create a direct to A
|
|
319
539
|
Vitest.scopedLive('should work for proxy channels', (test) => Effect.gen(function* () {
|
|
320
540
|
const nodeA = yield* makeMeshNode('A');
|
|
321
541
|
const nodeB = yield* makeMeshNode('B');
|
|
322
542
|
yield* connectNodesViaMessageChannel(nodeB, nodeA);
|
|
323
543
|
const err = yield* connectNodesViaBroadcastChannel(nodeA, nodeB).pipe(Effect.flip);
|
|
324
|
-
expect(err._tag).toBe('
|
|
544
|
+
expect(err._tag).toBe('EdgeAlreadyExistsError');
|
|
325
545
|
}).pipe(withCtx(test)));
|
|
326
|
-
|
|
327
|
-
Vitest.scopedLive.skip('should work for messagechannels', (test) => Effect.gen(function* () {
|
|
546
|
+
Vitest.scopedLive('should work for directs', (test) => Effect.gen(function* () {
|
|
328
547
|
const nodeA = yield* makeMeshNode('A');
|
|
329
548
|
const nodeB = yield* makeMeshNode('B');
|
|
549
|
+
const nodeC = yield* makeMeshNode('C');
|
|
330
550
|
yield* connectNodesViaMessageChannel(nodeB, nodeA);
|
|
331
|
-
yield* connectNodesViaBroadcastChannel(
|
|
551
|
+
yield* connectNodesViaBroadcastChannel(nodeB, nodeC);
|
|
552
|
+
const nodeACode = Effect.gen(function* () {
|
|
553
|
+
const channelAToC = yield* createChannel(nodeA, 'C', { mode: 'proxy' });
|
|
554
|
+
yield* channelAToC.send({ message: 'A1' });
|
|
555
|
+
expect(yield* getFirstMessage(channelAToC)).toEqual({ message: 'C1' });
|
|
556
|
+
});
|
|
557
|
+
const nodeCCode = Effect.gen(function* () {
|
|
558
|
+
const channelCToA = yield* createChannel(nodeC, 'A', { mode: 'proxy' });
|
|
559
|
+
yield* channelCToA.send({ message: 'C1' });
|
|
560
|
+
expect(yield* getFirstMessage(channelCToA)).toEqual({ message: 'A1' });
|
|
561
|
+
});
|
|
562
|
+
yield* Effect.all([nodeACode, nodeCCode], { concurrency: 'unbounded' });
|
|
563
|
+
}).pipe(withCtx(test)));
|
|
564
|
+
});
|
|
565
|
+
Vitest.describe('listenForChannel', () => {
|
|
566
|
+
Vitest.scopedLive('connect later', (test) => Effect.gen(function* () {
|
|
567
|
+
const nodeA = yield* makeMeshNode('A');
|
|
568
|
+
const mode = 'direct';
|
|
569
|
+
const connect = mode === 'direct' ? connectNodesViaMessageChannel : connectNodesViaBroadcastChannel;
|
|
332
570
|
const nodeACode = Effect.gen(function* () {
|
|
333
|
-
const channelAToB = yield* createChannel(nodeA, 'B', {
|
|
571
|
+
const channelAToB = yield* createChannel(nodeA, 'B', { channelName: 'test', mode });
|
|
334
572
|
yield* channelAToB.send({ message: 'A1' });
|
|
335
573
|
expect(yield* getFirstMessage(channelAToB)).toEqual({ message: 'B1' });
|
|
336
574
|
});
|
|
337
575
|
const nodeBCode = Effect.gen(function* () {
|
|
338
|
-
const
|
|
339
|
-
yield*
|
|
340
|
-
|
|
576
|
+
const nodeB = yield* makeMeshNode('B');
|
|
577
|
+
yield* connect(nodeA, nodeB);
|
|
578
|
+
yield* nodeB.listenForChannel.pipe(Stream.filter((_) => _.channelName === 'test' && _.source === 'A' && _.mode === mode), Stream.tap(Effect.fn(function* (channelInfo) {
|
|
579
|
+
const channel = yield* createChannel(nodeB, channelInfo.source, {
|
|
580
|
+
channelName: channelInfo.channelName,
|
|
581
|
+
mode,
|
|
582
|
+
});
|
|
583
|
+
yield* channel.send({ message: 'B1' });
|
|
584
|
+
expect(yield* getFirstMessage(channel)).toEqual({ message: 'A1' });
|
|
585
|
+
})), Stream.take(1), Stream.runDrain);
|
|
586
|
+
});
|
|
587
|
+
yield* Effect.all([nodeACode, nodeBCode.pipe(Effect.delay(500))], { concurrency: 'unbounded' });
|
|
588
|
+
}).pipe(withCtx(test)));
|
|
589
|
+
// TODO provide a way to allow for reconnecting in the `listenForChannel` case
|
|
590
|
+
Vitest.scopedLive.skip('reconnect', (test) => Effect.gen(function* () {
|
|
591
|
+
const nodeA = yield* makeMeshNode('A');
|
|
592
|
+
const mode = 'direct';
|
|
593
|
+
const connect = mode === 'direct' ? connectNodesViaMessageChannel : connectNodesViaBroadcastChannel;
|
|
594
|
+
const nodeACode = Effect.gen(function* () {
|
|
595
|
+
const channelAToB = yield* createChannel(nodeA, 'B', { channelName: 'test', mode });
|
|
596
|
+
yield* channelAToB.send({ message: 'A1' });
|
|
597
|
+
expect(yield* getFirstMessage(channelAToB)).toEqual({ message: 'B1' });
|
|
341
598
|
});
|
|
599
|
+
const nodeBCode = Effect.gen(function* () {
|
|
600
|
+
const nodeB = yield* makeMeshNode('B');
|
|
601
|
+
yield* connect(nodeA, nodeB);
|
|
602
|
+
yield* nodeB.listenForChannel.pipe(Stream.filter((_) => _.channelName === 'test' && _.source === 'A' && _.mode === mode), Stream.tap(Effect.fn(function* (channelInfo) {
|
|
603
|
+
const channel = yield* createChannel(nodeB, channelInfo.source, {
|
|
604
|
+
channelName: channelInfo.channelName,
|
|
605
|
+
mode,
|
|
606
|
+
});
|
|
607
|
+
yield* channel.send({ message: 'B1' });
|
|
608
|
+
expect(yield* getFirstMessage(channel)).toEqual({ message: 'A1' });
|
|
609
|
+
})), Stream.take(1), Stream.runDrain);
|
|
610
|
+
}).pipe(Effect.withSpan('nodeBCode:gen1'), Effect.andThen(Effect.gen(function* () {
|
|
611
|
+
const nodeB = yield* makeMeshNode('B');
|
|
612
|
+
yield* connect(nodeA, nodeB, { replaceIfExists: true });
|
|
613
|
+
yield* nodeB.listenForChannel.pipe(Stream.filter((_) => _.channelName === 'test' && _.source === 'A' && _.mode === mode), Stream.tap(Effect.fn(function* (channelInfo) {
|
|
614
|
+
const channel = yield* createChannel(nodeB, channelInfo.source, {
|
|
615
|
+
channelName: channelInfo.channelName,
|
|
616
|
+
mode,
|
|
617
|
+
});
|
|
618
|
+
console.log('recreated channel', channel);
|
|
619
|
+
// yield* channel.send({ message: 'B1' })
|
|
620
|
+
// expect(yield* getFirstMessage(channel)).toEqual({ message: 'A1' })
|
|
621
|
+
})), Stream.take(1), Stream.runDrain);
|
|
622
|
+
}).pipe(Effect.withSpan('nodeBCode:gen2'))));
|
|
342
623
|
yield* Effect.all([nodeACode, nodeBCode], { concurrency: 'unbounded' });
|
|
343
624
|
}).pipe(withCtx(test)));
|
|
625
|
+
Vitest.describe('prop tests', { timeout: propTestTimeout }, () => {
|
|
626
|
+
Vitest.scopedLive.prop('listenForChannel A <> B <> C', [Delay, Delay, Delay, Delay, ChannelType], ([delayNodeA, delayNodeC, delayConnectAB, delayConnectBC, channelType], test) => Effect.gen(function* () {
|
|
627
|
+
const nodeA = yield* makeMeshNode('A');
|
|
628
|
+
const nodeB = yield* makeMeshNode('B');
|
|
629
|
+
const nodeC = yield* makeMeshNode('C');
|
|
630
|
+
const mode = channelType.includes('proxy') ? 'proxy' : 'direct';
|
|
631
|
+
const connect = channelType === 'direct' ? connectNodesViaMessageChannel : connectNodesViaBroadcastChannel;
|
|
632
|
+
yield* connect(nodeA, nodeB).pipe(maybeDelay(delayConnectAB, 'delayConnectAB'));
|
|
633
|
+
yield* connect(nodeB, nodeC).pipe(maybeDelay(delayConnectBC, 'delayConnectBC'));
|
|
634
|
+
const nodeACode = Effect.gen(function* () {
|
|
635
|
+
const _channel2AToC = yield* createChannel(nodeA, 'C', { channelName: 'test-2', mode });
|
|
636
|
+
const channelAToC = yield* createChannel(nodeA, 'C', { channelName: 'test-1', mode });
|
|
637
|
+
yield* channelAToC.send({ message: 'A1' });
|
|
638
|
+
expect(yield* getFirstMessage(channelAToC)).toEqual({ message: 'C1' });
|
|
639
|
+
});
|
|
640
|
+
const nodeCCode = Effect.gen(function* () {
|
|
641
|
+
const _channel2CToA = yield* createChannel(nodeC, 'A', { channelName: 'test-2', mode });
|
|
642
|
+
yield* nodeC.listenForChannel.pipe(Stream.filter((_) => _.channelName === 'test-1' && _.source === 'A' && _.mode === mode), Stream.tap(Effect.fn(function* (channelInfo) {
|
|
643
|
+
const channel = yield* createChannel(nodeC, channelInfo.source, {
|
|
644
|
+
channelName: channelInfo.channelName,
|
|
645
|
+
mode,
|
|
646
|
+
});
|
|
647
|
+
yield* channel.send({ message: 'C1' });
|
|
648
|
+
expect(yield* getFirstMessage(channel)).toEqual({ message: 'A1' });
|
|
649
|
+
})), Stream.take(1), Stream.runDrain);
|
|
650
|
+
});
|
|
651
|
+
yield* Effect.all([
|
|
652
|
+
nodeACode.pipe(maybeDelay(delayNodeA, 'nodeACode')),
|
|
653
|
+
nodeCCode.pipe(maybeDelay(delayNodeC, 'nodeCCode')),
|
|
654
|
+
], { concurrency: 'unbounded' });
|
|
655
|
+
}).pipe(withCtx(test, {
|
|
656
|
+
skipOtel: true,
|
|
657
|
+
suffix: `delayNodeA=${delayNodeA} delayNodeC=${delayNodeC} delayConnectAB=${delayConnectAB} delayConnectBC=${delayConnectBC} channelType=${channelType}`,
|
|
658
|
+
timeout: testTimeout * 2,
|
|
659
|
+
})), { fastCheck: { numRuns: 10 } });
|
|
660
|
+
});
|
|
661
|
+
});
|
|
662
|
+
Vitest.describe('broadcast channel', () => {
|
|
663
|
+
Vitest.scopedLive('should work', (test) => Effect.gen(function* () {
|
|
664
|
+
const nodeA = yield* makeMeshNode('A');
|
|
665
|
+
const nodeB = yield* makeMeshNode('B');
|
|
666
|
+
const nodeC = yield* makeMeshNode('C');
|
|
667
|
+
yield* connectNodesViaMessageChannel(nodeA, nodeB);
|
|
668
|
+
yield* connectNodesViaMessageChannel(nodeB, nodeC);
|
|
669
|
+
const channelOnA = yield* nodeA.makeBroadcastChannel({ channelName: 'test', schema: Schema.String });
|
|
670
|
+
const channelOnC = yield* nodeC.makeBroadcastChannel({ channelName: 'test', schema: Schema.String });
|
|
671
|
+
const listenOnAFiber = yield* channelOnA.listen.pipe(Stream.flatten(), Stream.runHead, Effect.flatten, Effect.fork);
|
|
672
|
+
const listenOnCFiber = yield* channelOnC.listen.pipe(Stream.flatten(), Stream.runHead, Effect.flatten, Effect.fork);
|
|
673
|
+
yield* channelOnA.send('A1');
|
|
674
|
+
yield* channelOnC.send('C1');
|
|
675
|
+
expect(yield* listenOnAFiber).toEqual('C1');
|
|
676
|
+
expect(yield* listenOnCFiber).toEqual('A1');
|
|
677
|
+
}).pipe(withCtx(test)));
|
|
344
678
|
});
|
|
345
679
|
});
|
|
346
|
-
const
|
|
347
|
-
const
|
|
348
|
-
const otelLayer = isCi ? Layer.empty : OtelLiveHttp({ serviceName: 'webmesh-node-test', skipLogUrl: false });
|
|
349
|
-
const withCtx = (testContext, { suffix, skipOtel = false } = {}) => (self) => self.pipe(Effect.timeout(isCi ? 10_000 : 500), Effect.provide(Logger.pretty), Effect.scoped, // We need to scope the effect manually here because otherwise the span is not closed
|
|
680
|
+
const otelLayer = IS_CI ? Layer.empty : OtelLiveHttp({ serviceName: 'webmesh-node-test', skipLogUrl: false });
|
|
681
|
+
const withCtx = (testContext, { suffix, skipOtel = false, timeout = testTimeout } = {}) => (self) => self.pipe(Effect.timeout(timeout), Effect.provide(Logger.pretty), Logger.withMinimumLogLevel(LogLevel.Debug), Effect.scoped, // We need to scope the effect manually here because otherwise the span is not closed
|
|
350
682
|
Effect.withSpan(`${testContext.task.suite?.name}:${testContext.task.name}${suffix ? `:${suffix}` : ''}`), skipOtel ? identity : Effect.provide(otelLayer));
|
|
351
683
|
//# sourceMappingURL=node.test.js.map
|