kubernetes-fluent-client 3.2.2 → 3.3.1
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/fluent/watch.d.ts +5 -2
- package/dist/fluent/watch.d.ts.map +1 -1
- package/dist/fluent/watch.js +114 -149
- package/eslint.config.mjs +66 -0
- package/package.json +9 -5
- package/src/fluent/watch.ts +127 -174
package/dist/fluent/watch.d.ts
CHANGED
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
import { EventEmitter } from "events";
|
|
2
|
+
import { RequestInit } from "node-fetch";
|
|
3
|
+
import { Agent } from "undici";
|
|
2
4
|
import { GenericClass } from "../types";
|
|
3
5
|
import { Filters, WatchAction } from "./types";
|
|
4
6
|
export declare enum WatchEvent {
|
|
@@ -41,8 +43,8 @@ export type WatchCfg = {
|
|
|
41
43
|
relistIntervalSec?: number;
|
|
42
44
|
/** Max amount of seconds to go without receiving an event before reconciliation starts. Defaults to 300 (5 minutes). */
|
|
43
45
|
lastSeenLimitSeconds?: number;
|
|
44
|
-
/**
|
|
45
|
-
|
|
46
|
+
/** Watch Mechansism */
|
|
47
|
+
useLegacyWatch?: boolean;
|
|
46
48
|
};
|
|
47
49
|
/** A wrapper around the Kubernetes watch API. */
|
|
48
50
|
export declare class Watcher<T extends GenericClass> {
|
|
@@ -84,5 +86,6 @@ export declare class Watcher<T extends GenericClass> {
|
|
|
84
86
|
* @returns an EventEmitter
|
|
85
87
|
*/
|
|
86
88
|
get events(): EventEmitter;
|
|
89
|
+
static getHTTPSAgent: (opts: RequestInit) => Agent;
|
|
87
90
|
}
|
|
88
91
|
//# sourceMappingURL=watch.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"watch.d.ts","sourceRoot":"","sources":["../../src/fluent/watch.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"watch.d.ts","sourceRoot":"","sources":["../../src/fluent/watch.ts"],"names":[],"mappings":"AAIA,OAAO,EAAE,YAAY,EAAE,MAAM,QAAQ,CAAC;AAGtC,OAAoB,EAAE,WAAW,EAAE,MAAM,YAAY,CAAC;AACtD,OAAO,EAAE,KAAK,EAAS,MAAM,QAAQ,CAAC;AAEtC,OAAO,EAAE,YAAY,EAAwB,MAAM,UAAU,CAAC;AAC9D,OAAO,EAAE,OAAO,EAAE,WAAW,EAAc,MAAM,SAAS,CAAC;AAK3D,oBAAY,UAAU;IACpB,sCAAsC;IACtC,OAAO,YAAY;IACnB,2BAA2B;IAC3B,aAAa,kBAAkB;IAC/B,kDAAkD;IAClD,UAAU,eAAe;IACzB,0BAA0B;IAC1B,SAAS,cAAc;IACvB,8BAA8B;IAC9B,OAAO,YAAY;IACnB,sBAAsB;IACtB,KAAK,UAAU;IACf,mCAAmC;IACnC,IAAI,SAAS;IACb,wCAAwC;IACxC,oBAAoB,yBAAyB;IAC7C,qCAAqC;IACrC,iBAAiB,sBAAsB;IACvC,kCAAkC;IAClC,IAAI,SAAS;IACb,2BAA2B;IAC3B,UAAU,eAAe;IACzB,mBAAmB;IACnB,UAAU,eAAe;IACzB,qCAAqC;IACrC,wBAAwB,6BAA6B;IACrD,iCAAiC;IACjC,eAAe,oBAAoB;CACpC;AAED,4CAA4C;AAC5C,MAAM,MAAM,QAAQ,GAAG;IACrB,+HAA+H;IAC/H,gBAAgB,CAAC,EAAE,MAAM,CAAC;IAC1B,wDAAwD;IACxD,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB,+FAA+F;IAC/F,iBAAiB,CAAC,EAAE,MAAM,CAAC;IAC3B,wHAAwH;IACxH,oBAAoB,CAAC,EAAE,MAAM,CAAC;IAC9B,uBAAuB;IACvB,cAAc,CAAC,EAAE,OAAO,CAAC;CAC1B,CAAC;AAKF,iDAAiD;AACjD,qBAAa,OAAO,CAAC,CAAC,SAAS,YAAY;;IA0BzC,YAAY,CAAC,EAAE,MAAM,CAAC,OAAO,CAAC;IAgB9B;;;;;;;;;;;OAWG;gBACS,KAAK,EAAE,CAAC,EAAE,OAAO,EAAE,OAAO,EAAE,QAAQ,EAAE,WAAW,CAAC,CAAC,CAAC,EAAE,QAAQ,GAAE,QAAa;IA6CzF;;;;OAIG;IACU,KAAK,IAAI,OAAO,CAAC,eAAe,CAAC;IAU9C,gGAAgG;IACzF,KAAK;IAWZ;;;;;OAKG;IACI,UAAU;IAWjB;;;;;;OAMG;IACH,IAAW,MAAM,IAAI,YAAY,CAEhC;IA+PD,MAAM,CAAC,aAAa,SAAU,WAAW,WAqBvC;CA+KH"}
|
package/dist/fluent/watch.js
CHANGED
|
@@ -7,15 +7,16 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
|
7
7
|
var _a;
|
|
8
8
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
9
9
|
exports.Watcher = exports.WatchEvent = void 0;
|
|
10
|
-
const byline_1 = __importDefault(require("byline"));
|
|
11
10
|
const crypto_1 = require("crypto");
|
|
12
11
|
const events_1 = require("events");
|
|
12
|
+
const byline_1 = __importDefault(require("byline"));
|
|
13
13
|
const https_1 = __importDefault(require("https"));
|
|
14
|
-
const http2_1 = __importDefault(require("http2"));
|
|
15
14
|
const node_fetch_1 = __importDefault(require("node-fetch"));
|
|
15
|
+
const undici_1 = require("undici");
|
|
16
16
|
const fetch_1 = require("../fetch");
|
|
17
17
|
const types_1 = require("./types");
|
|
18
18
|
const utils_1 = require("./utils");
|
|
19
|
+
const stream_1 = require("stream");
|
|
19
20
|
const fs_1 = __importDefault(require("fs"));
|
|
20
21
|
var WatchEvent;
|
|
21
22
|
(function (WatchEvent) {
|
|
@@ -58,7 +59,7 @@ class Watcher {
|
|
|
58
59
|
#callback;
|
|
59
60
|
#watchCfg;
|
|
60
61
|
#latestRelistWindow = "";
|
|
61
|
-
#
|
|
62
|
+
#useLegacyWatch = false;
|
|
62
63
|
// Track the last time data was received
|
|
63
64
|
#lastSeenTime = NONE;
|
|
64
65
|
#lastSeenLimit;
|
|
@@ -68,6 +69,7 @@ class Watcher {
|
|
|
68
69
|
#resyncFailureCount = 0;
|
|
69
70
|
// Create a stream to read the response body
|
|
70
71
|
#stream;
|
|
72
|
+
#legacyStream;
|
|
71
73
|
// Create an EventEmitter to emit events
|
|
72
74
|
#events = new events_1.EventEmitter();
|
|
73
75
|
// Create a timer to relist the watch
|
|
@@ -101,12 +103,12 @@ class Watcher {
|
|
|
101
103
|
watchCfg.relistIntervalSec ??= 600;
|
|
102
104
|
// Set the resync interval to 10 minutes if not specified
|
|
103
105
|
watchCfg.lastSeenLimitSeconds ??= 600;
|
|
106
|
+
// Set the watch mechanism
|
|
107
|
+
this.#useLegacyWatch = watchCfg.useLegacyWatch || false;
|
|
104
108
|
// Set the last seen limit to the resync interval
|
|
105
109
|
this.#lastSeenLimit = watchCfg.lastSeenLimitSeconds * 1000;
|
|
106
110
|
// Set the latest relist interval to now
|
|
107
111
|
this.#latestRelistWindow = new Date().toISOString();
|
|
108
|
-
// Set the latest relist interval to now
|
|
109
|
-
this.#useHTTP2 = watchCfg.useHTTP2 ?? false;
|
|
110
112
|
// Add random jitter to the relist/resync intervals (up to 1 second)
|
|
111
113
|
const jitter = Math.floor(Math.random() * 1000);
|
|
112
114
|
// Check every relist interval for cache staleness
|
|
@@ -132,8 +134,8 @@ class Watcher {
|
|
|
132
134
|
*/
|
|
133
135
|
async start() {
|
|
134
136
|
this.#events.emit(WatchEvent.INIT_CACHE_MISS, this.#latestRelistWindow);
|
|
135
|
-
if (this.#
|
|
136
|
-
await this.#
|
|
137
|
+
if (this.#useLegacyWatch) {
|
|
138
|
+
await this.#legacyWatch();
|
|
137
139
|
}
|
|
138
140
|
else {
|
|
139
141
|
await this.#watch();
|
|
@@ -144,7 +146,12 @@ class Watcher {
|
|
|
144
146
|
close() {
|
|
145
147
|
clearInterval(this.$relistTimer);
|
|
146
148
|
clearInterval(this.#resyncTimer);
|
|
147
|
-
this.#
|
|
149
|
+
if (this.#useLegacyWatch) {
|
|
150
|
+
this.#legacyStreamCleanup();
|
|
151
|
+
}
|
|
152
|
+
else {
|
|
153
|
+
this.#streamCleanup();
|
|
154
|
+
}
|
|
148
155
|
this.#abortController.abort();
|
|
149
156
|
}
|
|
150
157
|
/**
|
|
@@ -269,7 +276,6 @@ class Watcher {
|
|
|
269
276
|
// If there is a continue token, call the list function again with the same removed items
|
|
270
277
|
if (continueToken) {
|
|
271
278
|
// If there is a continue token, call the list function again with the same removed items
|
|
272
|
-
// @todo: using all voids here is important for freshness, but is naive with regard to API load & pod resources
|
|
273
279
|
await this.#list(continueToken, removedItems);
|
|
274
280
|
}
|
|
275
281
|
else {
|
|
@@ -338,42 +344,40 @@ class Watcher {
|
|
|
338
344
|
this.#events.emit(WatchEvent.DATA_ERROR, err);
|
|
339
345
|
}
|
|
340
346
|
};
|
|
341
|
-
/**
|
|
342
|
-
|
|
343
|
-
*/
|
|
344
|
-
#watch = async () => {
|
|
347
|
+
/** node-fetch watch */
|
|
348
|
+
#legacyWatch = async () => {
|
|
345
349
|
try {
|
|
346
350
|
// Start with a list operation
|
|
347
351
|
await this.#list();
|
|
348
352
|
// Build the URL and request options
|
|
349
353
|
const { opts, url } = await this.#buildURL(true, this.#resourceVersion);
|
|
350
354
|
// Create a stream to read the response body
|
|
351
|
-
this.#
|
|
355
|
+
this.#legacyStream = byline_1.default.createStream();
|
|
352
356
|
// Bind the stream events
|
|
353
|
-
this.#
|
|
354
|
-
this.#
|
|
355
|
-
this.#
|
|
357
|
+
this.#legacyStream.on("error", this.#errHandler);
|
|
358
|
+
this.#legacyStream.on("close", this.#legacyStreamCleanup);
|
|
359
|
+
this.#legacyStream.on("finish", this.#legacyStreamCleanup);
|
|
356
360
|
// Make the actual request
|
|
357
361
|
const response = await (0, node_fetch_1.default)(url, { ...opts });
|
|
358
|
-
// Reset the pending reconnect flag
|
|
359
|
-
this.#pendingReconnect = false;
|
|
360
362
|
// If the request is successful, start listening for events
|
|
361
363
|
if (response.ok) {
|
|
364
|
+
// Reset the pending reconnect flag
|
|
365
|
+
this.#pendingReconnect = false;
|
|
362
366
|
this.#events.emit(WatchEvent.CONNECT, url.pathname);
|
|
363
367
|
const { body } = response;
|
|
364
368
|
// Reset the retry count
|
|
365
369
|
this.#resyncFailureCount = 0;
|
|
366
370
|
this.#events.emit(WatchEvent.INC_RESYNC_FAILURE_COUNT, this.#resyncFailureCount);
|
|
367
371
|
// Listen for events and call the callback function
|
|
368
|
-
this.#
|
|
372
|
+
this.#legacyStream.on("data", async (line) => {
|
|
369
373
|
await this.#processLine(line, this.#process);
|
|
370
374
|
});
|
|
371
375
|
// Bind the body events
|
|
372
376
|
body.on("error", this.#errHandler);
|
|
373
|
-
body.on("close", this.#
|
|
374
|
-
body.on("finish", this.#
|
|
377
|
+
body.on("close", this.#legacyStreamCleanup);
|
|
378
|
+
body.on("finish", this.#legacyStreamCleanup);
|
|
375
379
|
// Pipe the response body to the stream
|
|
376
|
-
body.pipe(this.#
|
|
380
|
+
body.pipe(this.#legacyStream);
|
|
377
381
|
}
|
|
378
382
|
else {
|
|
379
383
|
throw new Error(`watch connect failed: ${response.status} ${response.statusText}`);
|
|
@@ -383,69 +387,84 @@ class Watcher {
|
|
|
383
387
|
void this.#errHandler(e);
|
|
384
388
|
}
|
|
385
389
|
};
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
key: opts.agent.options.key,
|
|
391
|
-
cert: opts.agent.options.cert,
|
|
390
|
+
static getHTTPSAgent = (opts) => {
|
|
391
|
+
// In cluster there will be agent - testing or dev no
|
|
392
|
+
const agentOptions = opts.agent instanceof https_1.default.Agent
|
|
393
|
+
? {
|
|
392
394
|
ca: opts.agent.options.ca,
|
|
393
|
-
|
|
395
|
+
cert: opts.agent.options.cert,
|
|
396
|
+
key: opts.agent.options.key,
|
|
397
|
+
}
|
|
398
|
+
: {
|
|
399
|
+
ca: undefined,
|
|
400
|
+
cert: undefined,
|
|
401
|
+
key: undefined,
|
|
394
402
|
};
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
return http2_1.default.connect(origin, {
|
|
401
|
-
ca: agentOptions?.ca,
|
|
402
|
-
cert: agentOptions?.cert,
|
|
403
|
-
key: agentOptions?.key,
|
|
404
|
-
rejectUnauthorized: agentOptions?.rejectUnauthorized,
|
|
403
|
+
return new undici_1.Agent({
|
|
404
|
+
keepAliveMaxTimeout: 600000,
|
|
405
|
+
keepAliveTimeout: 600000,
|
|
406
|
+
bodyTimeout: 0,
|
|
407
|
+
connect: agentOptions,
|
|
405
408
|
});
|
|
406
|
-
}
|
|
407
|
-
// Generate the request headers for the HTTP/2 request
|
|
408
|
-
#generateRequestHeaders = async (url) => {
|
|
409
|
-
const token = await this.#getToken();
|
|
410
|
-
const headers = {
|
|
411
|
-
":method": "GET",
|
|
412
|
-
":path": url.pathname + url.search,
|
|
413
|
-
"content-type": "application/json",
|
|
414
|
-
"user-agent": "kubernetes-fluent-client",
|
|
415
|
-
};
|
|
416
|
-
if (token) {
|
|
417
|
-
headers["Authorization"] = `Bearer ${token}`;
|
|
418
|
-
}
|
|
419
|
-
return headers;
|
|
420
409
|
};
|
|
421
410
|
/**
|
|
422
411
|
* Watch for changes to the resource.
|
|
423
412
|
*/
|
|
424
|
-
#
|
|
413
|
+
#watch = async () => {
|
|
425
414
|
try {
|
|
426
415
|
// Start with a list operation
|
|
427
416
|
await this.#list();
|
|
428
417
|
// Build the URL and request options
|
|
429
418
|
const { opts, url } = await this.#buildURL(true, this.#resourceVersion);
|
|
430
|
-
const
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
}
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
const req = client.request(headers);
|
|
442
|
-
req.setEncoding("utf8");
|
|
443
|
-
// Handler events for the HTTP/2 request
|
|
444
|
-
this.#handleHttp2Request(req, client);
|
|
445
|
-
// Handle abort signal
|
|
446
|
-
this.#abortController.signal.addEventListener("abort", () => {
|
|
447
|
-
this.#streamCleanup(client);
|
|
419
|
+
const token = await this.#getToken();
|
|
420
|
+
const headers = {
|
|
421
|
+
"Content-Type": "application/json",
|
|
422
|
+
"User-Agent": "kubernetes-fluent-client",
|
|
423
|
+
};
|
|
424
|
+
if (token) {
|
|
425
|
+
headers["Authorization"] = `Bearer ${token}`;
|
|
426
|
+
}
|
|
427
|
+
const response = await (0, undici_1.fetch)(url, {
|
|
428
|
+
headers,
|
|
429
|
+
dispatcher: _a.getHTTPSAgent(opts),
|
|
448
430
|
});
|
|
431
|
+
// If the request is successful, start listening for events
|
|
432
|
+
if (response.ok) {
|
|
433
|
+
// Reset the pending reconnect flag
|
|
434
|
+
this.#pendingReconnect = false;
|
|
435
|
+
this.#events.emit(WatchEvent.CONNECT, url.pathname);
|
|
436
|
+
const { body } = response;
|
|
437
|
+
if (!body) {
|
|
438
|
+
throw new Error("No response body found");
|
|
439
|
+
}
|
|
440
|
+
// Reset the retry count
|
|
441
|
+
this.#resyncFailureCount = 0;
|
|
442
|
+
this.#events.emit(WatchEvent.INC_RESYNC_FAILURE_COUNT, this.#resyncFailureCount);
|
|
443
|
+
this.#stream = stream_1.Readable.from(body);
|
|
444
|
+
const decoder = new TextDecoder();
|
|
445
|
+
let buffer = "";
|
|
446
|
+
// Listen for events and call the callback function
|
|
447
|
+
this.#stream.on("data", async (chunk) => {
|
|
448
|
+
try {
|
|
449
|
+
buffer += decoder.decode(chunk, { stream: true });
|
|
450
|
+
const lines = buffer.split("\n");
|
|
451
|
+
buffer = lines.pop();
|
|
452
|
+
for (const line of lines) {
|
|
453
|
+
await this.#processLine(line, this.#process);
|
|
454
|
+
}
|
|
455
|
+
}
|
|
456
|
+
catch (err) {
|
|
457
|
+
void this.#errHandler(err);
|
|
458
|
+
}
|
|
459
|
+
});
|
|
460
|
+
this.#stream.on("close", this.#cleanupAndReconnect);
|
|
461
|
+
this.#stream.on("end", this.#cleanupAndReconnect);
|
|
462
|
+
this.#stream.on("error", this.#errHandler);
|
|
463
|
+
this.#stream.on("finish", this.#cleanupAndReconnect);
|
|
464
|
+
}
|
|
465
|
+
else {
|
|
466
|
+
throw new Error(`watch connect failed: ${response.status} ${response.statusText}`);
|
|
467
|
+
}
|
|
449
468
|
}
|
|
450
469
|
catch (e) {
|
|
451
470
|
void this.#errHandler(e);
|
|
@@ -475,12 +494,12 @@ class Watcher {
|
|
|
475
494
|
else {
|
|
476
495
|
this.#pendingReconnect = true;
|
|
477
496
|
this.#events.emit(WatchEvent.RECONNECT, this.#resyncFailureCount);
|
|
478
|
-
this.#
|
|
479
|
-
|
|
480
|
-
this.#
|
|
497
|
+
if (this.#useLegacyWatch) {
|
|
498
|
+
this.#legacyStreamCleanup();
|
|
499
|
+
void this.#legacyWatch();
|
|
481
500
|
}
|
|
482
|
-
|
|
483
|
-
|
|
501
|
+
else {
|
|
502
|
+
this.#cleanupAndReconnect();
|
|
484
503
|
}
|
|
485
504
|
}
|
|
486
505
|
}
|
|
@@ -501,9 +520,11 @@ class Watcher {
|
|
|
501
520
|
case "AbortError":
|
|
502
521
|
clearInterval(this.$relistTimer);
|
|
503
522
|
clearInterval(this.#resyncTimer);
|
|
504
|
-
this.#
|
|
505
|
-
|
|
506
|
-
|
|
523
|
+
if (this.#useLegacyWatch) {
|
|
524
|
+
this.#legacyStreamCleanup();
|
|
525
|
+
}
|
|
526
|
+
else {
|
|
527
|
+
this.#streamCleanup();
|
|
507
528
|
}
|
|
508
529
|
this.#events.emit(WatchEvent.ABORT, err);
|
|
509
530
|
return;
|
|
@@ -519,80 +540,24 @@ class Watcher {
|
|
|
519
540
|
// Force a resync
|
|
520
541
|
this.#lastSeenTime = OVERRIDE;
|
|
521
542
|
};
|
|
522
|
-
/**
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
req.on("response", headers => {
|
|
530
|
-
const statusCode = headers[":status"];
|
|
531
|
-
if (statusCode && statusCode >= 200 && statusCode < 300) {
|
|
532
|
-
this.#onWatchConnected();
|
|
533
|
-
}
|
|
534
|
-
else {
|
|
535
|
-
this.#cleanupAndReconnect(client, new Error(`watch connect failed: ${statusCode}`));
|
|
536
|
-
}
|
|
537
|
-
});
|
|
538
|
-
req.on("data", chunk => {
|
|
539
|
-
buffer += chunk;
|
|
540
|
-
const lines = buffer.split("\n");
|
|
541
|
-
buffer = lines.pop() || ""; // Keep any incomplete line for the next chunk
|
|
542
|
-
lines.forEach(line => {
|
|
543
|
-
void this.#processLine(line, this.#process);
|
|
544
|
-
});
|
|
545
|
-
});
|
|
546
|
-
req.on("end", () => this.#cleanupAndReconnect(client));
|
|
547
|
-
req.on("close", () => this.#cleanupAndReconnect(client));
|
|
548
|
-
req.on("error", error => this.#errHandler(error));
|
|
549
|
-
}
|
|
550
|
-
/** Schedules a reconnect with a delay to prevent rapid reconnections. */
|
|
551
|
-
#scheduleReconnect() {
|
|
552
|
-
const jitter = Math.floor(Math.random() * 1000);
|
|
553
|
-
const delay = (this.#watchCfg.resyncDelaySec ?? 5) * 1000 + jitter;
|
|
554
|
-
setTimeout(() => {
|
|
555
|
-
this.#events.emit(WatchEvent.RECONNECT, this.#resyncFailureCount);
|
|
556
|
-
void this.#http2Watch();
|
|
557
|
-
}, delay);
|
|
558
|
-
}
|
|
559
|
-
/**
|
|
560
|
-
* Handle a successful connection to the watch.
|
|
561
|
-
*/
|
|
562
|
-
#onWatchConnected() {
|
|
563
|
-
this.#pendingReconnect = false;
|
|
564
|
-
this.#events.emit(WatchEvent.CONNECT);
|
|
565
|
-
// Reset the retry count
|
|
566
|
-
this.#resyncFailureCount = 0;
|
|
567
|
-
this.#events.emit(WatchEvent.INC_RESYNC_FAILURE_COUNT, this.#resyncFailureCount);
|
|
568
|
-
}
|
|
569
|
-
/**
|
|
570
|
-
* Cleanup the stream and listeners.
|
|
571
|
-
*
|
|
572
|
-
* @param client - the client session
|
|
573
|
-
*/
|
|
574
|
-
#streamCleanup = (client) => {
|
|
543
|
+
/** Cleanup the stream and connect */
|
|
544
|
+
#cleanupAndReconnect = () => {
|
|
545
|
+
this.#streamCleanup();
|
|
546
|
+
void this.#watch();
|
|
547
|
+
};
|
|
548
|
+
/** Cleanup the stream and listeners. */
|
|
549
|
+
#streamCleanup = () => {
|
|
575
550
|
if (this.#stream) {
|
|
576
551
|
this.#stream.removeAllListeners();
|
|
577
552
|
this.#stream.destroy();
|
|
578
553
|
}
|
|
579
|
-
if (client) {
|
|
580
|
-
client.close();
|
|
581
|
-
}
|
|
582
554
|
};
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
* @param error - the error that occurred
|
|
588
|
-
*/
|
|
589
|
-
#cleanupAndReconnect(client, error) {
|
|
590
|
-
this.#streamCleanup(client);
|
|
591
|
-
if (error) {
|
|
592
|
-
void this.#errHandler(error);
|
|
555
|
+
#legacyStreamCleanup = () => {
|
|
556
|
+
if (this.#legacyStream) {
|
|
557
|
+
this.#legacyStream.removeAllListeners();
|
|
558
|
+
this.#legacyStream.destroy();
|
|
593
559
|
}
|
|
594
|
-
|
|
595
|
-
}
|
|
560
|
+
};
|
|
596
561
|
}
|
|
597
562
|
exports.Watcher = Watcher;
|
|
598
563
|
_a = Watcher;
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import typescriptEslint from "@typescript-eslint/eslint-plugin";
|
|
2
|
+
import globals from "globals";
|
|
3
|
+
import tsParser from "@typescript-eslint/parser";
|
|
4
|
+
import path from "node:path";
|
|
5
|
+
import { fileURLToPath } from "node:url";
|
|
6
|
+
import js from "@eslint/js";
|
|
7
|
+
import { FlatCompat } from "@eslint/eslintrc";
|
|
8
|
+
|
|
9
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
10
|
+
const __dirname = path.dirname(__filename);
|
|
11
|
+
const compat = new FlatCompat({
|
|
12
|
+
baseDirectory: __dirname,
|
|
13
|
+
recommendedConfig: js.configs.recommended,
|
|
14
|
+
allConfig: js.configs.all,
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
export default [
|
|
18
|
+
{
|
|
19
|
+
ignores: ["**/node_modules", "**/dist", "**/__mocks__", "e2e/crds", "e2e/crds"],
|
|
20
|
+
},
|
|
21
|
+
...compat.extends(
|
|
22
|
+
"eslint:recommended",
|
|
23
|
+
"plugin:@typescript-eslint/recommended",
|
|
24
|
+
"plugin:jsdoc/recommended-typescript-error",
|
|
25
|
+
),
|
|
26
|
+
{
|
|
27
|
+
plugins: {
|
|
28
|
+
"@typescript-eslint": typescriptEslint,
|
|
29
|
+
},
|
|
30
|
+
|
|
31
|
+
languageOptions: {
|
|
32
|
+
globals: {
|
|
33
|
+
...Object.fromEntries(Object.entries(globals.browser).map(([key]) => [key, "off"])),
|
|
34
|
+
},
|
|
35
|
+
|
|
36
|
+
parser: tsParser,
|
|
37
|
+
ecmaVersion: 2022,
|
|
38
|
+
sourceType: "script",
|
|
39
|
+
|
|
40
|
+
parserOptions: {
|
|
41
|
+
project: ["tsconfig.json"],
|
|
42
|
+
},
|
|
43
|
+
},
|
|
44
|
+
|
|
45
|
+
rules: {
|
|
46
|
+
"class-methods-use-this": "warn",
|
|
47
|
+
"consistent-this": "warn",
|
|
48
|
+
"no-invalid-this": "warn",
|
|
49
|
+
|
|
50
|
+
"@typescript-eslint/no-floating-promises": [
|
|
51
|
+
"warn",
|
|
52
|
+
{
|
|
53
|
+
ignoreVoid: true,
|
|
54
|
+
},
|
|
55
|
+
],
|
|
56
|
+
|
|
57
|
+
"jsdoc/tag-lines": [
|
|
58
|
+
"error",
|
|
59
|
+
"any",
|
|
60
|
+
{
|
|
61
|
+
startLines: 1,
|
|
62
|
+
},
|
|
63
|
+
],
|
|
64
|
+
},
|
|
65
|
+
},
|
|
66
|
+
];
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "kubernetes-fluent-client",
|
|
3
|
-
"version": "3.
|
|
3
|
+
"version": "3.3.1",
|
|
4
4
|
"description": "A @kubernetes/client-node fluent API wrapper that leverages K8s Server Side Apply.",
|
|
5
5
|
"bin": "./dist/cli.js",
|
|
6
6
|
"main": "dist/index.js",
|
|
@@ -44,23 +44,27 @@
|
|
|
44
44
|
"node-fetch": "2.7.0",
|
|
45
45
|
"quicktype-core": "23.0.170",
|
|
46
46
|
"type-fest": "4.26.1",
|
|
47
|
+
"undici": "6.20.1",
|
|
47
48
|
"yargs": "17.7.2"
|
|
48
49
|
},
|
|
49
50
|
"devDependencies": {
|
|
50
51
|
"@commitlint/cli": "19.5.0",
|
|
51
52
|
"@commitlint/config-conventional": "19.5.0",
|
|
53
|
+
"@eslint/eslintrc": "^3.1.0",
|
|
54
|
+
"@eslint/js": "^9.14.0",
|
|
52
55
|
"@jest/globals": "29.7.0",
|
|
53
56
|
"@types/byline": "4.2.36",
|
|
54
|
-
"@types/readable-stream": "4.0.
|
|
57
|
+
"@types/readable-stream": "4.0.18",
|
|
55
58
|
"@types/urijs": "^1.19.25",
|
|
56
59
|
"@types/yargs": "17.0.33",
|
|
57
|
-
"@typescript-eslint/eslint-plugin": "8.
|
|
58
|
-
"@typescript-eslint/parser": "8.
|
|
60
|
+
"@typescript-eslint/eslint-plugin": "8.14.0",
|
|
61
|
+
"@typescript-eslint/parser": "8.14.0",
|
|
59
62
|
"eslint-plugin-jsdoc": "50.4.3",
|
|
63
|
+
"globals": "^15.12.0",
|
|
60
64
|
"husky": "^9.1.6",
|
|
61
65
|
"jest": "29.7.0",
|
|
62
66
|
"lint-staged": "^15.2.10",
|
|
63
|
-
"nock": "13.5.
|
|
67
|
+
"nock": "13.5.6",
|
|
64
68
|
"prettier": "3.3.3",
|
|
65
69
|
"semantic-release": "24.2.0",
|
|
66
70
|
"ts-jest": "29.2.5",
|
package/src/fluent/watch.ts
CHANGED
|
@@ -1,16 +1,17 @@
|
|
|
1
1
|
// SPDX-License-Identifier: Apache-2.0
|
|
2
2
|
// SPDX-FileCopyrightText: 2023-Present The Kubernetes Fluent Client Authors
|
|
3
3
|
|
|
4
|
-
import byline from "byline";
|
|
5
4
|
import { createHash } from "crypto";
|
|
6
5
|
import { EventEmitter } from "events";
|
|
6
|
+
import byline from "byline";
|
|
7
7
|
import https from "https";
|
|
8
|
-
import
|
|
9
|
-
import fetch from "
|
|
8
|
+
import legacyFetch, { RequestInit } from "node-fetch";
|
|
9
|
+
import { Agent, fetch } from "undici";
|
|
10
10
|
import { fetch as wrappedFetch } from "../fetch";
|
|
11
11
|
import { GenericClass, KubernetesListObject } from "../types";
|
|
12
|
-
import { Filters, WatchAction, WatchPhase
|
|
12
|
+
import { Filters, WatchAction, WatchPhase } from "./types";
|
|
13
13
|
import { k8sCfg, pathBuilder } from "./utils";
|
|
14
|
+
import { Readable } from "stream";
|
|
14
15
|
import fs from "fs";
|
|
15
16
|
|
|
16
17
|
export enum WatchEvent {
|
|
@@ -54,8 +55,8 @@ export type WatchCfg = {
|
|
|
54
55
|
relistIntervalSec?: number;
|
|
55
56
|
/** Max amount of seconds to go without receiving an event before reconciliation starts. Defaults to 300 (5 minutes). */
|
|
56
57
|
lastSeenLimitSeconds?: number;
|
|
57
|
-
/**
|
|
58
|
-
|
|
58
|
+
/** Watch Mechansism */
|
|
59
|
+
useLegacyWatch?: boolean;
|
|
59
60
|
};
|
|
60
61
|
|
|
61
62
|
const NONE = 50;
|
|
@@ -69,8 +70,7 @@ export class Watcher<T extends GenericClass> {
|
|
|
69
70
|
#callback: WatchAction<T>;
|
|
70
71
|
#watchCfg: WatchCfg;
|
|
71
72
|
#latestRelistWindow: string = "";
|
|
72
|
-
#
|
|
73
|
-
|
|
73
|
+
#useLegacyWatch = false;
|
|
74
74
|
// Track the last time data was received
|
|
75
75
|
#lastSeenTime = NONE;
|
|
76
76
|
#lastSeenLimit: number;
|
|
@@ -82,7 +82,8 @@ export class Watcher<T extends GenericClass> {
|
|
|
82
82
|
#resyncFailureCount = 0;
|
|
83
83
|
|
|
84
84
|
// Create a stream to read the response body
|
|
85
|
-
#stream?:
|
|
85
|
+
#stream?: Readable;
|
|
86
|
+
#legacyStream?: byline.LineStream;
|
|
86
87
|
|
|
87
88
|
// Create an EventEmitter to emit events
|
|
88
89
|
#events = new EventEmitter();
|
|
@@ -126,15 +127,15 @@ export class Watcher<T extends GenericClass> {
|
|
|
126
127
|
// Set the resync interval to 10 minutes if not specified
|
|
127
128
|
watchCfg.lastSeenLimitSeconds ??= 600;
|
|
128
129
|
|
|
130
|
+
// Set the watch mechanism
|
|
131
|
+
this.#useLegacyWatch = watchCfg.useLegacyWatch || false;
|
|
132
|
+
|
|
129
133
|
// Set the last seen limit to the resync interval
|
|
130
134
|
this.#lastSeenLimit = watchCfg.lastSeenLimitSeconds * 1000;
|
|
131
135
|
|
|
132
136
|
// Set the latest relist interval to now
|
|
133
137
|
this.#latestRelistWindow = new Date().toISOString();
|
|
134
138
|
|
|
135
|
-
// Set the latest relist interval to now
|
|
136
|
-
this.#useHTTP2 = watchCfg.useHTTP2 ?? false;
|
|
137
|
-
|
|
138
139
|
// Add random jitter to the relist/resync intervals (up to 1 second)
|
|
139
140
|
const jitter = Math.floor(Math.random() * 1000);
|
|
140
141
|
|
|
@@ -168,8 +169,8 @@ export class Watcher<T extends GenericClass> {
|
|
|
168
169
|
*/
|
|
169
170
|
public async start(): Promise<AbortController> {
|
|
170
171
|
this.#events.emit(WatchEvent.INIT_CACHE_MISS, this.#latestRelistWindow);
|
|
171
|
-
if (this.#
|
|
172
|
-
await this.#
|
|
172
|
+
if (this.#useLegacyWatch) {
|
|
173
|
+
await this.#legacyWatch();
|
|
173
174
|
} else {
|
|
174
175
|
await this.#watch();
|
|
175
176
|
}
|
|
@@ -180,7 +181,11 @@ export class Watcher<T extends GenericClass> {
|
|
|
180
181
|
public close() {
|
|
181
182
|
clearInterval(this.$relistTimer);
|
|
182
183
|
clearInterval(this.#resyncTimer);
|
|
183
|
-
this.#
|
|
184
|
+
if (this.#useLegacyWatch) {
|
|
185
|
+
this.#legacyStreamCleanup();
|
|
186
|
+
} else {
|
|
187
|
+
this.#streamCleanup();
|
|
188
|
+
}
|
|
184
189
|
this.#abortController.abort();
|
|
185
190
|
}
|
|
186
191
|
|
|
@@ -333,7 +338,6 @@ export class Watcher<T extends GenericClass> {
|
|
|
333
338
|
// If there is a continue token, call the list function again with the same removed items
|
|
334
339
|
if (continueToken) {
|
|
335
340
|
// If there is a continue token, call the list function again with the same removed items
|
|
336
|
-
// @todo: using all voids here is important for freshness, but is naive with regard to API load & pod resources
|
|
337
341
|
await this.#list(continueToken, removedItems);
|
|
338
342
|
} else {
|
|
339
343
|
// Otherwise, process the removed items
|
|
@@ -413,11 +417,8 @@ export class Watcher<T extends GenericClass> {
|
|
|
413
417
|
this.#events.emit(WatchEvent.DATA_ERROR, err);
|
|
414
418
|
}
|
|
415
419
|
};
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
* Watch for changes to the resource.
|
|
419
|
-
*/
|
|
420
|
-
#watch = async () => {
|
|
420
|
+
/** node-fetch watch */
|
|
421
|
+
#legacyWatch = async () => {
|
|
421
422
|
try {
|
|
422
423
|
// Start with a list operation
|
|
423
424
|
await this.#list();
|
|
@@ -426,21 +427,21 @@ export class Watcher<T extends GenericClass> {
|
|
|
426
427
|
const { opts, url } = await this.#buildURL(true, this.#resourceVersion);
|
|
427
428
|
|
|
428
429
|
// Create a stream to read the response body
|
|
429
|
-
this.#
|
|
430
|
+
this.#legacyStream = byline.createStream();
|
|
430
431
|
|
|
431
432
|
// Bind the stream events
|
|
432
|
-
this.#
|
|
433
|
-
this.#
|
|
434
|
-
this.#
|
|
433
|
+
this.#legacyStream.on("error", this.#errHandler);
|
|
434
|
+
this.#legacyStream.on("close", this.#legacyStreamCleanup);
|
|
435
|
+
this.#legacyStream.on("finish", this.#legacyStreamCleanup);
|
|
435
436
|
|
|
436
437
|
// Make the actual request
|
|
437
|
-
const response = await
|
|
438
|
-
|
|
439
|
-
// Reset the pending reconnect flag
|
|
440
|
-
this.#pendingReconnect = false;
|
|
438
|
+
const response = await legacyFetch(url, { ...opts });
|
|
441
439
|
|
|
442
440
|
// If the request is successful, start listening for events
|
|
443
441
|
if (response.ok) {
|
|
442
|
+
// Reset the pending reconnect flag
|
|
443
|
+
this.#pendingReconnect = false;
|
|
444
|
+
|
|
444
445
|
this.#events.emit(WatchEvent.CONNECT, url.pathname);
|
|
445
446
|
|
|
446
447
|
const { body } = response;
|
|
@@ -450,17 +451,17 @@ export class Watcher<T extends GenericClass> {
|
|
|
450
451
|
this.#events.emit(WatchEvent.INC_RESYNC_FAILURE_COUNT, this.#resyncFailureCount);
|
|
451
452
|
|
|
452
453
|
// Listen for events and call the callback function
|
|
453
|
-
this.#
|
|
454
|
+
this.#legacyStream.on("data", async line => {
|
|
454
455
|
await this.#processLine(line, this.#process);
|
|
455
456
|
});
|
|
456
457
|
|
|
457
458
|
// Bind the body events
|
|
458
459
|
body.on("error", this.#errHandler);
|
|
459
|
-
body.on("close", this.#
|
|
460
|
-
body.on("finish", this.#
|
|
460
|
+
body.on("close", this.#legacyStreamCleanup);
|
|
461
|
+
body.on("finish", this.#legacyStreamCleanup);
|
|
461
462
|
|
|
462
463
|
// Pipe the response body to the stream
|
|
463
|
-
body.pipe(this.#
|
|
464
|
+
body.pipe(this.#legacyStream);
|
|
464
465
|
} else {
|
|
465
466
|
throw new Error(`watch connect failed: ${response.status} ${response.statusText}`);
|
|
466
467
|
}
|
|
@@ -469,79 +470,97 @@ export class Watcher<T extends GenericClass> {
|
|
|
469
470
|
}
|
|
470
471
|
};
|
|
471
472
|
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
return
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
473
|
+
static getHTTPSAgent = (opts: RequestInit) => {
|
|
474
|
+
// In cluster there will be agent - testing or dev no
|
|
475
|
+
const agentOptions =
|
|
476
|
+
opts.agent instanceof https.Agent
|
|
477
|
+
? {
|
|
478
|
+
ca: opts.agent.options.ca,
|
|
479
|
+
cert: opts.agent.options.cert,
|
|
480
|
+
key: opts.agent.options.key,
|
|
481
|
+
}
|
|
482
|
+
: {
|
|
483
|
+
ca: undefined,
|
|
484
|
+
cert: undefined,
|
|
485
|
+
key: undefined,
|
|
486
|
+
};
|
|
487
|
+
|
|
488
|
+
return new Agent({
|
|
489
|
+
keepAliveMaxTimeout: 600000,
|
|
490
|
+
keepAliveTimeout: 600000,
|
|
491
|
+
bodyTimeout: 0,
|
|
492
|
+
connect: agentOptions,
|
|
492
493
|
});
|
|
493
|
-
}
|
|
494
|
-
|
|
495
|
-
// Generate the request headers for the HTTP/2 request
|
|
496
|
-
#generateRequestHeaders = async (url: URL) => {
|
|
497
|
-
const token = await this.#getToken();
|
|
498
|
-
const headers: Record<string, string> = {
|
|
499
|
-
":method": "GET",
|
|
500
|
-
":path": url.pathname + url.search,
|
|
501
|
-
"content-type": "application/json",
|
|
502
|
-
"user-agent": "kubernetes-fluent-client",
|
|
503
|
-
};
|
|
504
|
-
if (token) {
|
|
505
|
-
headers["Authorization"] = `Bearer ${token}`;
|
|
506
|
-
}
|
|
507
|
-
return headers;
|
|
508
494
|
};
|
|
509
|
-
|
|
510
495
|
/**
|
|
511
496
|
* Watch for changes to the resource.
|
|
512
497
|
*/
|
|
513
|
-
#
|
|
498
|
+
#watch = async () => {
|
|
514
499
|
try {
|
|
515
500
|
// Start with a list operation
|
|
516
501
|
await this.#list();
|
|
517
502
|
|
|
518
503
|
// Build the URL and request options
|
|
519
504
|
const { opts, url } = await this.#buildURL(true, this.#resourceVersion);
|
|
520
|
-
const agentOptions = Watcher.#getAgentOptions(opts as Options);
|
|
521
505
|
|
|
522
|
-
|
|
523
|
-
const
|
|
506
|
+
const token = await this.#getToken();
|
|
507
|
+
const headers: Record<string, string> = {
|
|
508
|
+
"Content-Type": "application/json",
|
|
509
|
+
"User-Agent": "kubernetes-fluent-client",
|
|
510
|
+
};
|
|
524
511
|
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
512
|
+
if (token) {
|
|
513
|
+
headers["Authorization"] = `Bearer ${token}`;
|
|
514
|
+
}
|
|
515
|
+
|
|
516
|
+
const response = await fetch(url, {
|
|
517
|
+
headers,
|
|
518
|
+
dispatcher: Watcher.getHTTPSAgent(opts),
|
|
529
519
|
});
|
|
530
520
|
|
|
531
|
-
//
|
|
532
|
-
|
|
521
|
+
// If the request is successful, start listening for events
|
|
522
|
+
if (response.ok) {
|
|
523
|
+
// Reset the pending reconnect flag
|
|
524
|
+
this.#pendingReconnect = false;
|
|
525
|
+
|
|
526
|
+
this.#events.emit(WatchEvent.CONNECT, url.pathname);
|
|
527
|
+
|
|
528
|
+
const { body } = response;
|
|
533
529
|
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
530
|
+
if (!body) {
|
|
531
|
+
throw new Error("No response body found");
|
|
532
|
+
}
|
|
537
533
|
|
|
538
|
-
|
|
539
|
-
|
|
534
|
+
// Reset the retry count
|
|
535
|
+
this.#resyncFailureCount = 0;
|
|
536
|
+
this.#events.emit(WatchEvent.INC_RESYNC_FAILURE_COUNT, this.#resyncFailureCount);
|
|
540
537
|
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
538
|
+
this.#stream = Readable.from(body);
|
|
539
|
+
const decoder = new TextDecoder();
|
|
540
|
+
let buffer = "";
|
|
541
|
+
|
|
542
|
+
// Listen for events and call the callback function
|
|
543
|
+
this.#stream.on("data", async chunk => {
|
|
544
|
+
try {
|
|
545
|
+
buffer += decoder.decode(chunk, { stream: true });
|
|
546
|
+
const lines = buffer.split("\n");
|
|
547
|
+
buffer = lines.pop()!;
|
|
548
|
+
|
|
549
|
+
for (const line of lines) {
|
|
550
|
+
await this.#processLine(line, this.#process);
|
|
551
|
+
}
|
|
552
|
+
} catch (err) {
|
|
553
|
+
void this.#errHandler(err);
|
|
554
|
+
}
|
|
555
|
+
});
|
|
556
|
+
|
|
557
|
+
this.#stream.on("close", this.#cleanupAndReconnect);
|
|
558
|
+
this.#stream.on("end", this.#cleanupAndReconnect);
|
|
559
|
+
this.#stream.on("error", this.#errHandler);
|
|
560
|
+
this.#stream.on("finish", this.#cleanupAndReconnect);
|
|
561
|
+
} else {
|
|
562
|
+
throw new Error(`watch connect failed: ${response.status} ${response.statusText}`);
|
|
563
|
+
}
|
|
545
564
|
} catch (e) {
|
|
546
565
|
void this.#errHandler(e);
|
|
547
566
|
}
|
|
@@ -576,15 +595,12 @@ export class Watcher<T extends GenericClass> {
|
|
|
576
595
|
} else {
|
|
577
596
|
this.#pendingReconnect = true;
|
|
578
597
|
this.#events.emit(WatchEvent.RECONNECT, this.#resyncFailureCount);
|
|
579
|
-
this.#
|
|
580
|
-
|
|
581
|
-
|
|
598
|
+
if (this.#useLegacyWatch) {
|
|
599
|
+
this.#legacyStreamCleanup();
|
|
600
|
+
void this.#legacyWatch();
|
|
601
|
+
} else {
|
|
582
602
|
this.#cleanupAndReconnect();
|
|
583
603
|
}
|
|
584
|
-
|
|
585
|
-
if (!this.#useHTTP2) {
|
|
586
|
-
void this.#watch();
|
|
587
|
-
}
|
|
588
604
|
}
|
|
589
605
|
} else {
|
|
590
606
|
// Otherwise, call the finally function if it exists
|
|
@@ -607,9 +623,10 @@ export class Watcher<T extends GenericClass> {
|
|
|
607
623
|
case "AbortError":
|
|
608
624
|
clearInterval(this.$relistTimer);
|
|
609
625
|
clearInterval(this.#resyncTimer);
|
|
610
|
-
this.#
|
|
611
|
-
|
|
612
|
-
|
|
626
|
+
if (this.#useLegacyWatch) {
|
|
627
|
+
this.#legacyStreamCleanup();
|
|
628
|
+
} else {
|
|
629
|
+
this.#streamCleanup();
|
|
613
630
|
}
|
|
614
631
|
this.#events.emit(WatchEvent.ABORT, err);
|
|
615
632
|
return;
|
|
@@ -628,89 +645,25 @@ export class Watcher<T extends GenericClass> {
|
|
|
628
645
|
// Force a resync
|
|
629
646
|
this.#lastSeenTime = OVERRIDE;
|
|
630
647
|
};
|
|
631
|
-
/**
|
|
632
|
-
*
|
|
633
|
-
* @param req - the request stream
|
|
634
|
-
* @param client - the client session
|
|
635
|
-
*/
|
|
636
|
-
#handleHttp2Request(req: http2.ClientHttp2Stream, client: http2.ClientHttp2Session) {
|
|
637
|
-
let buffer = "";
|
|
638
|
-
|
|
639
|
-
req.on("response", headers => {
|
|
640
|
-
const statusCode = headers[":status"];
|
|
641
|
-
if (statusCode && statusCode >= 200 && statusCode < 300) {
|
|
642
|
-
this.#onWatchConnected();
|
|
643
|
-
} else {
|
|
644
|
-
this.#cleanupAndReconnect(client, new Error(`watch connect failed: ${statusCode}`));
|
|
645
|
-
}
|
|
646
|
-
});
|
|
647
|
-
|
|
648
|
-
req.on("data", chunk => {
|
|
649
|
-
buffer += chunk;
|
|
650
|
-
const lines = buffer.split("\n");
|
|
651
|
-
buffer = lines.pop() || ""; // Keep any incomplete line for the next chunk
|
|
652
|
-
|
|
653
|
-
lines.forEach(line => {
|
|
654
|
-
void this.#processLine(line, this.#process);
|
|
655
|
-
});
|
|
656
|
-
});
|
|
657
|
-
|
|
658
|
-
req.on("end", () => this.#cleanupAndReconnect(client));
|
|
659
|
-
req.on("close", () => this.#cleanupAndReconnect(client));
|
|
660
|
-
req.on("error", error => this.#errHandler(error));
|
|
661
|
-
}
|
|
662
|
-
|
|
663
|
-
/** Schedules a reconnect with a delay to prevent rapid reconnections. */
|
|
664
|
-
#scheduleReconnect() {
|
|
665
|
-
const jitter = Math.floor(Math.random() * 1000);
|
|
666
|
-
const delay = (this.#watchCfg.resyncDelaySec ?? 5) * 1000 + jitter;
|
|
667
648
|
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
}
|
|
673
|
-
|
|
674
|
-
/**
|
|
675
|
-
* Handle a successful connection to the watch.
|
|
676
|
-
*/
|
|
677
|
-
#onWatchConnected() {
|
|
678
|
-
this.#pendingReconnect = false;
|
|
679
|
-
this.#events.emit(WatchEvent.CONNECT);
|
|
680
|
-
|
|
681
|
-
// Reset the retry count
|
|
682
|
-
this.#resyncFailureCount = 0;
|
|
683
|
-
this.#events.emit(WatchEvent.INC_RESYNC_FAILURE_COUNT, this.#resyncFailureCount);
|
|
684
|
-
}
|
|
649
|
+
/** Cleanup the stream and connect */
|
|
650
|
+
#cleanupAndReconnect = () => {
|
|
651
|
+
this.#streamCleanup();
|
|
652
|
+
void this.#watch();
|
|
653
|
+
};
|
|
685
654
|
|
|
686
|
-
/**
|
|
687
|
-
|
|
688
|
-
*
|
|
689
|
-
* @param client - the client session
|
|
690
|
-
*/
|
|
691
|
-
#streamCleanup = (client?: http2.ClientHttp2Session) => {
|
|
655
|
+
/** Cleanup the stream and listeners. */
|
|
656
|
+
#streamCleanup = () => {
|
|
692
657
|
if (this.#stream) {
|
|
693
658
|
this.#stream.removeAllListeners();
|
|
694
659
|
this.#stream.destroy();
|
|
695
660
|
}
|
|
696
|
-
if (client) {
|
|
697
|
-
client.close();
|
|
698
|
-
}
|
|
699
661
|
};
|
|
700
662
|
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
* @param error - the error that occurred
|
|
706
|
-
*/
|
|
707
|
-
#cleanupAndReconnect(client?: http2.ClientHttp2Session, error?: Error) {
|
|
708
|
-
this.#streamCleanup(client);
|
|
709
|
-
|
|
710
|
-
if (error) {
|
|
711
|
-
void this.#errHandler(error);
|
|
663
|
+
#legacyStreamCleanup = () => {
|
|
664
|
+
if (this.#legacyStream) {
|
|
665
|
+
this.#legacyStream.removeAllListeners();
|
|
666
|
+
this.#legacyStream.destroy();
|
|
712
667
|
}
|
|
713
|
-
|
|
714
|
-
this.#scheduleReconnect();
|
|
715
|
-
}
|
|
668
|
+
};
|
|
716
669
|
}
|