@faable/deploy-sdk 2.17.1 → 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.
Files changed (36) hide show
  1. package/dist/FaableDeployApi.d.ts +89 -0
  2. package/dist/FaableDeployApi.d.ts.map +1 -1
  3. package/dist/FaableDeployApi.js +10 -0
  4. package/dist/api/api-types.d.ts +19 -0
  5. package/dist/api/api-types.d.ts.map +1 -1
  6. package/dist/api/generated-client.d.ts +856 -3
  7. package/dist/api/generated-client.d.ts.map +1 -1
  8. package/dist/api/generated-client.js +510 -3
  9. package/dist/api/types.d.ts +4897 -819
  10. package/dist/api/types.d.ts.map +1 -1
  11. package/dist/index.d.ts +1 -0
  12. package/dist/index.d.ts.map +1 -1
  13. package/dist/index.js +1 -0
  14. package/dist/informers/Indexer.d.ts +28 -0
  15. package/dist/informers/Indexer.d.ts.map +1 -0
  16. package/dist/informers/Indexer.js +89 -0
  17. package/dist/informers/InformerFactory.d.ts +113 -0
  18. package/dist/informers/InformerFactory.d.ts.map +1 -0
  19. package/dist/informers/InformerFactory.js +343 -0
  20. package/dist/informers/WatchClient.d.ts +38 -0
  21. package/dist/informers/WatchClient.d.ts.map +1 -0
  22. package/dist/informers/WatchClient.js +9 -0
  23. package/dist/informers/deployApiWatchClient.d.ts +4 -0
  24. package/dist/informers/deployApiWatchClient.d.ts.map +1 -0
  25. package/dist/informers/deployApiWatchClient.js +36 -0
  26. package/dist/informers/index.d.ts +6 -0
  27. package/dist/informers/index.d.ts.map +1 -0
  28. package/dist/informers/index.js +5 -0
  29. package/dist/informers/ndjson.d.ts +2 -0
  30. package/dist/informers/ndjson.d.ts.map +1 -0
  31. package/dist/informers/ndjson.js +19 -0
  32. package/dist/informers/rv.d.ts +15 -0
  33. package/dist/informers/rv.d.ts.map +1 -0
  34. package/dist/informers/rv.js +33 -0
  35. package/package.json +1 -1
  36. package/spec/openapi.json +16505 -7626
package/dist/index.d.ts CHANGED
@@ -1,4 +1,5 @@
1
1
  export * from "./FaableDeployApi.js";
2
+ export * from "./informers/index.js";
2
3
  export * from "./api/generated-client.js";
3
4
  export * from "./api/custom-types.js";
4
5
  export * from "./api/types.js";
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,cAAc,sBAAsB,CAAC;AACrC,cAAc,2BAA2B,CAAC;AAG1C,cAAc,uBAAuB,CAAC;AACtC,cAAc,gBAAgB,CAAC;AAM/B,cAAc,kBAAkB,CAAC;AAMjC,OAAO,KAAK,KAAK,MAAM,uBAAuB,CAAC;AAC/C,OAAO,EAAE,KAAK,EAAE,CAAC"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,cAAc,sBAAsB,CAAC;AACrC,cAAc,sBAAsB,CAAC;AACrC,cAAc,2BAA2B,CAAC;AAG1C,cAAc,uBAAuB,CAAC;AACtC,cAAc,gBAAgB,CAAC;AAM/B,cAAc,kBAAkB,CAAC;AAMjC,OAAO,KAAK,KAAK,MAAM,uBAAuB,CAAC;AAC/C,OAAO,EAAE,KAAK,EAAE,CAAC"}
package/dist/index.js CHANGED
@@ -1,4 +1,5 @@
1
1
  export * from "./FaableDeployApi.js";
2
+ export * from "./informers/index.js";
2
3
  export * from "./api/generated-client.js";
3
4
  // custom-types re-exports the auto-generated api-types plus the hand-maintained
4
5
  // extras (friendlier event names, the AppTraffic interface).
@@ -0,0 +1,28 @@
1
+ export type IndexFn<T> = (obj: T) => string[];
2
+ export declare class Indexer<T extends {
3
+ id: string;
4
+ }> {
5
+ private objects;
6
+ private indexFns;
7
+ private indexes;
8
+ addIndex(name: string, fn: IndexFn<T>): void;
9
+ get(id: string): T | undefined;
10
+ rvOf(id: string): string | undefined;
11
+ list(): T[];
12
+ ids(): string[];
13
+ byIndex(name: string, key: string): T[];
14
+ /** Upsert if newer. Returns the previous object, or null if dropped. */
15
+ upsert(obj: T, rv: string): {
16
+ applied: boolean;
17
+ old?: T;
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;
23
+ /** Remove unless what is held is newer than the delete. */
24
+ delete(id: string, rv: string): T | undefined;
25
+ private indexObj;
26
+ private unindex;
27
+ }
28
+ //# sourceMappingURL=Indexer.d.ts.map
@@ -0,0 +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,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"}
@@ -0,0 +1,89 @@
1
+ import { compareRv } from "./rv.js";
2
+ export class Indexer {
3
+ objects = new Map();
4
+ indexFns = new Map();
5
+ indexes = new Map();
6
+ addIndex(name, fn) {
7
+ this.indexFns.set(name, fn);
8
+ const idx = new Map();
9
+ this.indexes.set(name, idx);
10
+ for (const { obj } of this.objects.values())
11
+ this.indexObj(name, fn, obj);
12
+ }
13
+ get(id) {
14
+ return this.objects.get(id)?.obj;
15
+ }
16
+ rvOf(id) {
17
+ return this.objects.get(id)?.rv;
18
+ }
19
+ list() {
20
+ return [...this.objects.values()].map((e) => e.obj);
21
+ }
22
+ ids() {
23
+ return [...this.objects.keys()];
24
+ }
25
+ byIndex(name, key) {
26
+ const ids = this.indexes.get(name)?.get(key);
27
+ if (!ids)
28
+ return [];
29
+ return [...ids].map((id) => this.objects.get(id).obj);
30
+ }
31
+ /** Upsert if newer. Returns the previous object, or null if dropped. */
32
+ upsert(obj, rv) {
33
+ const cur = this.objects.get(obj.id);
34
+ if (cur && compareRv(rv, cur.rv) <= 0)
35
+ return { applied: false };
36
+ if (cur)
37
+ this.unindex(cur.obj);
38
+ this.objects.set(obj.id, { obj, rv });
39
+ for (const [name, fn] of this.indexFns)
40
+ this.indexObj(name, fn, obj);
41
+ return { applied: true, old: cur?.obj };
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
+ }
62
+ /** Remove unless what is held is newer than the delete. */
63
+ delete(id, rv) {
64
+ const cur = this.objects.get(id);
65
+ if (!cur || compareRv(cur.rv, rv) > 0)
66
+ return undefined;
67
+ this.unindex(cur.obj);
68
+ this.objects.delete(id);
69
+ return cur.obj;
70
+ }
71
+ indexObj(name, fn, obj) {
72
+ const idx = this.indexes.get(name);
73
+ for (const key of fn(obj)) {
74
+ if (!idx.has(key))
75
+ idx.set(key, new Set());
76
+ idx.get(key).add(obj.id);
77
+ }
78
+ }
79
+ unindex(obj) {
80
+ for (const [name, fn] of this.indexFns) {
81
+ const idx = this.indexes.get(name);
82
+ for (const key of fn(obj)) {
83
+ idx.get(key)?.delete(obj.id);
84
+ if (idx.get(key)?.size === 0)
85
+ idx.delete(key);
86
+ }
87
+ }
88
+ }
89
+ }
@@ -0,0 +1,113 @@
1
+ import { Indexer, type IndexFn } from "./Indexer.js";
2
+ import { type WatchClient, type WatchEvent, type WatchResource } from "./WatchClient.js";
3
+ export type InformerEvent = "add" | "update" | "delete";
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
+ }
13
+ export declare class SharedInformer<T extends {
14
+ id: string;
15
+ }> {
16
+ readonly store: Indexer<T>;
17
+ private handlers;
18
+ readonly resource: WatchResource;
19
+ private logger?;
20
+ constructor(resource: WatchResource, logger?: Logger);
21
+ on(event: InformerEvent, handler: Handler<T>): this;
22
+ addIndex(name: string, fn: IndexFn<T>): this;
23
+ get(id: string): T | undefined;
24
+ list(): T[];
25
+ byIndex(name: string, key: string): T[];
26
+ /** @internal */
27
+ emit(event: InformerEvent, obj: T, old?: T): void;
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
+ */
37
+ replace(items: {
38
+ rv: string;
39
+ object: T;
40
+ }[]): void;
41
+ }
42
+ export type InformerFactoryOptions = {
43
+ resources: WatchResource[];
44
+ logger?: Logger;
45
+ /** Backoff after an attempt without progress: first and max delay (±20 % jitter). */
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;
58
+ };
59
+ export declare class InformerFactory {
60
+ private informers;
61
+ private abort?;
62
+ private loop?;
63
+ private synced;
64
+ private syncWaiters;
65
+ private rv?;
66
+ /** The generation of the current lists; events of another one → relist. */
67
+ private gen;
68
+ private stopped;
69
+ readonly stats: {
70
+ relists: number;
71
+ reconnects: number;
72
+ events: number;
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;
84
+ };
85
+ private client;
86
+ private options;
87
+ constructor(client: WatchClient, options: InformerFactoryOptions);
88
+ informer<T extends {
89
+ id: string;
90
+ } = any>(resource: WatchResource): SharedInformer<T>;
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;
97
+ waitForSync(): Promise<void>;
98
+ start(): void;
99
+ stop(): Promise<void>;
100
+ /** Fetch every list first; replace only if all of them arrived (K4). */
101
+ private relist;
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;
110
+ private run;
111
+ }
112
+ export {};
113
+ //# sourceMappingURL=InformerFactory.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"InformerFactory.d.ts","sourceRoot":"","sources":["../../src/informers/InformerFactory.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,343 @@
1
+ import { Indexer } from "./Indexer.js";
2
+ import { compareRv, minRv, rvGeneration, rvTimeMs } from "./rv.js";
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
+ }
11
+ export class SharedInformer {
12
+ store = new Indexer();
13
+ handlers = {
14
+ add: [],
15
+ update: [],
16
+ delete: [],
17
+ };
18
+ resource;
19
+ logger;
20
+ constructor(resource, logger) {
21
+ this.resource = resource;
22
+ this.logger = logger;
23
+ }
24
+ on(event, handler) {
25
+ this.handlers[event].push(handler);
26
+ return this;
27
+ }
28
+ addIndex(name, fn) {
29
+ this.store.addIndex(name, fn);
30
+ return this;
31
+ }
32
+ get(id) {
33
+ return this.store.get(id);
34
+ }
35
+ list() {
36
+ return this.store.list();
37
+ }
38
+ byIndex(name, key) {
39
+ return this.store.byIndex(name, key);
40
+ }
41
+ /** @internal */
42
+ emit(event, obj, old) {
43
+ for (const h of this.handlers[event]) {
44
+ try {
45
+ h(obj, old);
46
+ }
47
+ catch (err) {
48
+ // A handler's failure is the handler's: the store and the other
49
+ // handlers carry on. But it is said.
50
+ this.logger?.warn({ err, resource: this.resource, event, id: obj?.id }, "informers: handler threw");
51
+ }
52
+ }
53
+ }
54
+ /** @internal Apply an upsert; emits add/update. false if dropped as stale. */
55
+ apply(obj, rv) {
56
+ const { applied, old } = this.store.upsert(obj, rv);
57
+ if (applied)
58
+ this.emit(old ? "update" : "add", obj, old);
59
+ return applied;
60
+ }
61
+ /** @internal Apply a delete; emits delete. false if dropped as stale. */
62
+ remove(id, rv) {
63
+ if (!this.store.get(id))
64
+ return true;
65
+ const gone = this.store.delete(id, rv);
66
+ if (gone)
67
+ this.emit("delete", gone);
68
+ return !!gone;
69
+ }
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) {
76
+ const seen = new Set();
77
+ for (const { rv, object } of items) {
78
+ seen.add(object.id);
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);
91
+ }
92
+ }
93
+ }
94
+ export class InformerFactory {
95
+ informers = new Map();
96
+ abort;
97
+ loop;
98
+ synced = false;
99
+ syncWaiters = [];
100
+ rv;
101
+ /** The generation of the current lists; events of another one → relist. */
102
+ gen = null;
103
+ stopped = true;
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
+ };
120
+ client;
121
+ options;
122
+ constructor(client, options) {
123
+ this.client = client;
124
+ this.options = options;
125
+ for (const r of options.resources)
126
+ this.informers.set(r, new SharedInformer(r, options.logger));
127
+ }
128
+ informer(resource) {
129
+ const inf = this.informers.get(resource);
130
+ if (!inf)
131
+ throw new Error(`informer for "${resource}" not in this factory`);
132
+ return inf;
133
+ }
134
+ hasSynced() {
135
+ return this.synced;
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
+ }
146
+ waitForSync() {
147
+ if (this.synced)
148
+ return Promise.resolve();
149
+ return new Promise((r) => this.syncWaiters.push(r));
150
+ }
151
+ start() {
152
+ if (!this.stopped)
153
+ return;
154
+ this.stopped = false;
155
+ this.abort = new AbortController();
156
+ this.loop = this.run();
157
+ }
158
+ async stop() {
159
+ this.stopped = true;
160
+ this.abort?.abort();
161
+ await this.loop?.catch(() => { });
162
+ }
163
+ /** Fetch every list first; replace only if all of them arrived (K4). */
164
+ async relist() {
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);
172
+ // Watch from the OLDEST list rv: events already reflected in a newer
173
+ // list are dropped by the per-object rv.
174
+ this.rv = minRv(rvs);
175
+ this.gen = rvGeneration(this.rv);
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");
183
+ if (!this.synced) {
184
+ this.synced = true;
185
+ for (const w of this.syncWaiters.splice(0))
186
+ w();
187
+ }
188
+ }
189
+ handle(ev) {
190
+ this.stats.events++;
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
+ }
215
+ switch (ev.type) {
216
+ case "BOOKMARK":
217
+ break;
218
+ case "MODIFIED":
219
+ if (this.informers.get(ev.resource)?.apply(ev.object, ev.objectRv) === false)
220
+ this.stats.dropped++;
221
+ break;
222
+ case "DELETED":
223
+ if (this.informers.get(ev.resource)?.remove(ev.object.id, ev.rv) === false)
224
+ this.stats.dropped++;
225
+ break;
226
+ }
227
+ // Resume point: the stream position, which only moves forward.
228
+ if (ev.rv && (!this.rv || compareRv(ev.rv, this.rv) > 0))
229
+ this.rv = ev.rv;
230
+ return "ok";
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
+ }
301
+ async run() {
302
+ const [first, max] = this.options.backoffMs ?? [500, 30_000];
303
+ let backoff = first;
304
+ let needList = true;
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;
309
+ try {
310
+ if (needList) {
311
+ await this.relist();
312
+ needList = false;
313
+ }
314
+ const out = await this.watchOnce(() => (progressed = true));
315
+ if (out === "relist") {
316
+ needList = true;
317
+ this.stats.relistPending = true;
318
+ }
319
+ }
320
+ catch (err) {
321
+ if (this.stopped)
322
+ break;
323
+ if (err instanceof WatchExpiredError) {
324
+ needList = true;
325
+ this.stats.relistPending = true;
326
+ }
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;
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);
341
+ }
342
+ }
343
+ }
@@ -0,0 +1,38 @@
1
+ export type WatchResource = "app" | "deployment" | "domain" | "project";
2
+ export type ListResult<T = any> = {
3
+ rv: string;
4
+ items: {
5
+ rv: string;
6
+ object: T;
7
+ }[];
8
+ };
9
+ export type WatchEvent<T = any> = {
10
+ type: "MODIFIED";
11
+ resource: WatchResource;
12
+ rv: string;
13
+ objectRv: string;
14
+ object: T;
15
+ } | {
16
+ type: "DELETED";
17
+ resource: WatchResource;
18
+ rv: string;
19
+ object: {
20
+ id: string;
21
+ };
22
+ } | {
23
+ type: "BOOKMARK";
24
+ rv: string;
25
+ } | {
26
+ type: "ERROR";
27
+ code: string;
28
+ message?: string;
29
+ };
30
+ /** Thrown by `watch` when the rv cannot be resumed (HTTP 410): relist. */
31
+ export declare class WatchExpiredError extends Error {
32
+ constructor(message?: string);
33
+ }
34
+ export interface WatchClient {
35
+ list(resource: WatchResource): Promise<ListResult>;
36
+ watch(resources: WatchResource[], rv: string, signal: AbortSignal): AsyncIterable<WatchEvent>;
37
+ }
38
+ //# sourceMappingURL=WatchClient.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"WatchClient.d.ts","sourceRoot":"","sources":["../../src/informers/WatchClient.ts"],"names":[],"mappings":"AAGA,MAAM,MAAM,aAAa,GAAG,KAAK,GAAG,YAAY,GAAG,QAAQ,GAAG,SAAS,CAAA;AAEvE,MAAM,MAAM,UAAU,CAAC,CAAC,GAAG,GAAG,IAAI;IAChC,EAAE,EAAE,MAAM,CAAA;IACV,KAAK,EAAE;QAAE,EAAE,EAAE,MAAM,CAAC;QAAC,MAAM,EAAE,CAAC,CAAA;KAAE,EAAE,CAAA;CACnC,CAAA;AAED,MAAM,MAAM,UAAU,CAAC,CAAC,GAAG,GAAG,IAC1B;IAAE,IAAI,EAAE,UAAU,CAAC;IAAC,QAAQ,EAAE,aAAa,CAAC;IAAC,EAAE,EAAE,MAAM,CAAC;IAAC,QAAQ,EAAE,MAAM,CAAC;IAAC,MAAM,EAAE,CAAC,CAAA;CAAE,GACtF;IAAE,IAAI,EAAE,SAAS,CAAC;IAAC,QAAQ,EAAE,aAAa,CAAC;IAAC,EAAE,EAAE,MAAM,CAAC;IAAC,MAAM,EAAE;QAAE,EAAE,EAAE,MAAM,CAAA;KAAE,CAAA;CAAE,GAChF;IAAE,IAAI,EAAE,UAAU,CAAC;IAAC,EAAE,EAAE,MAAM,CAAA;CAAE,GAChC;IAAE,IAAI,EAAE,OAAO,CAAC;IAAC,IAAI,EAAE,MAAM,CAAC;IAAC,OAAO,CAAC,EAAE,MAAM,CAAA;CAAE,CAAA;AAErD,0EAA0E;AAC1E,qBAAa,iBAAkB,SAAQ,KAAK;gBAC9B,OAAO,SAA0B;CAI9C;AAED,MAAM,WAAW,WAAW;IAC1B,IAAI,CAAC,QAAQ,EAAE,aAAa,GAAG,OAAO,CAAC,UAAU,CAAC,CAAA;IAClD,KAAK,CACH,SAAS,EAAE,aAAa,EAAE,EAC1B,EAAE,EAAE,MAAM,EACV,MAAM,EAAE,WAAW,GAClB,aAAa,CAAC,UAAU,CAAC,CAAA;CAC7B"}
@@ -0,0 +1,9 @@
1
+ // The transport under the informers: list a resource, watch from an rv
2
+ // (arch/api/informers.md § 3). An interface so tests can play a server.
3
+ /** Thrown by `watch` when the rv cannot be resumed (HTTP 410): relist. */
4
+ export class WatchExpiredError extends Error {
5
+ constructor(message = "watch expired: relist") {
6
+ super(message);
7
+ this.name = "WatchExpiredError";
8
+ }
9
+ }
@@ -0,0 +1,4 @@
1
+ import type { Fetcher } from "@faable/sdk-base";
2
+ import { type WatchClient } from "./WatchClient.js";
3
+ export declare const deployApiWatchClient: (fetcher: Fetcher) => WatchClient;
4
+ //# sourceMappingURL=deployApiWatchClient.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"deployApiWatchClient.d.ts","sourceRoot":"","sources":["../../src/informers/deployApiWatchClient.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,kBAAkB,CAAA;AAE/C,OAAO,EAGL,KAAK,WAAW,EAGjB,MAAM,kBAAkB,CAAA;AAWzB,eAAO,MAAM,oBAAoB,YAAa,OAAO,KAAG,WA2BtD,CAAA"}