kubernetes-fluent-client 2.6.5 → 3.0.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.
@@ -116,7 +116,7 @@ function K8s(model, filters = {}) {
116
116
  podList.push(object);
117
117
  }
118
118
  }
119
- catch (e) {
119
+ catch {
120
120
  throw new Error(`Failed to get logs in KFC Logs function`);
121
121
  }
122
122
  const podModel = { ...model, name: "V1Pod" };
@@ -23,18 +23,24 @@ export declare enum WatchEvent {
23
23
  /** Resource list operation run */
24
24
  LIST = "list",
25
25
  /** List operation error */
26
- LIST_ERROR = "list_error"
26
+ LIST_ERROR = "list_error",
27
+ /** Cache Misses */
28
+ CACHE_MISS = "cache_miss",
29
+ /** Increment resync failure count */
30
+ INC_RESYNC_FAILURE_COUNT = "inc_resync_failure_count",
31
+ /** Initialize a relist window */
32
+ INIT_CACHE_MISS = "init_cache_miss"
27
33
  }
28
34
  /** Configuration for the watch function. */
29
35
  export type WatchCfg = {
30
36
  /** The maximum number of times to retry the watch, the retry count is reset on success. Unlimited retries if not specified. */
31
- retryMax?: number;
32
- /** Seconds between each retry check. Defaults to 5. */
33
- retryDelaySec?: number;
37
+ resyncFailureMax?: number;
38
+ /** Seconds between each resync check. Defaults to 5. */
39
+ resyncDelaySec?: number;
34
40
  /** Amount of seconds to wait before relisting the watch list. Defaults to 600 (10 minutes). */
35
41
  relistIntervalSec?: number;
36
- /** Amount of seconds to wait before a forced-resyncing of the watch list. Defaults to 300 (5 minutes). */
37
- resyncIntervalSec?: number;
42
+ /** Max amount of seconds to go without receiving an event before reconciliation starts. Defaults to 300 (5 minutes). */
43
+ lastSeenLimitSeconds?: number;
38
44
  };
39
45
  /** A wrapper around the Kubernetes watch API. */
40
46
  export declare class Watcher<T extends GenericClass> {
@@ -1 +1 @@
1
- {"version":3,"file":"watch.d.ts","sourceRoot":"","sources":["../../src/fluent/watch.ts"],"names":[],"mappings":"AAKA,OAAO,EAAE,YAAY,EAAE,MAAM,QAAQ,CAAC;AAItC,OAAO,EAAE,YAAY,EAAwB,MAAM,UAAU,CAAC;AAC9D,OAAO,EAAE,OAAO,EAAE,WAAW,EAAc,MAAM,SAAS,CAAC;AAG3D,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;CAC1B;AAED,4CAA4C;AAC5C,MAAM,MAAM,QAAQ,GAAG;IACrB,+HAA+H;IAC/H,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,uDAAuD;IACvD,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB,+FAA+F;IAC/F,iBAAiB,CAAC,EAAE,MAAM,CAAC;IAC3B,0GAA0G;IAC1G,iBAAiB,CAAC,EAAE,MAAM,CAAC;CAC5B,CAAC;AAKF,iDAAiD;AACjD,qBAAa,OAAO,CAAC,CAAC,SAAS,YAAY;;IAwBzC,YAAY,CAAC,EAAE,MAAM,CAAC,OAAO,CAAC;IAc9B;;;;;;;;;;;OAWG;gBACS,KAAK,EAAE,CAAC,EAAE,OAAO,EAAE,OAAO,EAAE,QAAQ,EAAE,WAAW,CAAC,CAAC,CAAC,EAAE,QAAQ,GAAE,QAAa;IAgCzF;;;;OAIG;IACU,KAAK,IAAI,OAAO,CAAC,eAAe,CAAC;IAK9C,gGAAgG;IACzF,KAAK;IAOZ;;;;;OAKG;IACI,UAAU;IAWjB;;;;;;OAMG;IACH,IAAW,MAAM,IAAI,YAAY,CAEhC;CAyTF"}
1
+ {"version":3,"file":"watch.d.ts","sourceRoot":"","sources":["../../src/fluent/watch.ts"],"names":[],"mappings":"AAKA,OAAO,EAAE,YAAY,EAAE,MAAM,QAAQ,CAAC;AAItC,OAAO,EAAE,YAAY,EAAwB,MAAM,UAAU,CAAC;AAC9D,OAAO,EAAE,OAAO,EAAE,WAAW,EAAc,MAAM,SAAS,CAAC;AAG3D,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;CAC/B,CAAC;AAKF,iDAAiD;AACjD,qBAAa,OAAO,CAAC,CAAC,SAAS,YAAY;;IAyBzC,YAAY,CAAC,EAAE,MAAM,CAAC,OAAO,CAAC;IAc9B;;;;;;;;;;;OAWG;gBACS,KAAK,EAAE,CAAC,EAAE,OAAO,EAAE,OAAO,EAAE,QAAQ,EAAE,WAAW,CAAC,CAAC,CAAC,EAAE,QAAQ,GAAE,QAAa;IA0CzF;;;;OAIG;IACU,KAAK,IAAI,OAAO,CAAC,eAAe,CAAC;IAM9C,gGAAgG;IACzF,KAAK;IAOZ;;;;;OAKG;IACI,UAAU;IAWjB;;;;;;OAMG;IACH,IAAW,MAAM,IAAI,YAAY,CAEhC;CAiUF"}
@@ -37,6 +37,12 @@ var WatchEvent;
37
37
  WatchEvent["LIST"] = "list";
38
38
  /** List operation error */
39
39
  WatchEvent["LIST_ERROR"] = "list_error";
40
+ /** Cache Misses */
41
+ WatchEvent["CACHE_MISS"] = "cache_miss";
42
+ /** Increment resync failure count */
43
+ WatchEvent["INC_RESYNC_FAILURE_COUNT"] = "inc_resync_failure_count";
44
+ /** Initialize a relist window */
45
+ WatchEvent["INIT_CACHE_MISS"] = "init_cache_miss";
40
46
  })(WatchEvent || (exports.WatchEvent = WatchEvent = {}));
41
47
  const NONE = 50;
42
48
  const OVERRIDE = 100;
@@ -47,13 +53,14 @@ class Watcher {
47
53
  #filters;
48
54
  #callback;
49
55
  #watchCfg;
56
+ #latestRelistWindow = "";
50
57
  // Track the last time data was received
51
58
  #lastSeenTime = NONE;
52
59
  #lastSeenLimit;
53
60
  // Create a wrapped AbortController to allow the watch to be aborted externally
54
61
  #abortController;
55
62
  // Track the number of retries
56
- #retryCount = 0;
63
+ #resyncFailureCount = 0;
57
64
  // Create a stream to read the response body
58
65
  #stream;
59
66
  // Create an EventEmitter to emit events
@@ -82,19 +89,25 @@ class Watcher {
82
89
  */
83
90
  constructor(model, filters, callback, watchCfg = {}) {
84
91
  // Set the retry delay to 5 seconds if not specified
85
- watchCfg.retryDelaySec ??= 5;
92
+ watchCfg.resyncDelaySec ??= 5;
86
93
  // Set the relist interval to 30 minutes if not specified
87
94
  watchCfg.relistIntervalSec ??= 1800;
88
95
  // Set the resync interval to 10 minutes if not specified
89
- watchCfg.resyncIntervalSec ??= 600;
96
+ watchCfg.lastSeenLimitSeconds ??= 600;
90
97
  // Set the last seen limit to the resync interval
91
- this.#lastSeenLimit = watchCfg.resyncIntervalSec * 1000;
98
+ this.#lastSeenLimit = watchCfg.lastSeenLimitSeconds * 1000;
99
+ // Set the latest relist interval to now
100
+ this.#latestRelistWindow = new Date().toISOString();
92
101
  // Add random jitter to the relist/resync intervals (up to 1 second)
93
102
  const jitter = Math.floor(Math.random() * 1000);
94
103
  // Check every relist interval for cache staleness
95
- this.$relistTimer = setInterval(this.#list, watchCfg.relistIntervalSec * 1000 + jitter);
96
- // Rebuild the watch every retry delay interval
97
- this.#resyncTimer = setInterval(this.#checkResync, watchCfg.retryDelaySec * 1000 + jitter);
104
+ this.$relistTimer = setInterval(() => {
105
+ this.#latestRelistWindow = new Date().toISOString();
106
+ this.#events.emit(WatchEvent.INIT_CACHE_MISS, this.#latestRelistWindow);
107
+ void this.#list();
108
+ }, watchCfg.relistIntervalSec * 1000 + jitter);
109
+ // Rebuild the watch every resync delay interval
110
+ this.#resyncTimer = setInterval(this.#checkResync, watchCfg.resyncDelaySec * 1000 + jitter);
98
111
  // Bind class properties
99
112
  this.#model = model;
100
113
  this.#filters = filters;
@@ -109,6 +122,7 @@ class Watcher {
109
122
  * @returns The AbortController for the watch.
110
123
  */
111
124
  async start() {
125
+ this.#events.emit(WatchEvent.INIT_CACHE_MISS, this.#latestRelistWindow);
112
126
  await this.#watch();
113
127
  return this.#abortController;
114
128
  }
@@ -210,6 +224,7 @@ class Watcher {
210
224
  const alreadyExists = removedItems.delete(uid);
211
225
  // If the item does not exist, it is new and should be added
212
226
  if (!alreadyExists) {
227
+ this.#events.emit(WatchEvent.CACHE_MISS, this.#latestRelistWindow);
213
228
  // Send added event. Use void here because we don't care about the result (no consequences here if it fails)
214
229
  void this.#process(item, types_1.WatchPhase.Added);
215
230
  continue;
@@ -219,6 +234,7 @@ class Watcher {
219
234
  const itemRV = parseInt(item.metadata.resourceVersion);
220
235
  // Check if the resource version is newer than the cached version
221
236
  if (itemRV > cachedRV) {
237
+ this.#events.emit(WatchEvent.CACHE_MISS, this.#latestRelistWindow);
222
238
  // Send a modified event if the resource version has changed
223
239
  void this.#process(item, types_1.WatchPhase.Modified);
224
240
  }
@@ -232,6 +248,7 @@ class Watcher {
232
248
  else {
233
249
  // Otherwise, process the removed items
234
250
  for (const item of removedItems.values()) {
251
+ this.#events.emit(WatchEvent.CACHE_MISS, this.#latestRelistWindow);
235
252
  void this.#process(item, types_1.WatchPhase.Deleted);
236
253
  }
237
254
  }
@@ -292,7 +309,8 @@ class Watcher {
292
309
  this.#events.emit(WatchEvent.CONNECT, url.pathname);
293
310
  const { body } = response;
294
311
  // Reset the retry count
295
- this.#retryCount = 0;
312
+ this.#resyncFailureCount = 0;
313
+ this.#events.emit(WatchEvent.INC_RESYNC_FAILURE_COUNT, this.#resyncFailureCount);
296
314
  // Listen for events and call the callback function
297
315
  this.#stream.on("data", async (line) => {
298
316
  try {
@@ -348,23 +366,25 @@ class Watcher {
348
366
  // Reset the last seen time to now to allow the resync to be called again in case of failure
349
367
  this.#lastSeenTime = now;
350
368
  // If there are more attempts, retry the watch (undefined is unlimited retries)
351
- if (this.#watchCfg.retryMax === undefined || this.#watchCfg.retryMax > this.#retryCount) {
369
+ if (this.#watchCfg.resyncFailureMax === undefined ||
370
+ this.#watchCfg.resyncFailureMax > this.#resyncFailureCount) {
352
371
  // Increment the retry count
353
- this.#retryCount++;
372
+ this.#resyncFailureCount++;
373
+ this.#events.emit(WatchEvent.INC_RESYNC_FAILURE_COUNT, this.#resyncFailureCount);
354
374
  if (this.#pendingReconnect) {
355
375
  // wait for the connection to be re-established
356
376
  this.#events.emit(WatchEvent.RECONNECT_PENDING);
357
377
  }
358
378
  else {
359
379
  this.#pendingReconnect = true;
360
- this.#events.emit(WatchEvent.RECONNECT, this.#retryCount);
380
+ this.#events.emit(WatchEvent.RECONNECT, this.#resyncFailureCount);
361
381
  this.#streamCleanup();
362
382
  void this.#watch();
363
383
  }
364
384
  }
365
385
  else {
366
386
  // Otherwise, call the finally function if it exists
367
- this.#events.emit(WatchEvent.GIVE_UP, new Error(`Retry limit (${this.#watchCfg.retryMax}) exceeded, giving up`));
387
+ this.#events.emit(WatchEvent.GIVE_UP, new Error(`Retry limit (${this.#watchCfg.resyncFailureMax}) exceeded, giving up`));
368
388
  this.close();
369
389
  }
370
390
  }
@@ -116,13 +116,13 @@ const types_1 = require("./types");
116
116
  });
117
117
  (0, globals_1.it)("should return the cache id", () => {
118
118
  watcher = (0, _1.K8s)(__1.kind.Pod).Watch(evtMock, {
119
- retryDelaySec: 1,
119
+ resyncDelaySec: 1,
120
120
  });
121
121
  (0, globals_1.expect)(watcher.getCacheID()).toEqual("d69b75a611");
122
122
  });
123
123
  (0, globals_1.it)("should handle the CONNECT event", done => {
124
124
  watcher = (0, _1.K8s)(__1.kind.Pod).Watch(evtMock, {
125
- retryDelaySec: 1,
125
+ resyncDelaySec: 1,
126
126
  });
127
127
  setupAndStartWatcher(__1.WatchEvent.CONNECT, () => {
128
128
  done();
@@ -130,7 +130,7 @@ const types_1 = require("./types");
130
130
  });
131
131
  (0, globals_1.it)("should handle the DATA event", done => {
132
132
  watcher = (0, _1.K8s)(__1.kind.Pod).Watch(evtMock, {
133
- retryDelaySec: 1,
133
+ resyncDelaySec: 1,
134
134
  });
135
135
  setupAndStartWatcher(__1.WatchEvent.DATA, (pod, phase) => {
136
136
  (0, globals_1.expect)(pod.metadata?.name).toEqual(`pod-0`);
@@ -155,7 +155,7 @@ const types_1 = require("./types");
155
155
  .query({ watch: "true", resourceVersion: "45" })
156
156
  .replyWithError("Something bad happened");
157
157
  watcher = (0, _1.K8s)(__1.kind.Pod).Watch(evtMock, {
158
- retryDelaySec: 1,
158
+ resyncDelaySec: 1,
159
159
  });
160
160
  setupAndStartWatcher(__1.WatchEvent.NETWORK_ERROR, error => {
161
161
  (0, globals_1.expect)(error.message).toEqual("request to http://jest-test:8080/api/v1/pods?watch=true&resourceVersion=45 failed, reason: Something bad happened");
@@ -179,7 +179,7 @@ const types_1 = require("./types");
179
179
  .query({ watch: "true", resourceVersion: "65" })
180
180
  .replyWithError("Something bad happened");
181
181
  watcher = (0, _1.K8s)(__1.kind.Pod).Watch(evtMock, {
182
- retryDelaySec: 0.01,
182
+ resyncDelaySec: 0.01,
183
183
  });
184
184
  setupAndStartWatcher(__1.WatchEvent.RECONNECT, count => {
185
185
  (0, globals_1.expect)(count).toEqual(1);
@@ -188,8 +188,8 @@ const types_1 = require("./types");
188
188
  });
189
189
  (0, globals_1.it)("should perform a resync after the resync interval", done => {
190
190
  watcher = (0, _1.K8s)(__1.kind.Pod).Watch(evtMock, {
191
- retryDelaySec: 0.01,
192
- resyncIntervalSec: 0.01,
191
+ resyncDelaySec: 0.01,
192
+ lastSeenLimitSeconds: 0.01,
193
193
  });
194
194
  setupAndStartWatcher(__1.WatchEvent.RECONNECT, count => {
195
195
  (0, globals_1.expect)(count).toEqual(1);
@@ -213,9 +213,9 @@ const types_1 = require("./types");
213
213
  .query({ watch: "true", resourceVersion: "75" })
214
214
  .replyWithError("Something bad happened");
215
215
  watcher = (0, _1.K8s)(__1.kind.Pod).Watch(evtMock, {
216
- retryMax: 1,
217
- retryDelaySec: 0.01,
218
- resyncIntervalSec: 1,
216
+ resyncFailureMax: 1,
217
+ resyncDelaySec: 0.01,
218
+ lastSeenLimitSeconds: 1,
219
219
  });
220
220
  setupAndStartWatcher(__1.WatchEvent.GIVE_UP, error => {
221
221
  (0, globals_1.expect)(error.message).toContain("Retry limit (1) exceeded, giving up");
package/dist/generate.js CHANGED
@@ -133,7 +133,7 @@ async function readOrFetchCrd(opts) {
133
133
  return (0, client_node_1.loadAllYaml)(payload);
134
134
  }
135
135
  }
136
- catch (e) {
136
+ catch {
137
137
  // Ignore errors
138
138
  }
139
139
  // Next try to parse the source as a URL
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "kubernetes-fluent-client",
3
- "version": "2.6.5",
3
+ "version": "3.0.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",
@@ -52,14 +52,14 @@
52
52
  "@types/readable-stream": "4.0.15",
53
53
  "@types/urijs": "^1.19.25",
54
54
  "@types/yargs": "17.0.32",
55
- "@typescript-eslint/eslint-plugin": "7.17.0",
56
- "@typescript-eslint/parser": "7.17.0",
57
- "eslint-plugin-jsdoc": "48.8.3",
55
+ "@typescript-eslint/eslint-plugin": "8.0.0",
56
+ "@typescript-eslint/parser": "8.0.0",
57
+ "eslint-plugin-jsdoc": "48.10.2",
58
58
  "jest": "29.7.0",
59
59
  "nock": "13.5.4",
60
60
  "prettier": "3.3.3",
61
61
  "semantic-release": "24.0.0",
62
- "ts-jest": "29.2.3",
62
+ "ts-jest": "29.2.4",
63
63
  "typescript": "5.5.4"
64
64
  },
65
65
  "release": {
@@ -13,7 +13,7 @@ import { ApplyCfg, FetchMethods, Filters, K8sInit, Paths, WatchAction } from "./
13
13
  import { k8sCfg, k8sExec } from "./utils";
14
14
  import { WatchCfg, Watcher } from "./watch";
15
15
  import { hasLogs } from "../helpers";
16
- import { Pod, Service, ReplicaSet } from "../upstream";
16
+ import { Pod, type Service, type ReplicaSet } from "../upstream";
17
17
  /**
18
18
  * Kubernetes fluent API inspired by Kubectl. Pass in a model, then call filters and actions on it.
19
19
  *
@@ -138,7 +138,7 @@ export function K8s<T extends GenericClass, K extends KubernetesObject = Instanc
138
138
  } else {
139
139
  podList.push(object);
140
140
  }
141
- } catch (e) {
141
+ } catch {
142
142
  throw new Error(`Failed to get logs in KFC Logs function`);
143
143
  }
144
144
 
@@ -138,14 +138,14 @@ describe("Watcher", () => {
138
138
 
139
139
  it("should return the cache id", () => {
140
140
  watcher = K8s(kind.Pod).Watch(evtMock, {
141
- retryDelaySec: 1,
141
+ resyncDelaySec: 1,
142
142
  });
143
143
  expect(watcher.getCacheID()).toEqual("d69b75a611");
144
144
  });
145
145
 
146
146
  it("should handle the CONNECT event", done => {
147
147
  watcher = K8s(kind.Pod).Watch(evtMock, {
148
- retryDelaySec: 1,
148
+ resyncDelaySec: 1,
149
149
  });
150
150
  setupAndStartWatcher(WatchEvent.CONNECT, () => {
151
151
  done();
@@ -154,7 +154,7 @@ describe("Watcher", () => {
154
154
 
155
155
  it("should handle the DATA event", done => {
156
156
  watcher = K8s(kind.Pod).Watch(evtMock, {
157
- retryDelaySec: 1,
157
+ resyncDelaySec: 1,
158
158
  });
159
159
  setupAndStartWatcher(WatchEvent.DATA, (pod, phase) => {
160
160
  expect(pod.metadata?.name).toEqual(`pod-0`);
@@ -181,7 +181,7 @@ describe("Watcher", () => {
181
181
  .replyWithError("Something bad happened");
182
182
 
183
183
  watcher = K8s(kind.Pod).Watch(evtMock, {
184
- retryDelaySec: 1,
184
+ resyncDelaySec: 1,
185
185
  });
186
186
 
187
187
  setupAndStartWatcher(WatchEvent.NETWORK_ERROR, error => {
@@ -210,7 +210,7 @@ describe("Watcher", () => {
210
210
  .replyWithError("Something bad happened");
211
211
 
212
212
  watcher = K8s(kind.Pod).Watch(evtMock, {
213
- retryDelaySec: 0.01,
213
+ resyncDelaySec: 0.01,
214
214
  });
215
215
 
216
216
  setupAndStartWatcher(WatchEvent.RECONNECT, count => {
@@ -221,8 +221,8 @@ describe("Watcher", () => {
221
221
 
222
222
  it("should perform a resync after the resync interval", done => {
223
223
  watcher = K8s(kind.Pod).Watch(evtMock, {
224
- retryDelaySec: 0.01,
225
- resyncIntervalSec: 0.01,
224
+ resyncDelaySec: 0.01,
225
+ lastSeenLimitSeconds: 0.01,
226
226
  });
227
227
 
228
228
  setupAndStartWatcher(WatchEvent.RECONNECT, count => {
@@ -249,9 +249,9 @@ describe("Watcher", () => {
249
249
  .replyWithError("Something bad happened");
250
250
 
251
251
  watcher = K8s(kind.Pod).Watch(evtMock, {
252
- retryMax: 1,
253
- retryDelaySec: 0.01,
254
- resyncIntervalSec: 1,
252
+ resyncFailureMax: 1,
253
+ resyncDelaySec: 0.01,
254
+ lastSeenLimitSeconds: 1,
255
255
  });
256
256
 
257
257
  setupAndStartWatcher(WatchEvent.GIVE_UP, error => {
@@ -34,18 +34,24 @@ export enum WatchEvent {
34
34
  LIST = "list",
35
35
  /** List operation error */
36
36
  LIST_ERROR = "list_error",
37
+ /** Cache Misses */
38
+ CACHE_MISS = "cache_miss",
39
+ /** Increment resync failure count */
40
+ INC_RESYNC_FAILURE_COUNT = "inc_resync_failure_count",
41
+ /** Initialize a relist window */
42
+ INIT_CACHE_MISS = "init_cache_miss",
37
43
  }
38
44
 
39
45
  /** Configuration for the watch function. */
40
46
  export type WatchCfg = {
41
47
  /** The maximum number of times to retry the watch, the retry count is reset on success. Unlimited retries if not specified. */
42
- retryMax?: number;
43
- /** Seconds between each retry check. Defaults to 5. */
44
- retryDelaySec?: number;
48
+ resyncFailureMax?: number;
49
+ /** Seconds between each resync check. Defaults to 5. */
50
+ resyncDelaySec?: number;
45
51
  /** Amount of seconds to wait before relisting the watch list. Defaults to 600 (10 minutes). */
46
52
  relistIntervalSec?: number;
47
- /** Amount of seconds to wait before a forced-resyncing of the watch list. Defaults to 300 (5 minutes). */
48
- resyncIntervalSec?: number;
53
+ /** Max amount of seconds to go without receiving an event before reconciliation starts. Defaults to 300 (5 minutes). */
54
+ lastSeenLimitSeconds?: number;
49
55
  };
50
56
 
51
57
  const NONE = 50;
@@ -58,6 +64,7 @@ export class Watcher<T extends GenericClass> {
58
64
  #filters: Filters;
59
65
  #callback: WatchAction<T>;
60
66
  #watchCfg: WatchCfg;
67
+ #latestRelistWindow: string = "";
61
68
 
62
69
  // Track the last time data was received
63
70
  #lastSeenTime = NONE;
@@ -67,7 +74,7 @@ export class Watcher<T extends GenericClass> {
67
74
  #abortController: AbortController;
68
75
 
69
76
  // Track the number of retries
70
- #retryCount = 0;
77
+ #resyncFailureCount = 0;
71
78
 
72
79
  // Create a stream to read the response body
73
80
  #stream?: byline.LineStream;
@@ -104,25 +111,35 @@ export class Watcher<T extends GenericClass> {
104
111
  */
105
112
  constructor(model: T, filters: Filters, callback: WatchAction<T>, watchCfg: WatchCfg = {}) {
106
113
  // Set the retry delay to 5 seconds if not specified
107
- watchCfg.retryDelaySec ??= 5;
114
+ watchCfg.resyncDelaySec ??= 5;
108
115
 
109
116
  // Set the relist interval to 30 minutes if not specified
110
117
  watchCfg.relistIntervalSec ??= 1800;
111
118
 
112
119
  // Set the resync interval to 10 minutes if not specified
113
- watchCfg.resyncIntervalSec ??= 600;
120
+ watchCfg.lastSeenLimitSeconds ??= 600;
114
121
 
115
122
  // Set the last seen limit to the resync interval
116
- this.#lastSeenLimit = watchCfg.resyncIntervalSec * 1000;
123
+ this.#lastSeenLimit = watchCfg.lastSeenLimitSeconds * 1000;
124
+
125
+ // Set the latest relist interval to now
126
+ this.#latestRelistWindow = new Date().toISOString();
117
127
 
118
128
  // Add random jitter to the relist/resync intervals (up to 1 second)
119
129
  const jitter = Math.floor(Math.random() * 1000);
120
130
 
121
131
  // Check every relist interval for cache staleness
122
- this.$relistTimer = setInterval(this.#list, watchCfg.relistIntervalSec * 1000 + jitter);
123
-
124
- // Rebuild the watch every retry delay interval
125
- this.#resyncTimer = setInterval(this.#checkResync, watchCfg.retryDelaySec * 1000 + jitter);
132
+ this.$relistTimer = setInterval(
133
+ () => {
134
+ this.#latestRelistWindow = new Date().toISOString();
135
+ this.#events.emit(WatchEvent.INIT_CACHE_MISS, this.#latestRelistWindow);
136
+ void this.#list();
137
+ },
138
+ watchCfg.relistIntervalSec * 1000 + jitter,
139
+ );
140
+
141
+ // Rebuild the watch every resync delay interval
142
+ this.#resyncTimer = setInterval(this.#checkResync, watchCfg.resyncDelaySec * 1000 + jitter);
126
143
 
127
144
  // Bind class properties
128
145
  this.#model = model;
@@ -140,6 +157,7 @@ export class Watcher<T extends GenericClass> {
140
157
  * @returns The AbortController for the watch.
141
158
  */
142
159
  public async start(): Promise<AbortController> {
160
+ this.#events.emit(WatchEvent.INIT_CACHE_MISS, this.#latestRelistWindow);
143
161
  await this.#watch();
144
162
  return this.#abortController;
145
163
  }
@@ -267,6 +285,7 @@ export class Watcher<T extends GenericClass> {
267
285
 
268
286
  // If the item does not exist, it is new and should be added
269
287
  if (!alreadyExists) {
288
+ this.#events.emit(WatchEvent.CACHE_MISS, this.#latestRelistWindow);
270
289
  // Send added event. Use void here because we don't care about the result (no consequences here if it fails)
271
290
  void this.#process(item, WatchPhase.Added);
272
291
  continue;
@@ -278,6 +297,7 @@ export class Watcher<T extends GenericClass> {
278
297
 
279
298
  // Check if the resource version is newer than the cached version
280
299
  if (itemRV > cachedRV) {
300
+ this.#events.emit(WatchEvent.CACHE_MISS, this.#latestRelistWindow);
281
301
  // Send a modified event if the resource version has changed
282
302
  void this.#process(item, WatchPhase.Modified);
283
303
  }
@@ -291,6 +311,7 @@ export class Watcher<T extends GenericClass> {
291
311
  } else {
292
312
  // Otherwise, process the removed items
293
313
  for (const item of removedItems.values()) {
314
+ this.#events.emit(WatchEvent.CACHE_MISS, this.#latestRelistWindow);
294
315
  void this.#process(item, WatchPhase.Deleted);
295
316
  }
296
317
  }
@@ -362,7 +383,8 @@ export class Watcher<T extends GenericClass> {
362
383
  const { body } = response;
363
384
 
364
385
  // Reset the retry count
365
- this.#retryCount = 0;
386
+ this.#resyncFailureCount = 0;
387
+ this.#events.emit(WatchEvent.INC_RESYNC_FAILURE_COUNT, this.#resyncFailureCount);
366
388
 
367
389
  // Listen for events and call the callback function
368
390
  this.#stream.on("data", async line => {
@@ -430,16 +452,20 @@ export class Watcher<T extends GenericClass> {
430
452
  this.#lastSeenTime = now;
431
453
 
432
454
  // If there are more attempts, retry the watch (undefined is unlimited retries)
433
- if (this.#watchCfg.retryMax === undefined || this.#watchCfg.retryMax > this.#retryCount) {
455
+ if (
456
+ this.#watchCfg.resyncFailureMax === undefined ||
457
+ this.#watchCfg.resyncFailureMax > this.#resyncFailureCount
458
+ ) {
434
459
  // Increment the retry count
435
- this.#retryCount++;
460
+ this.#resyncFailureCount++;
461
+ this.#events.emit(WatchEvent.INC_RESYNC_FAILURE_COUNT, this.#resyncFailureCount);
436
462
 
437
463
  if (this.#pendingReconnect) {
438
464
  // wait for the connection to be re-established
439
465
  this.#events.emit(WatchEvent.RECONNECT_PENDING);
440
466
  } else {
441
467
  this.#pendingReconnect = true;
442
- this.#events.emit(WatchEvent.RECONNECT, this.#retryCount);
468
+ this.#events.emit(WatchEvent.RECONNECT, this.#resyncFailureCount);
443
469
  this.#streamCleanup();
444
470
 
445
471
  void this.#watch();
@@ -448,7 +474,7 @@ export class Watcher<T extends GenericClass> {
448
474
  // Otherwise, call the finally function if it exists
449
475
  this.#events.emit(
450
476
  WatchEvent.GIVE_UP,
451
- new Error(`Retry limit (${this.#watchCfg.retryMax}) exceeded, giving up`),
477
+ new Error(`Retry limit (${this.#watchCfg.resyncFailureMax}) exceeded, giving up`),
452
478
  );
453
479
  this.close();
454
480
  }
package/src/generate.ts CHANGED
@@ -160,7 +160,7 @@ async function readOrFetchCrd(opts: GenerateOptions): Promise<CustomResourceDefi
160
160
  const payload = fs.readFileSync(filePath, "utf8");
161
161
  return loadAllYaml(payload) as CustomResourceDefinition[];
162
162
  }
163
- } catch (e) {
163
+ } catch {
164
164
  // Ignore errors
165
165
  }
166
166