@webpieces/core-util 0.4.963 → 0.4.965

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 (64) hide show
  1. package/package.json +1 -1
  2. package/src/http/ApiCallInfo.d.ts +3 -3
  3. package/src/http/ApiCallInfo.js +1 -1
  4. package/src/http/ApiCallInfo.js.map +1 -1
  5. package/src/http/ApiMethodInfo.d.ts +1 -1
  6. package/src/http/ApiMethodInfo.js.map +1 -1
  7. package/src/http/ApiRegistry.d.ts +91 -0
  8. package/src/http/ApiRegistry.js +129 -0
  9. package/src/http/ApiRegistry.js.map +1 -0
  10. package/src/http/CallScope.d.ts +35 -8
  11. package/src/http/CallScope.js +59 -5
  12. package/src/http/CallScope.js.map +1 -1
  13. package/src/http/CallStrategy.d.ts +1 -1
  14. package/src/http/CallStrategy.js.map +1 -1
  15. package/src/http/ClientRegistry.d.ts +38 -71
  16. package/src/http/ClientRegistry.js +127 -97
  17. package/src/http/ClientRegistry.js.map +1 -1
  18. package/src/http/ClientRegistryCompileAssertions.js +63 -0
  19. package/src/http/ClientRegistryCompileAssertions.js.map +1 -0
  20. package/src/http/DeletedAuthCompileAssertions.js +1 -1
  21. package/src/http/DeletedAuthCompileAssertions.js.map +1 -1
  22. package/src/http/ErrorClassifier.d.ts +7 -7
  23. package/src/http/ErrorClassifier.js.map +1 -1
  24. package/src/http/ErrorLadderCompileAssertions.js +41 -17
  25. package/src/http/ErrorLadderCompileAssertions.js.map +1 -1
  26. package/src/http/ErrorTranslator.d.ts +6 -6
  27. package/src/http/ErrorTranslator.js.map +1 -1
  28. package/src/http/LogApiCall.d.ts +1 -1
  29. package/src/http/LogApiCall.js +4 -4
  30. package/src/http/LogApiCall.js.map +1 -1
  31. package/src/http/RetryStrategy.d.ts +2 -2
  32. package/src/http/RetryStrategy.js +4 -4
  33. package/src/http/RetryStrategy.js.map +1 -1
  34. package/src/http/RouteMetadata.d.ts +1 -1
  35. package/src/http/RouteMetadata.js +1 -1
  36. package/src/http/RouteMetadata.js.map +1 -1
  37. package/src/http/StreamErrorControl.js +2 -2
  38. package/src/http/StreamErrorControl.js.map +1 -1
  39. package/src/http/TransportDefault.d.ts +2 -2
  40. package/src/http/TransportDefault.js +2 -2
  41. package/src/http/TransportDefault.js.map +1 -1
  42. package/src/http/WebpiecesDefaultErrorTranslator.d.ts +2 -2
  43. package/src/http/WebpiecesDefaultErrorTranslator.js +2 -2
  44. package/src/http/WebpiecesDefaultErrorTranslator.js.map +1 -1
  45. package/src/http/auth-mode.d.ts +6 -4
  46. package/src/http/auth-mode.js +6 -4
  47. package/src/http/auth-mode.js.map +1 -1
  48. package/src/http/decorators.d.ts +1 -1
  49. package/src/http/decorators.js +3 -3
  50. package/src/http/decorators.js.map +1 -1
  51. package/src/index.d.ts +2 -3
  52. package/src/index.js +7 -5
  53. package/src/index.js.map +1 -1
  54. package/src/ipc/IpcErrorTranslator.d.ts +1 -1
  55. package/src/ipc/IpcErrorTranslator.js.map +1 -1
  56. package/src/ipc/IpcRegistry.d.ts +1 -1
  57. package/src/ipc/IpcRegistry.js +1 -1
  58. package/src/ipc/IpcRegistry.js.map +1 -1
  59. package/src/http/CallRegistry.d.ts +0 -42
  60. package/src/http/CallRegistry.js +0 -143
  61. package/src/http/CallRegistry.js.map +0 -1
  62. package/src/http/CallRegistryCompileAssertions.js +0 -27
  63. package/src/http/CallRegistryCompileAssertions.js.map +0 -1
  64. /package/src/http/{CallRegistryCompileAssertions.d.ts → ClientRegistryCompileAssertions.d.ts} +0 -0
@@ -1,7 +1,6 @@
1
- import { ErrorTranslator } from './ErrorTranslator';
2
- import { CallOutcome, ErrorClassifier } from './ErrorClassifier';
1
+ import { Attempt, CallContext, CallStrategy } from './CallStrategy';
2
+ import { TransportDefault } from './TransportDefault';
3
3
  import { CallScope } from './CallScope';
4
- import { ApiMethodInfo } from './ApiMethodInfo';
5
4
  /**
6
5
  * Derives a base URL for a service name that has NO registered mapping — the pluggable half of
7
6
  * {@link ClientRegistry} resolution. One per environment, installed once at startup:
@@ -48,33 +47,30 @@ export type ServiceUrlDeriver = (svcName: string) => Promise<string>;
48
47
  * ClientRegistry.addUrlMapping('email', 'https://email.other-region.example');
49
48
  * ClientRegistry.setDeriver(gcpCloudRunDeriver()); // everything else, on GCP
50
49
  * ```
50
+ *
51
+ * The OTHER half: everything ONLY an outbound call reads besides its URL — the per-call timeout and
52
+ * strategy, for browser RPC, node RPC, and Cloud Tasks ENQUEUE. ONE implementation; every transport
53
+ * calls {@link ClientRegistry.execute}. (What BOTH sides read — the error translator and classifier —
54
+ * lives on `ApiRegistry`.)
55
+ *
56
+ * Ladder: strategy method -> API -> ALL first; ANY strategy replaces the ENTIRE timeout ladder.
57
+ * Otherwise timeout method -> API -> ALL. Otherwise the transport's {@link TransportDefault} (its
58
+ * timeout, and its strategy if it has one). A registered TIMEOUT is therefore a decision for a single
59
+ * attempt and also replaces a transport's default strategy; to keep retrying with a different attempt
60
+ * deadline, register a `RetryStrategy` instead.
61
+ *
62
+ * RPC has no default retry (only the application knows whether repeating a POST is safe); Cloud Tasks
63
+ * enqueue does (see `TransportDefault`). An app opts RPC in with
64
+ * `ClientRegistry.setStrategy(new RetryStrategy(...).run, CallScope.api(CatalogApi))` — and `RetryStrategy` never
65
+ * retries a `WRITE`, even registered at ALL.
51
66
  */
52
67
  export declare class ClientRegistry {
53
68
  /** svcName -> resolved base URL. Process-global; populated at startup per environment. */
54
69
  private static readonly mappings;
55
70
  /** The fallback for svcNames with no mapping. Undefined = no derivation in this environment. */
56
71
  private static deriver;
57
- /**
58
- * The {@link ErrorTranslator} per {@link CallScope} — method -> API -> ALL, and below every rung
59
- * {@link WEBPIECES_DEFAULT_ERROR_TRANSLATOR}, so {@link ClientRegistry.getErrorTranslator} is
60
- * never undefined. Process-global, set once at startup on the SERVER and in the BROWSER — the
61
- * same no-DI pattern as {@link ClientRegistry.mappings} above.
62
- *
63
- * ONE translator per scope, not a list, on purpose: an app that has several layers of error
64
- * policy for one scope composes them INSIDE its own `toWire`, where the precedence is written
65
- * down. The ladder exists because vendors put their errors on the wire in their OWN formats — a
66
- * per-API translator reads one vendor's 4xx body (`fromWire`), or writes the error body an
67
- * inbound vendor expects back (`toWire`), while every other contract keeps the app's ALL one.
68
- */
69
- private static readonly errorTranslators;
70
- /**
71
- * The {@link ErrorClassifier} per {@link CallScope} — method -> API -> ALL, and below every rung
72
- * {@link WEBPIECES_DEFAULT_ERROR_CLASSIFIER}. A classifier that DEFERS (`undefined`) falls through
73
- * to the next rung. The ALL classifier reads {@link ApiMethodInfo.side}, so one classifier covers
74
- * the server router AND every internal client; an SDK-wrapping api-client registers its own at
75
- * its abstract `XxxApi` class. See {@link ClientRegistry.classify}.
76
- */
77
- private static readonly errorClassifiers;
72
+ /** The timeout / strategy per {@link CallScope} — method -> API -> ALL -> the transport default. */
73
+ private static readonly policies;
78
74
  /** Map a service name to `http://localhost:<port>`. */
79
75
  static addMapping(svcName: string, port: number): void;
80
76
  /**
@@ -111,60 +107,31 @@ export declare class ClientRegistry {
111
107
  * say how to fix it.
112
108
  */
113
109
  static resolve(svcName: string): Promise<string>;
110
+ static setTimeout(timeoutMs: number | undefined, scope: CallScope): void;
111
+ static setStrategy(strategy: CallStrategy<unknown> | undefined, scope: CallScope): void;
114
112
  /**
115
- * Install an {@link ErrorTranslator} for one {@link CallScope}: `'ALL'` (the app's translator,
116
- * server side AND every client side), one contract CLASS, or one method of one contract. The
117
- * scope is REQUIRED — there is no unscoped spelling. Call at startup, on the server AND in the
118
- * browser, mirroring {@link ClientRegistry.addMapping}.
113
+ * Run one call under its registered policy. `context` names the method (its `methodName` selects
114
+ * the per-method policy) and carries the endpoint's declared operation, so the SAME context a
115
+ * strategy sees is the one the transport's deadline puts on an `ApiCallTimeoutError`. The context
116
+ * a strategy sees always carries `api` as its {@link CallContext.contract}, so the error
117
+ * classification inside a strategy resolves on the same ladder.
119
118
  *
120
- * ```ts
121
- * ClientRegistry.setErrorTranslator(new AppErrorTranslator(), 'ALL');
122
- * ClientRegistry.setErrorTranslator(new VendorErrorTranslator(), VendorApi); // one contract
123
- * ClientRegistry.setErrorTranslator(new LegacyErrorTranslator(), VendorApi, 'sync'); // one method
124
- * ```
125
- *
126
- * `set`, not `add`: see the field doc above for why a per-scope LIST is not wanted. A translator
127
- * declines an error by DELEGATING to {@link WebpiecesDefaultErrorTranslator} — not to the next
128
- * rung — exactly as before.
129
- */
130
- static setErrorTranslator(translator: ErrorTranslator, ...scope: CallScope): void;
131
- /**
132
- * THE {@link ErrorTranslator} for one call — method -> API -> ALL ->
133
- * {@link WEBPIECES_DEFAULT_ERROR_TRANSLATOR}, never `undefined`, so a caller never branches.
134
- *
135
- * Both arguments are required so every call site states what it knows: the client and the server
136
- * pass the route's contract class and method; a failure with no route (an unmatched request, a
137
- * failure before routing) passes `undefined, undefined` and reaches the ALL rung alone.
119
+ * Every attempt is wrapped so a retried TRANSIENT failure logs one
120
+ * `WARN [API-client-attempt-TRANSIENT]` line (see {@link LoggedAttempts}).
138
121
  */
139
- static getErrorTranslator(api: Function | undefined, methodName: string | undefined): ErrorTranslator;
122
+ static execute<T>(api: Function, context: CallContext, attempt: Attempt<T>, fallback: TransportDefault): Promise<T>;
140
123
  /**
141
- * Install an {@link ErrorClassifier} for one {@link CallScope}: `'ALL'` (the app/company
142
- * classifier — it reads {@link ApiMethodInfo.side}, so it covers the server router and every
143
- * internal client), one contract CLASS (an SDK-wrapping api-client keys by its abstract `XxxApi`),
144
- * or one method of one contract. The scope is REQUIRED. Call at startup, on the server AND in the
145
- * browser. See {@link ErrorClassifier}.
124
+ * The registered TIMEOUT for a call — method -> API -> ALL -> `defaultMs` — ignoring every
125
+ * strategy. For a transport that hands timing to someone else for this call (Cloud Tasks enqueue
126
+ * with caller-supplied gax `CallOptions`) and still needs ONE safety cap so a hung channel cannot
127
+ * block forever. Raise it with {@link ClientRegistry.setTimeout}.
146
128
  */
147
- static setErrorClassifier(classifier: ErrorClassifier, ...scope: CallScope): void;
148
- /**
149
- * The {@link CallOutcome} of a thrown API-call error. Resolved on the {@link CallScope} ladder of
150
- * `methodInfo.contract` / `methodInfo.methodName`, most specific first, and ALWAYS definitive:
151
- * 1. the method classifier — unless it defers;
152
- * 2. else the API classifier — unless it defers;
153
- * 3. else the ALL classifier — unless it defers;
154
- * 4. else the webpieces built-in ({@link WEBPIECES_DEFAULT_ERROR_CLASSIFIER}), which never defers.
155
- *
156
- * The ONE verdict read by `RetryStrategy` (retry only TRANSIENT), `CallRegistry`'s per-attempt
157
- * log line, `LogApiCall` (EXPECTED → OTHER, else FAIL) and the MCP renderer's retry advice.
158
- */
159
- static classify(error: Error, methodInfo: ApiMethodInfo): CallOutcome;
129
+ static resolveTimeout(api: Function, methodName: string, defaultMs: number): number;
160
130
  /**
161
131
  * Put the process-globals back to the state a fresh process starts in. For TESTS, so one spec
162
- * cannot leak its registrations into the next.
163
- *
164
- * It empties every translator and classifier rung, which RESTORES the webpieces defaults:
165
- * `getErrorTranslator(...)` and `classify(...)` fall through to them below every rung, so
166
- * "cleared" is not a state they have. A spec that wants webpieces' own behaviour back asks for
167
- * exactly that.
132
+ * cannot leak its registrations into the next: every URL mapping, the deriver, and every
133
+ * timeout / strategy rung. (`ApiRegistry.resetForTests()` resets the error translators and
134
+ * classifiers.)
168
135
  */
169
136
  static resetForTests(): void;
170
137
  }
@@ -1,16 +1,68 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.ClientRegistry = void 0;
4
+ const CallStrategy_1 = require("./CallStrategy");
5
+ const CallDeadline_1 = require("./CallDeadline");
6
+ const ApiRegistry_1 = require("./ApiRegistry");
7
+ const ErrorClassifier_1 = require("./ErrorClassifier");
8
+ const ApiMethodInfo_1 = require("./ApiMethodInfo");
9
+ const LogManager_1 = require("../logging/LogManager");
10
+ const ApiCallLogName_1 = require("./ApiCallLogName");
11
+ const errorUtils_1 = require("../lib/errorUtils");
4
12
  const CallScope_1 = require("./CallScope");
5
- const WebpiecesDefaultErrorClassifier_1 = require("./WebpiecesDefaultErrorClassifier");
6
- const WebpiecesDefaultErrorTranslator_1 = require("./WebpiecesDefaultErrorTranslator");
7
- /** One {@link CallScope} rung of the error-translator ladder. Data only, so a class. */
8
- class TranslatorSlot {
9
- translator;
13
+ // The same logger LogApiCall writes its [API-*] lines to, so an attempt line sits beside the final one.
14
+ const log = LogManager_1.LogManager.getLogger(ApiCallLogName_1.LOG_API_CALL_LOGGER_NAME);
15
+ class CallPolicy {
16
+ timeoutMs;
17
+ // webpieces-disable no-any-unknown -- global policy spans heterogeneous response DTOs
18
+ strategy;
10
19
  }
11
- /** One {@link CallScope} rung of the error-classifier ladder. Data only, so a class. */
12
- class ClassifierSlot {
13
- classifier;
20
+ /**
21
+ * Counts the attempts of ONE call and logs each retried TRANSIENT failure. It wraps the attempt
22
+ * BEFORE any strategy sees it, so an app-written strategy gets the same lines as `RetryStrategy`.
23
+ *
24
+ * The line for attempt N is written when attempt N+1 STARTS — i.e. only for an attempt that was
25
+ * actually retried. The last attempt's failure is what escapes, and `LogApiCall`'s final
26
+ * `[API-client-resp-FAIL]` line covers it, so a call that gave up after 4 attempts logs 3 attempt
27
+ * lines and 1 FAIL line, never the last failure twice. An EXPECTED or FAILURE attempt is not logged
28
+ * here either; the final line covers it.
29
+ */
30
+ class LoggedAttempts {
31
+ context;
32
+ attempt;
33
+ attempts = 0;
34
+ pendingTransient;
35
+ constructor(context, attempt) {
36
+ this.context = context;
37
+ this.attempt = attempt;
38
+ }
39
+ run = async (timeoutMs) => {
40
+ this.flushRetried();
41
+ this.attempts += 1;
42
+ // webpieces-disable no-unmanaged-exceptions -- observe the attempt's failure, rethrow it unchanged
43
+ // eslint-disable-next-line @webpieces/no-unmanaged-exceptions
44
+ try {
45
+ return await this.attempt(timeoutMs);
46
+ }
47
+ catch (err) {
48
+ const error = (0, errorUtils_1.toError)(err);
49
+ const methodInfo = new ApiMethodInfo_1.ApiMethodInfo('client', this.context.apiName, this.context.methodName, this.context.contract);
50
+ this.pendingTransient =
51
+ ApiRegistry_1.ApiRegistry.classify(error, methodInfo) === ErrorClassifier_1.CallOutcome.TRANSIENT_FAILURE
52
+ ? error
53
+ : undefined;
54
+ // eslint-disable-next-line @webpieces/no-unmanaged-exceptions -- rethrows the attempt's own failure unchanged
55
+ throw err;
56
+ }
57
+ };
58
+ flushRetried() {
59
+ const error = this.pendingTransient;
60
+ if (error === undefined)
61
+ return;
62
+ this.pendingTransient = undefined;
63
+ log.warn(`[API-client-attempt-TRANSIENT] ${this.context.apiName}.${this.context.methodName} ` +
64
+ `attempt ${this.attempts} errorType=${error.constructor.name} error=${error.message}`);
65
+ }
14
66
  }
15
67
  /**
16
68
  * ClientRegistry - the ONE place a `svcName` becomes a base URL, for every outbound client in every
@@ -45,33 +97,30 @@ class ClassifierSlot {
45
97
  * ClientRegistry.addUrlMapping('email', 'https://email.other-region.example');
46
98
  * ClientRegistry.setDeriver(gcpCloudRunDeriver()); // everything else, on GCP
47
99
  * ```
100
+ *
101
+ * The OTHER half: everything ONLY an outbound call reads besides its URL — the per-call timeout and
102
+ * strategy, for browser RPC, node RPC, and Cloud Tasks ENQUEUE. ONE implementation; every transport
103
+ * calls {@link ClientRegistry.execute}. (What BOTH sides read — the error translator and classifier —
104
+ * lives on `ApiRegistry`.)
105
+ *
106
+ * Ladder: strategy method -> API -> ALL first; ANY strategy replaces the ENTIRE timeout ladder.
107
+ * Otherwise timeout method -> API -> ALL. Otherwise the transport's {@link TransportDefault} (its
108
+ * timeout, and its strategy if it has one). A registered TIMEOUT is therefore a decision for a single
109
+ * attempt and also replaces a transport's default strategy; to keep retrying with a different attempt
110
+ * deadline, register a `RetryStrategy` instead.
111
+ *
112
+ * RPC has no default retry (only the application knows whether repeating a POST is safe); Cloud Tasks
113
+ * enqueue does (see `TransportDefault`). An app opts RPC in with
114
+ * `ClientRegistry.setStrategy(new RetryStrategy(...).run, CallScope.api(CatalogApi))` — and `RetryStrategy` never
115
+ * retries a `WRITE`, even registered at ALL.
48
116
  */
49
117
  class ClientRegistry {
50
118
  /** svcName -> resolved base URL. Process-global; populated at startup per environment. */
51
119
  static mappings = new Map();
52
120
  /** The fallback for svcNames with no mapping. Undefined = no derivation in this environment. */
53
121
  static deriver;
54
- /**
55
- * The {@link ErrorTranslator} per {@link CallScope} — method -> API -> ALL, and below every rung
56
- * {@link WEBPIECES_DEFAULT_ERROR_TRANSLATOR}, so {@link ClientRegistry.getErrorTranslator} is
57
- * never undefined. Process-global, set once at startup on the SERVER and in the BROWSER — the
58
- * same no-DI pattern as {@link ClientRegistry.mappings} above.
59
- *
60
- * ONE translator per scope, not a list, on purpose: an app that has several layers of error
61
- * policy for one scope composes them INSIDE its own `toWire`, where the precedence is written
62
- * down. The ladder exists because vendors put their errors on the wire in their OWN formats — a
63
- * per-API translator reads one vendor's 4xx body (`fromWire`), or writes the error body an
64
- * inbound vendor expects back (`toWire`), while every other contract keeps the app's ALL one.
65
- */
66
- static errorTranslators = new CallScope_1.CallScopeLadder(() => new TranslatorSlot());
67
- /**
68
- * The {@link ErrorClassifier} per {@link CallScope} — method -> API -> ALL, and below every rung
69
- * {@link WEBPIECES_DEFAULT_ERROR_CLASSIFIER}. A classifier that DEFERS (`undefined`) falls through
70
- * to the next rung. The ALL classifier reads {@link ApiMethodInfo.side}, so one classifier covers
71
- * the server router AND every internal client; an SDK-wrapping api-client registers its own at
72
- * its abstract `XxxApi` class. See {@link ClientRegistry.classify}.
73
- */
74
- static errorClassifiers = new CallScope_1.CallScopeLadder(() => new ClassifierSlot());
122
+ /** The timeout / strategy per {@link CallScope} — method -> API -> ALL -> the transport default. */
123
+ static policies = new CallScope_1.CallScopeLadder(() => new CallPolicy());
75
124
  /** Map a service name to `http://localhost:<port>`. */
76
125
  // webpieces-disable no-function-outside-class -- static global singleton (like HeaderRegistry/LogManager); populated once at startup, never DI-injected
77
126
  static addMapping(svcName, port) {
@@ -152,89 +201,70 @@ class ClientRegistry {
152
201
  }
153
202
  return url;
154
203
  }
155
- /**
156
- * Install an {@link ErrorTranslator} for one {@link CallScope}: `'ALL'` (the app's translator,
157
- * server side AND every client side), one contract CLASS, or one method of one contract. The
158
- * scope is REQUIRED — there is no unscoped spelling. Call at startup, on the server AND in the
159
- * browser, mirroring {@link ClientRegistry.addMapping}.
160
- *
161
- * ```ts
162
- * ClientRegistry.setErrorTranslator(new AppErrorTranslator(), 'ALL');
163
- * ClientRegistry.setErrorTranslator(new VendorErrorTranslator(), VendorApi); // one contract
164
- * ClientRegistry.setErrorTranslator(new LegacyErrorTranslator(), VendorApi, 'sync'); // one method
165
- * ```
166
- *
167
- * `set`, not `add`: see the field doc above for why a per-scope LIST is not wanted. A translator
168
- * declines an error by DELEGATING to {@link WebpiecesDefaultErrorTranslator} — not to the next
169
- * rung — exactly as before.
170
- */
171
- // webpieces-disable no-function-outside-class -- static global singleton (like HeaderRegistry/LogManager); populated once at startup, never DI-injected
172
- static setErrorTranslator(translator, ...scope) {
173
- ClientRegistry.errorTranslators.at(...scope).translator = translator;
204
+ // webpieces-disable no-function-outside-class -- process-global startup registry
205
+ static setTimeout(timeoutMs, scope) {
206
+ if (timeoutMs !== undefined)
207
+ CallDeadline_1.CallDeadline.validate(timeoutMs);
208
+ ClientRegistry.policies.at(scope).timeoutMs = timeoutMs;
174
209
  }
175
- /**
176
- * THE {@link ErrorTranslator} for one call — method -> API -> ALL ->
177
- * {@link WEBPIECES_DEFAULT_ERROR_TRANSLATOR}, never `undefined`, so a caller never branches.
178
- *
179
- * Both arguments are required so every call site states what it knows: the client and the server
180
- * pass the route's contract class and method; a failure with no route (an unmatched request, a
181
- * failure before routing) passes `undefined, undefined` and reaches the ALL rung alone.
182
- */
183
- // webpieces-disable no-function-outside-class -- static global singleton (like HeaderRegistry/LogManager); populated once at startup, never DI-injected
184
- static getErrorTranslator(api, methodName) {
185
- const registered = ClientRegistry.errorTranslators
186
- .rungs(api, methodName)
187
- .find((slot) => slot.translator !== undefined)?.translator;
188
- return registered ?? WebpiecesDefaultErrorTranslator_1.WEBPIECES_DEFAULT_ERROR_TRANSLATOR;
210
+ // webpieces-disable no-function-outside-class -- process-global startup registry
211
+ static setStrategy(
212
+ // webpieces-disable no-any-unknown -- registry strategies span heterogeneous response DTOs
213
+ strategy, scope) {
214
+ ClientRegistry.policies.at(scope).strategy = strategy;
189
215
  }
190
216
  /**
191
- * Install an {@link ErrorClassifier} for one {@link CallScope}: `'ALL'` (the app/company
192
- * classifier — it reads {@link ApiMethodInfo.side}, so it covers the server router and every
193
- * internal client), one contract CLASS (an SDK-wrapping api-client keys by its abstract `XxxApi`),
194
- * or one method of one contract. The scope is REQUIRED. Call at startup, on the server AND in the
195
- * browser. See {@link ErrorClassifier}.
217
+ * Run one call under its registered policy. `context` names the method (its `methodName` selects
218
+ * the per-method policy) and carries the endpoint's declared operation, so the SAME context a
219
+ * strategy sees is the one the transport's deadline puts on an `ApiCallTimeoutError`. The context
220
+ * a strategy sees always carries `api` as its {@link CallContext.contract}, so the error
221
+ * classification inside a strategy resolves on the same ladder.
222
+ *
223
+ * Every attempt is wrapped so a retried TRANSIENT failure logs one
224
+ * `WARN [API-client-attempt-TRANSIENT]` line (see {@link LoggedAttempts}).
196
225
  */
197
- // webpieces-disable no-function-outside-class -- static global singleton (like HeaderRegistry/LogManager); populated once at startup, never DI-injected
198
- static setErrorClassifier(classifier, ...scope) {
199
- ClientRegistry.errorClassifiers.at(...scope).classifier = classifier;
226
+ // webpieces-disable no-function-outside-class -- transport-independent policy execution
227
+ static async execute(api, context, attempt, fallback) {
228
+ const scoped = context.contract === api
229
+ ? context
230
+ : new CallStrategy_1.CallContext(context.apiName, context.methodName, context.operation, api);
231
+ const logged = new LoggedAttempts(scoped, attempt).run;
232
+ const rungs = ClientRegistry.policies.rungs(api, scoped.methodName);
233
+ const strategy = rungs.find((p) => p.strategy !== undefined)?.strategy;
234
+ if (strategy !== undefined) {
235
+ return (await strategy(logged, scoped));
236
+ }
237
+ const timeoutMs = rungs.find((p) => p.timeoutMs !== undefined)?.timeoutMs;
238
+ if (timeoutMs !== undefined) {
239
+ return logged(timeoutMs);
240
+ }
241
+ if (fallback.strategy !== undefined) {
242
+ return (await fallback.strategy(logged, scoped));
243
+ }
244
+ return logged(fallback.timeoutMs);
200
245
  }
201
246
  /**
202
- * The {@link CallOutcome} of a thrown API-call error. Resolved on the {@link CallScope} ladder of
203
- * `methodInfo.contract` / `methodInfo.methodName`, most specific first, and ALWAYS definitive:
204
- * 1. the method classifier — unless it defers;
205
- * 2. else the API classifier — unless it defers;
206
- * 3. else the ALL classifier — unless it defers;
207
- * 4. else the webpieces built-in ({@link WEBPIECES_DEFAULT_ERROR_CLASSIFIER}), which never defers.
208
- *
209
- * The ONE verdict read by `RetryStrategy` (retry only TRANSIENT), `CallRegistry`'s per-attempt
210
- * log line, `LogApiCall` (EXPECTED → OTHER, else FAIL) and the MCP renderer's retry advice.
247
+ * The registered TIMEOUT for a call — method -> API -> ALL -> `defaultMs` — ignoring every
248
+ * strategy. For a transport that hands timing to someone else for this call (Cloud Tasks enqueue
249
+ * with caller-supplied gax `CallOptions`) and still needs ONE safety cap so a hung channel cannot
250
+ * block forever. Raise it with {@link ClientRegistry.setTimeout}.
211
251
  */
212
- // webpieces-disable no-function-outside-class -- static global singleton (like HeaderRegistry/LogManager); populated once at startup, never DI-injected
213
- static classify(error, methodInfo) {
214
- const rungs = ClientRegistry.errorClassifiers.rungs(methodInfo.contract, methodInfo.methodName);
215
- for (const slot of rungs) {
216
- const verdict = slot.classifier?.classify(error, methodInfo);
217
- if (verdict !== undefined) {
218
- return verdict;
219
- }
220
- }
221
- return WebpiecesDefaultErrorClassifier_1.WEBPIECES_DEFAULT_ERROR_CLASSIFIER.classify(error, methodInfo);
252
+ // webpieces-disable no-function-outside-class -- process-global registry lookup
253
+ static resolveTimeout(api, methodName, defaultMs) {
254
+ const rungs = ClientRegistry.policies.rungs(api, methodName);
255
+ return rungs.find((p) => p.timeoutMs !== undefined)?.timeoutMs ?? defaultMs;
222
256
  }
223
257
  /**
224
258
  * Put the process-globals back to the state a fresh process starts in. For TESTS, so one spec
225
- * cannot leak its registrations into the next.
226
- *
227
- * It empties every translator and classifier rung, which RESTORES the webpieces defaults:
228
- * `getErrorTranslator(...)` and `classify(...)` fall through to them below every rung, so
229
- * "cleared" is not a state they have. A spec that wants webpieces' own behaviour back asks for
230
- * exactly that.
259
+ * cannot leak its registrations into the next: every URL mapping, the deriver, and every
260
+ * timeout / strategy rung. (`ApiRegistry.resetForTests()` resets the error translators and
261
+ * classifiers.)
231
262
  */
232
263
  // webpieces-disable no-function-outside-class -- static global singleton (like HeaderRegistry/LogManager); populated once at startup, never DI-injected
233
264
  static resetForTests() {
234
265
  ClientRegistry.mappings.clear();
235
266
  ClientRegistry.deriver = undefined;
236
- ClientRegistry.errorTranslators.clear();
237
- ClientRegistry.errorClassifiers.clear();
267
+ ClientRegistry.policies.clear();
238
268
  }
239
269
  }
240
270
  exports.ClientRegistry = ClientRegistry;
@@ -1 +1 @@
1
- {"version":3,"file":"ClientRegistry.js","sourceRoot":"","sources":["../../../../../../packages/core/core-util/src/http/ClientRegistry.ts"],"names":[],"mappings":"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","sourcesContent":["import { ErrorTranslator } from './ErrorTranslator';\nimport { CallOutcome, ErrorClassifier } from './ErrorClassifier';\nimport { CallScope, CallScopeLadder } from './CallScope';\nimport { WEBPIECES_DEFAULT_ERROR_CLASSIFIER } from './WebpiecesDefaultErrorClassifier';\nimport { WEBPIECES_DEFAULT_ERROR_TRANSLATOR } from './WebpiecesDefaultErrorTranslator';\nimport { ApiMethodInfo } from './ApiMethodInfo';\n\n/** One {@link CallScope} rung of the error-translator ladder. Data only, so a class. */\nclass TranslatorSlot {\n translator?: ErrorTranslator;\n}\n\n/** One {@link CallScope} rung of the error-classifier ladder. Data only, so a class. */\nclass ClassifierSlot {\n classifier?: ErrorClassifier;\n}\n\n/**\n * Derives a base URL for a service name that has NO registered mapping — the pluggable half of\n * {@link ClientRegistry} resolution. One per environment, installed once at startup:\n *\n * - `gcpCloudRunDeriver()` (@webpieces/gcp-identity) — the Cloud Run formula, read from the metadata\n * server. For code running ON GCP.\n * - `gcpCloudRunDeriver(new GcpCloudRunTarget(projectNumber, region))` — the SAME formula with the\n * values supplied from config, for code running OFF GCP that still calls Cloud Run (a CLI, CI).\n * - {@link templateDeriver} (this package, browser-safe) — pure string substitution, for AWS or\n * anything else with predictable DNS.\n * - none at all — a browser goes same-origin, and localhost/tests hand-register their mappings.\n */\nexport type ServiceUrlDeriver = (svcName: string) => Promise<string>;\n\n/**\n * ClientRegistry - the ONE place a `svcName` becomes a base URL, for every outbound client in every\n * environment. A client is built once but a URL is per-environment, so the URL belongs here rather\n * than on the client: clients carry ONLY a svcName.\n *\n * Resolution is one precedence chain, identical in the browser, in node, and in Cloud Tasks:\n *\n * 1. a registered mapping wins — the localhost port table, AWS, an external API, another region\n * or project, a host that is not Cloud Run at all. Populated at startup from per-env config.\n * 2. else the installed {@link ServiceUrlDeriver}, if any — GCP's built-in, a DNS template, or\n * your own. OPTIONAL on purpose: localhost is inherently a TABLE (helper-fsdb -> :8401,\n * helper-portal -> :8201 have per-service ports), so explicit mappings must stay sufficient on\n * their own.\n * 3. else the caller's fallback: the BROWSER goes relative (same origin — see\n * {@link ClientRegistry.tryResolve}), while node THROWS (see {@link ClientRegistry.resolve}),\n * because a server has no \"own origin\" and an unresolvable peer is a setup bug.\n *\n * Configured like {@link HeaderRegistry} / LogManager — populated once at startup, then globally\n * accessible with NO DI wiring. It is browser-safe (no `process.env`, no node-only deps), which is\n * why it lives in core-util rather than gcp-identity.\n *\n * A `svcName` should be the nx MODULE name of the service you are calling — that is the one name\n * `architecture:validate-runtime-architecture` can verify actually exists, so a typo or a rename\n * fails the build instead of failing in production. When the DEPLOYED name differs (an environment\n * prefix like `tf-`, or a Cloud Run service named unlike its module), translate it in ONE place —\n * the deriver below, or a `serviceName` alias in the module's project.json — never at the call site.\n *\n * ```ts\n * // startup, from the current environment's config:\n * ClientRegistry.addMapping('server2', 8202); // -> http://localhost:8202\n * ClientRegistry.addUrlMapping('email', 'https://email.other-region.example');\n * ClientRegistry.setDeriver(gcpCloudRunDeriver()); // everything else, on GCP\n * ```\n */\nexport class ClientRegistry {\n /** svcName -> resolved base URL. Process-global; populated at startup per environment. */\n private static readonly mappings = new Map<string, string>();\n\n /** The fallback for svcNames with no mapping. Undefined = no derivation in this environment. */\n private static deriver: ServiceUrlDeriver | undefined;\n\n /**\n * The {@link ErrorTranslator} per {@link CallScope} — method -> API -> ALL, and below every rung\n * {@link WEBPIECES_DEFAULT_ERROR_TRANSLATOR}, so {@link ClientRegistry.getErrorTranslator} is\n * never undefined. Process-global, set once at startup on the SERVER and in the BROWSER — the\n * same no-DI pattern as {@link ClientRegistry.mappings} above.\n *\n * ONE translator per scope, not a list, on purpose: an app that has several layers of error\n * policy for one scope composes them INSIDE its own `toWire`, where the precedence is written\n * down. The ladder exists because vendors put their errors on the wire in their OWN formats — a\n * per-API translator reads one vendor's 4xx body (`fromWire`), or writes the error body an\n * inbound vendor expects back (`toWire`), while every other contract keeps the app's ALL one.\n */\n private static readonly errorTranslators = new CallScopeLadder<TranslatorSlot>(\n () => new TranslatorSlot(),\n );\n\n /**\n * The {@link ErrorClassifier} per {@link CallScope} — method -> API -> ALL, and below every rung\n * {@link WEBPIECES_DEFAULT_ERROR_CLASSIFIER}. A classifier that DEFERS (`undefined`) falls through\n * to the next rung. The ALL classifier reads {@link ApiMethodInfo.side}, so one classifier covers\n * the server router AND every internal client; an SDK-wrapping api-client registers its own at\n * its abstract `XxxApi` class. See {@link ClientRegistry.classify}.\n */\n private static readonly errorClassifiers = new CallScopeLadder<ClassifierSlot>(\n () => new ClassifierSlot(),\n );\n\n /** Map a service name to `http://localhost:<port>`. */\n // webpieces-disable no-function-outside-class -- static global singleton (like HeaderRegistry/LogManager); populated once at startup, never DI-injected\n static addMapping(svcName: string, port: number): void {\n ClientRegistry.mappings.set(svcName, `http://localhost:${port}`);\n }\n\n /**\n * Map a service name to an explicit base URL (any host / any environment).\n *\n * The EMPTY STRING is a legal, meaningful mapping: it makes the service relative, i.e.\n * same-origin, because the client builds its URL as `${baseUrl}${route.path}`.\n */\n // webpieces-disable no-function-outside-class -- static global singleton (like HeaderRegistry/LogManager); populated once at startup, never DI-injected\n static addUrlMapping(svcName: string, url: string): void {\n ClientRegistry.mappings.set(svcName, url);\n }\n\n /**\n * Install the environment's {@link ServiceUrlDeriver} — how to resolve a svcName that has NO\n * mapping. Optional: an environment that registers every svcName it calls needs none.\n */\n // webpieces-disable no-function-outside-class -- static global singleton (like HeaderRegistry/LogManager); populated once at startup, never DI-injected\n static setDeriver(fn: ServiceUrlDeriver): void {\n ClientRegistry.deriver = fn;\n }\n\n /** The registered override for `svcName`, or undefined if none. Registry only — no derivation. */\n // webpieces-disable no-function-outside-class -- static global singleton (like HeaderRegistry/LogManager); populated once at startup, never DI-injected\n static tryLookup(svcName: string): string | undefined {\n return ClientRegistry.mappings.get(svcName);\n }\n\n /**\n * Resolve `svcName` to its registered base URL. THROWS if the service was never registered.\n * Registry only — it does NOT consult the deriver; prefer {@link ClientRegistry.resolve}.\n */\n // webpieces-disable no-function-outside-class -- static global singleton (like HeaderRegistry/LogManager); populated once at startup, never DI-injected\n static lookup(svcName: string): string {\n const url = ClientRegistry.tryLookup(svcName);\n if (url === undefined) {\n throw new Error(\n `No URL registered for service \"${svcName}\". Register it at startup: ` +\n `ClientRegistry.addMapping(svcName, port) for a localhost port, or ` +\n `ClientRegistry.addUrlMapping(svcName, url) for an explicit URL.`,\n );\n }\n return url;\n }\n\n /**\n * Steps 1 + 2 of the chain: the registered mapping, else the installed deriver, else undefined.\n * Non-throwing — the BROWSER uses this and treats undefined as \"\" (relative → same origin),\n * which is what a browser app calling the backend it was served from wants by default.\n *\n * Note the `!== undefined` guard: an empty-string mapping is a legal answer (\"this service is\n * same-origin\"), so it must NOT fall through to derivation.\n */\n // webpieces-disable no-function-outside-class -- static global singleton (like HeaderRegistry/LogManager); populated once at startup, never DI-injected\n static tryResolve(svcName: string): Promise<string | undefined> {\n const override = ClientRegistry.tryLookup(svcName);\n if (override !== undefined) {\n return Promise.resolve(override);\n }\n if (!ClientRegistry.deriver) {\n return Promise.resolve(undefined);\n }\n return ClientRegistry.deriver(svcName);\n }\n\n /**\n * The full chain, with node's fallback: mapping, else deriver, else THROW. A server has no \"own\n * origin\" to go relative to, so an unresolvable peer is a setup bug and must fail loudly — and\n * say how to fix it.\n */\n // webpieces-disable no-function-outside-class -- static global singleton (like HeaderRegistry/LogManager); populated once at startup, never DI-injected\n static async resolve(svcName: string): Promise<string> {\n const url = await ClientRegistry.tryResolve(svcName);\n if (url === undefined) {\n throw new Error(\n `No URL for service \"${svcName}\".\\n` +\n ` - localhost/AWS: ClientRegistry.addMapping('${svcName}', 8401)\\n` +\n ` or ClientRegistry.addUrlMapping('${svcName}', 'https://...')\\n` +\n ` - GCP: install a deriver — ClientRegistry.setDeriver(gcpCloudRunDeriver())\\n` +\n ` - deployed name differs from the module name (e.g. a 'tf-' prefix)? Translate it ONCE\\n` +\n ` in the deriver — setDeriver(s => gcpCloudRunDeriver()('tf-' + s)) — so every call\\n` +\n ` site keeps naming the MODULE, which architecture:validate-runtime-architecture checks`,\n );\n }\n return url;\n }\n\n /**\n * Install an {@link ErrorTranslator} for one {@link CallScope}: `'ALL'` (the app's translator,\n * server side AND every client side), one contract CLASS, or one method of one contract. The\n * scope is REQUIRED — there is no unscoped spelling. Call at startup, on the server AND in the\n * browser, mirroring {@link ClientRegistry.addMapping}.\n *\n * ```ts\n * ClientRegistry.setErrorTranslator(new AppErrorTranslator(), 'ALL');\n * ClientRegistry.setErrorTranslator(new VendorErrorTranslator(), VendorApi); // one contract\n * ClientRegistry.setErrorTranslator(new LegacyErrorTranslator(), VendorApi, 'sync'); // one method\n * ```\n *\n * `set`, not `add`: see the field doc above for why a per-scope LIST is not wanted. A translator\n * declines an error by DELEGATING to {@link WebpiecesDefaultErrorTranslator} — not to the next\n * rung — exactly as before.\n */\n // webpieces-disable no-function-outside-class -- static global singleton (like HeaderRegistry/LogManager); populated once at startup, never DI-injected\n static setErrorTranslator(translator: ErrorTranslator, ...scope: CallScope): void {\n ClientRegistry.errorTranslators.at(...scope).translator = translator;\n }\n\n /**\n * THE {@link ErrorTranslator} for one call — method -> API -> ALL ->\n * {@link WEBPIECES_DEFAULT_ERROR_TRANSLATOR}, never `undefined`, so a caller never branches.\n *\n * Both arguments are required so every call site states what it knows: the client and the server\n * pass the route's contract class and method; a failure with no route (an unmatched request, a\n * failure before routing) passes `undefined, undefined` and reaches the ALL rung alone.\n */\n // webpieces-disable no-function-outside-class -- static global singleton (like HeaderRegistry/LogManager); populated once at startup, never DI-injected\n static getErrorTranslator(api: Function | undefined, methodName: string | undefined): ErrorTranslator {\n const registered = ClientRegistry.errorTranslators\n .rungs(api, methodName)\n .find((slot: TranslatorSlot) => slot.translator !== undefined)?.translator;\n return registered ?? WEBPIECES_DEFAULT_ERROR_TRANSLATOR;\n }\n\n /**\n * Install an {@link ErrorClassifier} for one {@link CallScope}: `'ALL'` (the app/company\n * classifier — it reads {@link ApiMethodInfo.side}, so it covers the server router and every\n * internal client), one contract CLASS (an SDK-wrapping api-client keys by its abstract `XxxApi`),\n * or one method of one contract. The scope is REQUIRED. Call at startup, on the server AND in the\n * browser. See {@link ErrorClassifier}.\n */\n // webpieces-disable no-function-outside-class -- static global singleton (like HeaderRegistry/LogManager); populated once at startup, never DI-injected\n static setErrorClassifier(classifier: ErrorClassifier, ...scope: CallScope): void {\n ClientRegistry.errorClassifiers.at(...scope).classifier = classifier;\n }\n\n /**\n * The {@link CallOutcome} of a thrown API-call error. Resolved on the {@link CallScope} ladder of\n * `methodInfo.contract` / `methodInfo.methodName`, most specific first, and ALWAYS definitive:\n * 1. the method classifier — unless it defers;\n * 2. else the API classifier — unless it defers;\n * 3. else the ALL classifier — unless it defers;\n * 4. else the webpieces built-in ({@link WEBPIECES_DEFAULT_ERROR_CLASSIFIER}), which never defers.\n *\n * The ONE verdict read by `RetryStrategy` (retry only TRANSIENT), `CallRegistry`'s per-attempt\n * log line, `LogApiCall` (EXPECTED → OTHER, else FAIL) and the MCP renderer's retry advice.\n */\n // webpieces-disable no-function-outside-class -- static global singleton (like HeaderRegistry/LogManager); populated once at startup, never DI-injected\n static classify(error: Error, methodInfo: ApiMethodInfo): CallOutcome {\n const rungs = ClientRegistry.errorClassifiers.rungs(methodInfo.contract, methodInfo.methodName);\n for (const slot of rungs) {\n const verdict = slot.classifier?.classify(error, methodInfo);\n if (verdict !== undefined) {\n return verdict;\n }\n }\n return WEBPIECES_DEFAULT_ERROR_CLASSIFIER.classify(error, methodInfo);\n }\n\n /**\n * Put the process-globals back to the state a fresh process starts in. For TESTS, so one spec\n * cannot leak its registrations into the next.\n *\n * It empties every translator and classifier rung, which RESTORES the webpieces defaults:\n * `getErrorTranslator(...)` and `classify(...)` fall through to them below every rung, so\n * \"cleared\" is not a state they have. A spec that wants webpieces' own behaviour back asks for\n * exactly that.\n */\n // webpieces-disable no-function-outside-class -- static global singleton (like HeaderRegistry/LogManager); populated once at startup, never DI-injected\n static resetForTests(): void {\n ClientRegistry.mappings.clear();\n ClientRegistry.deriver = undefined;\n ClientRegistry.errorTranslators.clear();\n ClientRegistry.errorClassifiers.clear();\n }\n}\n"]}
1
+ {"version":3,"file":"ClientRegistry.js","sourceRoot":"","sources":["../../../../../../packages/core/core-util/src/http/ClientRegistry.ts"],"names":[],"mappings":"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","sourcesContent":["import { Attempt, CallContext, CallStrategy } from './CallStrategy';\nimport { CallDeadline } from './CallDeadline';\nimport { TransportDefault } from './TransportDefault';\nimport { ApiRegistry } from './ApiRegistry';\nimport { CallOutcome } from './ErrorClassifier';\nimport { ApiMethodInfo } from './ApiMethodInfo';\nimport { LogManager } from '../logging/LogManager';\nimport { LOG_API_CALL_LOGGER_NAME } from './ApiCallLogName';\nimport { toError } from '../lib/errorUtils';\nimport { CallScope, CallScopeLadder } from './CallScope';\n\n// The same logger LogApiCall writes its [API-*] lines to, so an attempt line sits beside the final one.\nconst log = LogManager.getLogger(LOG_API_CALL_LOGGER_NAME);\n\nclass CallPolicy {\n timeoutMs?: number;\n // webpieces-disable no-any-unknown -- global policy spans heterogeneous response DTOs\n strategy?: CallStrategy<unknown>;\n}\n\n/**\n * Counts the attempts of ONE call and logs each retried TRANSIENT failure. It wraps the attempt\n * BEFORE any strategy sees it, so an app-written strategy gets the same lines as `RetryStrategy`.\n *\n * The line for attempt N is written when attempt N+1 STARTS — i.e. only for an attempt that was\n * actually retried. The last attempt's failure is what escapes, and `LogApiCall`'s final\n * `[API-client-resp-FAIL]` line covers it, so a call that gave up after 4 attempts logs 3 attempt\n * lines and 1 FAIL line, never the last failure twice. An EXPECTED or FAILURE attempt is not logged\n * here either; the final line covers it.\n */\nclass LoggedAttempts<T> {\n private attempts = 0;\n private pendingTransient?: Error;\n\n constructor(\n private readonly context: CallContext,\n private readonly attempt: Attempt<T>,\n ) {}\n\n readonly run: Attempt<T> = async (timeoutMs: number): Promise<T> => {\n this.flushRetried();\n this.attempts += 1;\n // webpieces-disable no-unmanaged-exceptions -- observe the attempt's failure, rethrow it unchanged\n // eslint-disable-next-line @webpieces/no-unmanaged-exceptions\n try {\n return await this.attempt(timeoutMs);\n } catch (err: unknown) {\n const error = toError(err);\n const methodInfo = new ApiMethodInfo(\n 'client',\n this.context.apiName,\n this.context.methodName,\n this.context.contract,\n );\n this.pendingTransient =\n ApiRegistry.classify(error, methodInfo) === CallOutcome.TRANSIENT_FAILURE\n ? error\n : undefined;\n // eslint-disable-next-line @webpieces/no-unmanaged-exceptions -- rethrows the attempt's own failure unchanged\n throw err;\n }\n };\n\n private flushRetried(): void {\n const error = this.pendingTransient;\n if (error === undefined) return;\n this.pendingTransient = undefined;\n log.warn(\n `[API-client-attempt-TRANSIENT] ${this.context.apiName}.${this.context.methodName} ` +\n `attempt ${this.attempts} errorType=${error.constructor.name} error=${error.message}`,\n );\n }\n}\n\n/**\n * Derives a base URL for a service name that has NO registered mapping — the pluggable half of\n * {@link ClientRegistry} resolution. One per environment, installed once at startup:\n *\n * - `gcpCloudRunDeriver()` (@webpieces/gcp-identity) — the Cloud Run formula, read from the metadata\n * server. For code running ON GCP.\n * - `gcpCloudRunDeriver(new GcpCloudRunTarget(projectNumber, region))` — the SAME formula with the\n * values supplied from config, for code running OFF GCP that still calls Cloud Run (a CLI, CI).\n * - {@link templateDeriver} (this package, browser-safe) — pure string substitution, for AWS or\n * anything else with predictable DNS.\n * - none at all — a browser goes same-origin, and localhost/tests hand-register their mappings.\n */\nexport type ServiceUrlDeriver = (svcName: string) => Promise<string>;\n\n/**\n * ClientRegistry - the ONE place a `svcName` becomes a base URL, for every outbound client in every\n * environment. A client is built once but a URL is per-environment, so the URL belongs here rather\n * than on the client: clients carry ONLY a svcName.\n *\n * Resolution is one precedence chain, identical in the browser, in node, and in Cloud Tasks:\n *\n * 1. a registered mapping wins — the localhost port table, AWS, an external API, another region\n * or project, a host that is not Cloud Run at all. Populated at startup from per-env config.\n * 2. else the installed {@link ServiceUrlDeriver}, if any — GCP's built-in, a DNS template, or\n * your own. OPTIONAL on purpose: localhost is inherently a TABLE (helper-fsdb -> :8401,\n * helper-portal -> :8201 have per-service ports), so explicit mappings must stay sufficient on\n * their own.\n * 3. else the caller's fallback: the BROWSER goes relative (same origin — see\n * {@link ClientRegistry.tryResolve}), while node THROWS (see {@link ClientRegistry.resolve}),\n * because a server has no \"own origin\" and an unresolvable peer is a setup bug.\n *\n * Configured like {@link HeaderRegistry} / LogManager — populated once at startup, then globally\n * accessible with NO DI wiring. It is browser-safe (no `process.env`, no node-only deps), which is\n * why it lives in core-util rather than gcp-identity.\n *\n * A `svcName` should be the nx MODULE name of the service you are calling — that is the one name\n * `architecture:validate-runtime-architecture` can verify actually exists, so a typo or a rename\n * fails the build instead of failing in production. When the DEPLOYED name differs (an environment\n * prefix like `tf-`, or a Cloud Run service named unlike its module), translate it in ONE place —\n * the deriver below, or a `serviceName` alias in the module's project.json — never at the call site.\n *\n * ```ts\n * // startup, from the current environment's config:\n * ClientRegistry.addMapping('server2', 8202); // -> http://localhost:8202\n * ClientRegistry.addUrlMapping('email', 'https://email.other-region.example');\n * ClientRegistry.setDeriver(gcpCloudRunDeriver()); // everything else, on GCP\n * ```\n *\n * The OTHER half: everything ONLY an outbound call reads besides its URL — the per-call timeout and\n * strategy, for browser RPC, node RPC, and Cloud Tasks ENQUEUE. ONE implementation; every transport\n * calls {@link ClientRegistry.execute}. (What BOTH sides read — the error translator and classifier —\n * lives on `ApiRegistry`.)\n *\n * Ladder: strategy method -> API -> ALL first; ANY strategy replaces the ENTIRE timeout ladder.\n * Otherwise timeout method -> API -> ALL. Otherwise the transport's {@link TransportDefault} (its\n * timeout, and its strategy if it has one). A registered TIMEOUT is therefore a decision for a single\n * attempt and also replaces a transport's default strategy; to keep retrying with a different attempt\n * deadline, register a `RetryStrategy` instead.\n *\n * RPC has no default retry (only the application knows whether repeating a POST is safe); Cloud Tasks\n * enqueue does (see `TransportDefault`). An app opts RPC in with\n * `ClientRegistry.setStrategy(new RetryStrategy(...).run, CallScope.api(CatalogApi))` — and `RetryStrategy` never\n * retries a `WRITE`, even registered at ALL.\n */\nexport class ClientRegistry {\n /** svcName -> resolved base URL. Process-global; populated at startup per environment. */\n private static readonly mappings = new Map<string, string>();\n\n /** The fallback for svcNames with no mapping. Undefined = no derivation in this environment. */\n private static deriver: ServiceUrlDeriver | undefined;\n\n /** The timeout / strategy per {@link CallScope} — method -> API -> ALL -> the transport default. */\n private static readonly policies = new CallScopeLadder<CallPolicy>(() => new CallPolicy());\n\n /** Map a service name to `http://localhost:<port>`. */\n // webpieces-disable no-function-outside-class -- static global singleton (like HeaderRegistry/LogManager); populated once at startup, never DI-injected\n static addMapping(svcName: string, port: number): void {\n ClientRegistry.mappings.set(svcName, `http://localhost:${port}`);\n }\n\n /**\n * Map a service name to an explicit base URL (any host / any environment).\n *\n * The EMPTY STRING is a legal, meaningful mapping: it makes the service relative, i.e.\n * same-origin, because the client builds its URL as `${baseUrl}${route.path}`.\n */\n // webpieces-disable no-function-outside-class -- static global singleton (like HeaderRegistry/LogManager); populated once at startup, never DI-injected\n static addUrlMapping(svcName: string, url: string): void {\n ClientRegistry.mappings.set(svcName, url);\n }\n\n /**\n * Install the environment's {@link ServiceUrlDeriver} — how to resolve a svcName that has NO\n * mapping. Optional: an environment that registers every svcName it calls needs none.\n */\n // webpieces-disable no-function-outside-class -- static global singleton (like HeaderRegistry/LogManager); populated once at startup, never DI-injected\n static setDeriver(fn: ServiceUrlDeriver): void {\n ClientRegistry.deriver = fn;\n }\n\n /** The registered override for `svcName`, or undefined if none. Registry only — no derivation. */\n // webpieces-disable no-function-outside-class -- static global singleton (like HeaderRegistry/LogManager); populated once at startup, never DI-injected\n static tryLookup(svcName: string): string | undefined {\n return ClientRegistry.mappings.get(svcName);\n }\n\n /**\n * Resolve `svcName` to its registered base URL. THROWS if the service was never registered.\n * Registry only — it does NOT consult the deriver; prefer {@link ClientRegistry.resolve}.\n */\n // webpieces-disable no-function-outside-class -- static global singleton (like HeaderRegistry/LogManager); populated once at startup, never DI-injected\n static lookup(svcName: string): string {\n const url = ClientRegistry.tryLookup(svcName);\n if (url === undefined) {\n throw new Error(\n `No URL registered for service \"${svcName}\". Register it at startup: ` +\n `ClientRegistry.addMapping(svcName, port) for a localhost port, or ` +\n `ClientRegistry.addUrlMapping(svcName, url) for an explicit URL.`,\n );\n }\n return url;\n }\n\n /**\n * Steps 1 + 2 of the chain: the registered mapping, else the installed deriver, else undefined.\n * Non-throwing — the BROWSER uses this and treats undefined as \"\" (relative → same origin),\n * which is what a browser app calling the backend it was served from wants by default.\n *\n * Note the `!== undefined` guard: an empty-string mapping is a legal answer (\"this service is\n * same-origin\"), so it must NOT fall through to derivation.\n */\n // webpieces-disable no-function-outside-class -- static global singleton (like HeaderRegistry/LogManager); populated once at startup, never DI-injected\n static tryResolve(svcName: string): Promise<string | undefined> {\n const override = ClientRegistry.tryLookup(svcName);\n if (override !== undefined) {\n return Promise.resolve(override);\n }\n if (!ClientRegistry.deriver) {\n return Promise.resolve(undefined);\n }\n return ClientRegistry.deriver(svcName);\n }\n\n /**\n * The full chain, with node's fallback: mapping, else deriver, else THROW. A server has no \"own\n * origin\" to go relative to, so an unresolvable peer is a setup bug and must fail loudly — and\n * say how to fix it.\n */\n // webpieces-disable no-function-outside-class -- static global singleton (like HeaderRegistry/LogManager); populated once at startup, never DI-injected\n static async resolve(svcName: string): Promise<string> {\n const url = await ClientRegistry.tryResolve(svcName);\n if (url === undefined) {\n throw new Error(\n `No URL for service \"${svcName}\".\\n` +\n ` - localhost/AWS: ClientRegistry.addMapping('${svcName}', 8401)\\n` +\n ` or ClientRegistry.addUrlMapping('${svcName}', 'https://...')\\n` +\n ` - GCP: install a deriver — ClientRegistry.setDeriver(gcpCloudRunDeriver())\\n` +\n ` - deployed name differs from the module name (e.g. a 'tf-' prefix)? Translate it ONCE\\n` +\n ` in the deriver — setDeriver(s => gcpCloudRunDeriver()('tf-' + s)) — so every call\\n` +\n ` site keeps naming the MODULE, which architecture:validate-runtime-architecture checks`,\n );\n }\n return url;\n }\n\n // webpieces-disable no-function-outside-class -- process-global startup registry\n static setTimeout(timeoutMs: number | undefined, scope: CallScope): void {\n if (timeoutMs !== undefined) CallDeadline.validate(timeoutMs);\n ClientRegistry.policies.at(scope).timeoutMs = timeoutMs;\n }\n\n // webpieces-disable no-function-outside-class -- process-global startup registry\n static setStrategy(\n // webpieces-disable no-any-unknown -- registry strategies span heterogeneous response DTOs\n strategy: CallStrategy<unknown> | undefined,\n scope: CallScope,\n ): void {\n ClientRegistry.policies.at(scope).strategy = strategy;\n }\n\n /**\n * Run one call under its registered policy. `context` names the method (its `methodName` selects\n * the per-method policy) and carries the endpoint's declared operation, so the SAME context a\n * strategy sees is the one the transport's deadline puts on an `ApiCallTimeoutError`. The context\n * a strategy sees always carries `api` as its {@link CallContext.contract}, so the error\n * classification inside a strategy resolves on the same ladder.\n *\n * Every attempt is wrapped so a retried TRANSIENT failure logs one\n * `WARN [API-client-attempt-TRANSIENT]` line (see {@link LoggedAttempts}).\n */\n // webpieces-disable no-function-outside-class -- transport-independent policy execution\n static async execute<T>(\n api: Function,\n context: CallContext,\n attempt: Attempt<T>,\n fallback: TransportDefault,\n ): Promise<T> {\n const scoped =\n context.contract === api\n ? context\n : new CallContext(context.apiName, context.methodName, context.operation, api);\n const logged = new LoggedAttempts<T>(scoped, attempt).run;\n const rungs = ClientRegistry.policies.rungs(api, scoped.methodName);\n const strategy = rungs.find((p: CallPolicy) => p.strategy !== undefined)?.strategy;\n if (strategy !== undefined) {\n return (await strategy(logged, scoped)) as T;\n }\n const timeoutMs = rungs.find((p: CallPolicy) => p.timeoutMs !== undefined)?.timeoutMs;\n if (timeoutMs !== undefined) {\n return logged(timeoutMs);\n }\n if (fallback.strategy !== undefined) {\n return (await fallback.strategy(logged, scoped)) as T;\n }\n return logged(fallback.timeoutMs);\n }\n\n /**\n * The registered TIMEOUT for a call — method -> API -> ALL -> `defaultMs` — ignoring every\n * strategy. For a transport that hands timing to someone else for this call (Cloud Tasks enqueue\n * with caller-supplied gax `CallOptions`) and still needs ONE safety cap so a hung channel cannot\n * block forever. Raise it with {@link ClientRegistry.setTimeout}.\n */\n // webpieces-disable no-function-outside-class -- process-global registry lookup\n static resolveTimeout(api: Function, methodName: string, defaultMs: number): number {\n const rungs = ClientRegistry.policies.rungs(api, methodName);\n return rungs.find((p: CallPolicy) => p.timeoutMs !== undefined)?.timeoutMs ?? defaultMs;\n }\n\n /**\n * Put the process-globals back to the state a fresh process starts in. For TESTS, so one spec\n * cannot leak its registrations into the next: every URL mapping, the deriver, and every\n * timeout / strategy rung. (`ApiRegistry.resetForTests()` resets the error translators and\n * classifiers.)\n */\n // webpieces-disable no-function-outside-class -- static global singleton (like HeaderRegistry/LogManager); populated once at startup, never DI-injected\n static resetForTests(): void {\n ClientRegistry.mappings.clear();\n ClientRegistry.deriver = undefined;\n ClientRegistry.policies.clear();\n }\n}\n"]}
@@ -0,0 +1,63 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ const ClientRegistry_1 = require("./ClientRegistry");
4
+ const CallScope_1 = require("./CallScope");
5
+ const CallStrategy_1 = require("./CallStrategy");
6
+ const HttpEndpointOptions_1 = require("./HttpEndpointOptions");
7
+ /** An abstract SDK-wrapping `XxxApi` — the shape an api-client keys its policies by. */
8
+ class AbstractTargetApi {
9
+ }
10
+ /** A concrete `@ApiPath`-style contract class. */
11
+ class ConcreteTargetApi {
12
+ work() {
13
+ return Promise.resolve();
14
+ }
15
+ }
16
+ /**
17
+ * Compiled by tsc (spec files are not), never executed. Pins the ONE spelling of a call scope —
18
+ * `CallScope.all()` / `.api(X)` / `.method(X, 'm')` — and that the per-call timeout and strategy
19
+ * live on `ClientRegistry`. tsc fails the build with TS2578 if any of these ever starts compiling.
20
+ */
21
+ class ClientRegistryCompileAssertions {
22
+ scopeIsACallScope() {
23
+ // @ts-expect-error the 'ALL' string became CallScope.all()
24
+ ClientRegistry_1.ClientRegistry.setTimeout(100, 'ALL');
25
+ // @ts-expect-error a string is never a scope — use CallScope.all() / .api(X) / .method(X, 'm')
26
+ ClientRegistry_1.ClientRegistry.setStrategy(undefined, 'random');
27
+ // @ts-expect-error the (Api, 'method') tuple became CallScope.method(Api, 'method')
28
+ ClientRegistry_1.ClientRegistry.setTimeout(100, ConcreteTargetApi, 'work');
29
+ // @ts-expect-error a bare class became CallScope.api(Api)
30
+ ClientRegistry_1.ClientRegistry.setTimeout(100, ConcreteTargetApi);
31
+ // @ts-expect-error an object literal is not a scope — the factories are the only way to get one
32
+ ClientRegistry_1.ClientRegistry.setTimeout(100, {});
33
+ // @ts-expect-error an object literal shaped like a scope is not one either (CallScope is nominal)
34
+ ClientRegistry_1.ClientRegistry.setTimeout(100, { api: ConcreteTargetApi, methodName: 'work' });
35
+ // @ts-expect-error the constructor is private — use CallScope.all() / .api(X) / .method(X, 'm')
36
+ void new CallScope_1.CallScope(undefined, undefined);
37
+ }
38
+ methodNameIsChecked() {
39
+ ClientRegistry_1.ClientRegistry.setTimeout(100, CallScope_1.CallScope.method(ConcreteTargetApi, 'work'));
40
+ ClientRegistry_1.ClientRegistry.setTimeout(100, CallScope_1.CallScope.method(AbstractTargetApi, 'work'));
41
+ // @ts-expect-error a misspelled method does not compile — it would silently match nothing
42
+ ClientRegistry_1.ClientRegistry.setTimeout(100, CallScope_1.CallScope.method(ConcreteTargetApi, 'wrok'));
43
+ // @ts-expect-error a misspelled method does not compile on an abstract XxxApi either
44
+ ClientRegistry_1.ClientRegistry.setStrategy(undefined, CallScope_1.CallScope.method(AbstractTargetApi, 'wrok'));
45
+ }
46
+ callRegistryIsDeleted() {
47
+ // @ts-expect-error CallRegistry is deleted: CallRegistry.setTimeout became ClientRegistry.setTimeout
48
+ CallRegistry.setTimeout(100, CallScope_1.CallScope.all());
49
+ }
50
+ /** A CallContext's contract is the CLASS, never undefined (#1350). */
51
+ contractIsNeverUndefined() {
52
+ // @ts-expect-error CallContext's contract is a CLASS, never undefined — pass the real contract class
53
+ void new CallStrategy_1.CallContext('Api', 'work', HttpEndpointOptions_1.WRITE, undefined);
54
+ }
55
+ /** The transport fallback is a TransportDefault (timeout + optional strategy), never a bare number. */
56
+ fallbackIsATransportDefault() {
57
+ const context = new CallStrategy_1.CallContext('Api', 'work', HttpEndpointOptions_1.WRITE, ClientRegistryCompileAssertions);
58
+ void ClientRegistry_1.ClientRegistry.execute(ClientRegistryCompileAssertions, context, (ms) => Promise.resolve(ms),
59
+ // @ts-expect-error execute(..., defaultMs: number) became execute(..., fallback: TransportDefault)
60
+ 30_000);
61
+ }
62
+ }
63
+ //# sourceMappingURL=ClientRegistryCompileAssertions.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"ClientRegistryCompileAssertions.js","sourceRoot":"","sources":["../../../../../../packages/core/core-util/src/http/ClientRegistryCompileAssertions.ts"],"names":[],"mappings":";;AAAA,qDAAkD;AAClD,2CAAwC;AACxC,iDAA6C;AAC7C,+DAA8C;AAE9C,wFAAwF;AACxF,MAAe,iBAAiB;CAE/B;AAED,kDAAkD;AAClD,MAAM,iBAAiB;IACnB,IAAI;QACA,OAAO,OAAO,CAAC,OAAO,EAAE,CAAC;IAC7B,CAAC;CACJ;AAED;;;;GAIG;AACH,MAAM,+BAA+B;IACjC,iBAAiB;QACb,2DAA2D;QAC3D,+BAAc,CAAC,UAAU,CAAC,GAAG,EAAE,KAAK,CAAC,CAAC;QACtC,+FAA+F;QAC/F,+BAAc,CAAC,WAAW,CAAC,SAAS,EAAE,QAAQ,CAAC,CAAC;QAChD,oFAAoF;QACpF,+BAAc,CAAC,UAAU,CAAC,GAAG,EAAE,iBAAiB,EAAE,MAAM,CAAC,CAAC;QAC1D,0DAA0D;QAC1D,+BAAc,CAAC,UAAU,CAAC,GAAG,EAAE,iBAAiB,CAAC,CAAC;QAClD,gGAAgG;QAChG,+BAAc,CAAC,UAAU,CAAC,GAAG,EAAE,EAAE,CAAC,CAAC;QACnC,kGAAkG;QAClG,+BAAc,CAAC,UAAU,CAAC,GAAG,EAAE,EAAE,GAAG,EAAE,iBAAiB,EAAE,UAAU,EAAE,MAAM,EAAE,CAAC,CAAC;QAC/E,gGAAgG;QAChG,KAAK,IAAI,qBAAS,CAAC,SAAS,EAAE,SAAS,CAAC,CAAC;IAC7C,CAAC;IAED,mBAAmB;QACf,+BAAc,CAAC,UAAU,CAAC,GAAG,EAAE,qBAAS,CAAC,MAAM,CAAC,iBAAiB,EAAE,MAAM,CAAC,CAAC,CAAC;QAC5E,+BAAc,CAAC,UAAU,CAAC,GAAG,EAAE,qBAAS,CAAC,MAAM,CAAC,iBAAiB,EAAE,MAAM,CAAC,CAAC,CAAC;QAC5E,0FAA0F;QAC1F,+BAAc,CAAC,UAAU,CAAC,GAAG,EAAE,qBAAS,CAAC,MAAM,CAAC,iBAAiB,EAAE,MAAM,CAAC,CAAC,CAAC;QAC5E,qFAAqF;QACrF,+BAAc,CAAC,WAAW,CAAC,SAAS,EAAE,qBAAS,CAAC,MAAM,CAAC,iBAAiB,EAAE,MAAM,CAAC,CAAC,CAAC;IACvF,CAAC;IAED,qBAAqB;QACjB,qGAAqG;QACrG,YAAY,CAAC,UAAU,CAAC,GAAG,EAAE,qBAAS,CAAC,GAAG,EAAE,CAAC,CAAC;IAClD,CAAC;IAED,sEAAsE;IACtE,wBAAwB;QACpB,qGAAqG;QACrG,KAAK,IAAI,0BAAW,CAAC,KAAK,EAAE,MAAM,EAAE,2BAAK,EAAE,SAAS,CAAC,CAAC;IAC1D,CAAC;IAED,uGAAuG;IACvG,2BAA2B;QACvB,MAAM,OAAO,GAAG,IAAI,0BAAW,CAAC,KAAK,EAAE,MAAM,EAAE,2BAAK,EAAE,+BAA+B,CAAC,CAAC;QACvF,KAAK,+BAAc,CAAC,OAAO,CACvB,+BAA+B,EAC/B,OAAO,EACP,CAAC,EAAU,EAAE,EAAE,CAAC,OAAO,CAAC,OAAO,CAAC,EAAE,CAAC;QACnC,mGAAmG;QACnG,MAAM,CACT,CAAC;IACN,CAAC;CACJ","sourcesContent":["import { ClientRegistry } from './ClientRegistry';\nimport { CallScope } from './CallScope';\nimport { CallContext } from './CallStrategy';\nimport { WRITE } from './HttpEndpointOptions';\n\n/** An abstract SDK-wrapping `XxxApi` — the shape an api-client keys its policies by. */\nabstract class AbstractTargetApi {\n abstract work(): Promise<void>;\n}\n\n/** A concrete `@ApiPath`-style contract class. */\nclass ConcreteTargetApi {\n work(): Promise<void> {\n return Promise.resolve();\n }\n}\n\n/**\n * Compiled by tsc (spec files are not), never executed. Pins the ONE spelling of a call scope —\n * `CallScope.all()` / `.api(X)` / `.method(X, 'm')` — and that the per-call timeout and strategy\n * live on `ClientRegistry`. tsc fails the build with TS2578 if any of these ever starts compiling.\n */\nclass ClientRegistryCompileAssertions {\n scopeIsACallScope(): void {\n // @ts-expect-error the 'ALL' string became CallScope.all()\n ClientRegistry.setTimeout(100, 'ALL');\n // @ts-expect-error a string is never a scope — use CallScope.all() / .api(X) / .method(X, 'm')\n ClientRegistry.setStrategy(undefined, 'random');\n // @ts-expect-error the (Api, 'method') tuple became CallScope.method(Api, 'method')\n ClientRegistry.setTimeout(100, ConcreteTargetApi, 'work');\n // @ts-expect-error a bare class became CallScope.api(Api)\n ClientRegistry.setTimeout(100, ConcreteTargetApi);\n // @ts-expect-error an object literal is not a scope — the factories are the only way to get one\n ClientRegistry.setTimeout(100, {});\n // @ts-expect-error an object literal shaped like a scope is not one either (CallScope is nominal)\n ClientRegistry.setTimeout(100, { api: ConcreteTargetApi, methodName: 'work' });\n // @ts-expect-error the constructor is private — use CallScope.all() / .api(X) / .method(X, 'm')\n void new CallScope(undefined, undefined);\n }\n\n methodNameIsChecked(): void {\n ClientRegistry.setTimeout(100, CallScope.method(ConcreteTargetApi, 'work'));\n ClientRegistry.setTimeout(100, CallScope.method(AbstractTargetApi, 'work'));\n // @ts-expect-error a misspelled method does not compile — it would silently match nothing\n ClientRegistry.setTimeout(100, CallScope.method(ConcreteTargetApi, 'wrok'));\n // @ts-expect-error a misspelled method does not compile on an abstract XxxApi either\n ClientRegistry.setStrategy(undefined, CallScope.method(AbstractTargetApi, 'wrok'));\n }\n\n callRegistryIsDeleted(): void {\n // @ts-expect-error CallRegistry is deleted: CallRegistry.setTimeout became ClientRegistry.setTimeout\n CallRegistry.setTimeout(100, CallScope.all());\n }\n\n /** A CallContext's contract is the CLASS, never undefined (#1350). */\n contractIsNeverUndefined(): void {\n // @ts-expect-error CallContext's contract is a CLASS, never undefined — pass the real contract class\n void new CallContext('Api', 'work', WRITE, undefined);\n }\n\n /** The transport fallback is a TransportDefault (timeout + optional strategy), never a bare number. */\n fallbackIsATransportDefault(): void {\n const context = new CallContext('Api', 'work', WRITE, ClientRegistryCompileAssertions);\n void ClientRegistry.execute(\n ClientRegistryCompileAssertions,\n context,\n (ms: number) => Promise.resolve(ms),\n // @ts-expect-error execute(..., defaultMs: number) became execute(..., fallback: TransportDefault)\n 30_000,\n );\n }\n}\n"]}
@@ -16,7 +16,7 @@ class DeletedAuthCompileAssertions {
16
16
  void CoreUtil.WpAuthSharedSecret;
17
17
  // @ts-expect-error use WpAuth([vendorAuth(...)]) and WpAuthorization
18
18
  void CoreUtil.WpAuthWebhook;
19
- // @ts-expect-error webhook(name) is renamed vendorAuth(name): one VendorAuthHook mints, refreshes and verifies
19
+ // @ts-expect-error webhook(name) is renamed vendorAuth(name): a VendorAuthMinter sends it, a VendorAuthVerifier receives it
20
20
  void CoreUtil.webhook;
21
21
  // @ts-expect-error EXTERNAL is split: EXTERNAL_INBOUND (an outside system calls us) or EXTERNAL_OUTBOUND (we call it)
22
22
  void CoreUtil.EXTERNAL;