@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.
- package/package.json +1 -1
- package/src/http/ApiCallInfo.d.ts +3 -3
- package/src/http/ApiCallInfo.js +1 -1
- package/src/http/ApiCallInfo.js.map +1 -1
- package/src/http/ApiMethodInfo.d.ts +1 -1
- package/src/http/ApiMethodInfo.js.map +1 -1
- package/src/http/ApiRegistry.d.ts +91 -0
- package/src/http/ApiRegistry.js +129 -0
- package/src/http/ApiRegistry.js.map +1 -0
- package/src/http/CallScope.d.ts +35 -8
- package/src/http/CallScope.js +59 -5
- package/src/http/CallScope.js.map +1 -1
- package/src/http/CallStrategy.d.ts +1 -1
- package/src/http/CallStrategy.js.map +1 -1
- package/src/http/ClientRegistry.d.ts +38 -71
- package/src/http/ClientRegistry.js +127 -97
- package/src/http/ClientRegistry.js.map +1 -1
- package/src/http/ClientRegistryCompileAssertions.js +63 -0
- package/src/http/ClientRegistryCompileAssertions.js.map +1 -0
- package/src/http/DeletedAuthCompileAssertions.js +1 -1
- package/src/http/DeletedAuthCompileAssertions.js.map +1 -1
- package/src/http/ErrorClassifier.d.ts +7 -7
- package/src/http/ErrorClassifier.js.map +1 -1
- package/src/http/ErrorLadderCompileAssertions.js +41 -17
- package/src/http/ErrorLadderCompileAssertions.js.map +1 -1
- package/src/http/ErrorTranslator.d.ts +6 -6
- package/src/http/ErrorTranslator.js.map +1 -1
- package/src/http/LogApiCall.d.ts +1 -1
- package/src/http/LogApiCall.js +4 -4
- package/src/http/LogApiCall.js.map +1 -1
- package/src/http/RetryStrategy.d.ts +2 -2
- package/src/http/RetryStrategy.js +4 -4
- package/src/http/RetryStrategy.js.map +1 -1
- package/src/http/RouteMetadata.d.ts +1 -1
- package/src/http/RouteMetadata.js +1 -1
- package/src/http/RouteMetadata.js.map +1 -1
- package/src/http/StreamErrorControl.js +2 -2
- package/src/http/StreamErrorControl.js.map +1 -1
- package/src/http/TransportDefault.d.ts +2 -2
- package/src/http/TransportDefault.js +2 -2
- package/src/http/TransportDefault.js.map +1 -1
- package/src/http/WebpiecesDefaultErrorTranslator.d.ts +2 -2
- package/src/http/WebpiecesDefaultErrorTranslator.js +2 -2
- package/src/http/WebpiecesDefaultErrorTranslator.js.map +1 -1
- package/src/http/auth-mode.d.ts +6 -4
- package/src/http/auth-mode.js +6 -4
- package/src/http/auth-mode.js.map +1 -1
- package/src/http/decorators.d.ts +1 -1
- package/src/http/decorators.js +3 -3
- package/src/http/decorators.js.map +1 -1
- package/src/index.d.ts +2 -3
- package/src/index.js +7 -5
- package/src/index.js.map +1 -1
- package/src/ipc/IpcErrorTranslator.d.ts +1 -1
- package/src/ipc/IpcErrorTranslator.js.map +1 -1
- package/src/ipc/IpcRegistry.d.ts +1 -1
- package/src/ipc/IpcRegistry.js +1 -1
- package/src/ipc/IpcRegistry.js.map +1 -1
- package/src/http/CallRegistry.d.ts +0 -42
- package/src/http/CallRegistry.js +0 -143
- package/src/http/CallRegistry.js.map +0 -1
- package/src/http/CallRegistryCompileAssertions.js +0 -27
- package/src/http/CallRegistryCompileAssertions.js.map +0 -1
- /package/src/http/{CallRegistryCompileAssertions.d.ts → ClientRegistryCompileAssertions.d.ts} +0 -0
|
@@ -1,7 +1,6 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
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
|
-
|
|
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
|
-
*
|
|
116
|
-
*
|
|
117
|
-
*
|
|
118
|
-
*
|
|
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
|
-
*
|
|
121
|
-
*
|
|
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
|
|
122
|
+
static execute<T>(api: Function, context: CallContext, attempt: Attempt<T>, fallback: TransportDefault): Promise<T>;
|
|
140
123
|
/**
|
|
141
|
-
*
|
|
142
|
-
*
|
|
143
|
-
*
|
|
144
|
-
*
|
|
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
|
|
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
|
-
*
|
|
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
|
-
|
|
6
|
-
const
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
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
|
-
/**
|
|
12
|
-
|
|
13
|
-
|
|
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
|
-
|
|
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
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
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
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
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
|
-
*
|
|
192
|
-
*
|
|
193
|
-
*
|
|
194
|
-
*
|
|
195
|
-
*
|
|
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 --
|
|
198
|
-
static
|
|
199
|
-
|
|
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
|
|
203
|
-
*
|
|
204
|
-
*
|
|
205
|
-
*
|
|
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 --
|
|
213
|
-
static
|
|
214
|
-
const rungs = ClientRegistry.
|
|
215
|
-
|
|
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
|
-
*
|
|
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.
|
|
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):
|
|
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;
|