@connexis/testing 1.0.0
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/LICENSE +21 -0
- package/dist/index.d.mts +69 -0
- package/dist/index.d.ts +69 -0
- package/dist/index.js +216 -0
- package/dist/index.mjs +186 -0
- package/package.json +24 -0
- package/src/__tests__/stress-and-bench.test.ts +33 -0
- package/src/index.ts +2 -0
- package/src/mock-transport.ts +107 -0
- package/src/stress-and-bench.ts +132 -0
- package/tsconfig.json +8 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Connexis Realtime Team
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/dist/index.d.mts
ADDED
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
import { Transport, TransportCapabilities, ConnectionState } from '@connexis/core';
|
|
2
|
+
|
|
3
|
+
declare class MockTransport implements Transport {
|
|
4
|
+
private caps;
|
|
5
|
+
readonly type = "mock";
|
|
6
|
+
private _state;
|
|
7
|
+
private messageCallback;
|
|
8
|
+
private stateCallback;
|
|
9
|
+
published: Array<{
|
|
10
|
+
topic: string;
|
|
11
|
+
data: any;
|
|
12
|
+
}>;
|
|
13
|
+
subscribed: Array<{
|
|
14
|
+
topic: string;
|
|
15
|
+
filter?: Record<string, any>;
|
|
16
|
+
}>;
|
|
17
|
+
unsubscribed: Array<{
|
|
18
|
+
topic: string;
|
|
19
|
+
filter?: Record<string, any>;
|
|
20
|
+
}>;
|
|
21
|
+
private failNextConnect;
|
|
22
|
+
private failError;
|
|
23
|
+
private connectDelay;
|
|
24
|
+
constructor(caps?: TransportCapabilities);
|
|
25
|
+
get state(): ConnectionState;
|
|
26
|
+
setFailNextConnect(fail: boolean, error?: Error): void;
|
|
27
|
+
setConnectDelay(delayMs: number): void;
|
|
28
|
+
connect(): Promise<void>;
|
|
29
|
+
disconnect(): Promise<void>;
|
|
30
|
+
publish(topic: string, data: any): Promise<void>;
|
|
31
|
+
subscribe(topic: string, filter?: Record<string, any>): Promise<void>;
|
|
32
|
+
unsubscribe(topic: string, filter?: Record<string, any>): Promise<void>;
|
|
33
|
+
capabilities(): TransportCapabilities;
|
|
34
|
+
onMessage(cb: (topic: string, data: any) => void): void;
|
|
35
|
+
onStateChange(cb: (state: ConnectionState, error?: Error) => void): void;
|
|
36
|
+
clone(): MockTransport;
|
|
37
|
+
simulateMessage(topic: string, data: any): void;
|
|
38
|
+
simulateStateChange(state: ConnectionState, error?: Error): void;
|
|
39
|
+
private updateState;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Benchmark Results Interface
|
|
44
|
+
*/
|
|
45
|
+
interface BenchmarkReport {
|
|
46
|
+
operation: string;
|
|
47
|
+
count: number;
|
|
48
|
+
durationMs: number;
|
|
49
|
+
opsPerSecond: number;
|
|
50
|
+
}
|
|
51
|
+
/**
|
|
52
|
+
* Runs a performance benchmark for core client operations.
|
|
53
|
+
*/
|
|
54
|
+
declare function runBenchmarks(): Promise<BenchmarkReport[]>;
|
|
55
|
+
/**
|
|
56
|
+
* Scenario: Stress test hybrid policy deduplication with 1,000 subscriptions.
|
|
57
|
+
*/
|
|
58
|
+
declare function runHybridStressTest(): Promise<{
|
|
59
|
+
connectionsCount: number;
|
|
60
|
+
subscriptionsCount: number;
|
|
61
|
+
}>;
|
|
62
|
+
/**
|
|
63
|
+
* Scenario: Simulate 10 tabs and trigger random network online/offline events.
|
|
64
|
+
*/
|
|
65
|
+
declare function runNetworkChaosStressTest(cycles?: number): Promise<{
|
|
66
|
+
success: boolean;
|
|
67
|
+
}>;
|
|
68
|
+
|
|
69
|
+
export { type BenchmarkReport, MockTransport, runBenchmarks, runHybridStressTest, runNetworkChaosStressTest };
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
import { Transport, TransportCapabilities, ConnectionState } from '@connexis/core';
|
|
2
|
+
|
|
3
|
+
declare class MockTransport implements Transport {
|
|
4
|
+
private caps;
|
|
5
|
+
readonly type = "mock";
|
|
6
|
+
private _state;
|
|
7
|
+
private messageCallback;
|
|
8
|
+
private stateCallback;
|
|
9
|
+
published: Array<{
|
|
10
|
+
topic: string;
|
|
11
|
+
data: any;
|
|
12
|
+
}>;
|
|
13
|
+
subscribed: Array<{
|
|
14
|
+
topic: string;
|
|
15
|
+
filter?: Record<string, any>;
|
|
16
|
+
}>;
|
|
17
|
+
unsubscribed: Array<{
|
|
18
|
+
topic: string;
|
|
19
|
+
filter?: Record<string, any>;
|
|
20
|
+
}>;
|
|
21
|
+
private failNextConnect;
|
|
22
|
+
private failError;
|
|
23
|
+
private connectDelay;
|
|
24
|
+
constructor(caps?: TransportCapabilities);
|
|
25
|
+
get state(): ConnectionState;
|
|
26
|
+
setFailNextConnect(fail: boolean, error?: Error): void;
|
|
27
|
+
setConnectDelay(delayMs: number): void;
|
|
28
|
+
connect(): Promise<void>;
|
|
29
|
+
disconnect(): Promise<void>;
|
|
30
|
+
publish(topic: string, data: any): Promise<void>;
|
|
31
|
+
subscribe(topic: string, filter?: Record<string, any>): Promise<void>;
|
|
32
|
+
unsubscribe(topic: string, filter?: Record<string, any>): Promise<void>;
|
|
33
|
+
capabilities(): TransportCapabilities;
|
|
34
|
+
onMessage(cb: (topic: string, data: any) => void): void;
|
|
35
|
+
onStateChange(cb: (state: ConnectionState, error?: Error) => void): void;
|
|
36
|
+
clone(): MockTransport;
|
|
37
|
+
simulateMessage(topic: string, data: any): void;
|
|
38
|
+
simulateStateChange(state: ConnectionState, error?: Error): void;
|
|
39
|
+
private updateState;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Benchmark Results Interface
|
|
44
|
+
*/
|
|
45
|
+
interface BenchmarkReport {
|
|
46
|
+
operation: string;
|
|
47
|
+
count: number;
|
|
48
|
+
durationMs: number;
|
|
49
|
+
opsPerSecond: number;
|
|
50
|
+
}
|
|
51
|
+
/**
|
|
52
|
+
* Runs a performance benchmark for core client operations.
|
|
53
|
+
*/
|
|
54
|
+
declare function runBenchmarks(): Promise<BenchmarkReport[]>;
|
|
55
|
+
/**
|
|
56
|
+
* Scenario: Stress test hybrid policy deduplication with 1,000 subscriptions.
|
|
57
|
+
*/
|
|
58
|
+
declare function runHybridStressTest(): Promise<{
|
|
59
|
+
connectionsCount: number;
|
|
60
|
+
subscriptionsCount: number;
|
|
61
|
+
}>;
|
|
62
|
+
/**
|
|
63
|
+
* Scenario: Simulate 10 tabs and trigger random network online/offline events.
|
|
64
|
+
*/
|
|
65
|
+
declare function runNetworkChaosStressTest(cycles?: number): Promise<{
|
|
66
|
+
success: boolean;
|
|
67
|
+
}>;
|
|
68
|
+
|
|
69
|
+
export { type BenchmarkReport, MockTransport, runBenchmarks, runHybridStressTest, runNetworkChaosStressTest };
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,216 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __defProp = Object.defineProperty;
|
|
3
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
4
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
5
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
6
|
+
var __export = (target, all) => {
|
|
7
|
+
for (var name in all)
|
|
8
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
9
|
+
};
|
|
10
|
+
var __copyProps = (to, from, except, desc) => {
|
|
11
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
12
|
+
for (let key of __getOwnPropNames(from))
|
|
13
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
14
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
15
|
+
}
|
|
16
|
+
return to;
|
|
17
|
+
};
|
|
18
|
+
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
19
|
+
|
|
20
|
+
// src/index.ts
|
|
21
|
+
var index_exports = {};
|
|
22
|
+
__export(index_exports, {
|
|
23
|
+
MockTransport: () => MockTransport,
|
|
24
|
+
runBenchmarks: () => runBenchmarks,
|
|
25
|
+
runHybridStressTest: () => runHybridStressTest,
|
|
26
|
+
runNetworkChaosStressTest: () => runNetworkChaosStressTest
|
|
27
|
+
});
|
|
28
|
+
module.exports = __toCommonJS(index_exports);
|
|
29
|
+
|
|
30
|
+
// src/mock-transport.ts
|
|
31
|
+
var MockTransport = class _MockTransport {
|
|
32
|
+
constructor(caps = { publish: true, subscribe: true, latency: "low" }) {
|
|
33
|
+
this.caps = caps;
|
|
34
|
+
}
|
|
35
|
+
caps;
|
|
36
|
+
type = "mock";
|
|
37
|
+
_state = "idle";
|
|
38
|
+
messageCallback = null;
|
|
39
|
+
stateCallback = null;
|
|
40
|
+
published = [];
|
|
41
|
+
subscribed = [];
|
|
42
|
+
unsubscribed = [];
|
|
43
|
+
failNextConnect = false;
|
|
44
|
+
failError = new Error("Mock connection failure");
|
|
45
|
+
connectDelay = 0;
|
|
46
|
+
get state() {
|
|
47
|
+
return this._state;
|
|
48
|
+
}
|
|
49
|
+
setFailNextConnect(fail, error) {
|
|
50
|
+
this.failNextConnect = fail;
|
|
51
|
+
if (error) this.failError = error;
|
|
52
|
+
}
|
|
53
|
+
setConnectDelay(delayMs) {
|
|
54
|
+
this.connectDelay = delayMs;
|
|
55
|
+
}
|
|
56
|
+
async connect() {
|
|
57
|
+
if (this._state === "connected") return;
|
|
58
|
+
this.updateState("connecting");
|
|
59
|
+
if (this.connectDelay > 0) {
|
|
60
|
+
await new Promise((resolve) => setTimeout(resolve, this.connectDelay));
|
|
61
|
+
}
|
|
62
|
+
if (this.failNextConnect) {
|
|
63
|
+
this.failNextConnect = false;
|
|
64
|
+
this.updateState("error", this.failError);
|
|
65
|
+
throw this.failError;
|
|
66
|
+
}
|
|
67
|
+
this.updateState("connected");
|
|
68
|
+
}
|
|
69
|
+
async disconnect() {
|
|
70
|
+
this.updateState("closed");
|
|
71
|
+
}
|
|
72
|
+
async publish(topic, data) {
|
|
73
|
+
if (this._state !== "connected") {
|
|
74
|
+
throw new Error("Transport not connected");
|
|
75
|
+
}
|
|
76
|
+
this.published.push({ topic, data });
|
|
77
|
+
}
|
|
78
|
+
async subscribe(topic, filter) {
|
|
79
|
+
this.subscribed.push({ topic, filter });
|
|
80
|
+
}
|
|
81
|
+
async unsubscribe(topic, filter) {
|
|
82
|
+
this.unsubscribed.push({ topic, filter });
|
|
83
|
+
}
|
|
84
|
+
capabilities() {
|
|
85
|
+
return this.caps;
|
|
86
|
+
}
|
|
87
|
+
onMessage(cb) {
|
|
88
|
+
this.messageCallback = cb;
|
|
89
|
+
}
|
|
90
|
+
onStateChange(cb) {
|
|
91
|
+
this.stateCallback = cb;
|
|
92
|
+
}
|
|
93
|
+
clone() {
|
|
94
|
+
const cloned = new _MockTransport(this.caps);
|
|
95
|
+
cloned.failNextConnect = this.failNextConnect;
|
|
96
|
+
cloned.failError = this.failError;
|
|
97
|
+
cloned.connectDelay = this.connectDelay;
|
|
98
|
+
return cloned;
|
|
99
|
+
}
|
|
100
|
+
// Simulator helper methods
|
|
101
|
+
simulateMessage(topic, data) {
|
|
102
|
+
if (this.messageCallback) {
|
|
103
|
+
this.messageCallback(topic, data);
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
simulateStateChange(state, error) {
|
|
107
|
+
this.updateState(state, error);
|
|
108
|
+
}
|
|
109
|
+
updateState(state, error) {
|
|
110
|
+
this._state = state;
|
|
111
|
+
if (this.stateCallback) {
|
|
112
|
+
this.stateCallback(state, error);
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
};
|
|
116
|
+
|
|
117
|
+
// src/stress-and-bench.ts
|
|
118
|
+
var import_core = require("@connexis/core");
|
|
119
|
+
async function runBenchmarks() {
|
|
120
|
+
const reports = [];
|
|
121
|
+
const transport = new MockTransport();
|
|
122
|
+
{
|
|
123
|
+
const client = (0, import_core.createRealtimeClient)({ transport });
|
|
124
|
+
const start = performance.now();
|
|
125
|
+
const count = 1e4;
|
|
126
|
+
const unsubs = [];
|
|
127
|
+
for (let i = 0; i < count; i++) {
|
|
128
|
+
const unsub = await client.subscribe(`topic_${i}`, () => {
|
|
129
|
+
});
|
|
130
|
+
unsubs.push(unsub);
|
|
131
|
+
}
|
|
132
|
+
for (const unsub of unsubs) {
|
|
133
|
+
await unsub();
|
|
134
|
+
}
|
|
135
|
+
const duration = performance.now() - start;
|
|
136
|
+
reports.push({
|
|
137
|
+
operation: "subscribe + unsubscribe",
|
|
138
|
+
count,
|
|
139
|
+
durationMs: duration,
|
|
140
|
+
opsPerSecond: count * 2 / (duration / 1e3)
|
|
141
|
+
});
|
|
142
|
+
await client.destroy();
|
|
143
|
+
}
|
|
144
|
+
{
|
|
145
|
+
const client = (0, import_core.createRealtimeClient)({ transport });
|
|
146
|
+
await client.subscribe("test", () => {
|
|
147
|
+
});
|
|
148
|
+
const count = 5e4;
|
|
149
|
+
const start = performance.now();
|
|
150
|
+
for (let i = 0; i < count; i++) {
|
|
151
|
+
await client.publish("test", { seq: i });
|
|
152
|
+
}
|
|
153
|
+
const duration = performance.now() - start;
|
|
154
|
+
reports.push({
|
|
155
|
+
operation: "publish message",
|
|
156
|
+
count,
|
|
157
|
+
durationMs: duration,
|
|
158
|
+
opsPerSecond: count / (duration / 1e3)
|
|
159
|
+
});
|
|
160
|
+
await client.destroy();
|
|
161
|
+
}
|
|
162
|
+
return reports;
|
|
163
|
+
}
|
|
164
|
+
async function runHybridStressTest() {
|
|
165
|
+
const transport = new MockTransport();
|
|
166
|
+
const manager = new import_core.ConnectionManager(transport, "hybrid");
|
|
167
|
+
const count = 1e3;
|
|
168
|
+
const filters = [
|
|
169
|
+
{ region: "US" },
|
|
170
|
+
{ region: "EU" },
|
|
171
|
+
{ region: "AP" },
|
|
172
|
+
{ region: "US" },
|
|
173
|
+
// duplicate of 0
|
|
174
|
+
{ region: "EU" }
|
|
175
|
+
// duplicate of 1
|
|
176
|
+
];
|
|
177
|
+
for (let i = 0; i < count; i++) {
|
|
178
|
+
const filter = filters[i % filters.length];
|
|
179
|
+
await manager.subscribe({ id: `sub_${i}`, topic: "orders", filter }, () => {
|
|
180
|
+
});
|
|
181
|
+
}
|
|
182
|
+
const connectionsCount = manager.getConnections().size;
|
|
183
|
+
const subscriptionsCount = manager.getActiveSubscriptionCount();
|
|
184
|
+
await manager.destroy();
|
|
185
|
+
return { connectionsCount, subscriptionsCount };
|
|
186
|
+
}
|
|
187
|
+
async function runNetworkChaosStressTest(cycles = 20) {
|
|
188
|
+
const transport = new MockTransport();
|
|
189
|
+
const connections = [];
|
|
190
|
+
for (let i = 0; i < 10; i++) {
|
|
191
|
+
const conn = (0, import_core.createRealtimeClient)({ transport });
|
|
192
|
+
await conn.subscribe("alerts", () => {
|
|
193
|
+
});
|
|
194
|
+
connections.push(conn);
|
|
195
|
+
}
|
|
196
|
+
for (let i = 0; i < cycles; i++) {
|
|
197
|
+
const shouldOffline = Math.random() > 0.5;
|
|
198
|
+
if (shouldOffline) {
|
|
199
|
+
transport.simulateStateChange("offline");
|
|
200
|
+
} else {
|
|
201
|
+
transport.simulateStateChange("connected");
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
transport.simulateStateChange("connected");
|
|
205
|
+
for (const conn of connections) {
|
|
206
|
+
await conn.destroy();
|
|
207
|
+
}
|
|
208
|
+
return { success: true };
|
|
209
|
+
}
|
|
210
|
+
// Annotate the CommonJS export names for ESM import in node:
|
|
211
|
+
0 && (module.exports = {
|
|
212
|
+
MockTransport,
|
|
213
|
+
runBenchmarks,
|
|
214
|
+
runHybridStressTest,
|
|
215
|
+
runNetworkChaosStressTest
|
|
216
|
+
});
|
package/dist/index.mjs
ADDED
|
@@ -0,0 +1,186 @@
|
|
|
1
|
+
// src/mock-transport.ts
|
|
2
|
+
var MockTransport = class _MockTransport {
|
|
3
|
+
constructor(caps = { publish: true, subscribe: true, latency: "low" }) {
|
|
4
|
+
this.caps = caps;
|
|
5
|
+
}
|
|
6
|
+
caps;
|
|
7
|
+
type = "mock";
|
|
8
|
+
_state = "idle";
|
|
9
|
+
messageCallback = null;
|
|
10
|
+
stateCallback = null;
|
|
11
|
+
published = [];
|
|
12
|
+
subscribed = [];
|
|
13
|
+
unsubscribed = [];
|
|
14
|
+
failNextConnect = false;
|
|
15
|
+
failError = new Error("Mock connection failure");
|
|
16
|
+
connectDelay = 0;
|
|
17
|
+
get state() {
|
|
18
|
+
return this._state;
|
|
19
|
+
}
|
|
20
|
+
setFailNextConnect(fail, error) {
|
|
21
|
+
this.failNextConnect = fail;
|
|
22
|
+
if (error) this.failError = error;
|
|
23
|
+
}
|
|
24
|
+
setConnectDelay(delayMs) {
|
|
25
|
+
this.connectDelay = delayMs;
|
|
26
|
+
}
|
|
27
|
+
async connect() {
|
|
28
|
+
if (this._state === "connected") return;
|
|
29
|
+
this.updateState("connecting");
|
|
30
|
+
if (this.connectDelay > 0) {
|
|
31
|
+
await new Promise((resolve) => setTimeout(resolve, this.connectDelay));
|
|
32
|
+
}
|
|
33
|
+
if (this.failNextConnect) {
|
|
34
|
+
this.failNextConnect = false;
|
|
35
|
+
this.updateState("error", this.failError);
|
|
36
|
+
throw this.failError;
|
|
37
|
+
}
|
|
38
|
+
this.updateState("connected");
|
|
39
|
+
}
|
|
40
|
+
async disconnect() {
|
|
41
|
+
this.updateState("closed");
|
|
42
|
+
}
|
|
43
|
+
async publish(topic, data) {
|
|
44
|
+
if (this._state !== "connected") {
|
|
45
|
+
throw new Error("Transport not connected");
|
|
46
|
+
}
|
|
47
|
+
this.published.push({ topic, data });
|
|
48
|
+
}
|
|
49
|
+
async subscribe(topic, filter) {
|
|
50
|
+
this.subscribed.push({ topic, filter });
|
|
51
|
+
}
|
|
52
|
+
async unsubscribe(topic, filter) {
|
|
53
|
+
this.unsubscribed.push({ topic, filter });
|
|
54
|
+
}
|
|
55
|
+
capabilities() {
|
|
56
|
+
return this.caps;
|
|
57
|
+
}
|
|
58
|
+
onMessage(cb) {
|
|
59
|
+
this.messageCallback = cb;
|
|
60
|
+
}
|
|
61
|
+
onStateChange(cb) {
|
|
62
|
+
this.stateCallback = cb;
|
|
63
|
+
}
|
|
64
|
+
clone() {
|
|
65
|
+
const cloned = new _MockTransport(this.caps);
|
|
66
|
+
cloned.failNextConnect = this.failNextConnect;
|
|
67
|
+
cloned.failError = this.failError;
|
|
68
|
+
cloned.connectDelay = this.connectDelay;
|
|
69
|
+
return cloned;
|
|
70
|
+
}
|
|
71
|
+
// Simulator helper methods
|
|
72
|
+
simulateMessage(topic, data) {
|
|
73
|
+
if (this.messageCallback) {
|
|
74
|
+
this.messageCallback(topic, data);
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
simulateStateChange(state, error) {
|
|
78
|
+
this.updateState(state, error);
|
|
79
|
+
}
|
|
80
|
+
updateState(state, error) {
|
|
81
|
+
this._state = state;
|
|
82
|
+
if (this.stateCallback) {
|
|
83
|
+
this.stateCallback(state, error);
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
};
|
|
87
|
+
|
|
88
|
+
// src/stress-and-bench.ts
|
|
89
|
+
import { createRealtimeClient, ConnectionManager } from "@connexis/core";
|
|
90
|
+
async function runBenchmarks() {
|
|
91
|
+
const reports = [];
|
|
92
|
+
const transport = new MockTransport();
|
|
93
|
+
{
|
|
94
|
+
const client = createRealtimeClient({ transport });
|
|
95
|
+
const start = performance.now();
|
|
96
|
+
const count = 1e4;
|
|
97
|
+
const unsubs = [];
|
|
98
|
+
for (let i = 0; i < count; i++) {
|
|
99
|
+
const unsub = await client.subscribe(`topic_${i}`, () => {
|
|
100
|
+
});
|
|
101
|
+
unsubs.push(unsub);
|
|
102
|
+
}
|
|
103
|
+
for (const unsub of unsubs) {
|
|
104
|
+
await unsub();
|
|
105
|
+
}
|
|
106
|
+
const duration = performance.now() - start;
|
|
107
|
+
reports.push({
|
|
108
|
+
operation: "subscribe + unsubscribe",
|
|
109
|
+
count,
|
|
110
|
+
durationMs: duration,
|
|
111
|
+
opsPerSecond: count * 2 / (duration / 1e3)
|
|
112
|
+
});
|
|
113
|
+
await client.destroy();
|
|
114
|
+
}
|
|
115
|
+
{
|
|
116
|
+
const client = createRealtimeClient({ transport });
|
|
117
|
+
await client.subscribe("test", () => {
|
|
118
|
+
});
|
|
119
|
+
const count = 5e4;
|
|
120
|
+
const start = performance.now();
|
|
121
|
+
for (let i = 0; i < count; i++) {
|
|
122
|
+
await client.publish("test", { seq: i });
|
|
123
|
+
}
|
|
124
|
+
const duration = performance.now() - start;
|
|
125
|
+
reports.push({
|
|
126
|
+
operation: "publish message",
|
|
127
|
+
count,
|
|
128
|
+
durationMs: duration,
|
|
129
|
+
opsPerSecond: count / (duration / 1e3)
|
|
130
|
+
});
|
|
131
|
+
await client.destroy();
|
|
132
|
+
}
|
|
133
|
+
return reports;
|
|
134
|
+
}
|
|
135
|
+
async function runHybridStressTest() {
|
|
136
|
+
const transport = new MockTransport();
|
|
137
|
+
const manager = new ConnectionManager(transport, "hybrid");
|
|
138
|
+
const count = 1e3;
|
|
139
|
+
const filters = [
|
|
140
|
+
{ region: "US" },
|
|
141
|
+
{ region: "EU" },
|
|
142
|
+
{ region: "AP" },
|
|
143
|
+
{ region: "US" },
|
|
144
|
+
// duplicate of 0
|
|
145
|
+
{ region: "EU" }
|
|
146
|
+
// duplicate of 1
|
|
147
|
+
];
|
|
148
|
+
for (let i = 0; i < count; i++) {
|
|
149
|
+
const filter = filters[i % filters.length];
|
|
150
|
+
await manager.subscribe({ id: `sub_${i}`, topic: "orders", filter }, () => {
|
|
151
|
+
});
|
|
152
|
+
}
|
|
153
|
+
const connectionsCount = manager.getConnections().size;
|
|
154
|
+
const subscriptionsCount = manager.getActiveSubscriptionCount();
|
|
155
|
+
await manager.destroy();
|
|
156
|
+
return { connectionsCount, subscriptionsCount };
|
|
157
|
+
}
|
|
158
|
+
async function runNetworkChaosStressTest(cycles = 20) {
|
|
159
|
+
const transport = new MockTransport();
|
|
160
|
+
const connections = [];
|
|
161
|
+
for (let i = 0; i < 10; i++) {
|
|
162
|
+
const conn = createRealtimeClient({ transport });
|
|
163
|
+
await conn.subscribe("alerts", () => {
|
|
164
|
+
});
|
|
165
|
+
connections.push(conn);
|
|
166
|
+
}
|
|
167
|
+
for (let i = 0; i < cycles; i++) {
|
|
168
|
+
const shouldOffline = Math.random() > 0.5;
|
|
169
|
+
if (shouldOffline) {
|
|
170
|
+
transport.simulateStateChange("offline");
|
|
171
|
+
} else {
|
|
172
|
+
transport.simulateStateChange("connected");
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
transport.simulateStateChange("connected");
|
|
176
|
+
for (const conn of connections) {
|
|
177
|
+
await conn.destroy();
|
|
178
|
+
}
|
|
179
|
+
return { success: true };
|
|
180
|
+
}
|
|
181
|
+
export {
|
|
182
|
+
MockTransport,
|
|
183
|
+
runBenchmarks,
|
|
184
|
+
runHybridStressTest,
|
|
185
|
+
runNetworkChaosStressTest
|
|
186
|
+
};
|
package/package.json
ADDED
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@connexis/testing",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "Testing helpers and mocks for @connexis",
|
|
5
|
+
"main": "./dist/index.js",
|
|
6
|
+
"module": "./dist/index.mjs",
|
|
7
|
+
"types": "./dist/index.d.ts",
|
|
8
|
+
"exports": {
|
|
9
|
+
".": {
|
|
10
|
+
"types": "./dist/index.d.ts",
|
|
11
|
+
"import": "./dist/index.mjs",
|
|
12
|
+
"require": "./dist/index.js"
|
|
13
|
+
}
|
|
14
|
+
},
|
|
15
|
+
"dependencies": {
|
|
16
|
+
"@connexis/core": "1.0.0"
|
|
17
|
+
},
|
|
18
|
+
"devDependencies": {
|
|
19
|
+
"typescript": "^5.5.2"
|
|
20
|
+
},
|
|
21
|
+
"scripts": {
|
|
22
|
+
"build": "tsup src/index.ts --format cjs,esm --dts --clean"
|
|
23
|
+
}
|
|
24
|
+
}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import { describe, it, expect } from 'vitest';
|
|
2
|
+
import {
|
|
3
|
+
runBenchmarks,
|
|
4
|
+
runHybridStressTest,
|
|
5
|
+
runNetworkChaosStressTest
|
|
6
|
+
} from '../stress-and-bench.js';
|
|
7
|
+
|
|
8
|
+
describe('Performance Benchmarks & Stress Tests', () => {
|
|
9
|
+
it('should run core performance benchmarks and yield throughput values', async () => {
|
|
10
|
+
const reports = await runBenchmarks();
|
|
11
|
+
|
|
12
|
+
// Print reports to stdout
|
|
13
|
+
console.table(reports);
|
|
14
|
+
|
|
15
|
+
expect(reports.length).toBe(2);
|
|
16
|
+
expect(reports[0].opsPerSecond).toBeGreaterThan(0);
|
|
17
|
+
expect(reports[1].opsPerSecond).toBeGreaterThan(0);
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
it('should run hybrid stress test and correctly deduplicate connection count', async () => {
|
|
21
|
+
// 1,000 subscriptions split across 5 unique filter groups -> should result in exactly 3 unique filter signatures
|
|
22
|
+
// (since region US, EU, AP are 3 distinct values, and the duplicates group onto them)
|
|
23
|
+
const { connectionsCount, subscriptionsCount } = await runHybridStressTest();
|
|
24
|
+
|
|
25
|
+
expect(subscriptionsCount).toBe(1000);
|
|
26
|
+
expect(connectionsCount).toBe(3); // 'orders' with region US, EU, AP
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
it('should run network chaos stress test without leaking subscriptions or crashing', async () => {
|
|
30
|
+
const { success } = await runNetworkChaosStressTest(10);
|
|
31
|
+
expect(success).toBe(true);
|
|
32
|
+
});
|
|
33
|
+
});
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
import { Transport, ConnectionState, TransportCapabilities } from '@connexis/core';
|
|
2
|
+
|
|
3
|
+
export class MockTransport implements Transport {
|
|
4
|
+
public readonly type = 'mock';
|
|
5
|
+
private _state: ConnectionState = 'idle';
|
|
6
|
+
private messageCallback: ((topic: string, data: any) => void) | null = null;
|
|
7
|
+
private stateCallback: ((state: ConnectionState, error?: Error) => void) | null = null;
|
|
8
|
+
|
|
9
|
+
public published: Array<{ topic: string; data: any }> = [];
|
|
10
|
+
public subscribed: Array<{ topic: string; filter?: Record<string, any> }> = [];
|
|
11
|
+
public unsubscribed: Array<{ topic: string; filter?: Record<string, any> }> = [];
|
|
12
|
+
|
|
13
|
+
private failNextConnect = false;
|
|
14
|
+
private failError: Error = new Error('Mock connection failure');
|
|
15
|
+
private connectDelay = 0;
|
|
16
|
+
|
|
17
|
+
constructor(
|
|
18
|
+
private caps: TransportCapabilities = { publish: true, subscribe: true, latency: 'low' }
|
|
19
|
+
) {}
|
|
20
|
+
|
|
21
|
+
get state(): ConnectionState {
|
|
22
|
+
return this._state;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
setFailNextConnect(fail: boolean, error?: Error): void {
|
|
26
|
+
this.failNextConnect = fail;
|
|
27
|
+
if (error) this.failError = error;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
setConnectDelay(delayMs: number): void {
|
|
31
|
+
this.connectDelay = delayMs;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
async connect(): Promise<void> {
|
|
35
|
+
if (this._state === 'connected') return;
|
|
36
|
+
this.updateState('connecting');
|
|
37
|
+
|
|
38
|
+
if (this.connectDelay > 0) {
|
|
39
|
+
await new Promise((resolve) => setTimeout(resolve, this.connectDelay));
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
if (this.failNextConnect) {
|
|
43
|
+
this.failNextConnect = false; // Reset
|
|
44
|
+
this.updateState('error', this.failError);
|
|
45
|
+
throw this.failError;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
this.updateState('connected');
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
async disconnect(): Promise<void> {
|
|
52
|
+
this.updateState('closed');
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
async publish(topic: string, data: any): Promise<void> {
|
|
56
|
+
if (this._state !== 'connected') {
|
|
57
|
+
throw new Error('Transport not connected');
|
|
58
|
+
}
|
|
59
|
+
this.published.push({ topic, data });
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
async subscribe(topic: string, filter?: Record<string, any>): Promise<void> {
|
|
63
|
+
this.subscribed.push({ topic, filter });
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
async unsubscribe(topic: string, filter?: Record<string, any>): Promise<void> {
|
|
67
|
+
this.unsubscribed.push({ topic, filter });
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
capabilities(): TransportCapabilities {
|
|
71
|
+
return this.caps;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
onMessage(cb: (topic: string, data: any) => void): void {
|
|
75
|
+
this.messageCallback = cb;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
onStateChange(cb: (state: ConnectionState, error?: Error) => void): void {
|
|
79
|
+
this.stateCallback = cb;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
clone(): MockTransport {
|
|
83
|
+
const cloned = new MockTransport(this.caps);
|
|
84
|
+
cloned.failNextConnect = this.failNextConnect;
|
|
85
|
+
cloned.failError = this.failError;
|
|
86
|
+
cloned.connectDelay = this.connectDelay;
|
|
87
|
+
return cloned;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
// Simulator helper methods
|
|
91
|
+
simulateMessage(topic: string, data: any): void {
|
|
92
|
+
if (this.messageCallback) {
|
|
93
|
+
this.messageCallback(topic, data);
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
simulateStateChange(state: ConnectionState, error?: Error): void {
|
|
98
|
+
this.updateState(state, error);
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
private updateState(state: ConnectionState, error?: Error): void {
|
|
102
|
+
this._state = state;
|
|
103
|
+
if (this.stateCallback) {
|
|
104
|
+
this.stateCallback(state, error);
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
}
|
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
import { createRealtimeClient, ConnectionManager } from '@connexis/core';
|
|
2
|
+
import { MockTransport } from './mock-transport.js';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Benchmark Results Interface
|
|
6
|
+
*/
|
|
7
|
+
export interface BenchmarkReport {
|
|
8
|
+
operation: string;
|
|
9
|
+
count: number;
|
|
10
|
+
durationMs: number;
|
|
11
|
+
opsPerSecond: number;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Runs a performance benchmark for core client operations.
|
|
16
|
+
*/
|
|
17
|
+
export async function runBenchmarks(): Promise<BenchmarkReport[]> {
|
|
18
|
+
const reports: BenchmarkReport[] = [];
|
|
19
|
+
const transport = new MockTransport();
|
|
20
|
+
|
|
21
|
+
// 1. Subscribe/Unsubscribe Benchmark
|
|
22
|
+
{
|
|
23
|
+
const client = createRealtimeClient({ transport });
|
|
24
|
+
const start = performance.now();
|
|
25
|
+
const count = 10000;
|
|
26
|
+
const unsubs: Array<() => Promise<void>> = [];
|
|
27
|
+
|
|
28
|
+
for (let i = 0; i < count; i++) {
|
|
29
|
+
const unsub = await client.subscribe(`topic_${i}`, () => {});
|
|
30
|
+
unsubs.push(unsub);
|
|
31
|
+
}
|
|
32
|
+
for (const unsub of unsubs) {
|
|
33
|
+
await unsub();
|
|
34
|
+
}
|
|
35
|
+
const duration = performance.now() - start;
|
|
36
|
+
reports.push({
|
|
37
|
+
operation: 'subscribe + unsubscribe',
|
|
38
|
+
count,
|
|
39
|
+
durationMs: duration,
|
|
40
|
+
opsPerSecond: (count * 2) / (duration / 1000)
|
|
41
|
+
});
|
|
42
|
+
await client.destroy();
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
// 2. Publish Throughput & Latency Benchmark
|
|
46
|
+
{
|
|
47
|
+
const client = createRealtimeClient({ transport });
|
|
48
|
+
// Spin up connection
|
|
49
|
+
await client.subscribe('test', () => {});
|
|
50
|
+
|
|
51
|
+
const count = 50000;
|
|
52
|
+
const start = performance.now();
|
|
53
|
+
for (let i = 0; i < count; i++) {
|
|
54
|
+
await client.publish('test', { seq: i });
|
|
55
|
+
}
|
|
56
|
+
const duration = performance.now() - start;
|
|
57
|
+
reports.push({
|
|
58
|
+
operation: 'publish message',
|
|
59
|
+
count,
|
|
60
|
+
durationMs: duration,
|
|
61
|
+
opsPerSecond: count / (duration / 1000)
|
|
62
|
+
});
|
|
63
|
+
await client.destroy();
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
return reports;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* Scenario: Stress test hybrid policy deduplication with 1,000 subscriptions.
|
|
71
|
+
*/
|
|
72
|
+
export async function runHybridStressTest(): Promise<{
|
|
73
|
+
connectionsCount: number;
|
|
74
|
+
subscriptionsCount: number;
|
|
75
|
+
}> {
|
|
76
|
+
const transport = new MockTransport();
|
|
77
|
+
const manager = new ConnectionManager(transport, 'hybrid');
|
|
78
|
+
|
|
79
|
+
const count = 1000;
|
|
80
|
+
// Deduplicate onto 5 unique filters
|
|
81
|
+
const filters = [
|
|
82
|
+
{ region: 'US' },
|
|
83
|
+
{ region: 'EU' },
|
|
84
|
+
{ region: 'AP' },
|
|
85
|
+
{ region: 'US' }, // duplicate of 0
|
|
86
|
+
{ region: 'EU' } // duplicate of 1
|
|
87
|
+
];
|
|
88
|
+
|
|
89
|
+
for (let i = 0; i < count; i++) {
|
|
90
|
+
const filter = filters[i % filters.length];
|
|
91
|
+
await manager.subscribe({ id: `sub_${i}`, topic: 'orders', filter }, () => {});
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
const connectionsCount = manager.getConnections().size;
|
|
95
|
+
const subscriptionsCount = manager.getActiveSubscriptionCount();
|
|
96
|
+
|
|
97
|
+
await manager.destroy();
|
|
98
|
+
|
|
99
|
+
return { connectionsCount, subscriptionsCount };
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* Scenario: Simulate 10 tabs and trigger random network online/offline events.
|
|
104
|
+
*/
|
|
105
|
+
export async function runNetworkChaosStressTest(cycles = 20): Promise<{ success: boolean }> {
|
|
106
|
+
const transport = new MockTransport();
|
|
107
|
+
const connections: any[] = [];
|
|
108
|
+
|
|
109
|
+
for (let i = 0; i < 10; i++) {
|
|
110
|
+
const conn = createRealtimeClient({ transport });
|
|
111
|
+
await conn.subscribe('alerts', () => {});
|
|
112
|
+
connections.push(conn);
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
for (let i = 0; i < cycles; i++) {
|
|
116
|
+
const shouldOffline = Math.random() > 0.5;
|
|
117
|
+
if (shouldOffline) {
|
|
118
|
+
transport.simulateStateChange('offline');
|
|
119
|
+
} else {
|
|
120
|
+
transport.simulateStateChange('connected');
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
// Recover everything
|
|
125
|
+
transport.simulateStateChange('connected');
|
|
126
|
+
|
|
127
|
+
for (const conn of connections) {
|
|
128
|
+
await conn.destroy();
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
return { success: true };
|
|
132
|
+
}
|