kubernetes-fluent-client 2.5.1 → 2.6.0

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.
@@ -1,5 +1,6 @@
1
1
  /// <reference types="node" />
2
2
  /// <reference types="node" />
3
+ /// <reference types="node" />
3
4
  import { EventEmitter } from "events";
4
5
  import { GenericClass } from "../types";
5
6
  import { Filters, WatchAction } from "./types";
@@ -16,35 +17,32 @@ export declare enum WatchEvent {
16
17
  GIVE_UP = "give_up",
17
18
  /** Abort is called */
18
19
  ABORT = "abort",
19
- /** Resync is called */
20
- RESYNC = "resync",
21
20
  /** Data is received and decoded */
22
21
  DATA = "data",
23
- /** Bookmark is received */
24
- BOOKMARK = "bookmark",
25
- /** ResourceVersion is updated */
26
- RESOURCE_VERSION = "resource_version",
27
22
  /** 410 (old resource version) occurs */
28
23
  OLD_RESOURCE_VERSION = "old_resource_version",
29
24
  /** A reconnect is already pending */
30
- RECONNECT_PENDING = "reconnect_pending"
25
+ RECONNECT_PENDING = "reconnect_pending",
26
+ /** Resource list operation run */
27
+ LIST = "list",
28
+ /** List operation error */
29
+ LIST_ERROR = "list_error"
31
30
  }
32
31
  /** Configuration for the watch function. */
33
32
  export type WatchCfg = {
34
- /** Whether to allow watch bookmarks. */
35
- allowWatchBookmarks?: boolean;
36
- /** The resource version to start the watch at, this will be updated on each event. */
37
- resourceVersion?: string;
38
33
  /** The maximum number of times to retry the watch, the retry count is reset on success. Unlimited retries if not specified. */
39
34
  retryMax?: number;
40
- /** The delay between retries in seconds. Defaults to 10 seconds. */
35
+ /** Seconds between each retry check. Defaults to 5. */
41
36
  retryDelaySec?: number;
37
+ /** Amount of seconds to wait before relisting the watch list. Defaults to 600 (10 minutes). */
38
+ relistIntervalSec?: number;
42
39
  /** Amount of seconds to wait before a forced-resyncing of the watch list. Defaults to 300 (5 minutes). */
43
40
  resyncIntervalSec?: number;
44
41
  };
45
42
  /** A wrapper around the Kubernetes watch API. */
46
43
  export declare class Watcher<T extends GenericClass> {
47
44
  #private;
45
+ $relistTimer?: NodeJS.Timeout;
48
46
  /**
49
47
  * Setup a Kubernetes watcher for the specified model and filters. The callback function will be called for each event received.
50
48
  * The watch can be aborted by calling {@link Watcher.close} or by calling abort() on the AbortController returned by {@link Watcher.start}.
@@ -73,18 +71,6 @@ export declare class Watcher<T extends GenericClass> {
73
71
  * @returns the watch CacheID
74
72
  */
75
73
  getCacheID(): string;
76
- /**
77
- * Get the current resource version.
78
- *
79
- * @returns the current resource version
80
- */
81
- get resourceVersion(): string | undefined;
82
- /**
83
- * Set the current resource version.
84
- *
85
- * @param resourceVersion - the new resource version
86
- */
87
- set resourceVersion(resourceVersion: string | undefined);
88
74
  /**
89
75
  * Subscribe to watch events. This is an EventEmitter that emits the following events:
90
76
  *
@@ -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;AAGtC,OAAO,EAAE,YAAY,EAAE,MAAM,UAAU,CAAC;AACxC,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,uBAAuB;IACvB,MAAM,WAAW;IACjB,mCAAmC;IACnC,IAAI,SAAS;IACb,2BAA2B;IAC3B,QAAQ,aAAa;IACrB,iCAAiC;IACjC,gBAAgB,qBAAqB;IACrC,wCAAwC;IACxC,oBAAoB,yBAAyB;IAC7C,qCAAqC;IACrC,iBAAiB,sBAAsB;CACxC;AAED,4CAA4C;AAC5C,MAAM,MAAM,QAAQ,GAAG;IACrB,wCAAwC;IACxC,mBAAmB,CAAC,EAAE,OAAO,CAAC;IAC9B,sFAAsF;IACtF,eAAe,CAAC,EAAE,MAAM,CAAC;IACzB,+HAA+H;IAC/H,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,oEAAoE;IACpE,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB,0GAA0G;IAC1G,iBAAiB,CAAC,EAAE,MAAM,CAAC;CAC5B,CAAC;AAEF,iDAAiD;AACjD,qBAAa,OAAO,CAAC,CAAC,SAAS,YAAY;;IAyBzC;;;;;;;;;;;OAWG;gBACS,KAAK,EAAE,CAAC,EAAE,OAAO,EAAE,OAAO,EAAE,QAAQ,EAAE,WAAW,CAAC,CAAC,CAAC,EAAE,QAAQ,GAAE,QAAa;IAiBzF;;;;OAIG;IACU,KAAK,IAAI,OAAO,CAAC,eAAe,CAAC;IAK9C,gGAAgG;IACzF,KAAK;IAMZ;;;;;OAKG;IACI,UAAU;IAWjB;;;;OAIG;IACH,IAAW,eAAe,IASkB,MAAM,GAAG,SAAS,CAP7D;IAED;;;;OAIG;IACH,IAAW,eAAe,CAAC,eAAe,EAAE,MAAM,GAAG,SAAS,EAE7D;IAED;;;;;;OAMG;IACH,IAAW,MAAM,IAAI,YAAY,CAEhC;CAwOF"}
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"}
@@ -10,6 +10,7 @@ const byline_1 = __importDefault(require("byline"));
10
10
  const crypto_1 = require("crypto");
11
11
  const events_1 = require("events");
12
12
  const node_fetch_1 = __importDefault(require("node-fetch"));
13
+ const fetch_1 = require("../fetch");
13
14
  const types_1 = require("./types");
14
15
  const utils_1 = require("./utils");
15
16
  var WatchEvent;
@@ -26,19 +27,19 @@ var WatchEvent;
26
27
  WatchEvent["GIVE_UP"] = "give_up";
27
28
  /** Abort is called */
28
29
  WatchEvent["ABORT"] = "abort";
29
- /** Resync is called */
30
- WatchEvent["RESYNC"] = "resync";
31
30
  /** Data is received and decoded */
32
31
  WatchEvent["DATA"] = "data";
33
- /** Bookmark is received */
34
- WatchEvent["BOOKMARK"] = "bookmark";
35
- /** ResourceVersion is updated */
36
- WatchEvent["RESOURCE_VERSION"] = "resource_version";
37
32
  /** 410 (old resource version) occurs */
38
33
  WatchEvent["OLD_RESOURCE_VERSION"] = "old_resource_version";
39
34
  /** A reconnect is already pending */
40
35
  WatchEvent["RECONNECT_PENDING"] = "reconnect_pending";
36
+ /** Resource list operation run */
37
+ WatchEvent["LIST"] = "list";
38
+ /** List operation error */
39
+ WatchEvent["LIST_ERROR"] = "list_error";
41
40
  })(WatchEvent || (exports.WatchEvent = WatchEvent = {}));
41
+ const NONE = 50;
42
+ const OVERRIDE = 100;
42
43
  /** A wrapper around the Kubernetes watch API. */
43
44
  class Watcher {
44
45
  // User-provided properties
@@ -46,6 +47,9 @@ class Watcher {
46
47
  #filters;
47
48
  #callback;
48
49
  #watchCfg;
50
+ // Track the last time data was received
51
+ #lastSeenTime = NONE;
52
+ #lastSeenLimit;
49
53
  // Create a wrapped AbortController to allow the watch to be aborted externally
50
54
  #abortController;
51
55
  // Track the number of retries
@@ -54,10 +58,16 @@ class Watcher {
54
58
  #stream;
55
59
  // Create an EventEmitter to emit events
56
60
  #events = new events_1.EventEmitter();
61
+ // Create a timer to relist the watch
62
+ $relistTimer;
57
63
  // Create a timer to resync the watch
58
64
  #resyncTimer;
59
65
  // Track if a reconnect is pending
60
66
  #pendingReconnect = false;
67
+ // The resource version to start the watch at, this will be updated after the list operation.
68
+ #resourceVersion;
69
+ // Track the list of items in the cache
70
+ #cache = new Map();
61
71
  /**
62
72
  * Setup a Kubernetes watcher for the specified model and filters. The callback function will be called for each event received.
63
73
  * The watch can be aborted by calling {@link Watcher.close} or by calling abort() on the AbortController returned by {@link Watcher.start}.
@@ -71,10 +81,20 @@ class Watcher {
71
81
  * @param watchCfg - (optional) watch configuration
72
82
  */
73
83
  constructor(model, filters, callback, watchCfg = {}) {
74
- // Set the retry delay to 10 seconds if not specified
75
- watchCfg.retryDelaySec ??= 10;
76
- // Set the resync interval to 5 minutes if not specified
77
- watchCfg.resyncIntervalSec ??= 300;
84
+ // Set the retry delay to 5 seconds if not specified
85
+ watchCfg.retryDelaySec ??= 5;
86
+ // Set the relist interval to 30 minutes if not specified
87
+ watchCfg.relistIntervalSec ??= 1800;
88
+ // Set the resync interval to 10 minutes if not specified
89
+ watchCfg.resyncIntervalSec ??= 600;
90
+ // Set the last seen limit to the resync interval
91
+ this.#lastSeenLimit = watchCfg.resyncIntervalSec * 1000;
92
+ // Add random jitter to the relist/resync intervals (up to 1 second)
93
+ const jitter = Math.floor(Math.random() * 1000);
94
+ // 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);
78
98
  // Bind class properties
79
99
  this.#model = model;
80
100
  this.#filters = filters;
@@ -89,13 +109,14 @@ class Watcher {
89
109
  * @returns The AbortController for the watch.
90
110
  */
91
111
  async start() {
92
- await this.#runner();
112
+ await this.#watch();
93
113
  return this.#abortController;
94
114
  }
95
115
  /** Close the watch. Also available on the AbortController returned by {@link Watcher.start}. */
96
116
  close() {
97
- clearTimeout(this.#resyncTimer);
98
- this.#cleanup();
117
+ clearInterval(this.$relistTimer);
118
+ clearInterval(this.#resyncTimer);
119
+ this.#streamCleanup();
99
120
  this.#abortController.abort();
100
121
  }
101
122
  /**
@@ -113,22 +134,6 @@ class Watcher {
113
134
  .digest("hex")
114
135
  .substring(0, 10);
115
136
  }
116
- /**
117
- * Get the current resource version.
118
- *
119
- * @returns the current resource version
120
- */
121
- get resourceVersion() {
122
- return this.#watchCfg.resourceVersion;
123
- }
124
- /**
125
- * Set the current resource version.
126
- *
127
- * @param resourceVersion - the new resource version
128
- */
129
- set resourceVersion(resourceVersion) {
130
- this.#watchCfg.resourceVersion = resourceVersion;
131
- }
132
137
  /**
133
138
  * Subscribe to watch events. This is an EventEmitter that emits the following events:
134
139
  *
@@ -142,45 +147,146 @@ class Watcher {
142
147
  /**
143
148
  * Build the URL and request options for the watch.
144
149
  *
150
+ * @param isWatch - whether the request is for a watch operation
151
+ * @param resourceVersion - the resource version to use for the watch
152
+ * @param continueToken - the continue token for the watch
153
+ *
145
154
  * @returns the URL and request options
146
155
  */
147
- #buildURL = async () => {
156
+ #buildURL = async (isWatch, resourceVersion, continueToken) => {
148
157
  // Build the path and query params for the resource, excluding the name
149
158
  const { opts, serverUrl } = await (0, utils_1.k8sCfg)("GET");
150
159
  const url = (0, utils_1.pathBuilder)(serverUrl, this.#model, this.#filters, true);
151
160
  // Enable the watch query param
152
- url.searchParams.set("watch", "true");
161
+ if (isWatch) {
162
+ url.searchParams.set("watch", "true");
163
+ }
164
+ if (continueToken) {
165
+ url.searchParams.set("continue", continueToken);
166
+ }
153
167
  // If a name is specified, add it to the query params
154
168
  if (this.#filters.name) {
155
169
  url.searchParams.set("fieldSelector", `metadata.name=${this.#filters.name}`);
156
170
  }
157
171
  // If a resource version is specified, add it to the query params
158
- if (this.#watchCfg.resourceVersion) {
159
- url.searchParams.set("resourceVersion", this.#watchCfg.resourceVersion);
172
+ if (resourceVersion) {
173
+ url.searchParams.set("resourceVersion", resourceVersion);
160
174
  }
161
- // Enable watch bookmarks
162
- url.searchParams.set("allowWatchBookmarks", this.#watchCfg.allowWatchBookmarks ? `${this.#watchCfg.allowWatchBookmarks}` : "true");
163
175
  // Add the abort signal to the request options
164
176
  opts.signal = this.#abortController.signal;
165
177
  return { opts, url };
166
178
  };
167
- /** Run the watch. */
168
- #runner = async () => {
179
+ /**
180
+ * Retrieve the list of resources and process the events.
181
+ *
182
+ * @param continueToken - the continue token for the list
183
+ * @param removedItems - the list of items that have been removed
184
+ */
185
+ #list = async (continueToken, removedItems) => {
169
186
  try {
187
+ const { opts, url } = await this.#buildURL(false, undefined, continueToken);
188
+ // Make the request to list the resources
189
+ const response = await (0, fetch_1.fetch)(url, opts);
190
+ const list = response.data;
191
+ // If the request fails, emit an error event and return
192
+ if (!response.ok) {
193
+ this.#events.emit(WatchEvent.LIST_ERROR, new Error(`list failed: ${response.status} ${response.statusText}`));
194
+ return;
195
+ }
196
+ // Gross hack, thanks upstream library :<
197
+ if (list.metadata.continue) {
198
+ continueToken = list.metadata.continue;
199
+ }
200
+ // Emit the list event
201
+ this.#events.emit(WatchEvent.LIST, list);
202
+ // Update the resource version from the list metadata
203
+ this.#resourceVersion = list.metadata?.resourceVersion;
204
+ // If removed items are not provided, clone the cache
205
+ removedItems = removedItems || new Map(this.#cache.entries());
206
+ // Process each item in the list
207
+ for (const item of list.items || []) {
208
+ const { uid } = item.metadata;
209
+ // Remove the item from the removed items list
210
+ const alreadyExists = removedItems.delete(uid);
211
+ // If the item does not exist, it is new and should be added
212
+ if (!alreadyExists) {
213
+ // Send added event. Use void here because we don't care about the result (no consequences here if it fails)
214
+ void this.#process(item, types_1.WatchPhase.Added);
215
+ continue;
216
+ }
217
+ // Check if the resource version has changed for items that already exist
218
+ const cachedRV = parseInt(this.#cache.get(uid)?.metadata?.resourceVersion);
219
+ const itemRV = parseInt(item.metadata.resourceVersion);
220
+ // Check if the resource version is newer than the cached version
221
+ if (itemRV > cachedRV) {
222
+ // Send a modified event if the resource version has changed
223
+ void this.#process(item, types_1.WatchPhase.Modified);
224
+ }
225
+ }
226
+ // If there is a continue token, call the list function again with the same removed items
227
+ if (continueToken) {
228
+ // If there is a continue token, call the list function again with the same removed items
229
+ // @todo: using all voids here is important for freshness, but is naive with regard to API load & pod resources
230
+ await this.#list(continueToken, removedItems);
231
+ }
232
+ else {
233
+ // Otherwise, process the removed items
234
+ for (const item of removedItems.values()) {
235
+ void this.#process(item, types_1.WatchPhase.Deleted);
236
+ }
237
+ }
238
+ }
239
+ catch (err) {
240
+ this.#events.emit(WatchEvent.LIST_ERROR, err);
241
+ }
242
+ };
243
+ /**
244
+ * Process the event payload.
245
+ *
246
+ * @param payload - the event payload
247
+ * @param phase - the event phase
248
+ */
249
+ #process = async (payload, phase) => {
250
+ try {
251
+ switch (phase) {
252
+ // If the event is added or modified, update the cache
253
+ case types_1.WatchPhase.Added:
254
+ case types_1.WatchPhase.Modified:
255
+ this.#cache.set(payload.metadata.uid, payload);
256
+ break;
257
+ // If the event is deleted, remove the item from the cache
258
+ case types_1.WatchPhase.Deleted:
259
+ this.#cache.delete(payload.metadata.uid);
260
+ break;
261
+ }
262
+ // Emit the data event
263
+ this.#events.emit(WatchEvent.DATA, payload, phase);
264
+ // Call the callback function with the parsed payload
265
+ await this.#callback(payload, phase);
266
+ }
267
+ catch (err) {
268
+ this.#events.emit(WatchEvent.DATA_ERROR, err);
269
+ }
270
+ };
271
+ /**
272
+ * Watch for changes to the resource.
273
+ */
274
+ #watch = async () => {
275
+ try {
276
+ // Start with a list operation
277
+ await this.#list();
170
278
  // Build the URL and request options
171
- const { opts, url } = await this.#buildURL();
279
+ const { opts, url } = await this.#buildURL(true, this.#resourceVersion);
172
280
  // Create a stream to read the response body
173
281
  this.#stream = byline_1.default.createStream();
174
282
  // Bind the stream events
175
283
  this.#stream.on("error", this.#errHandler);
176
- this.#stream.on("close", this.#cleanup);
177
- this.#stream.on("finish", this.#cleanup);
284
+ this.#stream.on("close", this.#streamCleanup);
285
+ this.#stream.on("finish", this.#streamCleanup);
178
286
  // Make the actual request
179
287
  const response = await (0, node_fetch_1.default)(url, { ...opts });
180
288
  // Reset the pending reconnect flag
181
289
  this.#pendingReconnect = false;
182
- // Reset the resync timer
183
- void this.#scheduleResync();
184
290
  // If the request is successful, start listening for events
185
291
  if (response.ok) {
186
292
  this.#events.emit(WatchEvent.CONNECT, url.pathname);
@@ -192,25 +298,17 @@ class Watcher {
192
298
  try {
193
299
  // Parse the event payload
194
300
  const { object: payload, type: phase } = JSON.parse(line);
195
- void this.#scheduleResync();
301
+ // Update the last seen time
302
+ this.#lastSeenTime = Date.now();
196
303
  // If the watch is too old, remove the resourceVersion and reload the watch
197
304
  if (phase === types_1.WatchPhase.Error && payload.code === 410) {
198
305
  throw {
199
306
  name: "TooOld",
200
- message: this.#watchCfg.resourceVersion,
307
+ message: this.#resourceVersion,
201
308
  };
202
309
  }
203
- // If the event is a bookmark, emit the event and skip the callback
204
- if (phase === types_1.WatchPhase.Bookmark) {
205
- this.#events.emit(WatchEvent.BOOKMARK, payload);
206
- }
207
- else {
208
- this.#events.emit(WatchEvent.DATA, payload, phase);
209
- // Call the callback function with the parsed payload
210
- await this.#callback(payload, phase);
211
- }
212
- // Update the resource version if the callback was successful
213
- this.#setResourceVersion(payload.metadata.resourceVersion);
310
+ // Process the event payload, do not update the resource version as that is handled by the list operation
311
+ await this.#process(payload, phase);
214
312
  }
215
313
  catch (err) {
216
314
  if (err.name === "TooOld") {
@@ -225,8 +323,8 @@ class Watcher {
225
323
  });
226
324
  // Bind the body events
227
325
  body.on("error", this.#errHandler);
228
- body.on("close", this.#cleanup);
229
- body.on("finish", this.#cleanup);
326
+ body.on("close", this.#streamCleanup);
327
+ body.on("finish", this.#streamCleanup);
230
328
  // Pipe the response body to the stream
231
329
  body.pipe(this.#stream);
232
330
  }
@@ -238,64 +336,38 @@ class Watcher {
238
336
  void this.#errHandler(e);
239
337
  }
240
338
  };
241
- /**
242
- * Resync the watch.
243
- *
244
- * @returns the error handler
245
- */
246
- #resync = () => this.#errHandler({
247
- name: "Resync",
248
- message: "Resync triggered by resyncIntervalSec",
249
- });
250
339
  /** Clear the resync timer and schedule a new one. */
251
- #scheduleResync = async () => {
252
- clearTimeout(this.#resyncTimer);
253
- this.#resyncTimer = setTimeout(this.#resync, this.#watchCfg.resyncIntervalSec * 1000);
254
- };
255
- /**
256
- * Update the resource version.
257
- *
258
- * @param resourceVersion - the new resource version
259
- */
260
- #setResourceVersion = (resourceVersion) => {
261
- this.#watchCfg.resourceVersion = resourceVersion;
262
- this.#events.emit(WatchEvent.RESOURCE_VERSION, resourceVersion);
263
- };
264
- /**
265
- * Reload the watch after an error.
266
- *
267
- * @param err - the error that occurred
268
- */
269
- #reconnect = async (err) => {
270
- // If there are more attempts, retry the watch (undefined is unlimited retries)
271
- if (this.#watchCfg.retryMax === undefined || this.#watchCfg.retryMax > this.#retryCount) {
272
- // Sleep for the specified delay, but check every 500ms if the watch has been aborted
273
- let delay = this.#watchCfg.retryDelaySec * 1000;
274
- while (delay > 0) {
275
- if (this.#abortController.signal.aborted) {
276
- return;
340
+ #checkResync = () => {
341
+ // Ignore if the last seen time is not set
342
+ if (this.#lastSeenTime === NONE) {
343
+ return;
344
+ }
345
+ const now = Date.now();
346
+ // If the last seen time is greater than the limit, trigger a resync
347
+ if (this.#lastSeenTime == OVERRIDE || now - this.#lastSeenTime > this.#lastSeenLimit) {
348
+ // Reset the last seen time to now to allow the resync to be called again in case of failure
349
+ this.#lastSeenTime = now;
350
+ // If there are more attempts, retry the watch (undefined is unlimited retries)
351
+ if (this.#watchCfg.retryMax === undefined || this.#watchCfg.retryMax > this.#retryCount) {
352
+ // Increment the retry count
353
+ this.#retryCount++;
354
+ if (this.#pendingReconnect) {
355
+ // wait for the connection to be re-established
356
+ this.#events.emit(WatchEvent.RECONNECT_PENDING);
357
+ }
358
+ else {
359
+ this.#pendingReconnect = true;
360
+ this.#events.emit(WatchEvent.RECONNECT, this.#retryCount);
361
+ this.#streamCleanup();
362
+ void this.#watch();
277
363
  }
278
- delay -= 500;
279
- await new Promise(resolve => setTimeout(resolve, 500));
280
- }
281
- this.#retryCount++;
282
- this.#events.emit(WatchEvent.RECONNECT, err, this.#retryCount);
283
- if (this.#pendingReconnect) {
284
- // wait for the connection to be re-established
285
- this.#events.emit(WatchEvent.RECONNECT_PENDING);
286
364
  }
287
365
  else {
288
- this.#pendingReconnect = true;
289
- this.#cleanup();
290
- // Retry the watch after the delay
291
- await this.#runner();
366
+ // 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`));
368
+ this.close();
292
369
  }
293
370
  }
294
- else {
295
- // Otherwise, call the finally function if it exists
296
- this.#events.emit(WatchEvent.GIVE_UP, err);
297
- this.close();
298
- }
299
371
  };
300
372
  /**
301
373
  * Handle errors from the stream.
@@ -305,26 +377,25 @@ class Watcher {
305
377
  #errHandler = async (err) => {
306
378
  switch (err.name) {
307
379
  case "AbortError":
308
- clearTimeout(this.#resyncTimer);
309
- this.#cleanup();
380
+ clearInterval(this.$relistTimer);
381
+ clearInterval(this.#resyncTimer);
382
+ this.#streamCleanup();
310
383
  this.#events.emit(WatchEvent.ABORT, err);
311
384
  return;
312
385
  case "TooOld":
313
386
  // Purge the resource version if it is too old
314
- this.#setResourceVersion(undefined);
387
+ this.#resourceVersion = undefined;
315
388
  this.#events.emit(WatchEvent.OLD_RESOURCE_VERSION, err.message);
316
389
  break;
317
- case "Resync":
318
- this.#events.emit(WatchEvent.RESYNC, err);
319
- break;
320
390
  default:
321
391
  this.#events.emit(WatchEvent.NETWORK_ERROR, err);
322
392
  break;
323
393
  }
324
- await this.#reconnect(err);
394
+ // Force a resync
395
+ this.#lastSeenTime = OVERRIDE;
325
396
  };
326
397
  /** Cleanup the stream and listeners. */
327
- #cleanup = () => {
398
+ #streamCleanup = () => {
328
399
  if (this.#stream) {
329
400
  this.#stream.removeAllListeners();
330
401
  this.#stream.destroy();