kubernetes-fluent-client 2.5.1 → 2.6.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/fluent/watch.d.ts +10 -24
- package/dist/fluent/watch.d.ts.map +1 -1
- package/dist/fluent/watch.js +188 -117
- package/dist/fluent/watch.spec.js +92 -66
- package/package.json +7 -6
- package/src/fluent/watch.spec.ts +96 -78
- package/src/fluent/watch.ts +227 -135
package/src/fluent/watch.ts
CHANGED
|
@@ -6,7 +6,8 @@ import { createHash } from "crypto";
|
|
|
6
6
|
import { EventEmitter } from "events";
|
|
7
7
|
import fetch from "node-fetch";
|
|
8
8
|
|
|
9
|
-
import {
|
|
9
|
+
import { fetch as wrappedFetch } from "../fetch";
|
|
10
|
+
import { GenericClass, KubernetesListObject } from "../types";
|
|
10
11
|
import { Filters, WatchAction, WatchPhase } from "./types";
|
|
11
12
|
import { k8sCfg, pathBuilder } from "./utils";
|
|
12
13
|
|
|
@@ -23,34 +24,33 @@ export enum WatchEvent {
|
|
|
23
24
|
GIVE_UP = "give_up",
|
|
24
25
|
/** Abort is called */
|
|
25
26
|
ABORT = "abort",
|
|
26
|
-
/** Resync is called */
|
|
27
|
-
RESYNC = "resync",
|
|
28
27
|
/** Data is received and decoded */
|
|
29
28
|
DATA = "data",
|
|
30
|
-
/** Bookmark is received */
|
|
31
|
-
BOOKMARK = "bookmark",
|
|
32
|
-
/** ResourceVersion is updated */
|
|
33
|
-
RESOURCE_VERSION = "resource_version",
|
|
34
29
|
/** 410 (old resource version) occurs */
|
|
35
30
|
OLD_RESOURCE_VERSION = "old_resource_version",
|
|
36
31
|
/** A reconnect is already pending */
|
|
37
32
|
RECONNECT_PENDING = "reconnect_pending",
|
|
33
|
+
/** Resource list operation run */
|
|
34
|
+
LIST = "list",
|
|
35
|
+
/** List operation error */
|
|
36
|
+
LIST_ERROR = "list_error",
|
|
38
37
|
}
|
|
39
38
|
|
|
40
39
|
/** Configuration for the watch function. */
|
|
41
40
|
export type WatchCfg = {
|
|
42
|
-
/** Whether to allow watch bookmarks. */
|
|
43
|
-
allowWatchBookmarks?: boolean;
|
|
44
|
-
/** The resource version to start the watch at, this will be updated on each event. */
|
|
45
|
-
resourceVersion?: string;
|
|
46
41
|
/** The maximum number of times to retry the watch, the retry count is reset on success. Unlimited retries if not specified. */
|
|
47
42
|
retryMax?: number;
|
|
48
|
-
/**
|
|
43
|
+
/** Seconds between each retry check. Defaults to 5. */
|
|
49
44
|
retryDelaySec?: number;
|
|
45
|
+
/** Amount of seconds to wait before relisting the watch list. Defaults to 600 (10 minutes). */
|
|
46
|
+
relistIntervalSec?: number;
|
|
50
47
|
/** Amount of seconds to wait before a forced-resyncing of the watch list. Defaults to 300 (5 minutes). */
|
|
51
48
|
resyncIntervalSec?: number;
|
|
52
49
|
};
|
|
53
50
|
|
|
51
|
+
const NONE = 50;
|
|
52
|
+
const OVERRIDE = 100;
|
|
53
|
+
|
|
54
54
|
/** A wrapper around the Kubernetes watch API. */
|
|
55
55
|
export class Watcher<T extends GenericClass> {
|
|
56
56
|
// User-provided properties
|
|
@@ -59,6 +59,10 @@ export class Watcher<T extends GenericClass> {
|
|
|
59
59
|
#callback: WatchAction<T>;
|
|
60
60
|
#watchCfg: WatchCfg;
|
|
61
61
|
|
|
62
|
+
// Track the last time data was received
|
|
63
|
+
#lastSeenTime = NONE;
|
|
64
|
+
#lastSeenLimit: number;
|
|
65
|
+
|
|
62
66
|
// Create a wrapped AbortController to allow the watch to be aborted externally
|
|
63
67
|
#abortController: AbortController;
|
|
64
68
|
|
|
@@ -71,12 +75,21 @@ export class Watcher<T extends GenericClass> {
|
|
|
71
75
|
// Create an EventEmitter to emit events
|
|
72
76
|
#events = new EventEmitter();
|
|
73
77
|
|
|
78
|
+
// Create a timer to relist the watch
|
|
79
|
+
$relistTimer?: NodeJS.Timeout;
|
|
80
|
+
|
|
74
81
|
// Create a timer to resync the watch
|
|
75
82
|
#resyncTimer?: NodeJS.Timeout;
|
|
76
83
|
|
|
77
84
|
// Track if a reconnect is pending
|
|
78
85
|
#pendingReconnect = false;
|
|
79
86
|
|
|
87
|
+
// The resource version to start the watch at, this will be updated after the list operation.
|
|
88
|
+
#resourceVersion?: string;
|
|
89
|
+
|
|
90
|
+
// Track the list of items in the cache
|
|
91
|
+
#cache = new Map<string, InstanceType<T>>();
|
|
92
|
+
|
|
80
93
|
/**
|
|
81
94
|
* Setup a Kubernetes watcher for the specified model and filters. The callback function will be called for each event received.
|
|
82
95
|
* The watch can be aborted by calling {@link Watcher.close} or by calling abort() on the AbortController returned by {@link Watcher.start}.
|
|
@@ -90,11 +103,26 @@ export class Watcher<T extends GenericClass> {
|
|
|
90
103
|
* @param watchCfg - (optional) watch configuration
|
|
91
104
|
*/
|
|
92
105
|
constructor(model: T, filters: Filters, callback: WatchAction<T>, watchCfg: WatchCfg = {}) {
|
|
93
|
-
// Set the retry delay to
|
|
94
|
-
watchCfg.retryDelaySec ??=
|
|
106
|
+
// Set the retry delay to 5 seconds if not specified
|
|
107
|
+
watchCfg.retryDelaySec ??= 5;
|
|
108
|
+
|
|
109
|
+
// Set the relist interval to 30 minutes if not specified
|
|
110
|
+
watchCfg.relistIntervalSec ??= 1800;
|
|
95
111
|
|
|
96
|
-
// Set the resync interval to
|
|
97
|
-
watchCfg.resyncIntervalSec ??=
|
|
112
|
+
// Set the resync interval to 10 minutes if not specified
|
|
113
|
+
watchCfg.resyncIntervalSec ??= 600;
|
|
114
|
+
|
|
115
|
+
// Set the last seen limit to the resync interval
|
|
116
|
+
this.#lastSeenLimit = watchCfg.resyncIntervalSec * 1000;
|
|
117
|
+
|
|
118
|
+
// Add random jitter to the relist/resync intervals (up to 1 second)
|
|
119
|
+
const jitter = Math.floor(Math.random() * 1000);
|
|
120
|
+
|
|
121
|
+
// 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);
|
|
98
126
|
|
|
99
127
|
// Bind class properties
|
|
100
128
|
this.#model = model;
|
|
@@ -112,14 +140,15 @@ export class Watcher<T extends GenericClass> {
|
|
|
112
140
|
* @returns The AbortController for the watch.
|
|
113
141
|
*/
|
|
114
142
|
public async start(): Promise<AbortController> {
|
|
115
|
-
await this.#
|
|
143
|
+
await this.#watch();
|
|
116
144
|
return this.#abortController;
|
|
117
145
|
}
|
|
118
146
|
|
|
119
147
|
/** Close the watch. Also available on the AbortController returned by {@link Watcher.start}. */
|
|
120
148
|
public close() {
|
|
121
|
-
|
|
122
|
-
this.#
|
|
149
|
+
clearInterval(this.$relistTimer);
|
|
150
|
+
clearInterval(this.#resyncTimer);
|
|
151
|
+
this.#streamCleanup();
|
|
123
152
|
this.#abortController.abort();
|
|
124
153
|
}
|
|
125
154
|
|
|
@@ -140,24 +169,6 @@ export class Watcher<T extends GenericClass> {
|
|
|
140
169
|
.substring(0, 10);
|
|
141
170
|
}
|
|
142
171
|
|
|
143
|
-
/**
|
|
144
|
-
* Get the current resource version.
|
|
145
|
-
*
|
|
146
|
-
* @returns the current resource version
|
|
147
|
-
*/
|
|
148
|
-
public get resourceVersion() {
|
|
149
|
-
return this.#watchCfg.resourceVersion;
|
|
150
|
-
}
|
|
151
|
-
|
|
152
|
-
/**
|
|
153
|
-
* Set the current resource version.
|
|
154
|
-
*
|
|
155
|
-
* @param resourceVersion - the new resource version
|
|
156
|
-
*/
|
|
157
|
-
public set resourceVersion(resourceVersion: string | undefined) {
|
|
158
|
-
this.#watchCfg.resourceVersion = resourceVersion;
|
|
159
|
-
}
|
|
160
|
-
|
|
161
172
|
/**
|
|
162
173
|
* Subscribe to watch events. This is an EventEmitter that emits the following events:
|
|
163
174
|
*
|
|
@@ -172,16 +183,26 @@ export class Watcher<T extends GenericClass> {
|
|
|
172
183
|
/**
|
|
173
184
|
* Build the URL and request options for the watch.
|
|
174
185
|
*
|
|
186
|
+
* @param isWatch - whether the request is for a watch operation
|
|
187
|
+
* @param resourceVersion - the resource version to use for the watch
|
|
188
|
+
* @param continueToken - the continue token for the watch
|
|
189
|
+
*
|
|
175
190
|
* @returns the URL and request options
|
|
176
191
|
*/
|
|
177
|
-
#buildURL = async () => {
|
|
192
|
+
#buildURL = async (isWatch: boolean, resourceVersion?: string, continueToken?: string) => {
|
|
178
193
|
// Build the path and query params for the resource, excluding the name
|
|
179
194
|
const { opts, serverUrl } = await k8sCfg("GET");
|
|
180
195
|
|
|
181
196
|
const url = pathBuilder(serverUrl, this.#model, this.#filters, true);
|
|
182
197
|
|
|
183
198
|
// Enable the watch query param
|
|
184
|
-
|
|
199
|
+
if (isWatch) {
|
|
200
|
+
url.searchParams.set("watch", "true");
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
if (continueToken) {
|
|
204
|
+
url.searchParams.set("continue", continueToken);
|
|
205
|
+
}
|
|
185
206
|
|
|
186
207
|
// If a name is specified, add it to the query params
|
|
187
208
|
if (this.#filters.name) {
|
|
@@ -189,35 +210,144 @@ export class Watcher<T extends GenericClass> {
|
|
|
189
210
|
}
|
|
190
211
|
|
|
191
212
|
// If a resource version is specified, add it to the query params
|
|
192
|
-
if (
|
|
193
|
-
url.searchParams.set("resourceVersion",
|
|
213
|
+
if (resourceVersion) {
|
|
214
|
+
url.searchParams.set("resourceVersion", resourceVersion);
|
|
194
215
|
}
|
|
195
216
|
|
|
196
|
-
// Enable watch bookmarks
|
|
197
|
-
url.searchParams.set(
|
|
198
|
-
"allowWatchBookmarks",
|
|
199
|
-
this.#watchCfg.allowWatchBookmarks ? `${this.#watchCfg.allowWatchBookmarks}` : "true",
|
|
200
|
-
);
|
|
201
|
-
|
|
202
217
|
// Add the abort signal to the request options
|
|
203
218
|
opts.signal = this.#abortController.signal;
|
|
204
219
|
|
|
205
220
|
return { opts, url };
|
|
206
221
|
};
|
|
207
222
|
|
|
208
|
-
/**
|
|
209
|
-
|
|
223
|
+
/**
|
|
224
|
+
* Retrieve the list of resources and process the events.
|
|
225
|
+
*
|
|
226
|
+
* @param continueToken - the continue token for the list
|
|
227
|
+
* @param removedItems - the list of items that have been removed
|
|
228
|
+
*/
|
|
229
|
+
#list = async (continueToken?: string, removedItems?: Map<string, InstanceType<T>>) => {
|
|
210
230
|
try {
|
|
231
|
+
const { opts, url } = await this.#buildURL(false, undefined, continueToken);
|
|
232
|
+
|
|
233
|
+
// Make the request to list the resources
|
|
234
|
+
const response = await wrappedFetch<KubernetesListObject<InstanceType<T>>>(url, opts);
|
|
235
|
+
const list = response.data;
|
|
236
|
+
|
|
237
|
+
// If the request fails, emit an error event and return
|
|
238
|
+
if (!response.ok) {
|
|
239
|
+
this.#events.emit(
|
|
240
|
+
WatchEvent.LIST_ERROR,
|
|
241
|
+
new Error(`list failed: ${response.status} ${response.statusText}`),
|
|
242
|
+
);
|
|
243
|
+
|
|
244
|
+
return;
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
// Gross hack, thanks upstream library :<
|
|
248
|
+
if ((list.metadata as { continue?: string }).continue) {
|
|
249
|
+
continueToken = (list.metadata as { continue?: string }).continue;
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
// Emit the list event
|
|
253
|
+
this.#events.emit(WatchEvent.LIST, list);
|
|
254
|
+
|
|
255
|
+
// Update the resource version from the list metadata
|
|
256
|
+
this.#resourceVersion = list.metadata?.resourceVersion;
|
|
257
|
+
|
|
258
|
+
// If removed items are not provided, clone the cache
|
|
259
|
+
removedItems = removedItems || new Map(this.#cache.entries());
|
|
260
|
+
|
|
261
|
+
// Process each item in the list
|
|
262
|
+
for (const item of list.items || []) {
|
|
263
|
+
const { uid } = item.metadata;
|
|
264
|
+
|
|
265
|
+
// Remove the item from the removed items list
|
|
266
|
+
const alreadyExists = removedItems.delete(uid);
|
|
267
|
+
|
|
268
|
+
// If the item does not exist, it is new and should be added
|
|
269
|
+
if (!alreadyExists) {
|
|
270
|
+
// Send added event. Use void here because we don't care about the result (no consequences here if it fails)
|
|
271
|
+
void this.#process(item, WatchPhase.Added);
|
|
272
|
+
continue;
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
// Check if the resource version has changed for items that already exist
|
|
276
|
+
const cachedRV = parseInt(this.#cache.get(uid)?.metadata?.resourceVersion);
|
|
277
|
+
const itemRV = parseInt(item.metadata.resourceVersion);
|
|
278
|
+
|
|
279
|
+
// Check if the resource version is newer than the cached version
|
|
280
|
+
if (itemRV > cachedRV) {
|
|
281
|
+
// Send a modified event if the resource version has changed
|
|
282
|
+
void this.#process(item, WatchPhase.Modified);
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
// If there is a continue token, call the list function again with the same removed items
|
|
287
|
+
if (continueToken) {
|
|
288
|
+
// If there is a continue token, call the list function again with the same removed items
|
|
289
|
+
// @todo: using all voids here is important for freshness, but is naive with regard to API load & pod resources
|
|
290
|
+
await this.#list(continueToken, removedItems);
|
|
291
|
+
} else {
|
|
292
|
+
// Otherwise, process the removed items
|
|
293
|
+
for (const item of removedItems.values()) {
|
|
294
|
+
void this.#process(item, WatchPhase.Deleted);
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
} catch (err) {
|
|
298
|
+
this.#events.emit(WatchEvent.LIST_ERROR, err);
|
|
299
|
+
}
|
|
300
|
+
};
|
|
301
|
+
|
|
302
|
+
/**
|
|
303
|
+
* Process the event payload.
|
|
304
|
+
*
|
|
305
|
+
* @param payload - the event payload
|
|
306
|
+
* @param phase - the event phase
|
|
307
|
+
*/
|
|
308
|
+
#process = async (payload: InstanceType<T>, phase: WatchPhase) => {
|
|
309
|
+
try {
|
|
310
|
+
switch (phase) {
|
|
311
|
+
// If the event is added or modified, update the cache
|
|
312
|
+
case WatchPhase.Added:
|
|
313
|
+
case WatchPhase.Modified:
|
|
314
|
+
this.#cache.set(payload.metadata.uid, payload);
|
|
315
|
+
break;
|
|
316
|
+
|
|
317
|
+
// If the event is deleted, remove the item from the cache
|
|
318
|
+
case WatchPhase.Deleted:
|
|
319
|
+
this.#cache.delete(payload.metadata.uid);
|
|
320
|
+
break;
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
// Emit the data event
|
|
324
|
+
this.#events.emit(WatchEvent.DATA, payload, phase);
|
|
325
|
+
|
|
326
|
+
// Call the callback function with the parsed payload
|
|
327
|
+
await this.#callback(payload, phase);
|
|
328
|
+
} catch (err) {
|
|
329
|
+
this.#events.emit(WatchEvent.DATA_ERROR, err);
|
|
330
|
+
}
|
|
331
|
+
};
|
|
332
|
+
|
|
333
|
+
/**
|
|
334
|
+
* Watch for changes to the resource.
|
|
335
|
+
*/
|
|
336
|
+
#watch = async () => {
|
|
337
|
+
try {
|
|
338
|
+
// Start with a list operation
|
|
339
|
+
await this.#list();
|
|
340
|
+
|
|
211
341
|
// Build the URL and request options
|
|
212
|
-
const { opts, url } = await this.#buildURL();
|
|
342
|
+
const { opts, url } = await this.#buildURL(true, this.#resourceVersion);
|
|
213
343
|
|
|
214
344
|
// Create a stream to read the response body
|
|
215
345
|
this.#stream = byline.createStream();
|
|
216
346
|
|
|
217
347
|
// Bind the stream events
|
|
218
348
|
this.#stream.on("error", this.#errHandler);
|
|
219
|
-
this.#stream.on("close", this.#
|
|
220
|
-
this.#stream.on("finish", this.#
|
|
349
|
+
this.#stream.on("close", this.#streamCleanup);
|
|
350
|
+
this.#stream.on("finish", this.#streamCleanup);
|
|
221
351
|
|
|
222
352
|
// Make the actual request
|
|
223
353
|
const response = await fetch(url, { ...opts });
|
|
@@ -225,9 +355,6 @@ export class Watcher<T extends GenericClass> {
|
|
|
225
355
|
// Reset the pending reconnect flag
|
|
226
356
|
this.#pendingReconnect = false;
|
|
227
357
|
|
|
228
|
-
// Reset the resync timer
|
|
229
|
-
void this.#scheduleResync();
|
|
230
|
-
|
|
231
358
|
// If the request is successful, start listening for events
|
|
232
359
|
if (response.ok) {
|
|
233
360
|
this.#events.emit(WatchEvent.CONNECT, url.pathname);
|
|
@@ -246,28 +373,19 @@ export class Watcher<T extends GenericClass> {
|
|
|
246
373
|
object: InstanceType<T>;
|
|
247
374
|
};
|
|
248
375
|
|
|
249
|
-
|
|
376
|
+
// Update the last seen time
|
|
377
|
+
this.#lastSeenTime = Date.now();
|
|
250
378
|
|
|
251
379
|
// If the watch is too old, remove the resourceVersion and reload the watch
|
|
252
380
|
if (phase === WatchPhase.Error && payload.code === 410) {
|
|
253
381
|
throw {
|
|
254
382
|
name: "TooOld",
|
|
255
|
-
message: this.#
|
|
383
|
+
message: this.#resourceVersion!,
|
|
256
384
|
};
|
|
257
385
|
}
|
|
258
386
|
|
|
259
|
-
//
|
|
260
|
-
|
|
261
|
-
this.#events.emit(WatchEvent.BOOKMARK, payload);
|
|
262
|
-
} else {
|
|
263
|
-
this.#events.emit(WatchEvent.DATA, payload, phase);
|
|
264
|
-
|
|
265
|
-
// Call the callback function with the parsed payload
|
|
266
|
-
await this.#callback(payload, phase as WatchPhase);
|
|
267
|
-
}
|
|
268
|
-
|
|
269
|
-
// Update the resource version if the callback was successful
|
|
270
|
-
this.#setResourceVersion(payload.metadata.resourceVersion);
|
|
387
|
+
// Process the event payload, do not update the resource version as that is handled by the list operation
|
|
388
|
+
await this.#process(payload, phase);
|
|
271
389
|
} catch (err) {
|
|
272
390
|
if (err.name === "TooOld") {
|
|
273
391
|
// Prevent any body events from firing
|
|
@@ -277,14 +395,15 @@ export class Watcher<T extends GenericClass> {
|
|
|
277
395
|
void this.#errHandler(err);
|
|
278
396
|
return;
|
|
279
397
|
}
|
|
398
|
+
|
|
280
399
|
this.#events.emit(WatchEvent.DATA_ERROR, err);
|
|
281
400
|
}
|
|
282
401
|
});
|
|
283
402
|
|
|
284
403
|
// Bind the body events
|
|
285
404
|
body.on("error", this.#errHandler);
|
|
286
|
-
body.on("close", this.#
|
|
287
|
-
body.on("finish", this.#
|
|
405
|
+
body.on("close", this.#streamCleanup);
|
|
406
|
+
body.on("finish", this.#streamCleanup);
|
|
288
407
|
|
|
289
408
|
// Pipe the response body to the stream
|
|
290
409
|
body.pipe(this.#stream);
|
|
@@ -296,68 +415,43 @@ export class Watcher<T extends GenericClass> {
|
|
|
296
415
|
}
|
|
297
416
|
};
|
|
298
417
|
|
|
299
|
-
/**
|
|
300
|
-
* Resync the watch.
|
|
301
|
-
*
|
|
302
|
-
* @returns the error handler
|
|
303
|
-
*/
|
|
304
|
-
#resync = () =>
|
|
305
|
-
this.#errHandler({
|
|
306
|
-
name: "Resync",
|
|
307
|
-
message: "Resync triggered by resyncIntervalSec",
|
|
308
|
-
});
|
|
309
|
-
|
|
310
418
|
/** Clear the resync timer and schedule a new one. */
|
|
311
|
-
#
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
419
|
+
#checkResync = () => {
|
|
420
|
+
// Ignore if the last seen time is not set
|
|
421
|
+
if (this.#lastSeenTime === NONE) {
|
|
422
|
+
return;
|
|
423
|
+
}
|
|
315
424
|
|
|
316
|
-
|
|
317
|
-
* Update the resource version.
|
|
318
|
-
*
|
|
319
|
-
* @param resourceVersion - the new resource version
|
|
320
|
-
*/
|
|
321
|
-
#setResourceVersion = (resourceVersion?: string) => {
|
|
322
|
-
this.#watchCfg.resourceVersion = resourceVersion;
|
|
323
|
-
this.#events.emit(WatchEvent.RESOURCE_VERSION, resourceVersion);
|
|
324
|
-
};
|
|
425
|
+
const now = Date.now();
|
|
325
426
|
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
*/
|
|
331
|
-
#reconnect = async (err: Error) => {
|
|
332
|
-
// If there are more attempts, retry the watch (undefined is unlimited retries)
|
|
333
|
-
if (this.#watchCfg.retryMax === undefined || this.#watchCfg.retryMax > this.#retryCount) {
|
|
334
|
-
// Sleep for the specified delay, but check every 500ms if the watch has been aborted
|
|
335
|
-
let delay = this.#watchCfg.retryDelaySec! * 1000;
|
|
336
|
-
while (delay > 0) {
|
|
337
|
-
if (this.#abortController.signal.aborted) {
|
|
338
|
-
return;
|
|
339
|
-
}
|
|
340
|
-
delay -= 500;
|
|
341
|
-
await new Promise(resolve => setTimeout(resolve, 500));
|
|
342
|
-
}
|
|
427
|
+
// If the last seen time is greater than the limit, trigger a resync
|
|
428
|
+
if (this.#lastSeenTime == OVERRIDE || now - this.#lastSeenTime > this.#lastSeenLimit) {
|
|
429
|
+
// Reset the last seen time to now to allow the resync to be called again in case of failure
|
|
430
|
+
this.#lastSeenTime = now;
|
|
343
431
|
|
|
344
|
-
|
|
345
|
-
this.#
|
|
432
|
+
// If there are more attempts, retry the watch (undefined is unlimited retries)
|
|
433
|
+
if (this.#watchCfg.retryMax === undefined || this.#watchCfg.retryMax > this.#retryCount) {
|
|
434
|
+
// Increment the retry count
|
|
435
|
+
this.#retryCount++;
|
|
346
436
|
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
437
|
+
if (this.#pendingReconnect) {
|
|
438
|
+
// wait for the connection to be re-established
|
|
439
|
+
this.#events.emit(WatchEvent.RECONNECT_PENDING);
|
|
440
|
+
} else {
|
|
441
|
+
this.#pendingReconnect = true;
|
|
442
|
+
this.#events.emit(WatchEvent.RECONNECT, this.#retryCount);
|
|
443
|
+
this.#streamCleanup();
|
|
353
444
|
|
|
354
|
-
|
|
355
|
-
|
|
445
|
+
void this.#watch();
|
|
446
|
+
}
|
|
447
|
+
} else {
|
|
448
|
+
// Otherwise, call the finally function if it exists
|
|
449
|
+
this.#events.emit(
|
|
450
|
+
WatchEvent.GIVE_UP,
|
|
451
|
+
new Error(`Retry limit (${this.#watchCfg.retryMax}) exceeded, giving up`),
|
|
452
|
+
);
|
|
453
|
+
this.close();
|
|
356
454
|
}
|
|
357
|
-
} else {
|
|
358
|
-
// Otherwise, call the finally function if it exists
|
|
359
|
-
this.#events.emit(WatchEvent.GIVE_UP, err);
|
|
360
|
-
this.close();
|
|
361
455
|
}
|
|
362
456
|
};
|
|
363
457
|
|
|
@@ -369,31 +463,29 @@ export class Watcher<T extends GenericClass> {
|
|
|
369
463
|
#errHandler = async (err: Error) => {
|
|
370
464
|
switch (err.name) {
|
|
371
465
|
case "AbortError":
|
|
372
|
-
|
|
373
|
-
this.#
|
|
466
|
+
clearInterval(this.$relistTimer);
|
|
467
|
+
clearInterval(this.#resyncTimer);
|
|
468
|
+
this.#streamCleanup();
|
|
374
469
|
this.#events.emit(WatchEvent.ABORT, err);
|
|
375
470
|
return;
|
|
376
471
|
|
|
377
472
|
case "TooOld":
|
|
378
473
|
// Purge the resource version if it is too old
|
|
379
|
-
this.#
|
|
474
|
+
this.#resourceVersion = undefined;
|
|
380
475
|
this.#events.emit(WatchEvent.OLD_RESOURCE_VERSION, err.message);
|
|
381
476
|
break;
|
|
382
477
|
|
|
383
|
-
case "Resync":
|
|
384
|
-
this.#events.emit(WatchEvent.RESYNC, err);
|
|
385
|
-
break;
|
|
386
|
-
|
|
387
478
|
default:
|
|
388
479
|
this.#events.emit(WatchEvent.NETWORK_ERROR, err);
|
|
389
480
|
break;
|
|
390
481
|
}
|
|
391
482
|
|
|
392
|
-
|
|
483
|
+
// Force a resync
|
|
484
|
+
this.#lastSeenTime = OVERRIDE;
|
|
393
485
|
};
|
|
394
486
|
|
|
395
487
|
/** Cleanup the stream and listeners. */
|
|
396
|
-
#
|
|
488
|
+
#streamCleanup = () => {
|
|
397
489
|
if (this.#stream) {
|
|
398
490
|
this.#stream.removeAllListeners();
|
|
399
491
|
this.#stream.destroy();
|