@faable/deploy-sdk 2.18.0 → 2.18.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/FaableDeployApi.d.ts.map +1 -1
- package/dist/api/generated-client.d.ts.map +1 -1
- package/dist/api/types.d.ts +4 -2
- package/dist/api/types.d.ts.map +1 -1
- package/dist/informers/Indexer.d.ts +4 -0
- package/dist/informers/Indexer.d.ts.map +1 -1
- package/dist/informers/Indexer.js +19 -0
- package/dist/informers/InformerFactory.d.ts +59 -13
- package/dist/informers/InformerFactory.d.ts.map +1 -1
- package/dist/informers/InformerFactory.js +199 -40
- package/dist/informers/index.d.ts +2 -2
- package/dist/informers/index.d.ts.map +1 -1
- package/dist/informers/index.js +2 -2
- package/dist/informers/rv.d.ts +11 -0
- package/dist/informers/rv.d.ts.map +1 -1
- package/dist/informers/rv.js +17 -0
- package/package.json +1 -1
- package/spec/openapi.json +4 -2
|
@@ -16,6 +16,10 @@ export declare class Indexer<T extends {
|
|
|
16
16
|
applied: boolean;
|
|
17
17
|
old?: T;
|
|
18
18
|
};
|
|
19
|
+
/** Set unconditionally (a relist is authoritative). Returns the previous object. */
|
|
20
|
+
set(obj: T, rv: string): T | undefined;
|
|
21
|
+
/** Remove unconditionally (a relist did not list it). */
|
|
22
|
+
evict(id: string): T | undefined;
|
|
19
23
|
/** Remove unless what is held is newer than the delete. */
|
|
20
24
|
delete(id: string, rv: string): T | undefined;
|
|
21
25
|
private indexObj;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"Indexer.d.ts","sourceRoot":"","sources":["../../src/informers/Indexer.ts"],"names":[],"mappings":"AAMA,MAAM,MAAM,OAAO,CAAC,CAAC,IAAI,CAAC,GAAG,EAAE,CAAC,KAAK,MAAM,EAAE,CAAA;AAE7C,qBAAa,OAAO,CAAC,CAAC,SAAS;IAAE,EAAE,EAAE,MAAM,CAAA;CAAE;IAC3C,OAAO,CAAC,OAAO,CAA4C;IAC3D,OAAO,CAAC,QAAQ,CAAgC;IAChD,OAAO,CAAC,OAAO,CAA8C;IAE7D,QAAQ,CAAC,IAAI,EAAE,MAAM,EAAE,EAAE,EAAE,OAAO,CAAC,CAAC,CAAC,GAAG,IAAI;IAO5C,GAAG,CAAC,EAAE,EAAE,MAAM,GAAG,CAAC,GAAG,SAAS;IAI9B,IAAI,CAAC,EAAE,EAAE,MAAM,GAAG,MAAM,GAAG,SAAS;IAIpC,IAAI,IAAI,CAAC,EAAE;IAIX,GAAG,IAAI,MAAM,EAAE;IAIf,OAAO,CAAC,IAAI,EAAE,MAAM,EAAE,GAAG,EAAE,MAAM,GAAG,CAAC,EAAE;IAMvC,wEAAwE;IACxE,MAAM,CAAC,GAAG,EAAE,CAAC,EAAE,EAAE,EAAE,MAAM,GAAG;QAAE,OAAO,EAAE,OAAO,CAAC;QAAC,GAAG,CAAC,EAAE,CAAC,CAAA;KAAE;IASzD,2DAA2D;IAC3D,MAAM,CAAC,EAAE,EAAE,MAAM,EAAE,EAAE,EAAE,MAAM,GAAG,CAAC,GAAG,SAAS;IAQ7C,OAAO,CAAC,QAAQ;IAQhB,OAAO,CAAC,OAAO;CAShB"}
|
|
1
|
+
{"version":3,"file":"Indexer.d.ts","sourceRoot":"","sources":["../../src/informers/Indexer.ts"],"names":[],"mappings":"AAMA,MAAM,MAAM,OAAO,CAAC,CAAC,IAAI,CAAC,GAAG,EAAE,CAAC,KAAK,MAAM,EAAE,CAAA;AAE7C,qBAAa,OAAO,CAAC,CAAC,SAAS;IAAE,EAAE,EAAE,MAAM,CAAA;CAAE;IAC3C,OAAO,CAAC,OAAO,CAA4C;IAC3D,OAAO,CAAC,QAAQ,CAAgC;IAChD,OAAO,CAAC,OAAO,CAA8C;IAE7D,QAAQ,CAAC,IAAI,EAAE,MAAM,EAAE,EAAE,EAAE,OAAO,CAAC,CAAC,CAAC,GAAG,IAAI;IAO5C,GAAG,CAAC,EAAE,EAAE,MAAM,GAAG,CAAC,GAAG,SAAS;IAI9B,IAAI,CAAC,EAAE,EAAE,MAAM,GAAG,MAAM,GAAG,SAAS;IAIpC,IAAI,IAAI,CAAC,EAAE;IAIX,GAAG,IAAI,MAAM,EAAE;IAIf,OAAO,CAAC,IAAI,EAAE,MAAM,EAAE,GAAG,EAAE,MAAM,GAAG,CAAC,EAAE;IAMvC,wEAAwE;IACxE,MAAM,CAAC,GAAG,EAAE,CAAC,EAAE,EAAE,EAAE,MAAM,GAAG;QAAE,OAAO,EAAE,OAAO,CAAC;QAAC,GAAG,CAAC,EAAE,CAAC,CAAA;KAAE;IASzD,oFAAoF;IACpF,GAAG,CAAC,GAAG,EAAE,CAAC,EAAE,EAAE,EAAE,MAAM,GAAG,CAAC,GAAG,SAAS;IAQtC,yDAAyD;IACzD,KAAK,CAAC,EAAE,EAAE,MAAM,GAAG,CAAC,GAAG,SAAS;IAQhC,2DAA2D;IAC3D,MAAM,CAAC,EAAE,EAAE,MAAM,EAAE,EAAE,EAAE,MAAM,GAAG,CAAC,GAAG,SAAS;IAQ7C,OAAO,CAAC,QAAQ;IAQhB,OAAO,CAAC,OAAO;CAShB"}
|
|
@@ -40,6 +40,25 @@ export class Indexer {
|
|
|
40
40
|
this.indexObj(name, fn, obj);
|
|
41
41
|
return { applied: true, old: cur?.obj };
|
|
42
42
|
}
|
|
43
|
+
/** Set unconditionally (a relist is authoritative). Returns the previous object. */
|
|
44
|
+
set(obj, rv) {
|
|
45
|
+
const cur = this.objects.get(obj.id);
|
|
46
|
+
if (cur)
|
|
47
|
+
this.unindex(cur.obj);
|
|
48
|
+
this.objects.set(obj.id, { obj, rv });
|
|
49
|
+
for (const [name, fn] of this.indexFns)
|
|
50
|
+
this.indexObj(name, fn, obj);
|
|
51
|
+
return cur?.obj;
|
|
52
|
+
}
|
|
53
|
+
/** Remove unconditionally (a relist did not list it). */
|
|
54
|
+
evict(id) {
|
|
55
|
+
const cur = this.objects.get(id);
|
|
56
|
+
if (!cur)
|
|
57
|
+
return undefined;
|
|
58
|
+
this.unindex(cur.obj);
|
|
59
|
+
this.objects.delete(id);
|
|
60
|
+
return cur.obj;
|
|
61
|
+
}
|
|
43
62
|
/** Remove unless what is held is newer than the delete. */
|
|
44
63
|
delete(id, rv) {
|
|
45
64
|
const cur = this.objects.get(id);
|
|
@@ -1,14 +1,23 @@
|
|
|
1
1
|
import { Indexer, type IndexFn } from "./Indexer.js";
|
|
2
|
-
import { type WatchClient, type WatchResource } from "./WatchClient.js";
|
|
2
|
+
import { type WatchClient, type WatchEvent, type WatchResource } from "./WatchClient.js";
|
|
3
3
|
export type InformerEvent = "add" | "update" | "delete";
|
|
4
4
|
type Handler<T> = (obj: T, old?: T) => void;
|
|
5
|
+
type Logger = {
|
|
6
|
+
warn: (o: object, m?: string) => void;
|
|
7
|
+
info?: (o: object, m?: string) => void;
|
|
8
|
+
};
|
|
9
|
+
/** Thrown into the loop when a watch sends nothing for `idleTimeoutMs`. */
|
|
10
|
+
export declare class WatchIdleError extends Error {
|
|
11
|
+
constructor(idleMs: number);
|
|
12
|
+
}
|
|
5
13
|
export declare class SharedInformer<T extends {
|
|
6
14
|
id: string;
|
|
7
15
|
}> {
|
|
8
16
|
readonly store: Indexer<T>;
|
|
9
17
|
private handlers;
|
|
10
18
|
readonly resource: WatchResource;
|
|
11
|
-
|
|
19
|
+
private logger?;
|
|
20
|
+
constructor(resource: WatchResource, logger?: Logger);
|
|
12
21
|
on(event: InformerEvent, handler: Handler<T>): this;
|
|
13
22
|
addIndex(name: string, fn: IndexFn<T>): this;
|
|
14
23
|
get(id: string): T | undefined;
|
|
@@ -16,24 +25,36 @@ export declare class SharedInformer<T extends {
|
|
|
16
25
|
byIndex(name: string, key: string): T[];
|
|
17
26
|
/** @internal */
|
|
18
27
|
emit(event: InformerEvent, obj: T, old?: T): void;
|
|
19
|
-
/** @internal Apply an upsert; emits add/update. */
|
|
20
|
-
apply(obj: T, rv: string):
|
|
21
|
-
/** @internal Apply a delete; emits delete. */
|
|
22
|
-
remove(id: string, rv: string):
|
|
23
|
-
/**
|
|
28
|
+
/** @internal Apply an upsert; emits add/update. false if dropped as stale. */
|
|
29
|
+
apply(obj: T, rv: string): boolean;
|
|
30
|
+
/** @internal Apply a delete; emits delete. false if dropped as stale. */
|
|
31
|
+
remove(id: string, rv: string): boolean;
|
|
32
|
+
/**
|
|
33
|
+
* @internal A relist, authoritative like client-go's Replace: the listed
|
|
34
|
+
* objects become the store regardless of rvs (rvs of another generation
|
|
35
|
+
* are not comparable). add for new, update for changed, delete for gone.
|
|
36
|
+
*/
|
|
24
37
|
replace(items: {
|
|
25
38
|
rv: string;
|
|
26
39
|
object: T;
|
|
27
|
-
}[]
|
|
40
|
+
}[]): void;
|
|
28
41
|
}
|
|
29
42
|
export type InformerFactoryOptions = {
|
|
30
43
|
resources: WatchResource[];
|
|
31
|
-
logger?:
|
|
32
|
-
|
|
33
|
-
info?: (o: object, m?: string) => void;
|
|
34
|
-
};
|
|
35
|
-
/** Backoff after a failed list or watch: first and max delay. */
|
|
44
|
+
logger?: Logger;
|
|
45
|
+
/** Backoff after an attempt without progress: first and max delay (±20 % jitter). */
|
|
36
46
|
backoffMs?: [number, number];
|
|
47
|
+
/**
|
|
48
|
+
* A watch that sends no line for this long is aborted and reconnected (the
|
|
49
|
+
* server sends a BOOKMARK every ≤ 15 s). Default 45 s.
|
|
50
|
+
*/
|
|
51
|
+
idleTimeoutMs?: number;
|
|
52
|
+
/**
|
|
53
|
+
* Every event off the watch, before it is applied. `lagMs` is how long a
|
|
54
|
+
* MODIFIED/DELETED took from the cache's stream to here (null for
|
|
55
|
+
* BOOKMARK/ERROR, whose rv is not a change). For metrics; must not throw.
|
|
56
|
+
*/
|
|
57
|
+
onEvent?: (ev: WatchEvent, lagMs: number | null) => void;
|
|
37
58
|
};
|
|
38
59
|
export declare class InformerFactory {
|
|
39
60
|
private informers;
|
|
@@ -42,12 +63,24 @@ export declare class InformerFactory {
|
|
|
42
63
|
private synced;
|
|
43
64
|
private syncWaiters;
|
|
44
65
|
private rv?;
|
|
66
|
+
/** The generation of the current lists; events of another one → relist. */
|
|
67
|
+
private gen;
|
|
45
68
|
private stopped;
|
|
46
69
|
readonly stats: {
|
|
47
70
|
relists: number;
|
|
48
71
|
reconnects: number;
|
|
49
72
|
events: number;
|
|
50
73
|
lastEventAt: number;
|
|
74
|
+
/** Failed lists/watches, idle watches included (each one retried with backoff). */
|
|
75
|
+
errors: number;
|
|
76
|
+
/** When the stores last held a complete list (initial sync or relist). */
|
|
77
|
+
lastSyncAt: number;
|
|
78
|
+
/** Events older than what the store held (same generation), not applied. */
|
|
79
|
+
dropped: number;
|
|
80
|
+
/** ERROR events other than watch_expired. */
|
|
81
|
+
watchErrors: number;
|
|
82
|
+
/** From a 410 / generation change until a relist succeeds. */
|
|
83
|
+
relistPending: boolean;
|
|
51
84
|
};
|
|
52
85
|
private client;
|
|
53
86
|
private options;
|
|
@@ -56,11 +89,24 @@ export declare class InformerFactory {
|
|
|
56
89
|
id: string;
|
|
57
90
|
} = any>(resource: WatchResource): SharedInformer<T>;
|
|
58
91
|
hasSynced(): boolean;
|
|
92
|
+
/**
|
|
93
|
+
* Synced, no relist pending, and something (an event — BOOKMARKs included —
|
|
94
|
+
* or a list) arrived within `maxQuietMs`.
|
|
95
|
+
*/
|
|
96
|
+
isFresh(maxQuietMs: number): boolean;
|
|
59
97
|
waitForSync(): Promise<void>;
|
|
60
98
|
start(): void;
|
|
61
99
|
stop(): Promise<void>;
|
|
100
|
+
/** Fetch every list first; replace only if all of them arrived (K4). */
|
|
62
101
|
private relist;
|
|
63
102
|
private handle;
|
|
103
|
+
/**
|
|
104
|
+
* One watch connection, read with an idle watchdog: no line for
|
|
105
|
+
* `idleTimeoutMs` aborts the request (its signal) and throws. The race
|
|
106
|
+
* does not trust the client to honour the signal.
|
|
107
|
+
*/
|
|
108
|
+
private watchOnce;
|
|
109
|
+
private sleep;
|
|
64
110
|
private run;
|
|
65
111
|
}
|
|
66
112
|
export {};
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"InformerFactory.d.ts","sourceRoot":"","sources":["../../src/informers/InformerFactory.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,OAAO,EAAE,KAAK,OAAO,EAAE,MAAM,cAAc,CAAA;AAEpD,OAAO,
|
|
1
|
+
{"version":3,"file":"InformerFactory.d.ts","sourceRoot":"","sources":["../../src/informers/InformerFactory.ts"],"names":[],"mappings":"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"}
|
|
@@ -1,6 +1,13 @@
|
|
|
1
1
|
import { Indexer } from "./Indexer.js";
|
|
2
|
-
import { compareRv, minRv } from "./rv.js";
|
|
2
|
+
import { compareRv, minRv, rvGeneration, rvTimeMs } from "./rv.js";
|
|
3
3
|
import { WatchExpiredError, } from "./WatchClient.js";
|
|
4
|
+
/** Thrown into the loop when a watch sends nothing for `idleTimeoutMs`. */
|
|
5
|
+
export class WatchIdleError extends Error {
|
|
6
|
+
constructor(idleMs) {
|
|
7
|
+
super(`watch idle for ${idleMs} ms: reconnecting`);
|
|
8
|
+
this.name = "WatchIdleError";
|
|
9
|
+
}
|
|
10
|
+
}
|
|
4
11
|
export class SharedInformer {
|
|
5
12
|
store = new Indexer();
|
|
6
13
|
handlers = {
|
|
@@ -9,8 +16,10 @@ export class SharedInformer {
|
|
|
9
16
|
delete: [],
|
|
10
17
|
};
|
|
11
18
|
resource;
|
|
12
|
-
|
|
19
|
+
logger;
|
|
20
|
+
constructor(resource, logger) {
|
|
13
21
|
this.resource = resource;
|
|
22
|
+
this.logger = logger;
|
|
14
23
|
}
|
|
15
24
|
on(event, handler) {
|
|
16
25
|
this.handlers[event].push(handler);
|
|
@@ -35,34 +44,51 @@ export class SharedInformer {
|
|
|
35
44
|
try {
|
|
36
45
|
h(obj, old);
|
|
37
46
|
}
|
|
38
|
-
catch {
|
|
47
|
+
catch (err) {
|
|
39
48
|
// A handler's failure is the handler's: the store and the other
|
|
40
|
-
// handlers carry on.
|
|
49
|
+
// handlers carry on. But it is said.
|
|
50
|
+
this.logger?.warn({ err, resource: this.resource, event, id: obj?.id }, "informers: handler threw");
|
|
41
51
|
}
|
|
42
52
|
}
|
|
43
53
|
}
|
|
44
|
-
/** @internal Apply an upsert; emits add/update. */
|
|
54
|
+
/** @internal Apply an upsert; emits add/update. false if dropped as stale. */
|
|
45
55
|
apply(obj, rv) {
|
|
46
56
|
const { applied, old } = this.store.upsert(obj, rv);
|
|
47
57
|
if (applied)
|
|
48
58
|
this.emit(old ? "update" : "add", obj, old);
|
|
59
|
+
return applied;
|
|
49
60
|
}
|
|
50
|
-
/** @internal Apply a delete; emits delete. */
|
|
61
|
+
/** @internal Apply a delete; emits delete. false if dropped as stale. */
|
|
51
62
|
remove(id, rv) {
|
|
63
|
+
if (!this.store.get(id))
|
|
64
|
+
return true;
|
|
52
65
|
const gone = this.store.delete(id, rv);
|
|
53
66
|
if (gone)
|
|
54
67
|
this.emit("delete", gone);
|
|
68
|
+
return !!gone;
|
|
55
69
|
}
|
|
56
|
-
/**
|
|
57
|
-
|
|
70
|
+
/**
|
|
71
|
+
* @internal A relist, authoritative like client-go's Replace: the listed
|
|
72
|
+
* objects become the store regardless of rvs (rvs of another generation
|
|
73
|
+
* are not comparable). add for new, update for changed, delete for gone.
|
|
74
|
+
*/
|
|
75
|
+
replace(items) {
|
|
58
76
|
const seen = new Set();
|
|
59
77
|
for (const { rv, object } of items) {
|
|
60
78
|
seen.add(object.id);
|
|
61
|
-
this.
|
|
79
|
+
const old = this.store.set(object, rv);
|
|
80
|
+
if (!old)
|
|
81
|
+
this.emit("add", object);
|
|
82
|
+
else if (JSON.stringify(old) !== JSON.stringify(object))
|
|
83
|
+
this.emit("update", object, old);
|
|
84
|
+
}
|
|
85
|
+
for (const id of this.store.ids()) {
|
|
86
|
+
if (seen.has(id))
|
|
87
|
+
continue;
|
|
88
|
+
const gone = this.store.evict(id);
|
|
89
|
+
if (gone)
|
|
90
|
+
this.emit("delete", gone);
|
|
62
91
|
}
|
|
63
|
-
for (const id of this.store.ids())
|
|
64
|
-
if (!seen.has(id))
|
|
65
|
-
this.remove(id, listRv);
|
|
66
92
|
}
|
|
67
93
|
}
|
|
68
94
|
export class InformerFactory {
|
|
@@ -72,15 +98,32 @@ export class InformerFactory {
|
|
|
72
98
|
synced = false;
|
|
73
99
|
syncWaiters = [];
|
|
74
100
|
rv;
|
|
101
|
+
/** The generation of the current lists; events of another one → relist. */
|
|
102
|
+
gen = null;
|
|
75
103
|
stopped = true;
|
|
76
|
-
stats = {
|
|
104
|
+
stats = {
|
|
105
|
+
relists: 0,
|
|
106
|
+
reconnects: 0,
|
|
107
|
+
events: 0,
|
|
108
|
+
lastEventAt: 0,
|
|
109
|
+
/** Failed lists/watches, idle watches included (each one retried with backoff). */
|
|
110
|
+
errors: 0,
|
|
111
|
+
/** When the stores last held a complete list (initial sync or relist). */
|
|
112
|
+
lastSyncAt: 0,
|
|
113
|
+
/** Events older than what the store held (same generation), not applied. */
|
|
114
|
+
dropped: 0,
|
|
115
|
+
/** ERROR events other than watch_expired. */
|
|
116
|
+
watchErrors: 0,
|
|
117
|
+
/** From a 410 / generation change until a relist succeeds. */
|
|
118
|
+
relistPending: false,
|
|
119
|
+
};
|
|
77
120
|
client;
|
|
78
121
|
options;
|
|
79
122
|
constructor(client, options) {
|
|
80
123
|
this.client = client;
|
|
81
124
|
this.options = options;
|
|
82
125
|
for (const r of options.resources)
|
|
83
|
-
this.informers.set(r, new SharedInformer(r));
|
|
126
|
+
this.informers.set(r, new SharedInformer(r, options.logger));
|
|
84
127
|
}
|
|
85
128
|
informer(resource) {
|
|
86
129
|
const inf = this.informers.get(resource);
|
|
@@ -91,6 +134,15 @@ export class InformerFactory {
|
|
|
91
134
|
hasSynced() {
|
|
92
135
|
return this.synced;
|
|
93
136
|
}
|
|
137
|
+
/**
|
|
138
|
+
* Synced, no relist pending, and something (an event — BOOKMARKs included —
|
|
139
|
+
* or a list) arrived within `maxQuietMs`.
|
|
140
|
+
*/
|
|
141
|
+
isFresh(maxQuietMs) {
|
|
142
|
+
if (!this.synced || this.stats.relistPending)
|
|
143
|
+
return false;
|
|
144
|
+
return Date.now() - Math.max(this.stats.lastEventAt, this.stats.lastSyncAt) < maxQuietMs;
|
|
145
|
+
}
|
|
94
146
|
waitForSync() {
|
|
95
147
|
if (this.synced)
|
|
96
148
|
return Promise.resolve();
|
|
@@ -108,17 +160,26 @@ export class InformerFactory {
|
|
|
108
160
|
this.abort?.abort();
|
|
109
161
|
await this.loop?.catch(() => { });
|
|
110
162
|
}
|
|
163
|
+
/** Fetch every list first; replace only if all of them arrived (K4). */
|
|
111
164
|
async relist() {
|
|
112
|
-
const
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
rvs.
|
|
117
|
-
|
|
165
|
+
const lists = await Promise.all([...this.informers].map(async ([resource, inf]) => [inf, await this.client.list(resource)]));
|
|
166
|
+
const rvs = lists.map(([, l]) => l.rv);
|
|
167
|
+
const gens = new Set(rvs.map(rvGeneration));
|
|
168
|
+
if (gens.size > 1)
|
|
169
|
+
throw new Error(`lists span generations (${rvs.join(", ")}): relisting`);
|
|
170
|
+
for (const [inf, l] of lists)
|
|
171
|
+
inf.replace(l.items);
|
|
118
172
|
// Watch from the OLDEST list rv: events already reflected in a newer
|
|
119
173
|
// list are dropped by the per-object rv.
|
|
120
174
|
this.rv = minRv(rvs);
|
|
175
|
+
this.gen = rvGeneration(this.rv);
|
|
121
176
|
this.stats.relists++;
|
|
177
|
+
this.stats.lastSyncAt = Date.now();
|
|
178
|
+
this.stats.relistPending = false;
|
|
179
|
+
const items = {};
|
|
180
|
+
for (const [inf, l] of lists)
|
|
181
|
+
items[inf.resource] = l.items.length;
|
|
182
|
+
this.options.logger?.info?.({ relists: this.stats.relists, rv: this.rv, items }, "informers: relisted");
|
|
122
183
|
if (!this.synced) {
|
|
123
184
|
this.synced = true;
|
|
124
185
|
for (const w of this.syncWaiters.splice(0))
|
|
@@ -128,57 +189,155 @@ export class InformerFactory {
|
|
|
128
189
|
handle(ev) {
|
|
129
190
|
this.stats.events++;
|
|
130
191
|
this.stats.lastEventAt = Date.now();
|
|
192
|
+
if (this.options.onEvent) {
|
|
193
|
+
const t = ev.type === "MODIFIED" || ev.type === "DELETED" ? rvTimeMs(ev.rv) : null;
|
|
194
|
+
try {
|
|
195
|
+
this.options.onEvent(ev, t === null ? null : Date.now() - t);
|
|
196
|
+
}
|
|
197
|
+
catch {
|
|
198
|
+
// Metrics must never break the watch.
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
if (ev.type === "ERROR") {
|
|
202
|
+
if (ev.code === "watch_expired")
|
|
203
|
+
return "relist";
|
|
204
|
+
this.stats.watchErrors++;
|
|
205
|
+
this.options.logger?.warn({ code: ev.code, message: ev.message }, "informers: watch ERROR event, reconnecting");
|
|
206
|
+
return "error";
|
|
207
|
+
}
|
|
208
|
+
// Another generation than the lists': the server's stream was reset and
|
|
209
|
+
// rvs across generations do not compare. Relist.
|
|
210
|
+
const gen = rvGeneration(ev.rv);
|
|
211
|
+
if (gen !== null && this.gen !== null && gen !== this.gen) {
|
|
212
|
+
this.options.logger?.info?.({ rv: ev.rv, generation: this.gen }, "informers: generation changed, relisting");
|
|
213
|
+
return "relist";
|
|
214
|
+
}
|
|
131
215
|
switch (ev.type) {
|
|
132
216
|
case "BOOKMARK":
|
|
133
217
|
break;
|
|
134
218
|
case "MODIFIED":
|
|
135
|
-
this.informers.get(ev.resource)?.apply(ev.object, ev.objectRv)
|
|
219
|
+
if (this.informers.get(ev.resource)?.apply(ev.object, ev.objectRv) === false)
|
|
220
|
+
this.stats.dropped++;
|
|
136
221
|
break;
|
|
137
222
|
case "DELETED":
|
|
138
|
-
this.informers.get(ev.resource)?.remove(ev.object.id, ev.rv)
|
|
223
|
+
if (this.informers.get(ev.resource)?.remove(ev.object.id, ev.rv) === false)
|
|
224
|
+
this.stats.dropped++;
|
|
139
225
|
break;
|
|
140
|
-
case "ERROR":
|
|
141
|
-
return ev.code === "watch_expired" ? "relist" : "ok";
|
|
142
226
|
}
|
|
143
227
|
// Resume point: the stream position, which only moves forward.
|
|
144
|
-
if (
|
|
228
|
+
if (ev.rv && (!this.rv || compareRv(ev.rv, this.rv) > 0))
|
|
145
229
|
this.rv = ev.rv;
|
|
146
230
|
return "ok";
|
|
147
231
|
}
|
|
232
|
+
/**
|
|
233
|
+
* One watch connection, read with an idle watchdog: no line for
|
|
234
|
+
* `idleTimeoutMs` aborts the request (its signal) and throws. The race
|
|
235
|
+
* does not trust the client to honour the signal.
|
|
236
|
+
*/
|
|
237
|
+
async watchOnce(onProgress) {
|
|
238
|
+
const idleMs = this.options.idleTimeoutMs ?? 45_000;
|
|
239
|
+
const conn = new AbortController();
|
|
240
|
+
const outer = this.abort.signal;
|
|
241
|
+
const onStop = () => conn.abort();
|
|
242
|
+
// stop() may have run already: an aborted signal never fires again.
|
|
243
|
+
if (outer.aborted)
|
|
244
|
+
conn.abort();
|
|
245
|
+
else
|
|
246
|
+
outer.addEventListener("abort", onStop, { once: true });
|
|
247
|
+
const stopped = new Promise((r) => conn.signal.aborted ? r("stopped") : conn.signal.addEventListener("abort", () => r("stopped")));
|
|
248
|
+
const it = this.client.watch(this.options.resources, this.rv, conn.signal)[Symbol.asyncIterator]();
|
|
249
|
+
let events = 0;
|
|
250
|
+
try {
|
|
251
|
+
for (;;) {
|
|
252
|
+
let timer;
|
|
253
|
+
const idle = new Promise((r) => (timer = setTimeout(() => r("idle"), idleMs)));
|
|
254
|
+
let next;
|
|
255
|
+
try {
|
|
256
|
+
next = await Promise.race([it.next(), idle, stopped]);
|
|
257
|
+
}
|
|
258
|
+
finally {
|
|
259
|
+
clearTimeout(timer);
|
|
260
|
+
}
|
|
261
|
+
if (next === "stopped")
|
|
262
|
+
return "closed";
|
|
263
|
+
if (next === "idle")
|
|
264
|
+
throw new WatchIdleError(idleMs);
|
|
265
|
+
if (next.done) {
|
|
266
|
+
this.stats.reconnects++;
|
|
267
|
+
this.options.logger?.info?.({ reconnects: this.stats.reconnects, events, rv: this.rv }, "informers: watch closed, reconnecting");
|
|
268
|
+
return "closed";
|
|
269
|
+
}
|
|
270
|
+
events++;
|
|
271
|
+
const before = this.rv;
|
|
272
|
+
const out = this.handle(next.value);
|
|
273
|
+
if (out !== "ok")
|
|
274
|
+
return out;
|
|
275
|
+
if (this.rv !== before)
|
|
276
|
+
onProgress();
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
finally {
|
|
280
|
+
conn.abort();
|
|
281
|
+
outer.removeEventListener("abort", onStop);
|
|
282
|
+
// Not awaited: a stalled iterator may never settle.
|
|
283
|
+
it.return?.()?.catch?.(() => { });
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
sleep(ms) {
|
|
287
|
+
return new Promise((r) => {
|
|
288
|
+
const timer = setTimeout(done, ms);
|
|
289
|
+
const signal = this.abort.signal;
|
|
290
|
+
function done() {
|
|
291
|
+
clearTimeout(timer);
|
|
292
|
+
signal.removeEventListener("abort", done);
|
|
293
|
+
r();
|
|
294
|
+
}
|
|
295
|
+
if (signal.aborted)
|
|
296
|
+
done();
|
|
297
|
+
else
|
|
298
|
+
signal.addEventListener("abort", done, { once: true });
|
|
299
|
+
});
|
|
300
|
+
}
|
|
148
301
|
async run() {
|
|
149
302
|
const [first, max] = this.options.backoffMs ?? [500, 30_000];
|
|
150
303
|
let backoff = first;
|
|
151
304
|
let needList = true;
|
|
152
305
|
while (!this.stopped) {
|
|
306
|
+
// Progress = the stream position moved. An attempt without it (error,
|
|
307
|
+
// empty stream, ERROR event, 410 right after a relist) backs off.
|
|
308
|
+
let progressed = false;
|
|
153
309
|
try {
|
|
154
310
|
if (needList) {
|
|
155
311
|
await this.relist();
|
|
156
312
|
needList = false;
|
|
157
313
|
}
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
if (this.handle(ev) === "relist") {
|
|
161
|
-
relist = true;
|
|
162
|
-
break;
|
|
163
|
-
}
|
|
164
|
-
backoff = first;
|
|
165
|
-
}
|
|
166
|
-
if (relist)
|
|
314
|
+
const out = await this.watchOnce(() => (progressed = true));
|
|
315
|
+
if (out === "relist") {
|
|
167
316
|
needList = true;
|
|
168
|
-
|
|
169
|
-
|
|
317
|
+
this.stats.relistPending = true;
|
|
318
|
+
}
|
|
170
319
|
}
|
|
171
320
|
catch (err) {
|
|
172
321
|
if (this.stopped)
|
|
173
322
|
break;
|
|
174
323
|
if (err instanceof WatchExpiredError) {
|
|
175
324
|
needList = true;
|
|
176
|
-
|
|
325
|
+
this.stats.relistPending = true;
|
|
177
326
|
}
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
327
|
+
else {
|
|
328
|
+
this.stats.errors++;
|
|
329
|
+
this.options.logger?.warn({ err, backoff }, "informers: list/watch failed, retrying");
|
|
330
|
+
}
|
|
331
|
+
}
|
|
332
|
+
if (this.stopped)
|
|
333
|
+
break;
|
|
334
|
+
if (progressed) {
|
|
335
|
+
backoff = first;
|
|
336
|
+
continue;
|
|
181
337
|
}
|
|
338
|
+
// ±20 % jitter, so a fleet does not reconnect in step.
|
|
339
|
+
await this.sleep(backoff * (0.8 + Math.random() * 0.4));
|
|
340
|
+
backoff = Math.min(backoff * 2, max);
|
|
182
341
|
}
|
|
183
342
|
}
|
|
184
343
|
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
export { Indexer, type IndexFn } from "./Indexer.js";
|
|
2
|
-
export { InformerFactory, SharedInformer, type InformerEvent, type InformerFactoryOptions, } from "./InformerFactory.js";
|
|
3
|
-
export { compareRv, minRv } from "./rv.js";
|
|
2
|
+
export { InformerFactory, SharedInformer, WatchIdleError, type InformerEvent, type InformerFactoryOptions, } from "./InformerFactory.js";
|
|
3
|
+
export { compareRv, minRv, rvGeneration, rvTimeMs } from "./rv.js";
|
|
4
4
|
export { WatchExpiredError, type ListResult, type WatchClient, type WatchEvent, type WatchResource, } from "./WatchClient.js";
|
|
5
5
|
export { deployApiWatchClient } from "./deployApiWatchClient.js";
|
|
6
6
|
//# sourceMappingURL=index.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/informers/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,OAAO,EAAE,KAAK,OAAO,EAAE,MAAM,cAAc,CAAA;AACpD,OAAO,EACL,eAAe,EACf,cAAc,EACd,KAAK,aAAa,EAClB,KAAK,sBAAsB,GAC5B,MAAM,sBAAsB,CAAA;AAC7B,OAAO,EAAE,SAAS,EAAE,KAAK,EAAE,MAAM,SAAS,CAAA;
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/informers/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,OAAO,EAAE,KAAK,OAAO,EAAE,MAAM,cAAc,CAAA;AACpD,OAAO,EACL,eAAe,EACf,cAAc,EACd,cAAc,EACd,KAAK,aAAa,EAClB,KAAK,sBAAsB,GAC5B,MAAM,sBAAsB,CAAA;AAC7B,OAAO,EAAE,SAAS,EAAE,KAAK,EAAE,YAAY,EAAE,QAAQ,EAAE,MAAM,SAAS,CAAA;AAClE,OAAO,EACL,iBAAiB,EACjB,KAAK,UAAU,EACf,KAAK,WAAW,EAChB,KAAK,UAAU,EACf,KAAK,aAAa,GACnB,MAAM,kBAAkB,CAAA;AACzB,OAAO,EAAE,oBAAoB,EAAE,MAAM,2BAA2B,CAAA"}
|
package/dist/informers/index.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
export { Indexer } from "./Indexer.js";
|
|
2
|
-
export { InformerFactory, SharedInformer, } from "./InformerFactory.js";
|
|
3
|
-
export { compareRv, minRv } from "./rv.js";
|
|
2
|
+
export { InformerFactory, SharedInformer, WatchIdleError, } from "./InformerFactory.js";
|
|
3
|
+
export { compareRv, minRv, rvGeneration, rvTimeMs } from "./rv.js";
|
|
4
4
|
export { WatchExpiredError, } from "./WatchClient.js";
|
|
5
5
|
export { deployApiWatchClient } from "./deployApiWatchClient.js";
|
package/dist/informers/rv.d.ts
CHANGED
|
@@ -1,4 +1,15 @@
|
|
|
1
1
|
/** <0, 0, >0 like a comparator; an unparsable rv sorts first. */
|
|
2
2
|
export declare const compareRv: (a: string, b: string) => number;
|
|
3
|
+
/**
|
|
4
|
+
* When the change entered the cache's stream (ms since epoch), from the
|
|
5
|
+
* stream position in an rv. `now - rvTimeMs(rv)` is how long the event took
|
|
6
|
+
* to reach this process — the informer's freshness. null if unparsable.
|
|
7
|
+
*/
|
|
8
|
+
export declare const rvTimeMs: (rv: string) => number | null;
|
|
9
|
+
/**
|
|
10
|
+
* The generation of an rv (it changes when the server's stream is reset).
|
|
11
|
+
* rvs of different generations are not comparable. null if unparsable.
|
|
12
|
+
*/
|
|
13
|
+
export declare const rvGeneration: (rv: string) => number | null;
|
|
3
14
|
export declare const minRv: (rvs: string[]) => string;
|
|
4
15
|
//# sourceMappingURL=rv.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"rv.d.ts","sourceRoot":"","sources":["../../src/informers/rv.ts"],"names":[],"mappings":"AASA,iEAAiE;AACjE,eAAO,MAAM,SAAS,MAAO,MAAM,KAAK,MAAM,KAAG,MAKhD,CAAA;AAED,eAAO,MAAM,KAAK,QAAS,MAAM,EAAE,KAAG,MACe,CAAA"}
|
|
1
|
+
{"version":3,"file":"rv.d.ts","sourceRoot":"","sources":["../../src/informers/rv.ts"],"names":[],"mappings":"AASA,iEAAiE;AACjE,eAAO,MAAM,SAAS,MAAO,MAAM,KAAK,MAAM,KAAG,MAKhD,CAAA;AAED;;;;GAIG;AACH,eAAO,MAAM,QAAQ,OAAQ,MAAM,KAAG,MAAM,GAAG,IAG9C,CAAA;AAED;;;GAGG;AACH,eAAO,MAAM,YAAY,OAAQ,MAAM,KAAG,MAAM,GAAG,IAGlD,CAAA;AAED,eAAO,MAAM,KAAK,QAAS,MAAM,EAAE,KAAG,MACe,CAAA"}
|
package/dist/informers/rv.js
CHANGED
|
@@ -13,4 +13,21 @@ export const compareRv = (a, b) => {
|
|
|
13
13
|
return pa ? 1 : pb ? -1 : 0;
|
|
14
14
|
return pa[0] - pb[0] || pa[1] - pb[1] || pa[2] - pb[2];
|
|
15
15
|
};
|
|
16
|
+
/**
|
|
17
|
+
* When the change entered the cache's stream (ms since epoch), from the
|
|
18
|
+
* stream position in an rv. `now - rvTimeMs(rv)` is how long the event took
|
|
19
|
+
* to reach this process — the informer's freshness. null if unparsable.
|
|
20
|
+
*/
|
|
21
|
+
export const rvTimeMs = (rv) => {
|
|
22
|
+
const p = parse(rv);
|
|
23
|
+
return p ? p[1] : null;
|
|
24
|
+
};
|
|
25
|
+
/**
|
|
26
|
+
* The generation of an rv (it changes when the server's stream is reset).
|
|
27
|
+
* rvs of different generations are not comparable. null if unparsable.
|
|
28
|
+
*/
|
|
29
|
+
export const rvGeneration = (rv) => {
|
|
30
|
+
const p = parse(rv);
|
|
31
|
+
return p ? p[0] : null;
|
|
32
|
+
};
|
|
16
33
|
export const minRv = (rvs) => rvs.reduce((m, r) => (compareRv(r, m) < 0 ? r : m));
|