@livestore/webmesh 0.0.0-snapshot-1d99fea7d2ce2c7a5d9ed0a3752f8a7bda6bc3db → 0.0.0-snapshot-7d3074f682f31cfc38b26ed2c4c2972ce1e9121e
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +20 -1
- package/dist/.tsbuildinfo +1 -1
- package/dist/channel/message-channel-internal.d.ts +26 -0
- package/dist/channel/message-channel-internal.d.ts.map +1 -0
- package/dist/channel/message-channel-internal.js +217 -0
- package/dist/channel/message-channel-internal.js.map +1 -0
- package/dist/channel/message-channel.d.ts +21 -19
- package/dist/channel/message-channel.d.ts.map +1 -1
- package/dist/channel/message-channel.js +132 -162
- package/dist/channel/message-channel.js.map +1 -1
- package/dist/channel/proxy-channel.d.ts +2 -2
- package/dist/channel/proxy-channel.d.ts.map +1 -1
- package/dist/channel/proxy-channel.js +7 -5
- package/dist/channel/proxy-channel.js.map +1 -1
- package/dist/common.d.ts +8 -4
- package/dist/common.d.ts.map +1 -1
- package/dist/common.js +2 -1
- package/dist/common.js.map +1 -1
- package/dist/mesh-schema.d.ts +23 -1
- package/dist/mesh-schema.d.ts.map +1 -1
- package/dist/mesh-schema.js +21 -2
- package/dist/mesh-schema.js.map +1 -1
- package/dist/node.d.ts +12 -1
- package/dist/node.d.ts.map +1 -1
- package/dist/node.js +40 -9
- package/dist/node.js.map +1 -1
- package/dist/node.test.d.ts +1 -1
- package/dist/node.test.d.ts.map +1 -1
- package/dist/node.test.js +312 -146
- package/dist/node.test.js.map +1 -1
- package/dist/websocket-connection.d.ts +1 -2
- package/dist/websocket-connection.d.ts.map +1 -1
- package/dist/websocket-connection.js +5 -4
- package/dist/websocket-connection.js.map +1 -1
- package/package.json +3 -3
- package/src/channel/message-channel-internal.ts +356 -0
- package/src/channel/message-channel.ts +190 -310
- package/src/channel/proxy-channel.ts +238 -230
- package/src/common.ts +3 -1
- package/src/mesh-schema.ts +20 -2
- package/src/node.test.ts +444 -174
- package/src/node.ts +70 -12
- package/src/websocket-connection.ts +83 -79
package/dist/node.test.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
|
+
import '@livestore/utils/node-vitest-polyfill';
|
|
1
2
|
import { IS_CI } from '@livestore/utils';
|
|
2
|
-
import { Chunk, Deferred, Effect, identity, Layer, Logger, Schema, Stream, WebChannel } from '@livestore/utils/effect';
|
|
3
|
+
import { Chunk, Deferred, Effect, Exit, identity, Layer, Logger, Schema, Scope, Stream, WebChannel, } from '@livestore/utils/effect';
|
|
3
4
|
import { OtelLiveHttp } from '@livestore/utils/node';
|
|
4
5
|
import { Vitest } from '@livestore/utils/node-vitest';
|
|
5
6
|
import { expect } from 'vitest';
|
|
@@ -12,28 +13,41 @@ import { makeMeshNode } from './node.js';
|
|
|
12
13
|
// TODO test cases where multiple entities try to claim to be the same channel end (e.g. A,B,B)
|
|
13
14
|
// TODO write tests with worker threads
|
|
14
15
|
const ExampleSchema = Schema.Struct({ message: Schema.String });
|
|
15
|
-
const connectNodesViaMessageChannel = (nodeA, nodeB) => Effect.gen(function* () {
|
|
16
|
+
const connectNodesViaMessageChannel = (nodeA, nodeB, options) => Effect.gen(function* () {
|
|
16
17
|
const mc = new MessageChannel();
|
|
17
18
|
const meshChannelAToB = yield* WebChannel.messagePortChannel({ port: mc.port1, schema: Packet });
|
|
18
19
|
const meshChannelBToA = yield* WebChannel.messagePortChannel({ port: mc.port2, schema: Packet });
|
|
19
|
-
yield* nodeA.addConnection({
|
|
20
|
-
|
|
21
|
-
|
|
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
|
+
});
|
|
22
30
|
}).pipe(Effect.withSpan(`connectNodesViaMessageChannel:${nodeA.nodeName}↔${nodeB.nodeName}`));
|
|
23
|
-
const connectNodesViaBroadcastChannel = (nodeA, nodeB) => Effect.gen(function* () {
|
|
31
|
+
const connectNodesViaBroadcastChannel = (nodeA, nodeB, options) => Effect.gen(function* () {
|
|
24
32
|
// Need to instantiate two different channels because they filter out messages they sent themselves
|
|
25
33
|
const broadcastWebChannelA = yield* WebChannel.broadcastChannelWithAck({
|
|
26
34
|
channelName: `${nodeA.nodeName}↔${nodeB.nodeName}`,
|
|
27
|
-
|
|
28
|
-
sendSchema: Packet,
|
|
35
|
+
schema: Packet,
|
|
29
36
|
});
|
|
30
37
|
const broadcastWebChannelB = yield* WebChannel.broadcastChannelWithAck({
|
|
31
38
|
channelName: `${nodeA.nodeName}↔${nodeB.nodeName}`,
|
|
32
|
-
|
|
33
|
-
|
|
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,
|
|
34
50
|
});
|
|
35
|
-
yield* nodeA.addConnection({ target: nodeB.nodeName, connectionChannel: broadcastWebChannelA });
|
|
36
|
-
yield* nodeB.addConnection({ target: nodeA.nodeName, connectionChannel: broadcastWebChannelB });
|
|
37
51
|
}).pipe(Effect.withSpan(`connectNodesViaBroadcastChannel:${nodeA.nodeName}↔${nodeB.nodeName}`));
|
|
38
52
|
const createChannel = (source, target, options) => source.makeChannel({
|
|
39
53
|
target,
|
|
@@ -48,142 +62,199 @@ const getFirstMessage = (channel) => channel.listen.pipe(Stream.flatten(), Strea
|
|
|
48
62
|
const maybeDelay = (delay, label) => (effect) => delay === undefined
|
|
49
63
|
? effect
|
|
50
64
|
: Effect.sleep(delay).pipe(Effect.withSpan(`${label}:delay(${delay})`), Effect.andThen(effect));
|
|
51
|
-
const testTimeout = IS_CI ? 30_000 :
|
|
65
|
+
const testTimeout = IS_CI ? 30_000 : 1000;
|
|
66
|
+
const propTestTimeout = IS_CI ? 60_000 : 20_000;
|
|
52
67
|
// TODO also make work without `Vitest.scopedLive` (i.e. with `Vitest.scoped`)
|
|
53
68
|
// probably requires controlling the clocks
|
|
54
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
|
+
});
|
|
55
112
|
Vitest.describe('A <> B', () => {
|
|
56
|
-
Vitest.describe('prop tests', () => {
|
|
57
|
-
const
|
|
58
|
-
//
|
|
59
|
-
const
|
|
60
|
-
const
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
}
|
|
65
|
-
case 'messagechannel': {
|
|
66
|
-
return { mode: 'messagechannel', connectNodes: connectNodesViaMessageChannel };
|
|
67
|
-
}
|
|
68
|
-
case 'messagechannel.proxy': {
|
|
69
|
-
return { mode: 'proxy', connectNodes: connectNodesViaMessageChannel };
|
|
70
|
-
}
|
|
71
|
-
}
|
|
72
|
-
};
|
|
73
|
-
Vitest.scopedLive.prop(
|
|
74
|
-
// Vitest.scopedLive.only(
|
|
75
|
-
'a / b connect at different times with different channel types', [Delay, Delay, Delay, ChannelType], ([delayA, delayB, connectDelay, channelType], test) =>
|
|
76
|
-
// (test) =>
|
|
77
|
-
Effect.gen(function* () {
|
|
78
|
-
// const delayA = 1
|
|
79
|
-
// const delayB = 10
|
|
80
|
-
// const connectDelay = 10
|
|
81
|
-
// const channelType = 'message.prefer'
|
|
82
|
-
// console.log('delayA', delayA, 'delayB', delayB, 'connectDelay', connectDelay, 'channelType', channelType)
|
|
83
|
-
const nodeA = yield* makeMeshNode('A');
|
|
84
|
-
const nodeB = yield* makeMeshNode('B');
|
|
85
|
-
const { mode, connectNodes } = fromChannelType(channelType);
|
|
86
|
-
const nodeACode = Effect.gen(function* () {
|
|
87
|
-
const channelAToB = yield* createChannel(nodeA, 'B', { mode });
|
|
88
|
-
yield* channelAToB.send({ message: 'A1' });
|
|
89
|
-
expect(yield* getFirstMessage(channelAToB)).toEqual({ message: 'A2' });
|
|
90
|
-
});
|
|
91
|
-
const nodeBCode = Effect.gen(function* () {
|
|
92
|
-
const channelBToA = yield* createChannel(nodeB, 'A', { mode });
|
|
93
|
-
yield* channelBToA.send({ message: 'A2' });
|
|
94
|
-
expect(yield* getFirstMessage(channelBToA)).toEqual({ message: 'A1' });
|
|
95
|
-
});
|
|
96
|
-
yield* Effect.all([
|
|
97
|
-
connectNodes(nodeA, nodeB).pipe(maybeDelay(connectDelay, 'connectNodes')),
|
|
98
|
-
nodeACode.pipe(maybeDelay(delayA, 'nodeACode')),
|
|
99
|
-
nodeBCode.pipe(maybeDelay(delayB, 'nodeBCode')),
|
|
100
|
-
], { concurrency: 'unbounded' });
|
|
101
|
-
}).pipe(withCtx(test, { skipOtel: true, suffix: `delayA=${delayA} delayB=${delayB} channelType=${channelType}` })));
|
|
102
|
-
// Vitest.scopedLive.only(
|
|
103
|
-
// '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',
|
|
104
121
|
// (test) =>
|
|
105
|
-
Vitest.scopedLive.prop('b
|
|
106
|
-
//
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
// channelType,
|
|
116
|
-
// )
|
|
117
|
-
const nodeA = yield* makeMeshNode('A');
|
|
118
|
-
const nodeB = yield* makeMeshNode('B');
|
|
119
|
-
const { mode, connectNodes } = fromChannelType(channelType);
|
|
120
|
-
// TODO also optionally delay the connection
|
|
121
|
-
yield* connectNodes(nodeA, nodeB);
|
|
122
|
-
const waitForBToBeOffline = waitForOfflineDelay === undefined ? undefined : yield* Deferred.make();
|
|
123
|
-
const nodeACode = Effect.gen(function* () {
|
|
124
|
-
const channelAToB = yield* createChannel(nodeA, 'B', { mode });
|
|
125
|
-
yield* channelAToB.send({ message: 'A1' });
|
|
126
|
-
expect(yield* getFirstMessage(channelAToB)).toEqual({ message: 'B1' });
|
|
127
|
-
if (waitForBToBeOffline !== undefined) {
|
|
128
|
-
yield* waitForBToBeOffline;
|
|
129
|
-
}
|
|
130
|
-
yield* channelAToB.send({ message: 'A2' });
|
|
131
|
-
expect(yield* getFirstMessage(channelAToB)).toEqual({ message: 'B2' });
|
|
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 },
|
|
132
132
|
});
|
|
133
|
-
// Simulating node b going offline and then coming back online
|
|
134
|
-
const nodeBCode = Effect.gen(function* () {
|
|
135
|
-
yield* Effect.gen(function* () {
|
|
136
|
-
const channelBToA = yield* createChannel(nodeB, 'A', { mode });
|
|
137
|
-
yield* channelBToA.send({ message: 'B1' });
|
|
138
|
-
expect(yield* getFirstMessage(channelBToA)).toEqual({ message: 'A1' });
|
|
139
|
-
}).pipe(Effect.scoped);
|
|
140
|
-
if (waitForBToBeOffline !== undefined) {
|
|
141
|
-
yield* Deferred.succeed(waitForBToBeOffline, void 0);
|
|
142
|
-
}
|
|
143
|
-
if (sleepDelay !== undefined) {
|
|
144
|
-
yield* Effect.sleep(sleepDelay).pipe(Effect.withSpan(`B:sleep(${sleepDelay})`));
|
|
145
|
-
}
|
|
146
|
-
yield* Effect.gen(function* () {
|
|
147
|
-
const channelBToA = yield* createChannel(nodeB, 'A', { mode });
|
|
148
|
-
yield* channelBToA.send({ message: 'B2' });
|
|
149
|
-
expect(yield* getFirstMessage(channelBToA)).toEqual({ message: 'A2' });
|
|
150
|
-
}).pipe(Effect.scoped);
|
|
151
|
-
});
|
|
152
|
-
yield* Effect.all([nodeACode, nodeBCode], { concurrency: 'unbounded' });
|
|
153
133
|
}).pipe(withCtx(test, {
|
|
154
134
|
skipOtel: true,
|
|
155
|
-
suffix: `
|
|
135
|
+
suffix: `delayX=${delayX} delayY=${delayY} connectDelay=${connectDelay} channelType=${channelType} nodeNames=${nodeNames}`,
|
|
156
136
|
})));
|
|
157
|
-
|
|
158
|
-
|
|
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();
|
|
159
198
|
const nodeA = yield* makeMeshNode('A');
|
|
160
|
-
const
|
|
161
|
-
|
|
162
|
-
yield*
|
|
163
|
-
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'));
|
|
164
203
|
const nodeACode = Effect.gen(function* () {
|
|
165
|
-
const channelAToB = yield*
|
|
204
|
+
const channelAToB = yield* channelAToBOnce;
|
|
166
205
|
yield* channelAToB.send({ message: 'A1' });
|
|
167
206
|
expect(yield* getFirstMessage(channelAToB)).toEqual({ message: 'B1' });
|
|
168
|
-
|
|
169
|
-
yield* channelAToB.send({ message: 'A2' });
|
|
170
|
-
expect(yield* getFirstMessage(channelAToB)).toEqual({ message: 'B2' });
|
|
207
|
+
// expect(channelAToB.debugInfo.connectCounter).toBe(1)
|
|
171
208
|
});
|
|
172
|
-
const nodeBCode = Effect.gen(function* () {
|
|
173
|
-
const channelBToA = yield* createChannel(nodeB, 'A'
|
|
209
|
+
const nodeBCode = (nodeB) => Effect.gen(function* () {
|
|
210
|
+
const channelBToA = yield* createChannel(nodeB, 'A');
|
|
174
211
|
yield* channelBToA.send({ message: 'B1' });
|
|
175
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` });
|
|
176
244
|
// Switch out connection while keeping the channel
|
|
177
|
-
yield*
|
|
178
|
-
yield*
|
|
179
|
-
yield* connectNodes(
|
|
245
|
+
yield* nodeX.removeConnection(nodeLabel.y);
|
|
246
|
+
yield* nodeY.removeConnection(nodeLabel.x);
|
|
247
|
+
yield* connectNodes(nodeX, nodeY);
|
|
180
248
|
yield* Deferred.succeed(waitForConnectionReplacement, void 0);
|
|
181
|
-
yield*
|
|
182
|
-
expect(yield* getFirstMessage(
|
|
249
|
+
yield* channelYToX.send({ message: `${nodeLabel.y}2` });
|
|
250
|
+
expect(yield* getFirstMessage(channelYToX)).toEqual({ message: `${nodeLabel.x}2` });
|
|
183
251
|
});
|
|
184
|
-
yield* Effect.all([
|
|
185
|
-
}).pipe(withCtx(test, {
|
|
186
|
-
|
|
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', () => {
|
|
187
258
|
// TODO we need to improve latency when sending messages concurrently
|
|
188
259
|
Vitest.scopedLive.prop('concurrent messages', [ChannelType, Schema.Int.pipe(Schema.between(1, 50))], ([channelType, count], test) => Effect.gen(function* () {
|
|
189
260
|
const nodeA = yield* makeMeshNode('A');
|
|
@@ -206,9 +277,34 @@ Vitest.describe('webmesh node', { timeout: testTimeout }, () => {
|
|
|
206
277
|
yield* Effect.all([nodeACode, nodeBCode, connectNodes(nodeA, nodeB).pipe(Effect.delay(100))], {
|
|
207
278
|
concurrency: 'unbounded',
|
|
208
279
|
});
|
|
209
|
-
}).pipe(withCtx(test, {
|
|
280
|
+
}).pipe(withCtx(test, {
|
|
281
|
+
skipOtel: true,
|
|
282
|
+
suffix: `channelType=${channelType} count=${count}`,
|
|
283
|
+
timeout: testTimeout * 2,
|
|
284
|
+
})), { fastCheck: { numRuns: 10 } });
|
|
210
285
|
});
|
|
211
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
|
+
});
|
|
212
308
|
Vitest.scopedLive('manual debug test', (test) => Effect.gen(function* () {
|
|
213
309
|
const nodeA = yield* makeMeshNode('A');
|
|
214
310
|
const nodeB = yield* makeMeshNode('B');
|
|
@@ -248,6 +344,7 @@ Vitest.describe('webmesh node', { timeout: testTimeout }, () => {
|
|
|
248
344
|
yield* channelAToC.send({ message: 'A1' });
|
|
249
345
|
expect(yield* getFirstMessage(channelAToC)).toEqual({ message: 'C1' });
|
|
250
346
|
expect(yield* getFirstMessage(channelAToC)).toEqual({ message: 'C2' });
|
|
347
|
+
expect(yield* getFirstMessage(channelAToC)).toEqual({ message: 'C3' });
|
|
251
348
|
});
|
|
252
349
|
const nodeCCode = Effect.gen(function* () {
|
|
253
350
|
const channelCToA = yield* createChannel(nodeC, 'A');
|
|
@@ -276,9 +373,11 @@ Vitest.describe('webmesh node', { timeout: testTimeout }, () => {
|
|
|
276
373
|
yield* channelCToA.send({ message: 'C1' });
|
|
277
374
|
expect(yield* getFirstMessage(channelCToA)).toEqual({ message: 'A1' });
|
|
278
375
|
});
|
|
279
|
-
yield* Effect.all([
|
|
280
|
-
|
|
281
|
-
|
|
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' });
|
|
282
381
|
}).pipe(withCtx(test)));
|
|
283
382
|
Vitest.scopedLive('proxy channel', (test) => Effect.gen(function* () {
|
|
284
383
|
const nodeA = yield* makeMeshNode('A');
|
|
@@ -298,7 +397,7 @@ Vitest.describe('webmesh node', { timeout: testTimeout }, () => {
|
|
|
298
397
|
});
|
|
299
398
|
yield* Effect.all([nodeACode, nodeCCode], { concurrency: 'unbounded' });
|
|
300
399
|
}).pipe(withCtx(test)));
|
|
301
|
-
Vitest.scopedLive('should fail', (test) => Effect.gen(function* () {
|
|
400
|
+
Vitest.scopedLive('should fail with timeout due to missing connection', (test) => Effect.gen(function* () {
|
|
302
401
|
const nodeA = yield* makeMeshNode('A');
|
|
303
402
|
const nodeB = yield* makeMeshNode('B');
|
|
304
403
|
const nodeC = yield* makeMeshNode('C');
|
|
@@ -314,6 +413,73 @@ Vitest.describe('webmesh node', { timeout: testTimeout }, () => {
|
|
|
314
413
|
});
|
|
315
414
|
yield* Effect.all([nodeACode, nodeCCode], { concurrency: 'unbounded' });
|
|
316
415
|
}).pipe(withCtx(test)));
|
|
416
|
+
Vitest.scopedLive('should fail with timeout due no transferable', (test) => Effect.gen(function* () {
|
|
417
|
+
const nodeA = yield* makeMeshNode('A');
|
|
418
|
+
const nodeB = yield* makeMeshNode('B');
|
|
419
|
+
yield* connectNodesViaBroadcastChannel(nodeA, nodeB);
|
|
420
|
+
const nodeACode = Effect.gen(function* () {
|
|
421
|
+
const err = yield* createChannel(nodeA, 'B').pipe(Effect.timeout(200), Effect.flip);
|
|
422
|
+
expect(err._tag).toBe('TimeoutException');
|
|
423
|
+
});
|
|
424
|
+
const nodeBCode = Effect.gen(function* () {
|
|
425
|
+
const err = yield* createChannel(nodeB, 'A').pipe(Effect.timeout(200), Effect.flip);
|
|
426
|
+
expect(err._tag).toBe('TimeoutException');
|
|
427
|
+
});
|
|
428
|
+
yield* Effect.all([nodeACode, nodeBCode], { concurrency: 'unbounded' });
|
|
429
|
+
}).pipe(withCtx(test)));
|
|
430
|
+
Vitest.scopedLive('reconnect with re-created node', (test) => Effect.gen(function* () {
|
|
431
|
+
const nodeCgen1Scope = yield* Scope.make();
|
|
432
|
+
const nodeA = yield* makeMeshNode('A');
|
|
433
|
+
const nodeB = yield* makeMeshNode('B');
|
|
434
|
+
const nodeCgen1 = yield* makeMeshNode('C').pipe(Scope.extend(nodeCgen1Scope));
|
|
435
|
+
yield* connectNodesViaMessageChannel(nodeA, nodeB);
|
|
436
|
+
yield* connectNodesViaMessageChannel(nodeB, nodeCgen1).pipe(Scope.extend(nodeCgen1Scope));
|
|
437
|
+
const nodeACode = Effect.gen(function* () {
|
|
438
|
+
const channelAToB = yield* createChannel(nodeA, 'C');
|
|
439
|
+
yield* channelAToB.send({ message: 'A1' });
|
|
440
|
+
expect(yield* getFirstMessage(channelAToB)).toEqual({ message: 'C1' });
|
|
441
|
+
});
|
|
442
|
+
const nodeCCode = (nodeB) => Effect.gen(function* () {
|
|
443
|
+
const channelBToA = yield* createChannel(nodeB, 'A');
|
|
444
|
+
yield* channelBToA.send({ message: 'C1' });
|
|
445
|
+
expect(yield* getFirstMessage(channelBToA)).toEqual({ message: 'A1' });
|
|
446
|
+
});
|
|
447
|
+
yield* Effect.all([nodeACode, nodeCCode(nodeCgen1)], { concurrency: 'unbounded' }).pipe(Effect.withSpan('test1'), Scope.extend(nodeCgen1Scope));
|
|
448
|
+
yield* Scope.close(nodeCgen1Scope, Exit.void);
|
|
449
|
+
const nodeCgen2 = yield* makeMeshNode('C');
|
|
450
|
+
yield* connectNodesViaMessageChannel(nodeB, nodeCgen2, { replaceIfExists: true });
|
|
451
|
+
yield* Effect.all([nodeACode, nodeCCode(nodeCgen2)], { concurrency: 'unbounded' }).pipe(Effect.withSpan('test2'));
|
|
452
|
+
}).pipe(withCtx(test)));
|
|
453
|
+
});
|
|
454
|
+
/**
|
|
455
|
+
* A
|
|
456
|
+
* / \
|
|
457
|
+
* B C
|
|
458
|
+
* \ /
|
|
459
|
+
* D
|
|
460
|
+
*/
|
|
461
|
+
Vitest.describe('diamond topology', () => {
|
|
462
|
+
Vitest.scopedLive('should work', (test) => Effect.gen(function* () {
|
|
463
|
+
const nodeA = yield* makeMeshNode('A');
|
|
464
|
+
const nodeB = yield* makeMeshNode('B');
|
|
465
|
+
const nodeC = yield* makeMeshNode('C');
|
|
466
|
+
const nodeD = yield* makeMeshNode('D');
|
|
467
|
+
yield* connectNodesViaMessageChannel(nodeA, nodeB);
|
|
468
|
+
yield* connectNodesViaMessageChannel(nodeA, nodeC);
|
|
469
|
+
yield* connectNodesViaMessageChannel(nodeB, nodeD);
|
|
470
|
+
yield* connectNodesViaMessageChannel(nodeC, nodeD);
|
|
471
|
+
const nodeACode = Effect.gen(function* () {
|
|
472
|
+
const channelAToD = yield* createChannel(nodeA, 'D');
|
|
473
|
+
yield* channelAToD.send({ message: 'A1' });
|
|
474
|
+
expect(yield* getFirstMessage(channelAToD)).toEqual({ message: 'D1' });
|
|
475
|
+
});
|
|
476
|
+
const nodeDCode = Effect.gen(function* () {
|
|
477
|
+
const channelDToA = yield* createChannel(nodeD, 'A');
|
|
478
|
+
yield* channelDToA.send({ message: 'D1' });
|
|
479
|
+
expect(yield* getFirstMessage(channelDToA)).toEqual({ message: 'A1' });
|
|
480
|
+
});
|
|
481
|
+
yield* Effect.all([nodeACode, nodeDCode], { concurrency: 'unbounded' });
|
|
482
|
+
}).pipe(withCtx(test)));
|
|
317
483
|
});
|
|
318
484
|
Vitest.describe('mixture of messagechannel and proxy connections', () => {
|
|
319
485
|
// TODO test case to better guard against case where side A tries to create a proxy channel to B
|
|
@@ -325,27 +491,27 @@ Vitest.describe('webmesh node', { timeout: testTimeout }, () => {
|
|
|
325
491
|
const err = yield* connectNodesViaBroadcastChannel(nodeA, nodeB).pipe(Effect.flip);
|
|
326
492
|
expect(err._tag).toBe('ConnectionAlreadyExistsError');
|
|
327
493
|
}).pipe(withCtx(test)));
|
|
328
|
-
|
|
329
|
-
Vitest.scopedLive.skip('should work for messagechannels', (test) => Effect.gen(function* () {
|
|
494
|
+
Vitest.scopedLive('should work for messagechannels', (test) => Effect.gen(function* () {
|
|
330
495
|
const nodeA = yield* makeMeshNode('A');
|
|
331
496
|
const nodeB = yield* makeMeshNode('B');
|
|
497
|
+
const nodeC = yield* makeMeshNode('C');
|
|
332
498
|
yield* connectNodesViaMessageChannel(nodeB, nodeA);
|
|
333
|
-
yield* connectNodesViaBroadcastChannel(
|
|
499
|
+
yield* connectNodesViaBroadcastChannel(nodeB, nodeC);
|
|
334
500
|
const nodeACode = Effect.gen(function* () {
|
|
335
|
-
const
|
|
336
|
-
yield*
|
|
337
|
-
expect(yield* getFirstMessage(
|
|
501
|
+
const channelAToC = yield* createChannel(nodeA, 'C', { mode: 'proxy' });
|
|
502
|
+
yield* channelAToC.send({ message: 'A1' });
|
|
503
|
+
expect(yield* getFirstMessage(channelAToC)).toEqual({ message: 'C1' });
|
|
338
504
|
});
|
|
339
|
-
const
|
|
340
|
-
const
|
|
341
|
-
yield*
|
|
342
|
-
expect(yield* getFirstMessage(
|
|
505
|
+
const nodeCCode = Effect.gen(function* () {
|
|
506
|
+
const channelCToA = yield* createChannel(nodeC, 'A', { mode: 'proxy' });
|
|
507
|
+
yield* channelCToA.send({ message: 'C1' });
|
|
508
|
+
expect(yield* getFirstMessage(channelCToA)).toEqual({ message: 'A1' });
|
|
343
509
|
});
|
|
344
|
-
yield* Effect.all([nodeACode,
|
|
510
|
+
yield* Effect.all([nodeACode, nodeCCode], { concurrency: 'unbounded' });
|
|
345
511
|
}).pipe(withCtx(test)));
|
|
346
512
|
});
|
|
347
513
|
});
|
|
348
514
|
const otelLayer = IS_CI ? Layer.empty : OtelLiveHttp({ serviceName: 'webmesh-node-test', skipLogUrl: false });
|
|
349
|
-
const withCtx = (testContext, { suffix, skipOtel = false } = {}) => (self) => self.pipe(Effect.timeout(
|
|
515
|
+
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
516
|
Effect.withSpan(`${testContext.task.suite?.name}:${testContext.task.name}${suffix ? `:${suffix}` : ''}`), skipOtel ? identity : Effect.provide(otelLayer));
|
|
351
517
|
//# sourceMappingURL=node.test.js.map
|