kubernetes-fluent-client 3.1.0 → 3.1.2

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.
Files changed (78) hide show
  1. package/dist/cli.d.ts +3 -0
  2. package/dist/cli.d.ts.map +1 -0
  3. package/dist/cli.js +68 -0
  4. package/dist/fetch.d.ts +22 -0
  5. package/dist/fetch.d.ts.map +1 -0
  6. package/dist/fetch.js +82 -0
  7. package/dist/fetch.test.d.ts +2 -0
  8. package/dist/fetch.test.d.ts.map +1 -0
  9. package/dist/fetch.test.js +97 -0
  10. package/dist/fileSystem.d.ts +11 -0
  11. package/dist/fileSystem.d.ts.map +1 -0
  12. package/dist/fileSystem.js +42 -0
  13. package/dist/fileSystem.test.d.ts +2 -0
  14. package/dist/fileSystem.test.d.ts.map +1 -0
  15. package/dist/fileSystem.test.js +75 -0
  16. package/dist/fluent/http2-watch.spec.d.ts +2 -0
  17. package/dist/fluent/http2-watch.spec.d.ts.map +1 -0
  18. package/dist/fluent/http2-watch.spec.js +284 -0
  19. package/dist/fluent/index.d.ts +12 -0
  20. package/dist/fluent/index.d.ts.map +1 -0
  21. package/dist/fluent/index.js +228 -0
  22. package/dist/fluent/index.test.d.ts +2 -0
  23. package/dist/fluent/index.test.d.ts.map +1 -0
  24. package/dist/fluent/index.test.js +193 -0
  25. package/dist/fluent/types.d.ts +201 -0
  26. package/dist/fluent/types.d.ts.map +1 -0
  27. package/dist/fluent/types.js +16 -0
  28. package/dist/fluent/utils.d.ts +41 -0
  29. package/dist/fluent/utils.d.ts.map +1 -0
  30. package/dist/fluent/utils.js +153 -0
  31. package/dist/fluent/utils.test.d.ts +2 -0
  32. package/dist/fluent/utils.test.d.ts.map +1 -0
  33. package/dist/fluent/utils.test.js +215 -0
  34. package/dist/fluent/watch.d.ts +88 -0
  35. package/dist/fluent/watch.d.ts.map +1 -0
  36. package/dist/fluent/watch.js +595 -0
  37. package/dist/fluent/watch.spec.d.ts +2 -0
  38. package/dist/fluent/watch.spec.d.ts.map +1 -0
  39. package/dist/fluent/watch.spec.js +261 -0
  40. package/dist/generate.d.ts +84 -0
  41. package/dist/generate.d.ts.map +1 -0
  42. package/dist/generate.js +208 -0
  43. package/dist/generate.test.d.ts +2 -0
  44. package/dist/generate.test.d.ts.map +1 -0
  45. package/dist/generate.test.js +320 -0
  46. package/dist/helpers.d.ts +33 -0
  47. package/dist/helpers.d.ts.map +1 -0
  48. package/dist/helpers.js +103 -0
  49. package/dist/helpers.test.d.ts +2 -0
  50. package/dist/helpers.test.d.ts.map +1 -0
  51. package/dist/helpers.test.js +37 -0
  52. package/dist/index.d.ts +14 -0
  53. package/dist/index.d.ts.map +1 -0
  54. package/dist/index.js +60 -0
  55. package/dist/kinds.d.ts +16 -0
  56. package/dist/kinds.d.ts.map +1 -0
  57. package/dist/kinds.js +570 -0
  58. package/dist/kinds.test.d.ts +2 -0
  59. package/dist/kinds.test.d.ts.map +1 -0
  60. package/dist/kinds.test.js +155 -0
  61. package/dist/patch.d.ts +7 -0
  62. package/dist/patch.d.ts.map +1 -0
  63. package/dist/patch.js +2 -0
  64. package/dist/postProcessing.d.ts +246 -0
  65. package/dist/postProcessing.d.ts.map +1 -0
  66. package/dist/postProcessing.js +497 -0
  67. package/dist/postProcessing.test.d.ts +2 -0
  68. package/dist/postProcessing.test.d.ts.map +1 -0
  69. package/dist/postProcessing.test.js +550 -0
  70. package/dist/types.d.ts +32 -0
  71. package/dist/types.d.ts.map +1 -0
  72. package/dist/types.js +16 -0
  73. package/dist/upstream.d.ts +4 -0
  74. package/dist/upstream.d.ts.map +1 -0
  75. package/dist/upstream.js +56 -0
  76. package/package.json +1 -1
  77. package/src/fluent/types.ts +16 -0
  78. package/src/fluent/watch.ts +134 -81
@@ -0,0 +1,595 @@
1
+ "use strict";
2
+ // SPDX-License-Identifier: Apache-2.0
3
+ // SPDX-FileCopyrightText: 2023-Present The Kubernetes Fluent Client Authors
4
+ var __importDefault = (this && this.__importDefault) || function (mod) {
5
+ return (mod && mod.__esModule) ? mod : { "default": mod };
6
+ };
7
+ var _a;
8
+ Object.defineProperty(exports, "__esModule", { value: true });
9
+ exports.Watcher = exports.WatchEvent = void 0;
10
+ const byline_1 = __importDefault(require("byline"));
11
+ const crypto_1 = require("crypto");
12
+ const events_1 = require("events");
13
+ const https_1 = __importDefault(require("https"));
14
+ const http2_1 = __importDefault(require("http2"));
15
+ const node_fetch_1 = __importDefault(require("node-fetch"));
16
+ const fetch_1 = require("../fetch");
17
+ const types_1 = require("./types");
18
+ const utils_1 = require("./utils");
19
+ const fs_1 = __importDefault(require("fs"));
20
+ var WatchEvent;
21
+ (function (WatchEvent) {
22
+ /** Watch is connected successfully */
23
+ WatchEvent["CONNECT"] = "connect";
24
+ /** Network error occurs */
25
+ WatchEvent["NETWORK_ERROR"] = "network_error";
26
+ /** Error decoding data or running the callback */
27
+ WatchEvent["DATA_ERROR"] = "data_error";
28
+ /** Reconnect is called */
29
+ WatchEvent["RECONNECT"] = "reconnect";
30
+ /** Retry limit is exceeded */
31
+ WatchEvent["GIVE_UP"] = "give_up";
32
+ /** Abort is called */
33
+ WatchEvent["ABORT"] = "abort";
34
+ /** Data is received and decoded */
35
+ WatchEvent["DATA"] = "data";
36
+ /** 410 (old resource version) occurs */
37
+ WatchEvent["OLD_RESOURCE_VERSION"] = "old_resource_version";
38
+ /** A reconnect is already pending */
39
+ WatchEvent["RECONNECT_PENDING"] = "reconnect_pending";
40
+ /** Resource list operation run */
41
+ WatchEvent["LIST"] = "list";
42
+ /** List operation error */
43
+ WatchEvent["LIST_ERROR"] = "list_error";
44
+ /** Cache Misses */
45
+ WatchEvent["CACHE_MISS"] = "cache_miss";
46
+ /** Increment resync failure count */
47
+ WatchEvent["INC_RESYNC_FAILURE_COUNT"] = "inc_resync_failure_count";
48
+ /** Initialize a relist window */
49
+ WatchEvent["INIT_CACHE_MISS"] = "init_cache_miss";
50
+ })(WatchEvent || (exports.WatchEvent = WatchEvent = {}));
51
+ const NONE = 50;
52
+ const OVERRIDE = 100;
53
+ /** A wrapper around the Kubernetes watch API. */
54
+ class Watcher {
55
+ // User-provided properties
56
+ #model;
57
+ #filters;
58
+ #callback;
59
+ #watchCfg;
60
+ #latestRelistWindow = "";
61
+ #useHTTP2 = false;
62
+ // Track the last time data was received
63
+ #lastSeenTime = NONE;
64
+ #lastSeenLimit;
65
+ // Create a wrapped AbortController to allow the watch to be aborted externally
66
+ #abortController;
67
+ // Track the number of retries
68
+ #resyncFailureCount = 0;
69
+ // Create a stream to read the response body
70
+ #stream;
71
+ // Create an EventEmitter to emit events
72
+ #events = new events_1.EventEmitter();
73
+ // Create a timer to relist the watch
74
+ $relistTimer;
75
+ // Create a timer to resync the watch
76
+ #resyncTimer;
77
+ // Track if a reconnect is pending
78
+ #pendingReconnect = false;
79
+ // The resource version to start the watch at, this will be updated after the list operation.
80
+ #resourceVersion;
81
+ // Track the list of items in the cache
82
+ #cache = new Map();
83
+ // Token Path
84
+ #TOKEN_PATH = "/var/run/secrets/kubernetes.io/serviceaccount/token";
85
+ /**
86
+ * Setup a Kubernetes watcher for the specified model and filters. The callback function will be called for each event received.
87
+ * The watch can be aborted by calling {@link Watcher.close} or by calling abort() on the AbortController returned by {@link Watcher.start}.
88
+ *
89
+ *
90
+ * Kubernetes API docs: {@link https://kubernetes.io/docs/reference/using-api/api-concepts/#efficient-detection-of-changes}
91
+ *
92
+ * @param model - the model to use for the API
93
+ * @param filters - (optional) filter overrides, can also be chained
94
+ * @param callback - the callback function to call when an event is received
95
+ * @param watchCfg - (optional) watch configuration
96
+ */
97
+ constructor(model, filters, callback, watchCfg = {}) {
98
+ // Set the retry delay to 5 seconds if not specified
99
+ watchCfg.resyncDelaySec ??= 5;
100
+ // Set the relist interval to 10 minutes if not specified
101
+ watchCfg.relistIntervalSec ??= 600;
102
+ // Set the resync interval to 10 minutes if not specified
103
+ watchCfg.lastSeenLimitSeconds ??= 600;
104
+ // Set the last seen limit to the resync interval
105
+ this.#lastSeenLimit = watchCfg.lastSeenLimitSeconds * 1000;
106
+ // Set the latest relist interval to now
107
+ this.#latestRelistWindow = new Date().toISOString();
108
+ // Set the latest relist interval to now
109
+ this.#useHTTP2 = watchCfg.useHTTP2 ?? false;
110
+ // Add random jitter to the relist/resync intervals (up to 1 second)
111
+ const jitter = Math.floor(Math.random() * 1000);
112
+ // Check every relist interval for cache staleness
113
+ this.$relistTimer = setInterval(() => {
114
+ this.#latestRelistWindow = new Date().toISOString();
115
+ this.#events.emit(WatchEvent.INIT_CACHE_MISS, this.#latestRelistWindow);
116
+ void this.#list();
117
+ }, watchCfg.relistIntervalSec * 1000 + jitter);
118
+ // Rebuild the watch every resync delay interval
119
+ this.#resyncTimer = setInterval(this.#checkResync, watchCfg.resyncDelaySec * 1000 + jitter);
120
+ // Bind class properties
121
+ this.#model = model;
122
+ this.#filters = filters;
123
+ this.#callback = callback;
124
+ this.#watchCfg = watchCfg;
125
+ // Create a new AbortController
126
+ this.#abortController = new AbortController();
127
+ }
128
+ /**
129
+ * Start the watch.
130
+ *
131
+ * @returns The AbortController for the watch.
132
+ */
133
+ async start() {
134
+ this.#events.emit(WatchEvent.INIT_CACHE_MISS, this.#latestRelistWindow);
135
+ if (this.#useHTTP2) {
136
+ await this.#http2Watch();
137
+ }
138
+ else {
139
+ await this.#watch();
140
+ }
141
+ return this.#abortController;
142
+ }
143
+ /** Close the watch. Also available on the AbortController returned by {@link Watcher.start}. */
144
+ close() {
145
+ clearInterval(this.$relistTimer);
146
+ clearInterval(this.#resyncTimer);
147
+ this.#streamCleanup();
148
+ this.#abortController.abort();
149
+ }
150
+ /**
151
+ * Get a unique ID for the watch based on the model and filters.
152
+ * This is useful for caching the watch data or resource versions.
153
+ *
154
+ * @returns the watch CacheID
155
+ */
156
+ getCacheID() {
157
+ // Build the URL, we don't care about the server URL or resourceVersion
158
+ const url = (0, utils_1.pathBuilder)("https://ignore", this.#model, this.#filters, false);
159
+ // Hash and truncate the ID to 10 characters, cache the result
160
+ return (0, crypto_1.createHash)("sha224")
161
+ .update(url.pathname + url.search)
162
+ .digest("hex")
163
+ .substring(0, 10);
164
+ }
165
+ /**
166
+ * Subscribe to watch events. This is an EventEmitter that emits the following events:
167
+ *
168
+ * Use {@link WatchEvent} for the event names.
169
+ *
170
+ * @returns an EventEmitter
171
+ */
172
+ get events() {
173
+ return this.#events;
174
+ }
175
+ /**
176
+ * Read the serviceAccount Token
177
+ *
178
+ * @returns token or null
179
+ */
180
+ async #getToken() {
181
+ try {
182
+ return (await fs_1.default.promises.readFile(this.#TOKEN_PATH, "utf8")).trim();
183
+ }
184
+ catch {
185
+ return null;
186
+ }
187
+ }
188
+ /**
189
+ * Build the URL and request options for the watch.
190
+ *
191
+ * @param isWatch - whether the request is for a watch operation
192
+ * @param resourceVersion - the resource version to use for the watch
193
+ * @param continueToken - the continue token for the watch
194
+ *
195
+ * @returns the URL and request options
196
+ */
197
+ #buildURL = async (isWatch, resourceVersion, continueToken) => {
198
+ // Build the path and query params for the resource, excluding the name
199
+ const { opts, serverUrl } = await (0, utils_1.k8sCfg)("GET");
200
+ const url = (0, utils_1.pathBuilder)(serverUrl, this.#model, this.#filters, true);
201
+ // Enable the watch query param
202
+ if (isWatch) {
203
+ url.searchParams.set("watch", "true");
204
+ }
205
+ if (continueToken) {
206
+ url.searchParams.set("continue", continueToken);
207
+ }
208
+ // If a name is specified, add it to the query params
209
+ if (this.#filters.name) {
210
+ url.searchParams.set("fieldSelector", `metadata.name=${this.#filters.name}`);
211
+ }
212
+ // If a resource version is specified, add it to the query params
213
+ if (resourceVersion) {
214
+ url.searchParams.set("resourceVersion", resourceVersion);
215
+ }
216
+ // Add the abort signal to the request options
217
+ opts.signal = this.#abortController.signal;
218
+ return { opts, url };
219
+ };
220
+ /**
221
+ * Retrieve the list of resources and process the events.
222
+ *
223
+ * @param continueToken - the continue token for the list
224
+ * @param removedItems - the list of items that have been removed
225
+ */
226
+ #list = async (continueToken, removedItems) => {
227
+ try {
228
+ const { opts, url } = await this.#buildURL(false, undefined, continueToken);
229
+ // Make the request to list the resources
230
+ const response = await (0, fetch_1.fetch)(url, opts);
231
+ const list = response.data;
232
+ // If the request fails, emit an error event and return
233
+ if (!response.ok) {
234
+ this.#events.emit(WatchEvent.LIST_ERROR, new Error(`list failed: ${response.status} ${response.statusText}`));
235
+ return;
236
+ }
237
+ // Gross hack, thanks upstream library :<
238
+ if (list.metadata.continue) {
239
+ continueToken = list.metadata.continue;
240
+ }
241
+ // Emit the list event
242
+ this.#events.emit(WatchEvent.LIST, list);
243
+ // Update the resource version from the list metadata
244
+ this.#resourceVersion = list.metadata?.resourceVersion;
245
+ // If removed items are not provided, clone the cache
246
+ removedItems = removedItems || new Map(this.#cache.entries());
247
+ // Process each item in the list
248
+ for (const item of list.items || []) {
249
+ const { uid } = item.metadata;
250
+ // Remove the item from the removed items list
251
+ const alreadyExists = removedItems.delete(uid);
252
+ // If the item does not exist, it is new and should be added
253
+ if (!alreadyExists) {
254
+ this.#events.emit(WatchEvent.CACHE_MISS, this.#latestRelistWindow);
255
+ // Send added event. Use void here because we don't care about the result (no consequences here if it fails)
256
+ void this.#process(item, types_1.WatchPhase.Added);
257
+ continue;
258
+ }
259
+ // Check if the resource version has changed for items that already exist
260
+ const cachedRV = parseInt(this.#cache.get(uid)?.metadata?.resourceVersion);
261
+ const itemRV = parseInt(item.metadata.resourceVersion);
262
+ // Check if the resource version is newer than the cached version
263
+ if (itemRV > cachedRV) {
264
+ this.#events.emit(WatchEvent.CACHE_MISS, this.#latestRelistWindow);
265
+ // Send a modified event if the resource version has changed
266
+ void this.#process(item, types_1.WatchPhase.Modified);
267
+ }
268
+ }
269
+ // If there is a continue token, call the list function again with the same removed items
270
+ if (continueToken) {
271
+ // 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
+ await this.#list(continueToken, removedItems);
274
+ }
275
+ else {
276
+ // Otherwise, process the removed items
277
+ for (const item of removedItems.values()) {
278
+ this.#events.emit(WatchEvent.CACHE_MISS, this.#latestRelistWindow);
279
+ void this.#process(item, types_1.WatchPhase.Deleted);
280
+ }
281
+ }
282
+ }
283
+ catch (err) {
284
+ this.#events.emit(WatchEvent.LIST_ERROR, err);
285
+ }
286
+ };
287
+ /**
288
+ * Process the event payload.
289
+ *
290
+ * @param payload - the event payload
291
+ * @param phase - the event phase
292
+ */
293
+ #process = async (payload, phase) => {
294
+ try {
295
+ switch (phase) {
296
+ // If the event is added or modified, update the cache
297
+ case types_1.WatchPhase.Added:
298
+ case types_1.WatchPhase.Modified:
299
+ this.#cache.set(payload.metadata.uid, payload);
300
+ break;
301
+ // If the event is deleted, remove the item from the cache
302
+ case types_1.WatchPhase.Deleted:
303
+ this.#cache.delete(payload.metadata.uid);
304
+ break;
305
+ }
306
+ // Emit the data event
307
+ this.#events.emit(WatchEvent.DATA, payload, phase);
308
+ // Call the callback function with the parsed payload
309
+ await this.#callback(payload, phase);
310
+ }
311
+ catch (err) {
312
+ this.#events.emit(WatchEvent.DATA_ERROR, err);
313
+ }
314
+ };
315
+ // process a line from the chunk
316
+ #processLine = async (line, process) => {
317
+ try {
318
+ // Parse the event payload
319
+ const { object: payload, type: phase } = JSON.parse(line);
320
+ // Update the last seen time
321
+ this.#lastSeenTime = Date.now();
322
+ // If the watch is too old, remove the resourceVersion and reload the watch
323
+ if (phase === types_1.WatchPhase.Error && payload.code === 410) {
324
+ throw {
325
+ name: "TooOld",
326
+ message: this.#resourceVersion,
327
+ };
328
+ }
329
+ // Process the event payload, do not update the resource version as that is handled by the list operation
330
+ await process(payload, phase);
331
+ }
332
+ catch (err) {
333
+ if (err.name === "TooOld") {
334
+ // Reload the watch
335
+ void this.#errHandler(err);
336
+ return;
337
+ }
338
+ this.#events.emit(WatchEvent.DATA_ERROR, err);
339
+ }
340
+ };
341
+ /**
342
+ * Watch for changes to the resource.
343
+ */
344
+ #watch = async () => {
345
+ try {
346
+ // Start with a list operation
347
+ await this.#list();
348
+ // Build the URL and request options
349
+ const { opts, url } = await this.#buildURL(true, this.#resourceVersion);
350
+ // Create a stream to read the response body
351
+ this.#stream = byline_1.default.createStream();
352
+ // Bind the stream events
353
+ this.#stream.on("error", this.#errHandler);
354
+ this.#stream.on("close", this.#streamCleanup);
355
+ this.#stream.on("finish", this.#streamCleanup);
356
+ // Make the actual request
357
+ const response = await (0, node_fetch_1.default)(url, { ...opts });
358
+ // Reset the pending reconnect flag
359
+ this.#pendingReconnect = false;
360
+ // If the request is successful, start listening for events
361
+ if (response.ok) {
362
+ this.#events.emit(WatchEvent.CONNECT, url.pathname);
363
+ const { body } = response;
364
+ // Reset the retry count
365
+ this.#resyncFailureCount = 0;
366
+ this.#events.emit(WatchEvent.INC_RESYNC_FAILURE_COUNT, this.#resyncFailureCount);
367
+ // Listen for events and call the callback function
368
+ this.#stream.on("data", async (line) => {
369
+ await this.#processLine(line, this.#process);
370
+ });
371
+ // Bind the body events
372
+ body.on("error", this.#errHandler);
373
+ body.on("close", this.#streamCleanup);
374
+ body.on("finish", this.#streamCleanup);
375
+ // Pipe the response body to the stream
376
+ body.pipe(this.#stream);
377
+ }
378
+ else {
379
+ throw new Error(`watch connect failed: ${response.status} ${response.statusText}`);
380
+ }
381
+ }
382
+ catch (e) {
383
+ void this.#errHandler(e);
384
+ }
385
+ };
386
+ // Configure the agent options for the HTTP/2 client
387
+ static #getAgentOptions(opts) {
388
+ if (opts.agent && opts.agent instanceof https_1.default.Agent) {
389
+ return {
390
+ key: opts.agent.options.key,
391
+ cert: opts.agent.options.cert,
392
+ ca: opts.agent.options.ca,
393
+ rejectUnauthorized: false,
394
+ };
395
+ }
396
+ return undefined;
397
+ }
398
+ // Create an HTTP/2 client
399
+ static #createHttp2Client(origin, agentOptions) {
400
+ return http2_1.default.connect(origin, {
401
+ ca: agentOptions?.ca,
402
+ cert: agentOptions?.cert,
403
+ key: agentOptions?.key,
404
+ rejectUnauthorized: agentOptions?.rejectUnauthorized,
405
+ });
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
+ };
421
+ /**
422
+ * Watch for changes to the resource.
423
+ */
424
+ #http2Watch = async () => {
425
+ try {
426
+ // Start with a list operation
427
+ await this.#list();
428
+ // Build the URL and request options
429
+ const { opts, url } = await this.#buildURL(true, this.#resourceVersion);
430
+ const agentOptions = _a.#getAgentOptions(opts);
431
+ // HTTP/2 client connection setup
432
+ const client = _a.#createHttp2Client(url.origin, agentOptions);
433
+ // Handle client connection errors
434
+ client.on("error", err => {
435
+ this.#events.emit(WatchEvent.NETWORK_ERROR, err);
436
+ this.#streamCleanup(client);
437
+ this.#scheduleReconnect();
438
+ });
439
+ // Set up headers for the HTTP/2 request
440
+ const headers = await this.#generateRequestHeaders(url);
441
+ // Make the HTTP/2 request
442
+ const req = client.request(headers);
443
+ req.setEncoding("utf8");
444
+ // Handler events for the HTTP/2 request
445
+ this.#handleHttp2Request(req, client);
446
+ // Handle abort signal
447
+ this.#abortController.signal.addEventListener("abort", () => {
448
+ this.#streamCleanup(client);
449
+ });
450
+ }
451
+ catch (e) {
452
+ void this.#errHandler(e);
453
+ }
454
+ };
455
+ /** Clear the resync timer and schedule a new one. */
456
+ #checkResync = () => {
457
+ // Ignore if the last seen time is not set
458
+ if (this.#lastSeenTime === NONE) {
459
+ return;
460
+ }
461
+ const now = Date.now();
462
+ // If the last seen time is greater than the limit, trigger a resync
463
+ if (this.#lastSeenTime == OVERRIDE || now - this.#lastSeenTime > this.#lastSeenLimit) {
464
+ // Reset the last seen time to now to allow the resync to be called again in case of failure
465
+ this.#lastSeenTime = now;
466
+ // If there are more attempts, retry the watch (undefined is unlimited retries)
467
+ if (this.#watchCfg.resyncFailureMax === undefined ||
468
+ this.#watchCfg.resyncFailureMax > this.#resyncFailureCount) {
469
+ // Increment the retry count
470
+ this.#resyncFailureCount++;
471
+ this.#events.emit(WatchEvent.INC_RESYNC_FAILURE_COUNT, this.#resyncFailureCount);
472
+ if (this.#pendingReconnect) {
473
+ // wait for the connection to be re-established
474
+ this.#events.emit(WatchEvent.RECONNECT_PENDING);
475
+ }
476
+ else {
477
+ this.#pendingReconnect = true;
478
+ this.#events.emit(WatchEvent.RECONNECT, this.#resyncFailureCount);
479
+ this.#streamCleanup();
480
+ if (!this.#useHTTP2) {
481
+ void this.#watch();
482
+ }
483
+ }
484
+ }
485
+ else {
486
+ // Otherwise, call the finally function if it exists
487
+ this.#events.emit(WatchEvent.GIVE_UP, new Error(`Retry limit (${this.#watchCfg.resyncFailureMax}) exceeded, giving up`));
488
+ this.close();
489
+ }
490
+ }
491
+ };
492
+ /**
493
+ * Handle errors from the stream.
494
+ *
495
+ * @param err - the error that occurred
496
+ */
497
+ #errHandler = async (err) => {
498
+ switch (err.name) {
499
+ case "AbortError":
500
+ clearInterval(this.$relistTimer);
501
+ clearInterval(this.#resyncTimer);
502
+ this.#streamCleanup();
503
+ this.#scheduleReconnect();
504
+ this.#events.emit(WatchEvent.ABORT, err);
505
+ return;
506
+ case "TooOld":
507
+ // Purge the resource version if it is too old
508
+ this.#resourceVersion = undefined;
509
+ this.#events.emit(WatchEvent.OLD_RESOURCE_VERSION, err.message);
510
+ break;
511
+ default:
512
+ this.#events.emit(WatchEvent.NETWORK_ERROR, err);
513
+ break;
514
+ }
515
+ // Force a resync
516
+ this.#lastSeenTime = OVERRIDE;
517
+ };
518
+ /**
519
+ *
520
+ * @param req - the request stream
521
+ * @param client - the client session
522
+ */
523
+ #handleHttp2Request(req, client) {
524
+ let buffer = "";
525
+ req.on("response", headers => {
526
+ const statusCode = headers[":status"];
527
+ if (statusCode && statusCode >= 200 && statusCode < 300) {
528
+ this.#onWatchConnected();
529
+ }
530
+ else {
531
+ this.#cleanupAndReconnect(client, new Error(`watch connect failed: ${statusCode}`));
532
+ }
533
+ });
534
+ req.on("data", chunk => {
535
+ buffer += chunk;
536
+ const lines = buffer.split("\n");
537
+ buffer = lines.pop() || ""; // Keep any incomplete line for the next chunk
538
+ lines.forEach(line => {
539
+ void this.#processLine(line, this.#process);
540
+ });
541
+ });
542
+ req.on("end", () => this.#cleanupAndReconnect(client));
543
+ req.on("close", () => this.#cleanupAndReconnect(client));
544
+ req.on("error", error => this.#errHandler(error));
545
+ }
546
+ /** Schedules a reconnect with a delay to prevent rapid reconnections. */
547
+ #scheduleReconnect() {
548
+ const jitter = Math.floor(Math.random() * 1000);
549
+ const delay = (this.#watchCfg.resyncDelaySec ?? 5) * 1000 + jitter;
550
+ setTimeout(() => {
551
+ this.#events.emit(WatchEvent.RECONNECT, this.#resyncFailureCount);
552
+ void this.#http2Watch();
553
+ }, delay);
554
+ }
555
+ /**
556
+ * Handle a successful connection to the watch.
557
+ */
558
+ #onWatchConnected() {
559
+ this.#pendingReconnect = false;
560
+ this.#events.emit(WatchEvent.CONNECT);
561
+ // Reset the retry count
562
+ this.#resyncFailureCount = 0;
563
+ this.#events.emit(WatchEvent.INC_RESYNC_FAILURE_COUNT, this.#resyncFailureCount);
564
+ }
565
+ /**
566
+ * Cleanup the stream and listeners.
567
+ *
568
+ * @param client - the client session
569
+ */
570
+ #streamCleanup = (client) => {
571
+ if (this.#stream) {
572
+ this.#stream.removeAllListeners();
573
+ this.#stream.destroy();
574
+ }
575
+ if (client) {
576
+ client.close();
577
+ }
578
+ };
579
+ /**
580
+ * Cleanup the stream and listeners and reconnect.
581
+ *
582
+ * @param client - the client session
583
+ * @param error - the error that occurred
584
+ */
585
+ #cleanupAndReconnect(client, error) {
586
+ this.#streamCleanup(client);
587
+ if (error) {
588
+ this.#events.emit(WatchEvent.NETWORK_ERROR, error);
589
+ void this.#errHandler(error);
590
+ }
591
+ this.#scheduleReconnect();
592
+ }
593
+ }
594
+ exports.Watcher = Watcher;
595
+ _a = Watcher;
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=watch.spec.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"watch.spec.d.ts","sourceRoot":"","sources":["../../src/fluent/watch.spec.ts"],"names":[],"mappings":""}