recker 1.0.19 → 1.0.20-next.595cc59
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/dist/cli/index.d.ts +0 -1
- package/dist/cli/index.js +495 -2
- package/dist/cli/tui/shell.js +1 -1
- package/dist/core/client.d.ts +2 -0
- package/dist/core/client.d.ts.map +1 -1
- package/dist/core/client.js +6 -7
- package/dist/plugins/cache.js +1 -1
- package/dist/plugins/hls.d.ts +90 -17
- package/dist/plugins/hls.d.ts.map +1 -1
- package/dist/plugins/hls.js +343 -173
- package/dist/plugins/retry.js +2 -2
- package/dist/testing/index.d.ts +16 -0
- package/dist/testing/index.d.ts.map +1 -1
- package/dist/testing/index.js +8 -0
- package/dist/testing/mock-dns-server.d.ts +70 -0
- package/dist/testing/mock-dns-server.d.ts.map +1 -0
- package/dist/testing/mock-dns-server.js +269 -0
- package/dist/testing/mock-ftp-server.d.ts +90 -0
- package/dist/testing/mock-ftp-server.d.ts.map +1 -0
- package/dist/testing/mock-ftp-server.js +562 -0
- package/dist/testing/mock-hls-server.d.ts +81 -0
- package/dist/testing/mock-hls-server.d.ts.map +1 -0
- package/dist/testing/mock-hls-server.js +381 -0
- package/dist/testing/mock-http-server.d.ts +100 -0
- package/dist/testing/mock-http-server.d.ts.map +1 -0
- package/dist/testing/mock-http-server.js +298 -0
- package/dist/testing/mock-sse-server.d.ts +77 -0
- package/dist/testing/mock-sse-server.d.ts.map +1 -0
- package/dist/testing/mock-sse-server.js +291 -0
- package/dist/testing/mock-telnet-server.d.ts +60 -0
- package/dist/testing/mock-telnet-server.d.ts.map +1 -0
- package/dist/testing/mock-telnet-server.js +273 -0
- package/dist/testing/mock-websocket-server.d.ts +78 -0
- package/dist/testing/mock-websocket-server.d.ts.map +1 -0
- package/dist/testing/mock-websocket-server.js +316 -0
- package/dist/testing/mock-whois-server.d.ts +57 -0
- package/dist/testing/mock-whois-server.d.ts.map +1 -0
- package/dist/testing/mock-whois-server.js +234 -0
- package/dist/transport/undici.d.ts +1 -1
- package/dist/transport/undici.d.ts.map +1 -1
- package/dist/transport/undici.js +11 -5
- package/dist/utils/dns-toolkit.js +1 -1
- package/dist/utils/dns.js +2 -2
- package/dist/utils/optional-require.js +1 -1
- package/dist/webrtc/index.js +1 -1
- package/package.json +3 -1
|
@@ -0,0 +1,298 @@
|
|
|
1
|
+
import { EventEmitter } from 'node:events';
|
|
2
|
+
import { createServer } from 'node:http';
|
|
3
|
+
export class MockHttpServer extends EventEmitter {
|
|
4
|
+
options;
|
|
5
|
+
httpServer = null;
|
|
6
|
+
routes = [];
|
|
7
|
+
_port = 0;
|
|
8
|
+
_started = false;
|
|
9
|
+
stats = {
|
|
10
|
+
totalRequests: 0,
|
|
11
|
+
requestsByMethod: {},
|
|
12
|
+
requestsByPath: {},
|
|
13
|
+
requestLog: [],
|
|
14
|
+
};
|
|
15
|
+
constructor(options = {}) {
|
|
16
|
+
super();
|
|
17
|
+
this.options = {
|
|
18
|
+
port: 0,
|
|
19
|
+
host: '127.0.0.1',
|
|
20
|
+
defaultResponse: { status: 404, body: { error: 'Not Found' } },
|
|
21
|
+
delay: 0,
|
|
22
|
+
cors: true,
|
|
23
|
+
corsOrigin: '*',
|
|
24
|
+
...options,
|
|
25
|
+
};
|
|
26
|
+
}
|
|
27
|
+
get port() {
|
|
28
|
+
return this._port;
|
|
29
|
+
}
|
|
30
|
+
get address() {
|
|
31
|
+
return this.options.host;
|
|
32
|
+
}
|
|
33
|
+
get url() {
|
|
34
|
+
return `http://${this.options.host}:${this._port}`;
|
|
35
|
+
}
|
|
36
|
+
get isRunning() {
|
|
37
|
+
return this._started;
|
|
38
|
+
}
|
|
39
|
+
get statistics() {
|
|
40
|
+
return { ...this.stats };
|
|
41
|
+
}
|
|
42
|
+
get routeCount() {
|
|
43
|
+
return this.routes.length;
|
|
44
|
+
}
|
|
45
|
+
async start() {
|
|
46
|
+
if (this._started) {
|
|
47
|
+
throw new Error('Server already started');
|
|
48
|
+
}
|
|
49
|
+
return new Promise((resolve, reject) => {
|
|
50
|
+
this.httpServer = createServer((req, res) => this.handleRequest(req, res));
|
|
51
|
+
this.httpServer.on('error', reject);
|
|
52
|
+
this.httpServer.listen(this.options.port, this.options.host, () => {
|
|
53
|
+
const addr = this.httpServer.address();
|
|
54
|
+
this._port = typeof addr === 'string' ? 0 : addr?.port ?? 0;
|
|
55
|
+
this._started = true;
|
|
56
|
+
this.emit('listening', this._port);
|
|
57
|
+
resolve();
|
|
58
|
+
});
|
|
59
|
+
});
|
|
60
|
+
}
|
|
61
|
+
async stop() {
|
|
62
|
+
if (!this._started)
|
|
63
|
+
return;
|
|
64
|
+
return new Promise((resolve) => {
|
|
65
|
+
this.httpServer?.close(() => {
|
|
66
|
+
this._started = false;
|
|
67
|
+
this.httpServer = null;
|
|
68
|
+
this.emit('close');
|
|
69
|
+
resolve();
|
|
70
|
+
});
|
|
71
|
+
});
|
|
72
|
+
}
|
|
73
|
+
reset() {
|
|
74
|
+
this.routes = [];
|
|
75
|
+
this.stats = {
|
|
76
|
+
totalRequests: 0,
|
|
77
|
+
requestsByMethod: {},
|
|
78
|
+
requestsByPath: {},
|
|
79
|
+
requestLog: [],
|
|
80
|
+
};
|
|
81
|
+
this.emit('reset');
|
|
82
|
+
}
|
|
83
|
+
route(method, path, handler, options = {}) {
|
|
84
|
+
const pattern = this.pathToRegex(path);
|
|
85
|
+
this.routes.push({
|
|
86
|
+
method: method.toUpperCase(),
|
|
87
|
+
pattern,
|
|
88
|
+
pathPattern: path,
|
|
89
|
+
handler,
|
|
90
|
+
times: options.times,
|
|
91
|
+
callCount: 0,
|
|
92
|
+
});
|
|
93
|
+
return this;
|
|
94
|
+
}
|
|
95
|
+
get(path, handler, options) {
|
|
96
|
+
return this.route('GET', path, handler, options);
|
|
97
|
+
}
|
|
98
|
+
post(path, handler, options) {
|
|
99
|
+
return this.route('POST', path, handler, options);
|
|
100
|
+
}
|
|
101
|
+
put(path, handler, options) {
|
|
102
|
+
return this.route('PUT', path, handler, options);
|
|
103
|
+
}
|
|
104
|
+
patch(path, handler, options) {
|
|
105
|
+
return this.route('PATCH', path, handler, options);
|
|
106
|
+
}
|
|
107
|
+
delete(path, handler, options) {
|
|
108
|
+
return this.route('DELETE', path, handler, options);
|
|
109
|
+
}
|
|
110
|
+
head(path, handler, options) {
|
|
111
|
+
return this.route('HEAD', path, handler, options);
|
|
112
|
+
}
|
|
113
|
+
optionsRoute(path, handler, options) {
|
|
114
|
+
return this.route('OPTIONS', path, handler, options);
|
|
115
|
+
}
|
|
116
|
+
any(path, handler, options) {
|
|
117
|
+
return this.route('*', path, handler, options);
|
|
118
|
+
}
|
|
119
|
+
removeRoute(method, path) {
|
|
120
|
+
const index = this.routes.findIndex((r) => r.method === method.toUpperCase() && r.pathPattern === path);
|
|
121
|
+
if (index >= 0) {
|
|
122
|
+
this.routes.splice(index, 1);
|
|
123
|
+
return true;
|
|
124
|
+
}
|
|
125
|
+
return false;
|
|
126
|
+
}
|
|
127
|
+
clearRoutes() {
|
|
128
|
+
this.routes = [];
|
|
129
|
+
}
|
|
130
|
+
async handleRequest(req, res) {
|
|
131
|
+
const startTime = Date.now();
|
|
132
|
+
const method = req.method?.toUpperCase() ?? 'GET';
|
|
133
|
+
const urlParts = new URL(req.url ?? '/', `http://${req.headers.host}`);
|
|
134
|
+
const path = urlParts.pathname;
|
|
135
|
+
const query = Object.fromEntries(urlParts.searchParams);
|
|
136
|
+
this.stats.totalRequests++;
|
|
137
|
+
this.stats.requestsByMethod[method] = (this.stats.requestsByMethod[method] ?? 0) + 1;
|
|
138
|
+
this.stats.requestsByPath[path] = (this.stats.requestsByPath[path] ?? 0) + 1;
|
|
139
|
+
if (this.options.cors && method === 'OPTIONS') {
|
|
140
|
+
this.sendCorsHeaders(res);
|
|
141
|
+
res.writeHead(204);
|
|
142
|
+
res.end();
|
|
143
|
+
return;
|
|
144
|
+
}
|
|
145
|
+
const body = await this.parseBody(req);
|
|
146
|
+
const mockReq = {
|
|
147
|
+
method,
|
|
148
|
+
path,
|
|
149
|
+
query,
|
|
150
|
+
headers: req.headers,
|
|
151
|
+
body,
|
|
152
|
+
raw: req,
|
|
153
|
+
};
|
|
154
|
+
this.emit('request', mockReq);
|
|
155
|
+
const route = this.findRoute(method, path);
|
|
156
|
+
let response;
|
|
157
|
+
if (route) {
|
|
158
|
+
route.callCount++;
|
|
159
|
+
if (route.times !== undefined && route.callCount > route.times) {
|
|
160
|
+
response = this.options.defaultResponse;
|
|
161
|
+
}
|
|
162
|
+
else {
|
|
163
|
+
response = typeof route.handler === 'function'
|
|
164
|
+
? await route.handler(mockReq)
|
|
165
|
+
: route.handler;
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
else {
|
|
169
|
+
response = this.options.defaultResponse;
|
|
170
|
+
}
|
|
171
|
+
await this.sendResponse(res, response);
|
|
172
|
+
const duration = Date.now() - startTime;
|
|
173
|
+
this.stats.requestLog.push({
|
|
174
|
+
method,
|
|
175
|
+
path,
|
|
176
|
+
status: response.status ?? 200,
|
|
177
|
+
timestamp: startTime,
|
|
178
|
+
duration,
|
|
179
|
+
});
|
|
180
|
+
this.emit('response', mockReq, response, duration);
|
|
181
|
+
}
|
|
182
|
+
findRoute(method, path) {
|
|
183
|
+
for (const route of this.routes) {
|
|
184
|
+
if ((route.method === method || route.method === '*') && route.pattern.test(path)) {
|
|
185
|
+
return route;
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
return undefined;
|
|
189
|
+
}
|
|
190
|
+
async sendResponse(res, response) {
|
|
191
|
+
if (response.drop) {
|
|
192
|
+
res.destroy();
|
|
193
|
+
return;
|
|
194
|
+
}
|
|
195
|
+
const delay = response.delay ?? this.options.delay;
|
|
196
|
+
if (delay > 0) {
|
|
197
|
+
await new Promise((resolve) => setTimeout(resolve, delay));
|
|
198
|
+
}
|
|
199
|
+
if (this.options.cors) {
|
|
200
|
+
this.sendCorsHeaders(res);
|
|
201
|
+
}
|
|
202
|
+
const status = response.status ?? 200;
|
|
203
|
+
const headers = {
|
|
204
|
+
'Content-Type': 'application/json',
|
|
205
|
+
...response.headers,
|
|
206
|
+
};
|
|
207
|
+
if (response.stream) {
|
|
208
|
+
res.writeHead(status, headers);
|
|
209
|
+
for (const chunk of response.stream.chunks) {
|
|
210
|
+
res.write(chunk);
|
|
211
|
+
await new Promise((resolve) => setTimeout(resolve, response.stream.interval));
|
|
212
|
+
}
|
|
213
|
+
res.end();
|
|
214
|
+
return;
|
|
215
|
+
}
|
|
216
|
+
let body = '';
|
|
217
|
+
if (response.body !== undefined) {
|
|
218
|
+
if (typeof response.body === 'string' || Buffer.isBuffer(response.body)) {
|
|
219
|
+
body = response.body;
|
|
220
|
+
if (typeof response.body === 'string' && !headers['Content-Type'].includes('json')) {
|
|
221
|
+
headers['Content-Type'] = 'text/plain';
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
else {
|
|
225
|
+
body = JSON.stringify(response.body);
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
headers['Content-Length'] = String(Buffer.byteLength(body));
|
|
229
|
+
res.writeHead(status, headers);
|
|
230
|
+
res.end(body);
|
|
231
|
+
}
|
|
232
|
+
sendCorsHeaders(res) {
|
|
233
|
+
res.setHeader('Access-Control-Allow-Origin', this.options.corsOrigin);
|
|
234
|
+
res.setHeader('Access-Control-Allow-Methods', 'GET, POST, PUT, PATCH, DELETE, OPTIONS');
|
|
235
|
+
res.setHeader('Access-Control-Allow-Headers', 'Content-Type, Authorization');
|
|
236
|
+
res.setHeader('Access-Control-Max-Age', '86400');
|
|
237
|
+
}
|
|
238
|
+
async parseBody(req) {
|
|
239
|
+
return new Promise((resolve) => {
|
|
240
|
+
const chunks = [];
|
|
241
|
+
req.on('data', (chunk) => chunks.push(chunk));
|
|
242
|
+
req.on('end', () => {
|
|
243
|
+
if (chunks.length === 0) {
|
|
244
|
+
resolve(undefined);
|
|
245
|
+
return;
|
|
246
|
+
}
|
|
247
|
+
const raw = Buffer.concat(chunks).toString('utf-8');
|
|
248
|
+
const contentType = req.headers['content-type'] ?? '';
|
|
249
|
+
if (contentType.includes('application/json')) {
|
|
250
|
+
try {
|
|
251
|
+
resolve(JSON.parse(raw));
|
|
252
|
+
}
|
|
253
|
+
catch {
|
|
254
|
+
resolve(raw);
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
else {
|
|
258
|
+
resolve(raw);
|
|
259
|
+
}
|
|
260
|
+
});
|
|
261
|
+
req.on('error', () => resolve(undefined));
|
|
262
|
+
});
|
|
263
|
+
}
|
|
264
|
+
pathToRegex(path) {
|
|
265
|
+
const escaped = path.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
266
|
+
const withParams = escaped.replace(/:(\w+)/g, '([^/]+)');
|
|
267
|
+
return new RegExp(`^${withParams}$`);
|
|
268
|
+
}
|
|
269
|
+
async waitForRequests(count, timeout = 5000) {
|
|
270
|
+
const start = Date.now();
|
|
271
|
+
while (this.stats.totalRequests < count) {
|
|
272
|
+
if (Date.now() - start > timeout) {
|
|
273
|
+
throw new Error(`Timeout waiting for ${count} requests (have ${this.stats.totalRequests})`);
|
|
274
|
+
}
|
|
275
|
+
await new Promise((resolve) => setTimeout(resolve, 10));
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
getCallCount(method, path) {
|
|
279
|
+
const route = this.routes.find((r) => r.method === method.toUpperCase() && r.pathPattern === path);
|
|
280
|
+
return route?.callCount ?? 0;
|
|
281
|
+
}
|
|
282
|
+
static async create(options = {}) {
|
|
283
|
+
const server = new MockHttpServer(options);
|
|
284
|
+
await server.start();
|
|
285
|
+
return server;
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
export async function createMockHttpServer(routes, options) {
|
|
289
|
+
const server = new MockHttpServer(options);
|
|
290
|
+
if (routes) {
|
|
291
|
+
for (const [key, response] of Object.entries(routes)) {
|
|
292
|
+
const [method, path] = key.includes(' ') ? key.split(' ') : ['GET', key];
|
|
293
|
+
server.route(method, path, response);
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
await server.start();
|
|
297
|
+
return server;
|
|
298
|
+
}
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
import { EventEmitter } from 'node:events';
|
|
2
|
+
import { type ServerResponse } from 'node:http';
|
|
3
|
+
export interface MockSSEServerOptions {
|
|
4
|
+
port?: number;
|
|
5
|
+
host?: string;
|
|
6
|
+
path?: string;
|
|
7
|
+
retryInterval?: number;
|
|
8
|
+
sendRetry?: boolean;
|
|
9
|
+
maxConnections?: number;
|
|
10
|
+
keepAliveInterval?: number;
|
|
11
|
+
corsOrigin?: string;
|
|
12
|
+
}
|
|
13
|
+
export interface SSEEvent {
|
|
14
|
+
event?: string;
|
|
15
|
+
data: string;
|
|
16
|
+
id?: string;
|
|
17
|
+
retry?: number;
|
|
18
|
+
}
|
|
19
|
+
export interface MockSSEClient {
|
|
20
|
+
id: string;
|
|
21
|
+
response: ServerResponse;
|
|
22
|
+
connectedAt: number;
|
|
23
|
+
lastEventId?: string;
|
|
24
|
+
eventsSent: number;
|
|
25
|
+
metadata: Record<string, any>;
|
|
26
|
+
}
|
|
27
|
+
export interface MockSSEStats {
|
|
28
|
+
totalConnections: number;
|
|
29
|
+
currentConnections: number;
|
|
30
|
+
totalEventsSent: number;
|
|
31
|
+
eventLog: Array<{
|
|
32
|
+
event: SSEEvent;
|
|
33
|
+
timestamp: number;
|
|
34
|
+
clientCount: number;
|
|
35
|
+
}>;
|
|
36
|
+
}
|
|
37
|
+
export declare class MockSSEServer extends EventEmitter {
|
|
38
|
+
private options;
|
|
39
|
+
private httpServer;
|
|
40
|
+
private clients;
|
|
41
|
+
private _port;
|
|
42
|
+
private _started;
|
|
43
|
+
private clientIdCounter;
|
|
44
|
+
private periodicIntervals;
|
|
45
|
+
private keepAliveInterval;
|
|
46
|
+
private eventIdCounter;
|
|
47
|
+
private stats;
|
|
48
|
+
constructor(options?: MockSSEServerOptions);
|
|
49
|
+
get port(): number;
|
|
50
|
+
get address(): string;
|
|
51
|
+
get url(): string;
|
|
52
|
+
get isRunning(): boolean;
|
|
53
|
+
get connectionCount(): number;
|
|
54
|
+
get statistics(): MockSSEStats;
|
|
55
|
+
get allClients(): MockSSEClient[];
|
|
56
|
+
start(): Promise<void>;
|
|
57
|
+
stop(): Promise<void>;
|
|
58
|
+
reset(): void;
|
|
59
|
+
sendEvent(event: SSEEvent): number;
|
|
60
|
+
sendEventTo(clientId: string, event: SSEEvent): boolean;
|
|
61
|
+
sendData(data: string, event?: string): number;
|
|
62
|
+
sendJSON(data: any, event?: string): number;
|
|
63
|
+
sendComment(comment: string): void;
|
|
64
|
+
startPeriodicEvents(eventType: string, intervalMs: number, dataGenerator?: () => string): void;
|
|
65
|
+
stopPeriodicEvents(eventType?: string): void;
|
|
66
|
+
getClient(id: string): MockSSEClient | undefined;
|
|
67
|
+
disconnectClient(id: string): void;
|
|
68
|
+
disconnectAll(): void;
|
|
69
|
+
waitForConnections(count: number, timeout?: number): Promise<MockSSEClient[]>;
|
|
70
|
+
private handleRequest;
|
|
71
|
+
private formatEvent;
|
|
72
|
+
private writeToClient;
|
|
73
|
+
nextEventId(): string;
|
|
74
|
+
static create(options?: MockSSEServerOptions): Promise<MockSSEServer>;
|
|
75
|
+
}
|
|
76
|
+
export declare function createMockSSEServer(options?: MockSSEServerOptions): Promise<MockSSEServer>;
|
|
77
|
+
//# sourceMappingURL=mock-sse-server.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"mock-sse-server.d.ts","sourceRoot":"","sources":["../../src/testing/mock-sse-server.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,291 @@
|
|
|
1
|
+
import { EventEmitter } from 'node:events';
|
|
2
|
+
import { createServer } from 'node:http';
|
|
3
|
+
export class MockSSEServer extends EventEmitter {
|
|
4
|
+
options;
|
|
5
|
+
httpServer = null;
|
|
6
|
+
clients = new Map();
|
|
7
|
+
_port = 0;
|
|
8
|
+
_started = false;
|
|
9
|
+
clientIdCounter = 0;
|
|
10
|
+
periodicIntervals = new Map();
|
|
11
|
+
keepAliveInterval = null;
|
|
12
|
+
eventIdCounter = 0;
|
|
13
|
+
stats = {
|
|
14
|
+
totalConnections: 0,
|
|
15
|
+
currentConnections: 0,
|
|
16
|
+
totalEventsSent: 0,
|
|
17
|
+
eventLog: [],
|
|
18
|
+
};
|
|
19
|
+
constructor(options = {}) {
|
|
20
|
+
super();
|
|
21
|
+
this.options = {
|
|
22
|
+
port: 0,
|
|
23
|
+
host: '127.0.0.1',
|
|
24
|
+
path: '/events',
|
|
25
|
+
retryInterval: 3000,
|
|
26
|
+
sendRetry: true,
|
|
27
|
+
maxConnections: 0,
|
|
28
|
+
keepAliveInterval: 15000,
|
|
29
|
+
corsOrigin: '*',
|
|
30
|
+
...options,
|
|
31
|
+
};
|
|
32
|
+
}
|
|
33
|
+
get port() {
|
|
34
|
+
return this._port;
|
|
35
|
+
}
|
|
36
|
+
get address() {
|
|
37
|
+
return this.options.host;
|
|
38
|
+
}
|
|
39
|
+
get url() {
|
|
40
|
+
return `http://${this.options.host}:${this._port}${this.options.path}`;
|
|
41
|
+
}
|
|
42
|
+
get isRunning() {
|
|
43
|
+
return this._started;
|
|
44
|
+
}
|
|
45
|
+
get connectionCount() {
|
|
46
|
+
return this.clients.size;
|
|
47
|
+
}
|
|
48
|
+
get statistics() {
|
|
49
|
+
return { ...this.stats };
|
|
50
|
+
}
|
|
51
|
+
get allClients() {
|
|
52
|
+
return [...this.clients.values()];
|
|
53
|
+
}
|
|
54
|
+
async start() {
|
|
55
|
+
if (this._started) {
|
|
56
|
+
throw new Error('Server already started');
|
|
57
|
+
}
|
|
58
|
+
return new Promise((resolve, reject) => {
|
|
59
|
+
this.httpServer = createServer((req, res) => this.handleRequest(req, res));
|
|
60
|
+
this.httpServer.on('error', reject);
|
|
61
|
+
this.httpServer.listen(this.options.port, this.options.host, () => {
|
|
62
|
+
const addr = this.httpServer.address();
|
|
63
|
+
this._port = typeof addr === 'string' ? 0 : addr?.port ?? 0;
|
|
64
|
+
this._started = true;
|
|
65
|
+
if (this.options.keepAliveInterval > 0) {
|
|
66
|
+
this.keepAliveInterval = setInterval(() => {
|
|
67
|
+
this.sendComment('keep-alive');
|
|
68
|
+
}, this.options.keepAliveInterval);
|
|
69
|
+
}
|
|
70
|
+
this.emit('listening', this._port);
|
|
71
|
+
resolve();
|
|
72
|
+
});
|
|
73
|
+
});
|
|
74
|
+
}
|
|
75
|
+
async stop() {
|
|
76
|
+
if (!this._started)
|
|
77
|
+
return;
|
|
78
|
+
if (this.keepAliveInterval) {
|
|
79
|
+
clearInterval(this.keepAliveInterval);
|
|
80
|
+
this.keepAliveInterval = null;
|
|
81
|
+
}
|
|
82
|
+
for (const interval of this.periodicIntervals.values()) {
|
|
83
|
+
clearInterval(interval);
|
|
84
|
+
}
|
|
85
|
+
this.periodicIntervals.clear();
|
|
86
|
+
for (const client of this.clients.values()) {
|
|
87
|
+
client.response.end();
|
|
88
|
+
}
|
|
89
|
+
this.clients.clear();
|
|
90
|
+
return new Promise((resolve) => {
|
|
91
|
+
this.httpServer?.close(() => {
|
|
92
|
+
this._started = false;
|
|
93
|
+
this.httpServer = null;
|
|
94
|
+
this.emit('close');
|
|
95
|
+
resolve();
|
|
96
|
+
});
|
|
97
|
+
});
|
|
98
|
+
}
|
|
99
|
+
reset() {
|
|
100
|
+
this.stats = {
|
|
101
|
+
totalConnections: 0,
|
|
102
|
+
currentConnections: 0,
|
|
103
|
+
totalEventsSent: 0,
|
|
104
|
+
eventLog: [],
|
|
105
|
+
};
|
|
106
|
+
this.eventIdCounter = 0;
|
|
107
|
+
for (const interval of this.periodicIntervals.values()) {
|
|
108
|
+
clearInterval(interval);
|
|
109
|
+
}
|
|
110
|
+
this.periodicIntervals.clear();
|
|
111
|
+
this.emit('reset');
|
|
112
|
+
}
|
|
113
|
+
sendEvent(event) {
|
|
114
|
+
let sent = 0;
|
|
115
|
+
const formatted = this.formatEvent(event);
|
|
116
|
+
for (const client of this.clients.values()) {
|
|
117
|
+
if (this.writeToClient(client, formatted)) {
|
|
118
|
+
client.eventsSent++;
|
|
119
|
+
sent++;
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
this.stats.totalEventsSent += sent;
|
|
123
|
+
this.stats.eventLog.push({
|
|
124
|
+
event,
|
|
125
|
+
timestamp: Date.now(),
|
|
126
|
+
clientCount: sent,
|
|
127
|
+
});
|
|
128
|
+
this.emit('event', event, sent);
|
|
129
|
+
return sent;
|
|
130
|
+
}
|
|
131
|
+
sendEventTo(clientId, event) {
|
|
132
|
+
const client = this.clients.get(clientId);
|
|
133
|
+
if (!client)
|
|
134
|
+
return false;
|
|
135
|
+
const formatted = this.formatEvent(event);
|
|
136
|
+
if (this.writeToClient(client, formatted)) {
|
|
137
|
+
client.eventsSent++;
|
|
138
|
+
this.stats.totalEventsSent++;
|
|
139
|
+
return true;
|
|
140
|
+
}
|
|
141
|
+
return false;
|
|
142
|
+
}
|
|
143
|
+
sendData(data, event) {
|
|
144
|
+
return this.sendEvent({ data, event });
|
|
145
|
+
}
|
|
146
|
+
sendJSON(data, event) {
|
|
147
|
+
return this.sendEvent({ data: JSON.stringify(data), event });
|
|
148
|
+
}
|
|
149
|
+
sendComment(comment) {
|
|
150
|
+
const formatted = `: ${comment}\n\n`;
|
|
151
|
+
for (const client of this.clients.values()) {
|
|
152
|
+
this.writeToClient(client, formatted);
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
startPeriodicEvents(eventType, intervalMs, dataGenerator) {
|
|
156
|
+
if (this.periodicIntervals.has(eventType)) {
|
|
157
|
+
clearInterval(this.periodicIntervals.get(eventType));
|
|
158
|
+
}
|
|
159
|
+
const interval = setInterval(() => {
|
|
160
|
+
const data = dataGenerator?.() ?? new Date().toISOString();
|
|
161
|
+
this.sendEvent({ event: eventType, data });
|
|
162
|
+
}, intervalMs);
|
|
163
|
+
this.periodicIntervals.set(eventType, interval);
|
|
164
|
+
}
|
|
165
|
+
stopPeriodicEvents(eventType) {
|
|
166
|
+
if (eventType) {
|
|
167
|
+
const interval = this.periodicIntervals.get(eventType);
|
|
168
|
+
if (interval) {
|
|
169
|
+
clearInterval(interval);
|
|
170
|
+
this.periodicIntervals.delete(eventType);
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
else {
|
|
174
|
+
for (const interval of this.periodicIntervals.values()) {
|
|
175
|
+
clearInterval(interval);
|
|
176
|
+
}
|
|
177
|
+
this.periodicIntervals.clear();
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
getClient(id) {
|
|
181
|
+
return this.clients.get(id);
|
|
182
|
+
}
|
|
183
|
+
disconnectClient(id) {
|
|
184
|
+
const client = this.clients.get(id);
|
|
185
|
+
if (client) {
|
|
186
|
+
client.response.end();
|
|
187
|
+
this.clients.delete(id);
|
|
188
|
+
this.stats.currentConnections--;
|
|
189
|
+
this.emit('disconnect', client);
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
disconnectAll() {
|
|
193
|
+
for (const client of this.clients.values()) {
|
|
194
|
+
client.response.end();
|
|
195
|
+
}
|
|
196
|
+
this.clients.clear();
|
|
197
|
+
this.stats.currentConnections = 0;
|
|
198
|
+
}
|
|
199
|
+
async waitForConnections(count, timeout = 5000) {
|
|
200
|
+
const start = Date.now();
|
|
201
|
+
while (this.clients.size < count) {
|
|
202
|
+
if (Date.now() - start > timeout) {
|
|
203
|
+
throw new Error(`Timeout waiting for ${count} connections (have ${this.clients.size})`);
|
|
204
|
+
}
|
|
205
|
+
await new Promise((resolve) => setTimeout(resolve, 10));
|
|
206
|
+
}
|
|
207
|
+
return [...this.clients.values()].slice(0, count);
|
|
208
|
+
}
|
|
209
|
+
handleRequest(req, res) {
|
|
210
|
+
if (req.method !== 'GET' || req.url !== this.options.path) {
|
|
211
|
+
res.writeHead(404);
|
|
212
|
+
res.end('Not Found');
|
|
213
|
+
return;
|
|
214
|
+
}
|
|
215
|
+
if (this.options.maxConnections > 0 && this.clients.size >= this.options.maxConnections) {
|
|
216
|
+
res.writeHead(503);
|
|
217
|
+
res.end('Max connections reached');
|
|
218
|
+
return;
|
|
219
|
+
}
|
|
220
|
+
const lastEventId = req.headers['last-event-id'];
|
|
221
|
+
res.writeHead(200, {
|
|
222
|
+
'Content-Type': 'text/event-stream',
|
|
223
|
+
'Cache-Control': 'no-cache',
|
|
224
|
+
'Connection': 'keep-alive',
|
|
225
|
+
'Access-Control-Allow-Origin': this.options.corsOrigin,
|
|
226
|
+
'X-Accel-Buffering': 'no',
|
|
227
|
+
});
|
|
228
|
+
if (this.options.sendRetry) {
|
|
229
|
+
res.write(`retry: ${this.options.retryInterval}\n\n`);
|
|
230
|
+
}
|
|
231
|
+
const clientId = `sse-client-${++this.clientIdCounter}`;
|
|
232
|
+
const client = {
|
|
233
|
+
id: clientId,
|
|
234
|
+
response: res,
|
|
235
|
+
connectedAt: Date.now(),
|
|
236
|
+
lastEventId,
|
|
237
|
+
eventsSent: 0,
|
|
238
|
+
metadata: {},
|
|
239
|
+
};
|
|
240
|
+
this.clients.set(clientId, client);
|
|
241
|
+
this.stats.totalConnections++;
|
|
242
|
+
this.stats.currentConnections++;
|
|
243
|
+
this.emit('connection', client);
|
|
244
|
+
req.on('close', () => {
|
|
245
|
+
this.clients.delete(clientId);
|
|
246
|
+
this.stats.currentConnections--;
|
|
247
|
+
this.emit('disconnect', client);
|
|
248
|
+
});
|
|
249
|
+
res.on('error', (err) => {
|
|
250
|
+
this.emit('clientError', client, err);
|
|
251
|
+
this.clients.delete(clientId);
|
|
252
|
+
this.stats.currentConnections--;
|
|
253
|
+
});
|
|
254
|
+
}
|
|
255
|
+
formatEvent(event) {
|
|
256
|
+
const lines = [];
|
|
257
|
+
if (event.id) {
|
|
258
|
+
lines.push(`id: ${event.id}`);
|
|
259
|
+
}
|
|
260
|
+
if (event.event) {
|
|
261
|
+
lines.push(`event: ${event.event}`);
|
|
262
|
+
}
|
|
263
|
+
if (event.retry !== undefined) {
|
|
264
|
+
lines.push(`retry: ${event.retry}`);
|
|
265
|
+
}
|
|
266
|
+
const dataLines = event.data.split('\n');
|
|
267
|
+
for (const line of dataLines) {
|
|
268
|
+
lines.push(`data: ${line}`);
|
|
269
|
+
}
|
|
270
|
+
return lines.join('\n') + '\n\n';
|
|
271
|
+
}
|
|
272
|
+
writeToClient(client, data) {
|
|
273
|
+
try {
|
|
274
|
+
return client.response.write(data);
|
|
275
|
+
}
|
|
276
|
+
catch {
|
|
277
|
+
return false;
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
nextEventId() {
|
|
281
|
+
return String(++this.eventIdCounter);
|
|
282
|
+
}
|
|
283
|
+
static async create(options = {}) {
|
|
284
|
+
const server = new MockSSEServer(options);
|
|
285
|
+
await server.start();
|
|
286
|
+
return server;
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
export async function createMockSSEServer(options) {
|
|
290
|
+
return MockSSEServer.create(options);
|
|
291
|
+
}
|