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