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/dist/fluent/watch.d.ts
CHANGED
|
@@ -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
|
-
/**
|
|
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":"
|
|
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"}
|
package/dist/fluent/watch.js
CHANGED
|
@@ -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
|
|
75
|
-
watchCfg.retryDelaySec ??=
|
|
76
|
-
// Set the
|
|
77
|
-
watchCfg.
|
|
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.#
|
|
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
|
-
|
|
98
|
-
this.#
|
|
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
|
-
|
|
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 (
|
|
159
|
-
url.searchParams.set("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
|
-
/**
|
|
168
|
-
|
|
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.#
|
|
177
|
-
this.#stream.on("finish", this.#
|
|
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
|
-
|
|
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.#
|
|
307
|
+
message: this.#resourceVersion,
|
|
201
308
|
};
|
|
202
309
|
}
|
|
203
|
-
//
|
|
204
|
-
|
|
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.#
|
|
229
|
-
body.on("finish", this.#
|
|
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
|
-
#
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
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
|
-
|
|
289
|
-
this.#
|
|
290
|
-
|
|
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
|
-
|
|
309
|
-
this.#
|
|
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.#
|
|
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
|
-
|
|
394
|
+
// Force a resync
|
|
395
|
+
this.#lastSeenTime = OVERRIDE;
|
|
325
396
|
};
|
|
326
397
|
/** Cleanup the stream and listeners. */
|
|
327
|
-
#
|
|
398
|
+
#streamCleanup = () => {
|
|
328
399
|
if (this.#stream) {
|
|
329
400
|
this.#stream.removeAllListeners();
|
|
330
401
|
this.#stream.destroy();
|