@livestore/webmesh 0.3.0-dev.10 → 0.3.0-dev.12
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +20 -1
- package/dist/.tsbuildinfo +1 -1
- package/dist/channel/message-channel copy.d.ts +9 -0
- package/dist/channel/message-channel copy.d.ts.map +1 -0
- package/dist/channel/message-channel copy.js +137 -0
- package/dist/channel/message-channel copy.js.map +1 -0
- package/dist/channel/message-channel-internal copy.d.ts +42 -0
- package/dist/channel/message-channel-internal copy.d.ts.map +1 -0
- package/dist/channel/message-channel-internal copy.js +239 -0
- package/dist/channel/message-channel-internal copy.js.map +1 -0
- package/dist/channel/message-channel-internal.d.ts +26 -0
- package/dist/channel/message-channel-internal.d.ts.map +1 -0
- package/dist/channel/message-channel-internal.js +217 -0
- package/dist/channel/message-channel-internal.js.map +1 -0
- package/dist/channel/message-channel.d.ts +21 -19
- package/dist/channel/message-channel.d.ts.map +1 -1
- package/dist/channel/message-channel.js +128 -162
- package/dist/channel/message-channel.js.map +1 -1
- package/dist/channel/proxy-channel.d.ts +2 -2
- package/dist/channel/proxy-channel.d.ts.map +1 -1
- package/dist/channel/proxy-channel.js +7 -5
- package/dist/channel/proxy-channel.js.map +1 -1
- package/dist/common.d.ts +8 -4
- package/dist/common.d.ts.map +1 -1
- package/dist/common.js +2 -1
- package/dist/common.js.map +1 -1
- package/dist/mesh-schema.d.ts +23 -1
- package/dist/mesh-schema.d.ts.map +1 -1
- package/dist/mesh-schema.js +21 -2
- package/dist/mesh-schema.js.map +1 -1
- package/dist/node.d.ts +12 -1
- package/dist/node.d.ts.map +1 -1
- package/dist/node.js +40 -9
- package/dist/node.js.map +1 -1
- package/dist/node.test.d.ts +1 -1
- package/dist/node.test.d.ts.map +1 -1
- package/dist/node.test.js +300 -147
- package/dist/node.test.js.map +1 -1
- package/dist/websocket-connection.d.ts +1 -2
- package/dist/websocket-connection.d.ts.map +1 -1
- package/dist/websocket-connection.js +5 -4
- package/dist/websocket-connection.js.map +1 -1
- package/package.json +3 -3
- package/src/channel/message-channel-internal.ts +356 -0
- package/src/channel/message-channel.ts +183 -311
- 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 +426 -177
- package/src/node.ts +70 -12
- package/src/websocket-connection.ts +83 -79
package/dist/node.test.js
CHANGED
|
@@ -1,4 +1,6 @@
|
|
|
1
|
-
import
|
|
1
|
+
import '@livestore/utils/node-vitest-polyfill';
|
|
2
|
+
import { IS_CI } from '@livestore/utils';
|
|
3
|
+
import { Chunk, Deferred, Effect, Exit, identity, Layer, Logger, Schema, Scope, Stream, WebChannel, } from '@livestore/utils/effect';
|
|
2
4
|
import { OtelLiveHttp } from '@livestore/utils/node';
|
|
3
5
|
import { Vitest } from '@livestore/utils/node-vitest';
|
|
4
6
|
import { expect } from 'vitest';
|
|
@@ -11,28 +13,41 @@ import { makeMeshNode } from './node.js';
|
|
|
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.addConnection({
|
|
19
|
-
|
|
20
|
-
|
|
20
|
+
yield* nodeA.addConnection({
|
|
21
|
+
target: nodeB.nodeName,
|
|
22
|
+
connectionChannel: meshChannelAToB,
|
|
23
|
+
replaceIfExists: options?.replaceIfExists,
|
|
24
|
+
});
|
|
25
|
+
yield* nodeB.addConnection({
|
|
26
|
+
target: nodeA.nodeName,
|
|
27
|
+
connectionChannel: 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.addConnection({
|
|
42
|
+
target: nodeB.nodeName,
|
|
43
|
+
connectionChannel: broadcastWebChannelA,
|
|
44
|
+
replaceIfExists: options?.replaceIfExists,
|
|
45
|
+
});
|
|
46
|
+
yield* nodeB.addConnection({
|
|
47
|
+
target: nodeA.nodeName,
|
|
48
|
+
connectionChannel: 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,
|
|
@@ -47,141 +62,199 @@ 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('messagechannel', 'messagechannel.proxy', '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 'messagechannel': {
|
|
80
|
+
return { mode: 'messagechannel', connectNodes: connectNodesViaMessageChannel };
|
|
81
|
+
}
|
|
82
|
+
case 'messagechannel.proxy': {
|
|
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 = 'messagechannel'
|
|
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.all([nodeACode, nodeBCode], { concurrency: 'unbounded' });
|
|
151
133
|
}).pipe(withCtx(test, {
|
|
152
134
|
skipOtel: true,
|
|
153
|
-
suffix: `
|
|
135
|
+
suffix: `delayX=${delayX} delayY=${delayY} connectDelay=${connectDelay} channelType=${channelType} nodeNames=${nodeNames}`,
|
|
154
136
|
})));
|
|
155
|
-
|
|
156
|
-
|
|
137
|
+
{
|
|
138
|
+
// const waitForOfflineDelay = undefined
|
|
139
|
+
// const sleepDelay = 0
|
|
140
|
+
// const channelType = 'messagechannel'
|
|
141
|
+
// Vitest.scopedLive(
|
|
142
|
+
// 'b reconnects',
|
|
143
|
+
// (test) =>
|
|
144
|
+
Vitest.scopedLive.prop('b reconnects', [Delay, Delay, ChannelType], ([waitForOfflineDelay, sleepDelay, channelType], test) => Effect.gen(function* () {
|
|
145
|
+
// console.log({ waitForOfflineDelay, sleepDelay, channelType })
|
|
146
|
+
if (waitForOfflineDelay === undefined) {
|
|
147
|
+
// TODO we still need to fix this scenario but it shouldn't really be common in practice
|
|
148
|
+
return;
|
|
149
|
+
}
|
|
150
|
+
const nodeA = yield* makeMeshNode('A');
|
|
151
|
+
const nodeB = yield* makeMeshNode('B');
|
|
152
|
+
const { mode, connectNodes } = fromChannelType(channelType);
|
|
153
|
+
// TODO also optionally delay the connection
|
|
154
|
+
yield* connectNodes(nodeA, nodeB);
|
|
155
|
+
const waitForBToBeOffline = waitForOfflineDelay === undefined ? undefined : yield* Deferred.make();
|
|
156
|
+
const nodeACode = Effect.gen(function* () {
|
|
157
|
+
const channelAToB = yield* createChannel(nodeA, 'B', { mode });
|
|
158
|
+
yield* channelAToB.send({ message: 'A1' });
|
|
159
|
+
expect(yield* getFirstMessage(channelAToB)).toEqual({ message: 'B1' });
|
|
160
|
+
console.log('nodeACode:waiting for B to be offline');
|
|
161
|
+
if (waitForBToBeOffline !== undefined) {
|
|
162
|
+
yield* waitForBToBeOffline;
|
|
163
|
+
}
|
|
164
|
+
yield* channelAToB.send({ message: 'A2' });
|
|
165
|
+
expect(yield* getFirstMessage(channelAToB)).toEqual({ message: 'B2' });
|
|
166
|
+
});
|
|
167
|
+
// Simulating node b going offline and then coming back online
|
|
168
|
+
// This test also illustrates why we need a ack-message channel since otherwise
|
|
169
|
+
// sent messages might get lost
|
|
170
|
+
const nodeBCode = Effect.gen(function* () {
|
|
171
|
+
yield* Effect.gen(function* () {
|
|
172
|
+
const channelBToA = yield* createChannel(nodeB, 'A', { mode });
|
|
173
|
+
yield* channelBToA.send({ message: 'B1' });
|
|
174
|
+
expect(yield* getFirstMessage(channelBToA)).toEqual({ message: 'A1' });
|
|
175
|
+
}).pipe(Effect.scoped, Effect.withSpan('nodeBCode:part1'));
|
|
176
|
+
console.log('nodeBCode:B node going offline');
|
|
177
|
+
if (waitForBToBeOffline !== undefined) {
|
|
178
|
+
yield* Deferred.succeed(waitForBToBeOffline, void 0);
|
|
179
|
+
}
|
|
180
|
+
if (sleepDelay !== undefined) {
|
|
181
|
+
yield* Effect.sleep(sleepDelay).pipe(Effect.withSpan(`B:sleep(${sleepDelay})`));
|
|
182
|
+
}
|
|
183
|
+
// Recreating the channel
|
|
184
|
+
yield* Effect.gen(function* () {
|
|
185
|
+
const channelBToA = yield* createChannel(nodeB, 'A', { mode });
|
|
186
|
+
yield* channelBToA.send({ message: 'B2' });
|
|
187
|
+
expect(yield* getFirstMessage(channelBToA)).toEqual({ message: 'A2' });
|
|
188
|
+
}).pipe(Effect.scoped, Effect.withSpan('nodeBCode:part2'));
|
|
189
|
+
});
|
|
190
|
+
yield* Effect.all([nodeACode, nodeBCode], { concurrency: 'unbounded' }).pipe(Effect.withSpan('test'));
|
|
191
|
+
}).pipe(withCtx(test, {
|
|
192
|
+
skipOtel: true,
|
|
193
|
+
suffix: `waitForOfflineDelay=${waitForOfflineDelay} sleepDelay=${sleepDelay} channelType=${channelType}`,
|
|
194
|
+
})), { fastCheck: { numRuns: 20 } });
|
|
195
|
+
}
|
|
196
|
+
Vitest.scopedLive('reconnect with re-created node', (test) => Effect.gen(function* () {
|
|
197
|
+
const nodeBgen1Scope = yield* Scope.make();
|
|
157
198
|
const nodeA = yield* makeMeshNode('A');
|
|
158
|
-
const
|
|
159
|
-
|
|
160
|
-
yield*
|
|
161
|
-
const
|
|
199
|
+
const nodeBgen1 = yield* makeMeshNode('B').pipe(Scope.extend(nodeBgen1Scope));
|
|
200
|
+
yield* connectNodesViaMessageChannel(nodeA, nodeBgen1).pipe(Scope.extend(nodeBgen1Scope));
|
|
201
|
+
// yield* Effect.sleep(100)
|
|
202
|
+
const channelAToBOnce = yield* Effect.cached(createChannel(nodeA, 'B'));
|
|
162
203
|
const nodeACode = Effect.gen(function* () {
|
|
163
|
-
const channelAToB = yield*
|
|
204
|
+
const channelAToB = yield* channelAToBOnce;
|
|
164
205
|
yield* channelAToB.send({ message: 'A1' });
|
|
165
206
|
expect(yield* getFirstMessage(channelAToB)).toEqual({ message: 'B1' });
|
|
166
|
-
|
|
167
|
-
yield* channelAToB.send({ message: 'A2' });
|
|
168
|
-
expect(yield* getFirstMessage(channelAToB)).toEqual({ message: 'B2' });
|
|
207
|
+
// expect(channelAToB.debugInfo.connectCounter).toBe(1)
|
|
169
208
|
});
|
|
170
|
-
const nodeBCode = Effect.gen(function* () {
|
|
171
|
-
const channelBToA = yield* createChannel(nodeB, 'A'
|
|
209
|
+
const nodeBCode = (nodeB) => Effect.gen(function* () {
|
|
210
|
+
const channelBToA = yield* createChannel(nodeB, 'A');
|
|
172
211
|
yield* channelBToA.send({ message: 'B1' });
|
|
173
212
|
expect(yield* getFirstMessage(channelBToA)).toEqual({ message: 'A1' });
|
|
213
|
+
// expect(channelBToA.debugInfo.connectCounter).toBe(1)
|
|
214
|
+
});
|
|
215
|
+
yield* Effect.all([nodeACode, nodeBCode(nodeBgen1).pipe(Scope.extend(nodeBgen1Scope))], {
|
|
216
|
+
concurrency: 'unbounded',
|
|
217
|
+
}).pipe(Effect.withSpan('test1'));
|
|
218
|
+
yield* Scope.close(nodeBgen1Scope, Exit.void);
|
|
219
|
+
const nodeBgen2 = yield* makeMeshNode('B');
|
|
220
|
+
yield* connectNodesViaMessageChannel(nodeA, nodeBgen2, { replaceIfExists: true });
|
|
221
|
+
yield* Effect.all([nodeACode, nodeBCode(nodeBgen2)], { concurrency: 'unbounded' }).pipe(Effect.withSpan('test2'));
|
|
222
|
+
}).pipe(withCtx(test)));
|
|
223
|
+
const ChannelTypeWithoutMessageChannelProxy = Schema.Literal('proxy', 'messagechannel');
|
|
224
|
+
Vitest.scopedLive.prop('replace connection while keeping the channel', [ChannelTypeWithoutMessageChannelProxy, NodeNames], ([channelType, nodeNames], test) => Effect.gen(function* () {
|
|
225
|
+
const [nodeNameX, nodeNameY] = nodeNames;
|
|
226
|
+
const nodeX = yield* makeMeshNode(nodeNameX);
|
|
227
|
+
const nodeY = yield* makeMeshNode(nodeNameY);
|
|
228
|
+
const nodeLabel = { x: nodeX.nodeName, y: nodeY.nodeName };
|
|
229
|
+
const { mode, connectNodes } = fromChannelType(channelType);
|
|
230
|
+
yield* connectNodes(nodeX, nodeY);
|
|
231
|
+
const waitForConnectionReplacement = yield* Deferred.make();
|
|
232
|
+
const nodeXCode = Effect.gen(function* () {
|
|
233
|
+
const channelXToY = yield* createChannel(nodeX, nodeLabel.y, { mode });
|
|
234
|
+
yield* channelXToY.send({ message: `${nodeLabel.x}1` });
|
|
235
|
+
expect(yield* getFirstMessage(channelXToY)).toEqual({ message: `${nodeLabel.y}1` });
|
|
236
|
+
yield* waitForConnectionReplacement;
|
|
237
|
+
yield* channelXToY.send({ message: `${nodeLabel.x}2` });
|
|
238
|
+
expect(yield* getFirstMessage(channelXToY)).toEqual({ message: `${nodeLabel.y}2` });
|
|
239
|
+
});
|
|
240
|
+
const nodeYCode = Effect.gen(function* () {
|
|
241
|
+
const channelYToX = yield* createChannel(nodeY, nodeLabel.x, { mode });
|
|
242
|
+
yield* channelYToX.send({ message: `${nodeLabel.y}1` });
|
|
243
|
+
expect(yield* getFirstMessage(channelYToX)).toEqual({ message: `${nodeLabel.x}1` });
|
|
174
244
|
// Switch out connection while keeping the channel
|
|
175
|
-
yield*
|
|
176
|
-
yield*
|
|
177
|
-
yield* connectNodes(
|
|
245
|
+
yield* nodeX.removeConnection(nodeLabel.y);
|
|
246
|
+
yield* nodeY.removeConnection(nodeLabel.x);
|
|
247
|
+
yield* connectNodes(nodeX, nodeY);
|
|
178
248
|
yield* Deferred.succeed(waitForConnectionReplacement, void 0);
|
|
179
|
-
yield*
|
|
180
|
-
expect(yield* getFirstMessage(
|
|
249
|
+
yield* channelYToX.send({ message: `${nodeLabel.y}2` });
|
|
250
|
+
expect(yield* getFirstMessage(channelYToX)).toEqual({ message: `${nodeLabel.x}2` });
|
|
181
251
|
});
|
|
182
|
-
yield* Effect.all([
|
|
183
|
-
}).pipe(withCtx(test, {
|
|
184
|
-
|
|
252
|
+
yield* Effect.all([nodeXCode, nodeYCode], { concurrency: 'unbounded' });
|
|
253
|
+
}).pipe(withCtx(test, {
|
|
254
|
+
skipOtel: true,
|
|
255
|
+
suffix: `channelType=${channelType} nodeNames=${nodeNames}`,
|
|
256
|
+
})), { fastCheck: { numRuns: 10 } });
|
|
257
|
+
Vitest.describe('TODO improve latency', () => {
|
|
185
258
|
// TODO we need to improve latency when sending messages concurrently
|
|
186
259
|
Vitest.scopedLive.prop('concurrent messages', [ChannelType, Schema.Int.pipe(Schema.between(1, 50))], ([channelType, count], test) => Effect.gen(function* () {
|
|
187
260
|
const nodeA = yield* makeMeshNode('A');
|
|
@@ -204,9 +277,34 @@ Vitest.describe('webmesh node', { timeout: 1000 }, () => {
|
|
|
204
277
|
yield* Effect.all([nodeACode, nodeBCode, connectNodes(nodeA, nodeB).pipe(Effect.delay(100))], {
|
|
205
278
|
concurrency: 'unbounded',
|
|
206
279
|
});
|
|
207
|
-
}).pipe(withCtx(test, {
|
|
280
|
+
}).pipe(withCtx(test, {
|
|
281
|
+
skipOtel: true,
|
|
282
|
+
suffix: `channelType=${channelType} count=${count}`,
|
|
283
|
+
timeout: testTimeout * 2,
|
|
284
|
+
})), { fastCheck: { numRuns: 10 } });
|
|
208
285
|
});
|
|
209
286
|
});
|
|
287
|
+
Vitest.describe('message channel specific tests', () => {
|
|
288
|
+
Vitest.scopedLive('differing initial connection counter', (test) => Effect.gen(function* () {
|
|
289
|
+
const nodeA = yield* makeMeshNode('A');
|
|
290
|
+
const nodeB = yield* makeMeshNode('B');
|
|
291
|
+
yield* connectNodesViaMessageChannel(nodeA, nodeB);
|
|
292
|
+
const messageCount = 3;
|
|
293
|
+
const bFiber = yield* Effect.gen(function* () {
|
|
294
|
+
const channelBToA = yield* createChannel(nodeB, 'A');
|
|
295
|
+
yield* channelBToA.listen.pipe(Stream.flatten(), Stream.tap((msg) => channelBToA.send({ message: `resp:${msg.message}` })), Stream.take(messageCount), Stream.runDrain);
|
|
296
|
+
}).pipe(Effect.scoped, Effect.fork);
|
|
297
|
+
// yield* createChannel(nodeA, 'B').pipe(Effect.andThen(WebChannel.shutdown))
|
|
298
|
+
// // yield* createChannel(nodeA, 'B').pipe(Effect.andThen(WebChannel.shutdown))
|
|
299
|
+
// // yield* createChannel(nodeA, 'B').pipe(Effect.andThen(WebChannel.shutdown))
|
|
300
|
+
yield* Effect.gen(function* () {
|
|
301
|
+
const channelAToB = yield* createChannel(nodeA, 'B');
|
|
302
|
+
yield* channelAToB.send({ message: 'A' });
|
|
303
|
+
expect(yield* getFirstMessage(channelAToB)).toEqual({ message: 'resp:A' });
|
|
304
|
+
}).pipe(Effect.scoped, Effect.repeatN(messageCount));
|
|
305
|
+
yield* bFiber;
|
|
306
|
+
}).pipe(withCtx(test)));
|
|
307
|
+
});
|
|
210
308
|
Vitest.scopedLive('manual debug test', (test) => Effect.gen(function* () {
|
|
211
309
|
const nodeA = yield* makeMeshNode('A');
|
|
212
310
|
const nodeB = yield* makeMeshNode('B');
|
|
@@ -246,6 +344,7 @@ Vitest.describe('webmesh node', { timeout: 1000 }, () => {
|
|
|
246
344
|
yield* channelAToC.send({ message: 'A1' });
|
|
247
345
|
expect(yield* getFirstMessage(channelAToC)).toEqual({ message: 'C1' });
|
|
248
346
|
expect(yield* getFirstMessage(channelAToC)).toEqual({ message: 'C2' });
|
|
347
|
+
expect(yield* getFirstMessage(channelAToC)).toEqual({ message: 'C3' });
|
|
249
348
|
});
|
|
250
349
|
const nodeCCode = Effect.gen(function* () {
|
|
251
350
|
const channelCToA = yield* createChannel(nodeC, 'A');
|
|
@@ -274,9 +373,11 @@ Vitest.describe('webmesh node', { timeout: 1000 }, () => {
|
|
|
274
373
|
yield* channelCToA.send({ message: 'C1' });
|
|
275
374
|
expect(yield* getFirstMessage(channelCToA)).toEqual({ message: 'A1' });
|
|
276
375
|
});
|
|
277
|
-
yield* Effect.all([
|
|
278
|
-
|
|
279
|
-
|
|
376
|
+
yield* Effect.all([
|
|
377
|
+
nodeACode,
|
|
378
|
+
nodeCCode,
|
|
379
|
+
connectNodes(nodeB, nodeC).pipe(Effect.delay(100), Effect.withSpan('connect-nodeB-nodeC-delay(100)')),
|
|
380
|
+
], { concurrency: 'unbounded' });
|
|
280
381
|
}).pipe(withCtx(test)));
|
|
281
382
|
Vitest.scopedLive('proxy channel', (test) => Effect.gen(function* () {
|
|
282
383
|
const nodeA = yield* makeMeshNode('A');
|
|
@@ -312,6 +413,59 @@ Vitest.describe('webmesh node', { timeout: 1000 }, () => {
|
|
|
312
413
|
});
|
|
313
414
|
yield* Effect.all([nodeACode, nodeCCode], { concurrency: 'unbounded' });
|
|
314
415
|
}).pipe(withCtx(test)));
|
|
416
|
+
Vitest.scopedLive('reconnect with re-created node', (test) => Effect.gen(function* () {
|
|
417
|
+
const nodeCgen1Scope = yield* Scope.make();
|
|
418
|
+
const nodeA = yield* makeMeshNode('A');
|
|
419
|
+
const nodeB = yield* makeMeshNode('B');
|
|
420
|
+
const nodeCgen1 = yield* makeMeshNode('C').pipe(Scope.extend(nodeCgen1Scope));
|
|
421
|
+
yield* connectNodesViaMessageChannel(nodeA, nodeB);
|
|
422
|
+
yield* connectNodesViaMessageChannel(nodeB, nodeCgen1).pipe(Scope.extend(nodeCgen1Scope));
|
|
423
|
+
const nodeACode = Effect.gen(function* () {
|
|
424
|
+
const channelAToB = yield* createChannel(nodeA, 'C');
|
|
425
|
+
yield* channelAToB.send({ message: 'A1' });
|
|
426
|
+
expect(yield* getFirstMessage(channelAToB)).toEqual({ message: 'C1' });
|
|
427
|
+
});
|
|
428
|
+
const nodeCCode = (nodeB) => Effect.gen(function* () {
|
|
429
|
+
const channelBToA = yield* createChannel(nodeB, 'A');
|
|
430
|
+
yield* channelBToA.send({ message: 'C1' });
|
|
431
|
+
expect(yield* getFirstMessage(channelBToA)).toEqual({ message: 'A1' });
|
|
432
|
+
});
|
|
433
|
+
yield* Effect.all([nodeACode, nodeCCode(nodeCgen1)], { concurrency: 'unbounded' }).pipe(Effect.withSpan('test1'), Scope.extend(nodeCgen1Scope));
|
|
434
|
+
yield* Scope.close(nodeCgen1Scope, Exit.void);
|
|
435
|
+
const nodeCgen2 = yield* makeMeshNode('C');
|
|
436
|
+
yield* connectNodesViaMessageChannel(nodeB, nodeCgen2, { replaceIfExists: true });
|
|
437
|
+
yield* Effect.all([nodeACode, nodeCCode(nodeCgen2)], { concurrency: 'unbounded' }).pipe(Effect.withSpan('test2'));
|
|
438
|
+
}).pipe(withCtx(test)));
|
|
439
|
+
});
|
|
440
|
+
/**
|
|
441
|
+
* A
|
|
442
|
+
* / \
|
|
443
|
+
* B C
|
|
444
|
+
* \ /
|
|
445
|
+
* D
|
|
446
|
+
*/
|
|
447
|
+
Vitest.describe('diamond topology', () => {
|
|
448
|
+
Vitest.scopedLive('should work', (test) => Effect.gen(function* () {
|
|
449
|
+
const nodeA = yield* makeMeshNode('A');
|
|
450
|
+
const nodeB = yield* makeMeshNode('B');
|
|
451
|
+
const nodeC = yield* makeMeshNode('C');
|
|
452
|
+
const nodeD = yield* makeMeshNode('D');
|
|
453
|
+
yield* connectNodesViaMessageChannel(nodeA, nodeB);
|
|
454
|
+
yield* connectNodesViaMessageChannel(nodeA, nodeC);
|
|
455
|
+
yield* connectNodesViaMessageChannel(nodeB, nodeD);
|
|
456
|
+
yield* connectNodesViaMessageChannel(nodeC, nodeD);
|
|
457
|
+
const nodeACode = Effect.gen(function* () {
|
|
458
|
+
const channelAToD = yield* createChannel(nodeA, 'D');
|
|
459
|
+
yield* channelAToD.send({ message: 'A1' });
|
|
460
|
+
expect(yield* getFirstMessage(channelAToD)).toEqual({ message: 'D1' });
|
|
461
|
+
});
|
|
462
|
+
const nodeDCode = Effect.gen(function* () {
|
|
463
|
+
const channelDToA = yield* createChannel(nodeD, 'A');
|
|
464
|
+
yield* channelDToA.send({ message: 'D1' });
|
|
465
|
+
expect(yield* getFirstMessage(channelDToA)).toEqual({ message: 'A1' });
|
|
466
|
+
});
|
|
467
|
+
yield* Effect.all([nodeACode, nodeDCode], { concurrency: 'unbounded' });
|
|
468
|
+
}).pipe(withCtx(test)));
|
|
315
469
|
});
|
|
316
470
|
Vitest.describe('mixture of messagechannel and proxy connections', () => {
|
|
317
471
|
// TODO test case to better guard against case where side A tries to create a proxy channel to B
|
|
@@ -324,28 +478,27 @@ Vitest.describe('webmesh node', { timeout: 1000 }, () => {
|
|
|
324
478
|
expect(err._tag).toBe('ConnectionAlreadyExistsError');
|
|
325
479
|
}).pipe(withCtx(test)));
|
|
326
480
|
// TODO this currently fails but should work. probably needs some more guarding internally.
|
|
327
|
-
Vitest.scopedLive
|
|
481
|
+
Vitest.scopedLive('should work for messagechannels', (test) => Effect.gen(function* () {
|
|
328
482
|
const nodeA = yield* makeMeshNode('A');
|
|
329
483
|
const nodeB = yield* makeMeshNode('B');
|
|
484
|
+
const nodeC = yield* makeMeshNode('C');
|
|
330
485
|
yield* connectNodesViaMessageChannel(nodeB, nodeA);
|
|
331
|
-
yield* connectNodesViaBroadcastChannel(
|
|
486
|
+
yield* connectNodesViaBroadcastChannel(nodeB, nodeC);
|
|
332
487
|
const nodeACode = Effect.gen(function* () {
|
|
333
|
-
const
|
|
334
|
-
yield*
|
|
335
|
-
expect(yield* getFirstMessage(
|
|
488
|
+
const channelAToC = yield* createChannel(nodeA, 'C', { mode: 'proxy' });
|
|
489
|
+
yield* channelAToC.send({ message: 'A1' });
|
|
490
|
+
expect(yield* getFirstMessage(channelAToC)).toEqual({ message: 'C1' });
|
|
336
491
|
});
|
|
337
|
-
const
|
|
338
|
-
const
|
|
339
|
-
yield*
|
|
340
|
-
expect(yield* getFirstMessage(
|
|
492
|
+
const nodeCCode = Effect.gen(function* () {
|
|
493
|
+
const channelCToA = yield* createChannel(nodeC, 'A', { mode: 'proxy' });
|
|
494
|
+
yield* channelCToA.send({ message: 'C1' });
|
|
495
|
+
expect(yield* getFirstMessage(channelCToA)).toEqual({ message: 'A1' });
|
|
341
496
|
});
|
|
342
|
-
yield* Effect.all([nodeACode,
|
|
497
|
+
yield* Effect.all([nodeACode, nodeCCode], { concurrency: 'unbounded' });
|
|
343
498
|
}).pipe(withCtx(test)));
|
|
344
499
|
});
|
|
345
500
|
});
|
|
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
|
|
501
|
+
const otelLayer = IS_CI ? Layer.empty : OtelLiveHttp({ serviceName: 'webmesh-node-test', skipLogUrl: false });
|
|
502
|
+
const withCtx = (testContext, { suffix, skipOtel = false, timeout = testTimeout } = {}) => (self) => self.pipe(Effect.timeout(timeout), Effect.provide(Logger.pretty), Effect.scoped, // We need to scope the effect manually here because otherwise the span is not closed
|
|
350
503
|
Effect.withSpan(`${testContext.task.suite?.name}:${testContext.task.name}${suffix ? `:${suffix}` : ''}`), skipOtel ? identity : Effect.provide(otelLayer));
|
|
351
504
|
//# sourceMappingURL=node.test.js.map
|