@webpieces/core-util 0.4.894 → 0.4.896
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/errors/ApiError.d.ts +27 -1
- package/src/errors/ApiError.js +37 -1
- package/src/errors/ApiError.js.map +1 -1
- package/src/errors/ApiErrorCodec.js +5 -0
- package/src/errors/ApiErrorCodec.js.map +1 -1
- package/src/errors/ReceivedApiErrorRule.d.ts +6 -2
- package/src/errors/ReceivedApiErrorRule.js +6 -2
- package/src/errors/ReceivedApiErrorRule.js.map +1 -1
- package/src/errors/index.d.ts +1 -1
- package/src/errors/index.js +2 -1
- package/src/errors/index.js.map +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/ApiErrorHttpStatus.js +3 -0
- package/src/http/ApiErrorHttpStatus.js.map +1 -1
- package/src/http/CallRegistry.d.ts +31 -8
- package/src/http/CallRegistry.js +93 -7
- package/src/http/CallRegistry.js.map +1 -1
- package/src/http/CallRegistryCompileAssertions.js +9 -0
- package/src/http/CallRegistryCompileAssertions.js.map +1 -1
- package/src/http/ClientRegistry.d.ts +25 -23
- package/src/http/ClientRegistry.js +33 -31
- package/src/http/ClientRegistry.js.map +1 -1
- package/src/http/ErrorClassifier.d.ts +64 -0
- package/src/http/ErrorClassifier.js +39 -0
- package/src/http/ErrorClassifier.js.map +1 -0
- package/src/http/LogApiCall.d.ts +2 -18
- package/src/http/LogApiCall.js +7 -24
- package/src/http/LogApiCall.js.map +1 -1
- package/src/http/RetryStrategy.d.ts +32 -0
- package/src/http/RetryStrategy.js +96 -0
- package/src/http/RetryStrategy.js.map +1 -0
- package/src/http/TransportDefault.d.ts +25 -0
- package/src/http/TransportDefault.js +31 -0
- package/src/http/TransportDefault.js.map +1 -0
- package/src/http/WebpiecesDefaultErrorClassifier.d.ts +42 -0
- package/src/http/WebpiecesDefaultErrorClassifier.js +87 -0
- package/src/http/WebpiecesDefaultErrorClassifier.js.map +1 -0
- package/src/index.d.ts +7 -4
- package/src/index.js +13 -7
- package/src/index.js.map +1 -1
- package/src/http/FailureClassifier.d.ts +0 -48
- package/src/http/FailureClassifier.js +0 -19
- package/src/http/FailureClassifier.js.map +0 -1
- package/src/http/WebpiecesDefaultFailureClassifier.d.ts +0 -40
- package/src/http/WebpiecesDefaultFailureClassifier.js +0 -67
- package/src/http/WebpiecesDefaultFailureClassifier.js.map +0 -1
|
@@ -1,11 +1,25 @@
|
|
|
1
1
|
import { Attempt, CallContext, CallStrategy } from './CallStrategy';
|
|
2
|
-
|
|
3
|
-
type CallScope = [api: 'ALL'] | [api: Function, methodName?: string];
|
|
2
|
+
import { TransportDefault } from './TransportDefault';
|
|
4
3
|
/**
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
|
|
4
|
+
* Where a policy applies: every contract (`'ALL'`, which has no method), one contract, or one method
|
|
5
|
+
* of one contract. Exported so other per-call registries (e.g. cloudtasks-client's `GcpTaskRegistry`)
|
|
6
|
+
* scope their settings with the SAME ladder.
|
|
7
|
+
*/
|
|
8
|
+
export type CallScope = [api: 'ALL'] | [api: Function, methodName?: string];
|
|
9
|
+
/**
|
|
10
|
+
* Shared startup registry for browser RPC, node RPC, and Cloud Tasks ENQUEUE — ONE implementation,
|
|
11
|
+
* every transport calls {@link CallRegistry.execute}.
|
|
12
|
+
*
|
|
13
|
+
* Ladder: strategy method -> API -> ALL first; ANY strategy replaces the ENTIRE timeout ladder.
|
|
14
|
+
* Otherwise timeout method -> API -> ALL. Otherwise the transport's {@link TransportDefault} (its
|
|
15
|
+
* timeout, and its strategy if it has one). A registered TIMEOUT is therefore a decision for a single
|
|
16
|
+
* attempt and also replaces a transport's default strategy; to keep retrying with a different attempt
|
|
17
|
+
* deadline, register a `RetryStrategy` instead.
|
|
18
|
+
*
|
|
19
|
+
* RPC has no default retry (only the application knows whether repeating a POST is safe); Cloud Tasks
|
|
20
|
+
* enqueue does (see `TransportDefault`). An app opts RPC in with
|
|
21
|
+
* `CallRegistry.setStrategy(new RetryStrategy(...).run, CatalogApi)` — and `RetryStrategy` never
|
|
22
|
+
* retries a `WRITE`, even registered at ALL.
|
|
9
23
|
*/
|
|
10
24
|
export declare class CallRegistry {
|
|
11
25
|
private static all;
|
|
@@ -16,9 +30,18 @@ export declare class CallRegistry {
|
|
|
16
30
|
* Run one call under its registered policy. `context` names the method (its `methodName` selects
|
|
17
31
|
* the per-method policy) and carries the endpoint's declared operation, so the SAME context a
|
|
18
32
|
* strategy sees is the one the transport's deadline puts on an `ApiCallTimeoutError`.
|
|
33
|
+
*
|
|
34
|
+
* Every attempt is wrapped so a retried TRANSIENT failure logs one
|
|
35
|
+
* `WARN [API-client-attempt-TRANSIENT]` line (see {@link LoggedAttempts}).
|
|
36
|
+
*/
|
|
37
|
+
static execute<T>(api: Function, context: CallContext, attempt: Attempt<T>, fallback: TransportDefault): Promise<T>;
|
|
38
|
+
/**
|
|
39
|
+
* The registered TIMEOUT for a call — method -> API -> ALL -> `defaultMs` — ignoring every
|
|
40
|
+
* strategy. For a transport that hands timing to someone else for this call (Cloud Tasks enqueue
|
|
41
|
+
* with caller-supplied gax `CallOptions`) and still needs ONE safety cap so a hung channel cannot
|
|
42
|
+
* block forever. Raise it with {@link CallRegistry.setTimeout}.
|
|
19
43
|
*/
|
|
20
|
-
static
|
|
44
|
+
static resolveTimeout(api: Function, methodName: string, defaultMs: number): number;
|
|
21
45
|
static clear(): void;
|
|
22
46
|
private static policy;
|
|
23
47
|
}
|
|
24
|
-
export {};
|
package/src/http/CallRegistry.js
CHANGED
|
@@ -2,6 +2,14 @@
|
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.CallRegistry = void 0;
|
|
4
4
|
const CallDeadline_1 = require("./CallDeadline");
|
|
5
|
+
const ClientRegistry_1 = require("./ClientRegistry");
|
|
6
|
+
const ErrorClassifier_1 = require("./ErrorClassifier");
|
|
7
|
+
const ApiMethodInfo_1 = require("./ApiMethodInfo");
|
|
8
|
+
const LogManager_1 = require("../logging/LogManager");
|
|
9
|
+
const ApiCallLogName_1 = require("./ApiCallLogName");
|
|
10
|
+
const errorUtils_1 = require("../lib/errorUtils");
|
|
11
|
+
// The same logger LogApiCall writes its [API-*] lines to, so an attempt line sits beside the final one.
|
|
12
|
+
const log = LogManager_1.LogManager.getLogger(ApiCallLogName_1.LOG_API_CALL_LOGGER_NAME);
|
|
5
13
|
class CallPolicy {
|
|
6
14
|
timeoutMs;
|
|
7
15
|
// webpieces-disable no-any-unknown -- global policy spans heterogeneous response DTOs
|
|
@@ -12,10 +20,66 @@ class ApiCallPolicies {
|
|
|
12
20
|
methods = new Map();
|
|
13
21
|
}
|
|
14
22
|
/**
|
|
15
|
-
*
|
|
16
|
-
*
|
|
17
|
-
*
|
|
18
|
-
*
|
|
23
|
+
* Counts the attempts of ONE call and logs each retried TRANSIENT failure. It wraps the attempt
|
|
24
|
+
* BEFORE any strategy sees it, so an app-written strategy gets the same lines as `RetryStrategy`.
|
|
25
|
+
*
|
|
26
|
+
* The line for attempt N is written when attempt N+1 STARTS — i.e. only for an attempt that was
|
|
27
|
+
* actually retried. The last attempt's failure is what escapes, and `LogApiCall`'s final
|
|
28
|
+
* `[API-client-resp-FAIL]` line covers it, so a call that gave up after 4 attempts logs 3 attempt
|
|
29
|
+
* lines and 1 FAIL line, never the last failure twice. An EXPECTED or FAILURE attempt is not logged
|
|
30
|
+
* here either; the final line covers it.
|
|
31
|
+
*/
|
|
32
|
+
class LoggedAttempts {
|
|
33
|
+
context;
|
|
34
|
+
attempt;
|
|
35
|
+
attempts = 0;
|
|
36
|
+
pendingTransient;
|
|
37
|
+
constructor(context, attempt) {
|
|
38
|
+
this.context = context;
|
|
39
|
+
this.attempt = attempt;
|
|
40
|
+
}
|
|
41
|
+
run = async (timeoutMs) => {
|
|
42
|
+
this.flushRetried();
|
|
43
|
+
this.attempts += 1;
|
|
44
|
+
// webpieces-disable no-unmanaged-exceptions -- observe the attempt's failure, rethrow it unchanged
|
|
45
|
+
// eslint-disable-next-line @webpieces/no-unmanaged-exceptions
|
|
46
|
+
try {
|
|
47
|
+
return await this.attempt(timeoutMs);
|
|
48
|
+
}
|
|
49
|
+
catch (err) {
|
|
50
|
+
const error = (0, errorUtils_1.toError)(err);
|
|
51
|
+
const methodInfo = new ApiMethodInfo_1.ApiMethodInfo('client', this.context.apiName, this.context.methodName);
|
|
52
|
+
this.pendingTransient =
|
|
53
|
+
ClientRegistry_1.ClientRegistry.classify(error, methodInfo) === ErrorClassifier_1.CallOutcome.TRANSIENT_FAILURE
|
|
54
|
+
? error
|
|
55
|
+
: undefined;
|
|
56
|
+
// eslint-disable-next-line @webpieces/no-unmanaged-exceptions -- rethrows the attempt's own failure unchanged
|
|
57
|
+
throw err;
|
|
58
|
+
}
|
|
59
|
+
};
|
|
60
|
+
flushRetried() {
|
|
61
|
+
const error = this.pendingTransient;
|
|
62
|
+
if (error === undefined)
|
|
63
|
+
return;
|
|
64
|
+
this.pendingTransient = undefined;
|
|
65
|
+
log.warn(`[API-client-attempt-TRANSIENT] ${this.context.apiName}.${this.context.methodName} ` +
|
|
66
|
+
`attempt ${this.attempts} errorType=${error.constructor.name} error=${error.message}`);
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
/**
|
|
70
|
+
* Shared startup registry for browser RPC, node RPC, and Cloud Tasks ENQUEUE — ONE implementation,
|
|
71
|
+
* every transport calls {@link CallRegistry.execute}.
|
|
72
|
+
*
|
|
73
|
+
* Ladder: strategy method -> API -> ALL first; ANY strategy replaces the ENTIRE timeout ladder.
|
|
74
|
+
* Otherwise timeout method -> API -> ALL. Otherwise the transport's {@link TransportDefault} (its
|
|
75
|
+
* timeout, and its strategy if it has one). A registered TIMEOUT is therefore a decision for a single
|
|
76
|
+
* attempt and also replaces a transport's default strategy; to keep retrying with a different attempt
|
|
77
|
+
* deadline, register a `RetryStrategy` instead.
|
|
78
|
+
*
|
|
79
|
+
* RPC has no default retry (only the application knows whether repeating a POST is safe); Cloud Tasks
|
|
80
|
+
* enqueue does (see `TransportDefault`). An app opts RPC in with
|
|
81
|
+
* `CallRegistry.setStrategy(new RetryStrategy(...).run, CatalogApi)` — and `RetryStrategy` never
|
|
82
|
+
* retries a `WRITE`, even registered at ALL.
|
|
19
83
|
*/
|
|
20
84
|
class CallRegistry {
|
|
21
85
|
static all = new CallPolicy();
|
|
@@ -36,16 +100,38 @@ class CallRegistry {
|
|
|
36
100
|
* Run one call under its registered policy. `context` names the method (its `methodName` selects
|
|
37
101
|
* the per-method policy) and carries the endpoint's declared operation, so the SAME context a
|
|
38
102
|
* strategy sees is the one the transport's deadline puts on an `ApiCallTimeoutError`.
|
|
103
|
+
*
|
|
104
|
+
* Every attempt is wrapped so a retried TRANSIENT failure logs one
|
|
105
|
+
* `WARN [API-client-attempt-TRANSIENT]` line (see {@link LoggedAttempts}).
|
|
39
106
|
*/
|
|
40
107
|
// webpieces-disable no-function-outside-class -- transport-independent policy execution
|
|
41
|
-
static async execute(api, context, attempt,
|
|
108
|
+
static async execute(api, context, attempt, fallback) {
|
|
109
|
+
const logged = new LoggedAttempts(context, attempt).run;
|
|
42
110
|
const policies = CallRegistry.apis.get(api);
|
|
43
111
|
const method = policies?.methods.get(context.methodName);
|
|
44
112
|
const strategy = method?.strategy ?? policies?.policy.strategy ?? CallRegistry.all.strategy;
|
|
45
113
|
if (strategy !== undefined) {
|
|
46
|
-
return (await strategy(
|
|
114
|
+
return (await strategy(logged, context));
|
|
47
115
|
}
|
|
48
|
-
|
|
116
|
+
const timeoutMs = method?.timeoutMs ?? policies?.policy.timeoutMs ?? CallRegistry.all.timeoutMs;
|
|
117
|
+
if (timeoutMs !== undefined) {
|
|
118
|
+
return logged(timeoutMs);
|
|
119
|
+
}
|
|
120
|
+
if (fallback.strategy !== undefined) {
|
|
121
|
+
return (await fallback.strategy(logged, context));
|
|
122
|
+
}
|
|
123
|
+
return logged(fallback.timeoutMs);
|
|
124
|
+
}
|
|
125
|
+
/**
|
|
126
|
+
* The registered TIMEOUT for a call — method -> API -> ALL -> `defaultMs` — ignoring every
|
|
127
|
+
* strategy. For a transport that hands timing to someone else for this call (Cloud Tasks enqueue
|
|
128
|
+
* with caller-supplied gax `CallOptions`) and still needs ONE safety cap so a hung channel cannot
|
|
129
|
+
* block forever. Raise it with {@link CallRegistry.setTimeout}.
|
|
130
|
+
*/
|
|
131
|
+
// webpieces-disable no-function-outside-class -- process-global registry lookup
|
|
132
|
+
static resolveTimeout(api, methodName, defaultMs) {
|
|
133
|
+
const policies = CallRegistry.apis.get(api);
|
|
134
|
+
return (policies?.methods.get(methodName)?.timeoutMs ??
|
|
49
135
|
policies?.policy.timeoutMs ??
|
|
50
136
|
CallRegistry.all.timeoutMs ??
|
|
51
137
|
defaultMs);
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"CallRegistry.js","sourceRoot":"","sources":["../../../../../../packages/core/core-util/src/http/CallRegistry.ts"],"names":[],"mappings":"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","sourcesContent":["import { Attempt, CallContext, CallStrategy } from './CallStrategy';\nimport { CallDeadline } from './CallDeadline';\n\n/** ALL has no method; a contract can optionally select one method. */\ntype CallScope = [api: 'ALL'] | [api: Function, methodName?: string];\n\nclass CallPolicy {\n timeoutMs?: number;\n // webpieces-disable no-any-unknown -- global policy spans heterogeneous response DTOs\n strategy?: CallStrategy<unknown>;\n}\n\nclass ApiCallPolicies {\n readonly policy = new CallPolicy();\n readonly methods = new Map<string, CallPolicy>();\n}\n\n/**\n * Shared startup registry for browser RPC, node RPC, and Cloud Tasks ENQUEUE.\n * Strategy method -> API -> ALL first; ANY strategy replaces the ENTIRE timeout ladder.\n * Otherwise timeout method -> API -> ALL -> transport default (30s today).\n * No default retry: only the application knows whether repeating a POST is safe.\n */\nexport class CallRegistry {\n private static all = new CallPolicy();\n private static readonly apis = new Map<Function, ApiCallPolicies>();\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 CallRegistry.policy(scope[0], scope[1]).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 CallRegistry.policy(scope[0], scope[1]).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`.\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 defaultMs: number,\n ): Promise<T> {\n const policies = CallRegistry.apis.get(api);\n const method = policies?.methods.get(context.methodName);\n const strategy = method?.strategy ?? policies?.policy.strategy ?? CallRegistry.all.strategy;\n if (strategy !== undefined) {\n return (await strategy(attempt, context)) as T;\n }\n return attempt(\n method?.timeoutMs ??\n policies?.policy.timeoutMs ??\n CallRegistry.all.timeoutMs ??\n defaultMs,\n );\n }\n\n // webpieces-disable no-function-outside-class -- process-global registry reset for tests\n static clear(): void {\n CallRegistry.all = new CallPolicy();\n CallRegistry.apis.clear();\n }\n\n // webpieces-disable no-function-outside-class -- process-global registry storage\n private static policy(api: Function | 'ALL', methodName?: string): CallPolicy {\n if (api === 'ALL') {\n return CallRegistry.all;\n }\n let policies = CallRegistry.apis.get(api);\n if (!policies) {\n policies = new ApiCallPolicies();\n CallRegistry.apis.set(api, policies);\n }\n if (methodName === undefined) return policies.policy;\n let policy = policies.methods.get(methodName);\n if (!policy) {\n policy = new CallPolicy();\n policies.methods.set(methodName, policy);\n }\n return policy;\n }\n}\n"]}
|
|
1
|
+
{"version":3,"file":"CallRegistry.js","sourceRoot":"","sources":["../../../../../../packages/core/core-util/src/http/CallRegistry.ts"],"names":[],"mappings":"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","sourcesContent":["import { Attempt, CallContext, CallStrategy } from './CallStrategy';\nimport { CallDeadline } from './CallDeadline';\nimport { TransportDefault } from './TransportDefault';\nimport { ClientRegistry } from './ClientRegistry';\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';\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\n/**\n * Where a policy applies: every contract (`'ALL'`, which has no method), one contract, or one method\n * of one contract. Exported so other per-call registries (e.g. cloudtasks-client's `GcpTaskRegistry`)\n * scope their settings with the SAME ladder.\n */\nexport type CallScope = [api: 'ALL'] | [api: Function, methodName?: string];\n\nclass CallPolicy {\n timeoutMs?: number;\n // webpieces-disable no-any-unknown -- global policy spans heterogeneous response DTOs\n strategy?: CallStrategy<unknown>;\n}\n\nclass ApiCallPolicies {\n readonly policy = new CallPolicy();\n readonly methods = new Map<string, CallPolicy>();\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('client', this.context.apiName, this.context.methodName);\n this.pendingTransient =\n ClientRegistry.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 * Shared startup registry for browser RPC, node RPC, and Cloud Tasks ENQUEUE — ONE implementation,\n * every transport calls {@link CallRegistry.execute}.\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 * `CallRegistry.setStrategy(new RetryStrategy(...).run, CatalogApi)` — and `RetryStrategy` never\n * retries a `WRITE`, even registered at ALL.\n */\nexport class CallRegistry {\n private static all = new CallPolicy();\n private static readonly apis = new Map<Function, ApiCallPolicies>();\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 CallRegistry.policy(scope[0], scope[1]).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 CallRegistry.policy(scope[0], scope[1]).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`.\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 logged = new LoggedAttempts<T>(context, attempt).run;\n const policies = CallRegistry.apis.get(api);\n const method = policies?.methods.get(context.methodName);\n const strategy = method?.strategy ?? policies?.policy.strategy ?? CallRegistry.all.strategy;\n if (strategy !== undefined) {\n return (await strategy(logged, context)) as T;\n }\n const timeoutMs = method?.timeoutMs ?? policies?.policy.timeoutMs ?? CallRegistry.all.timeoutMs;\n if (timeoutMs !== undefined) {\n return logged(timeoutMs);\n }\n if (fallback.strategy !== undefined) {\n return (await fallback.strategy(logged, context)) 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 CallRegistry.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 policies = CallRegistry.apis.get(api);\n return (\n policies?.methods.get(methodName)?.timeoutMs ??\n policies?.policy.timeoutMs ??\n CallRegistry.all.timeoutMs ??\n defaultMs\n );\n }\n\n // webpieces-disable no-function-outside-class -- process-global registry reset for tests\n static clear(): void {\n CallRegistry.all = new CallPolicy();\n CallRegistry.apis.clear();\n }\n\n // webpieces-disable no-function-outside-class -- process-global registry storage\n private static policy(api: Function | 'ALL', methodName?: string): CallPolicy {\n if (api === 'ALL') {\n return CallRegistry.all;\n }\n let policies = CallRegistry.apis.get(api);\n if (!policies) {\n policies = new ApiCallPolicies();\n CallRegistry.apis.set(api, policies);\n }\n if (methodName === undefined) return policies.policy;\n let policy = policies.methods.get(methodName);\n if (!policy) {\n policy = new CallPolicy();\n policies.methods.set(methodName, policy);\n }\n return policy;\n }\n}\n"]}
|
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
const CallRegistry_1 = require("./CallRegistry");
|
|
4
|
+
const CallStrategy_1 = require("./CallStrategy");
|
|
5
|
+
const HttpEndpointOptions_1 = require("./HttpEndpointOptions");
|
|
4
6
|
/** Compiled by tsc (spec files are not), never executed. */
|
|
5
7
|
class CallRegistryCompileAssertions {
|
|
6
8
|
check() {
|
|
@@ -9,5 +11,12 @@ class CallRegistryCompileAssertions {
|
|
|
9
11
|
// @ts-expect-error ALL cannot select a method
|
|
10
12
|
CallRegistry_1.CallRegistry.setStrategy(undefined, 'ALL', 'work');
|
|
11
13
|
}
|
|
14
|
+
/** The transport fallback is a TransportDefault (timeout + optional strategy), never a bare number. */
|
|
15
|
+
fallbackIsATransportDefault() {
|
|
16
|
+
const context = new CallStrategy_1.CallContext('Api', 'work', HttpEndpointOptions_1.WRITE);
|
|
17
|
+
void CallRegistry_1.CallRegistry.execute(CallRegistryCompileAssertions, context, (ms) => Promise.resolve(ms),
|
|
18
|
+
// @ts-expect-error execute(..., defaultMs: number) became execute(..., fallback: TransportDefault)
|
|
19
|
+
30_000);
|
|
20
|
+
}
|
|
12
21
|
}
|
|
13
22
|
//# sourceMappingURL=CallRegistryCompileAssertions.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"CallRegistryCompileAssertions.js","sourceRoot":"","sources":["../../../../../../packages/core/core-util/src/http/CallRegistryCompileAssertions.ts"],"names":[],"mappings":";;AAAA,iDAA8C;AAE9C,4DAA4D;AAC5D,MAAM,6BAA6B;IAC/B,KAAK;QACD,8CAA8C;QAC9C,2BAAY,CAAC,UAAU,CAAC,GAAG,EAAE,KAAK,EAAE,MAAM,CAAC,CAAC;QAC5C,8CAA8C;QAC9C,2BAAY,CAAC,WAAW,CAAC,SAAS,EAAE,KAAK,EAAE,MAAM,CAAC,CAAC;IACvD,CAAC;CACJ","sourcesContent":["import { CallRegistry } from './CallRegistry';\n\n/** Compiled by tsc (spec files are not), never executed. */\nclass CallRegistryCompileAssertions {\n check(): void {\n // @ts-expect-error ALL cannot select a method\n CallRegistry.setTimeout(100, 'ALL', 'work');\n // @ts-expect-error ALL cannot select a method\n CallRegistry.setStrategy(undefined, 'ALL', 'work');\n }\n}\n"]}
|
|
1
|
+
{"version":3,"file":"CallRegistryCompileAssertions.js","sourceRoot":"","sources":["../../../../../../packages/core/core-util/src/http/CallRegistryCompileAssertions.ts"],"names":[],"mappings":";;AAAA,iDAA8C;AAC9C,iDAA6C;AAC7C,+DAA8C;AAE9C,4DAA4D;AAC5D,MAAM,6BAA6B;IAC/B,KAAK;QACD,8CAA8C;QAC9C,2BAAY,CAAC,UAAU,CAAC,GAAG,EAAE,KAAK,EAAE,MAAM,CAAC,CAAC;QAC5C,8CAA8C;QAC9C,2BAAY,CAAC,WAAW,CAAC,SAAS,EAAE,KAAK,EAAE,MAAM,CAAC,CAAC;IACvD,CAAC;IAED,uGAAuG;IACvG,2BAA2B;QACvB,MAAM,OAAO,GAAG,IAAI,0BAAW,CAAC,KAAK,EAAE,MAAM,EAAE,2BAAK,CAAC,CAAC;QACtD,KAAK,2BAAY,CAAC,OAAO,CACrB,6BAA6B,EAC7B,OAAO,EACP,CAAC,EAAU,EAAE,EAAE,CAAC,OAAO,CAAC,OAAO,CAAC,EAAE,CAAC;QACnC,mGAAmG;QACnG,MAAM,CACT,CAAC;IACN,CAAC;CACJ","sourcesContent":["import { CallRegistry } from './CallRegistry';\nimport { CallContext } from './CallStrategy';\nimport { WRITE } from './HttpEndpointOptions';\n\n/** Compiled by tsc (spec files are not), never executed. */\nclass CallRegistryCompileAssertions {\n check(): void {\n // @ts-expect-error ALL cannot select a method\n CallRegistry.setTimeout(100, 'ALL', 'work');\n // @ts-expect-error ALL cannot select a method\n CallRegistry.setStrategy(undefined, 'ALL', 'work');\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);\n void CallRegistry.execute(\n CallRegistryCompileAssertions,\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"]}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { ErrorTranslator } from './ErrorTranslator';
|
|
2
|
-
import {
|
|
2
|
+
import { CallOutcome, ErrorClassifier, KeyedErrorClassifier } from './ErrorClassifier';
|
|
3
3
|
import { ApiMethodInfo } from './ApiMethodInfo';
|
|
4
4
|
/**
|
|
5
5
|
* Derives a base URL for a service name that has NO registered mapping — the pluggable half of
|
|
@@ -69,20 +69,20 @@ export declare class ClientRegistry {
|
|
|
69
69
|
*/
|
|
70
70
|
private static errorTranslator;
|
|
71
71
|
/**
|
|
72
|
-
* The app/company DEFAULT {@link
|
|
73
|
-
* so a single
|
|
74
|
-
* starts life holding the webpieces built-in ({@link
|
|
75
|
-
* {@link ClientRegistry.
|
|
72
|
+
* The app/company DEFAULT {@link ErrorClassifier} — ONE per process, reads {@link ApiMethodInfo.side}
|
|
73
|
+
* so a single classifier covers the server router AND all internal clients. NEVER undefined: it
|
|
74
|
+
* starts life holding the webpieces built-in ({@link WEBPIECES_DEFAULT_ERROR_CLASSIFIER}) and
|
|
75
|
+
* {@link ClientRegistry.setDefaultErrorClassifier} replaces it. Populated once at startup on the
|
|
76
76
|
* SERVER and in the BROWSER — same no-DI pattern as {@link ClientRegistry.mappings}.
|
|
77
77
|
*/
|
|
78
|
-
private static
|
|
78
|
+
private static appDefaultErrorClassifier;
|
|
79
79
|
/**
|
|
80
|
-
* Per-EXTERNAL-client {@link
|
|
80
|
+
* Per-EXTERNAL-client {@link ErrorClassifier}s, keyed by {@link ApiMethodInfo.apiClass}
|
|
81
81
|
* ('FirestoreAdminClient', 'ClaudeApi', ...). Consulted BEFORE the default tier, so an external
|
|
82
82
|
* client overrides the default for its own apiClass only. Internal clients / the server register
|
|
83
|
-
* nothing here and fall through to the default. See {@link ClientRegistry.
|
|
83
|
+
* nothing here and fall through to the default. See {@link ClientRegistry.classify}.
|
|
84
84
|
*/
|
|
85
|
-
private static readonly
|
|
85
|
+
private static readonly errorClassifiersByApiClass;
|
|
86
86
|
/** Map a service name to `http://localhost:<port>`. */
|
|
87
87
|
static addMapping(svcName: string, port: number): void;
|
|
88
88
|
/**
|
|
@@ -138,33 +138,35 @@ export declare class ClientRegistry {
|
|
|
138
138
|
*/
|
|
139
139
|
static getErrorTranslator(): ErrorTranslator;
|
|
140
140
|
/**
|
|
141
|
-
* Set the app/company DEFAULT
|
|
141
|
+
* Set the app/company DEFAULT error classifier — ONE per process, covering the server router and
|
|
142
142
|
* all internal clients (it branches on {@link ApiMethodInfo.side}). Call ONCE at startup, on the
|
|
143
143
|
* server AND in the browser. Overrides the webpieces built-in for calls no per-apiClass classifier
|
|
144
|
-
* claims. See {@link
|
|
144
|
+
* claims. See {@link ErrorClassifier}.
|
|
145
145
|
*/
|
|
146
|
-
static
|
|
146
|
+
static setDefaultErrorClassifier(classifier: ErrorClassifier): void;
|
|
147
147
|
/**
|
|
148
|
-
* Register a per-EXTERNAL-client
|
|
149
|
-
* (e.g. 'FirestoreAdminClient'). Consulted BEFORE the
|
|
150
|
-
* Call ONCE at startup. See {@link
|
|
148
|
+
* Register a per-EXTERNAL-client error classifier, keyed by its
|
|
149
|
+
* {@link KeyedErrorClassifier.apiClass} (e.g. 'FirestoreAdminClient'). Consulted BEFORE the
|
|
150
|
+
* default tier for calls with that apiClass. Call ONCE at startup. See {@link ErrorClassifier}.
|
|
151
151
|
*/
|
|
152
|
-
static
|
|
152
|
+
static addErrorClassifier(keyed: KeyedErrorClassifier): void;
|
|
153
153
|
/**
|
|
154
|
-
*
|
|
155
|
-
*
|
|
154
|
+
* The {@link CallOutcome} of a thrown API-call error. Resolved most-specific-first, and ALWAYS
|
|
155
|
+
* definitive:
|
|
156
156
|
* 1. the per-apiClass classifier, if one is registered for `methodInfo.apiClass` — unless it defers;
|
|
157
|
-
* 2. else the app default classifier
|
|
158
|
-
* 3. else the webpieces built-in ({@link
|
|
159
|
-
*
|
|
157
|
+
* 2. else the app default classifier — unless it defers;
|
|
158
|
+
* 3. else the webpieces built-in ({@link WEBPIECES_DEFAULT_ERROR_CLASSIFIER}), which never defers.
|
|
159
|
+
*
|
|
160
|
+
* The ONE verdict read by `RetryStrategy` (retry only TRANSIENT), `CallRegistry`'s per-attempt
|
|
161
|
+
* log line, `LogApiCall` (EXPECTED → OTHER, else FAIL) and the MCP renderer's retry advice.
|
|
160
162
|
*/
|
|
161
|
-
static
|
|
163
|
+
static classify(error: Error, methodInfo: ApiMethodInfo): CallOutcome;
|
|
162
164
|
/**
|
|
163
165
|
* Put the process-globals back to the state a fresh process starts in. For TESTS, so one spec
|
|
164
166
|
* cannot leak its registrations into the next.
|
|
165
167
|
*
|
|
166
168
|
* It RESTORES the webpieces defaults rather than emptying the pluggable slots, which is the whole
|
|
167
|
-
* reason it is not called `clear()` any more: `getErrorTranslator()` and the default
|
|
169
|
+
* reason it is not called `clear()` any more: `getErrorTranslator()` and the default error
|
|
168
170
|
* classifier are non-optional now, so "cleared" is not a state they have. A spec that wants
|
|
169
171
|
* webpieces' own behaviour back asks for exactly that.
|
|
170
172
|
*/
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.ClientRegistry = void 0;
|
|
4
|
-
const
|
|
4
|
+
const WebpiecesDefaultErrorClassifier_1 = require("./WebpiecesDefaultErrorClassifier");
|
|
5
5
|
const WebpiecesDefaultErrorTranslator_1 = require("./WebpiecesDefaultErrorTranslator");
|
|
6
6
|
/**
|
|
7
7
|
* ClientRegistry - the ONE place a `svcName` becomes a base URL, for every outbound client in every
|
|
@@ -58,20 +58,20 @@ class ClientRegistry {
|
|
|
58
58
|
*/
|
|
59
59
|
static errorTranslator = WebpiecesDefaultErrorTranslator_1.WEBPIECES_DEFAULT_ERROR_TRANSLATOR;
|
|
60
60
|
/**
|
|
61
|
-
* The app/company DEFAULT {@link
|
|
62
|
-
* so a single
|
|
63
|
-
* starts life holding the webpieces built-in ({@link
|
|
64
|
-
* {@link ClientRegistry.
|
|
61
|
+
* The app/company DEFAULT {@link ErrorClassifier} — ONE per process, reads {@link ApiMethodInfo.side}
|
|
62
|
+
* so a single classifier covers the server router AND all internal clients. NEVER undefined: it
|
|
63
|
+
* starts life holding the webpieces built-in ({@link WEBPIECES_DEFAULT_ERROR_CLASSIFIER}) and
|
|
64
|
+
* {@link ClientRegistry.setDefaultErrorClassifier} replaces it. Populated once at startup on the
|
|
65
65
|
* SERVER and in the BROWSER — same no-DI pattern as {@link ClientRegistry.mappings}.
|
|
66
66
|
*/
|
|
67
|
-
static
|
|
67
|
+
static appDefaultErrorClassifier = WebpiecesDefaultErrorClassifier_1.WEBPIECES_DEFAULT_ERROR_CLASSIFIER;
|
|
68
68
|
/**
|
|
69
|
-
* Per-EXTERNAL-client {@link
|
|
69
|
+
* Per-EXTERNAL-client {@link ErrorClassifier}s, keyed by {@link ApiMethodInfo.apiClass}
|
|
70
70
|
* ('FirestoreAdminClient', 'ClaudeApi', ...). Consulted BEFORE the default tier, so an external
|
|
71
71
|
* client overrides the default for its own apiClass only. Internal clients / the server register
|
|
72
|
-
* nothing here and fall through to the default. See {@link ClientRegistry.
|
|
72
|
+
* nothing here and fall through to the default. See {@link ClientRegistry.classify}.
|
|
73
73
|
*/
|
|
74
|
-
static
|
|
74
|
+
static errorClassifiersByApiClass = new Map();
|
|
75
75
|
/** Map a service name to `http://localhost:<port>`. */
|
|
76
76
|
// webpieces-disable no-function-outside-class -- static global singleton (like HeaderRegistry/LogManager); populated once at startup, never DI-injected
|
|
77
77
|
static addMapping(svcName, port) {
|
|
@@ -177,53 +177,55 @@ class ClientRegistry {
|
|
|
177
177
|
return ClientRegistry.errorTranslator;
|
|
178
178
|
}
|
|
179
179
|
/**
|
|
180
|
-
* Set the app/company DEFAULT
|
|
180
|
+
* Set the app/company DEFAULT error classifier — ONE per process, covering the server router and
|
|
181
181
|
* all internal clients (it branches on {@link ApiMethodInfo.side}). Call ONCE at startup, on the
|
|
182
182
|
* server AND in the browser. Overrides the webpieces built-in for calls no per-apiClass classifier
|
|
183
|
-
* claims. See {@link
|
|
183
|
+
* claims. See {@link ErrorClassifier}.
|
|
184
184
|
*/
|
|
185
185
|
// webpieces-disable no-function-outside-class -- static global singleton (like HeaderRegistry/LogManager); populated once at startup, never DI-injected
|
|
186
|
-
static
|
|
187
|
-
ClientRegistry.
|
|
186
|
+
static setDefaultErrorClassifier(classifier) {
|
|
187
|
+
ClientRegistry.appDefaultErrorClassifier = classifier;
|
|
188
188
|
}
|
|
189
189
|
/**
|
|
190
|
-
* Register a per-EXTERNAL-client
|
|
191
|
-
* (e.g. 'FirestoreAdminClient'). Consulted BEFORE the
|
|
192
|
-
* Call ONCE at startup. See {@link
|
|
190
|
+
* Register a per-EXTERNAL-client error classifier, keyed by its
|
|
191
|
+
* {@link KeyedErrorClassifier.apiClass} (e.g. 'FirestoreAdminClient'). Consulted BEFORE the
|
|
192
|
+
* default tier for calls with that apiClass. Call ONCE at startup. See {@link ErrorClassifier}.
|
|
193
193
|
*/
|
|
194
194
|
// webpieces-disable no-function-outside-class -- static global singleton (like HeaderRegistry/LogManager); populated once at startup, never DI-injected
|
|
195
|
-
static
|
|
196
|
-
ClientRegistry.
|
|
195
|
+
static addErrorClassifier(keyed) {
|
|
196
|
+
ClientRegistry.errorClassifiersByApiClass.set(keyed.apiClass, keyed.classifier);
|
|
197
197
|
}
|
|
198
198
|
/**
|
|
199
|
-
*
|
|
200
|
-
*
|
|
199
|
+
* The {@link CallOutcome} of a thrown API-call error. Resolved most-specific-first, and ALWAYS
|
|
200
|
+
* definitive:
|
|
201
201
|
* 1. the per-apiClass classifier, if one is registered for `methodInfo.apiClass` — unless it defers;
|
|
202
|
-
* 2. else the app default classifier
|
|
203
|
-
* 3. else the webpieces built-in ({@link
|
|
204
|
-
*
|
|
202
|
+
* 2. else the app default classifier — unless it defers;
|
|
203
|
+
* 3. else the webpieces built-in ({@link WEBPIECES_DEFAULT_ERROR_CLASSIFIER}), which never defers.
|
|
204
|
+
*
|
|
205
|
+
* The ONE verdict read by `RetryStrategy` (retry only TRANSIENT), `CallRegistry`'s per-attempt
|
|
206
|
+
* log line, `LogApiCall` (EXPECTED → OTHER, else FAIL) and the MCP renderer's retry advice.
|
|
205
207
|
*/
|
|
206
208
|
// webpieces-disable no-function-outside-class -- static global singleton (like HeaderRegistry/LogManager); populated once at startup, never DI-injected
|
|
207
|
-
static
|
|
208
|
-
const perClient = ClientRegistry.
|
|
209
|
+
static classify(error, methodInfo) {
|
|
210
|
+
const perClient = ClientRegistry.errorClassifiersByApiClass.get(methodInfo.apiClass);
|
|
209
211
|
if (perClient !== undefined) {
|
|
210
|
-
const verdict = perClient.
|
|
212
|
+
const verdict = perClient.classify(error, methodInfo);
|
|
211
213
|
if (verdict !== undefined) {
|
|
212
214
|
return verdict;
|
|
213
215
|
}
|
|
214
216
|
}
|
|
215
|
-
const verdict = ClientRegistry.
|
|
217
|
+
const verdict = ClientRegistry.appDefaultErrorClassifier.classify(error, methodInfo);
|
|
216
218
|
if (verdict !== undefined) {
|
|
217
219
|
return verdict;
|
|
218
220
|
}
|
|
219
|
-
return
|
|
221
|
+
return WebpiecesDefaultErrorClassifier_1.WEBPIECES_DEFAULT_ERROR_CLASSIFIER.classify(error, methodInfo);
|
|
220
222
|
}
|
|
221
223
|
/**
|
|
222
224
|
* Put the process-globals back to the state a fresh process starts in. For TESTS, so one spec
|
|
223
225
|
* cannot leak its registrations into the next.
|
|
224
226
|
*
|
|
225
227
|
* It RESTORES the webpieces defaults rather than emptying the pluggable slots, which is the whole
|
|
226
|
-
* reason it is not called `clear()` any more: `getErrorTranslator()` and the default
|
|
228
|
+
* reason it is not called `clear()` any more: `getErrorTranslator()` and the default error
|
|
227
229
|
* classifier are non-optional now, so "cleared" is not a state they have. A spec that wants
|
|
228
230
|
* webpieces' own behaviour back asks for exactly that.
|
|
229
231
|
*/
|
|
@@ -232,8 +234,8 @@ class ClientRegistry {
|
|
|
232
234
|
ClientRegistry.mappings.clear();
|
|
233
235
|
ClientRegistry.deriver = undefined;
|
|
234
236
|
ClientRegistry.errorTranslator = WebpiecesDefaultErrorTranslator_1.WEBPIECES_DEFAULT_ERROR_TRANSLATOR;
|
|
235
|
-
ClientRegistry.
|
|
236
|
-
ClientRegistry.
|
|
237
|
+
ClientRegistry.appDefaultErrorClassifier = WebpiecesDefaultErrorClassifier_1.WEBPIECES_DEFAULT_ERROR_CLASSIFIER;
|
|
238
|
+
ClientRegistry.errorClassifiersByApiClass.clear();
|
|
237
239
|
}
|
|
238
240
|
}
|
|
239
241
|
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 { FailureClassifier } from './FailureClassifier';\nimport { WEBPIECES_DEFAULT_FAILURE_CLASSIFIER } from './WebpiecesDefaultFailureClassifier';\nimport { WEBPIECES_DEFAULT_ERROR_TRANSLATOR } from './WebpiecesDefaultErrorTranslator';\nimport { ApiMethodInfo } from './ApiMethodInfo';\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 process's ONE {@link ErrorTranslator}. NEVER undefined — it starts life holding\n * {@link WEBPIECES_DEFAULT_ERROR_TRANSLATOR}, and {@link ClientRegistry.setErrorTranslator}\n * REPLACES it. Process-global, set once at startup on the SERVER and in the BROWSER — the same\n * no-DI pattern as {@link ClientRegistry.mappings} above.\n *\n * There is no \"did anyone register one\" question anywhere in the framework because this field\n * cannot be empty: every caller makes ONE unconditional call, and an app translator declines an\n * error by DELEGATING to {@link WebpiecesDefaultErrorTranslator}.\n *\n * ONE, not a list, on purpose: an app that has several layers of error policy composes them\n * INSIDE its own `toWire`, where the precedence is written down, instead of leaving it implicit\n * in the order two unrelated startup paths happened to register.\n */\n private static errorTranslator: ErrorTranslator = WEBPIECES_DEFAULT_ERROR_TRANSLATOR;\n\n /**\n * The app/company DEFAULT {@link FailureClassifier} — ONE per process, reads {@link ApiMethodInfo.side}\n * so a single strategy covers the server router AND all internal clients. NEVER undefined: it\n * starts life holding the webpieces built-in ({@link WEBPIECES_DEFAULT_FAILURE_CLASSIFIER}) and\n * {@link ClientRegistry.setDefaultFailureClassifier} replaces it. Populated once at startup on the\n * SERVER and in the BROWSER — same no-DI pattern as {@link ClientRegistry.mappings}.\n */\n private static appDefaultFailureClassifier: FailureClassifier =\n WEBPIECES_DEFAULT_FAILURE_CLASSIFIER;\n\n /**\n * Per-EXTERNAL-client {@link FailureClassifier}s, keyed by {@link ApiMethodInfo.apiClass}\n * ('FirestoreAdminClient', 'ClaudeApi', ...). Consulted BEFORE the default tier, so an external\n * client overrides the default for its own apiClass only. Internal clients / the server register\n * nothing here and fall through to the default. See {@link ClientRegistry.classifyFailure}.\n */\n private static readonly failureClassifiersByApiClass = new Map<string, FailureClassifier>();\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 the app's {@link ErrorTranslator} — the ONE symmetric owner of error translation for\n * this process, server side AND every client side. Consulted BEFORE webpieces' built-in mapping\n * in both directions. Call ONCE at startup, on the server AND in the browser, mirroring\n * {@link ClientRegistry.addMapping}.\n *\n * `set`, not `add`: see the field doc above for why a registry LIST is not wanted.\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): void {\n ClientRegistry.errorTranslator = translator;\n }\n\n /**\n * THE {@link ErrorTranslator} for this process — never `undefined`, so a caller never branches.\n *\n * Until an app calls {@link ClientRegistry.setErrorTranslator} this is\n * {@link WEBPIECES_DEFAULT_ERROR_TRANSLATOR}. A registered translator answers EVERY error and\n * EVERY response, declining by delegating to the webpieces default (see {@link ErrorTranslator}),\n * so every call site in the framework is one unconditional line.\n */\n // webpieces-disable no-function-outside-class -- static global singleton (like HeaderRegistry/LogManager); populated once at startup, never DI-injected\n static getErrorTranslator(): ErrorTranslator {\n return ClientRegistry.errorTranslator;\n }\n\n /**\n * Set the app/company DEFAULT failure classifier — ONE per process, covering the server router and\n * all internal clients (it branches on {@link ApiMethodInfo.side}). Call ONCE at startup, on the\n * server AND in the browser. Overrides the webpieces built-in for calls no per-apiClass classifier\n * claims. See {@link FailureClassifier}.\n */\n // webpieces-disable no-function-outside-class -- static global singleton (like HeaderRegistry/LogManager); populated once at startup, never DI-injected\n static setDefaultFailureClassifier(classifier: FailureClassifier): void {\n ClientRegistry.appDefaultFailureClassifier = classifier;\n }\n\n /**\n * Register a per-EXTERNAL-client failure classifier, keyed by {@link ApiMethodInfo.apiClass}\n * (e.g. 'FirestoreAdminClient'). Consulted BEFORE the default tier for calls with that apiClass.\n * Call ONCE at startup. See {@link FailureClassifier}.\n */\n // webpieces-disable no-function-outside-class -- static global singleton (like HeaderRegistry/LogManager); populated once at startup, never DI-injected\n static addFailureClassifier(apiClass: string, classifier: FailureClassifier): void {\n ClientRegistry.failureClassifiersByApiClass.set(apiClass, classifier);\n }\n\n /**\n * Is this thrown API-call error a real FAILURE (true) or an expected non-failure (false)?\n * Resolved most-specific-first, and ALWAYS definitive (returns a boolean):\n * 1. the per-apiClass classifier, if one is registered for `methodInfo.apiClass` — unless it defers;\n * 2. else the app default classifier, if one was set — unless it defers;\n * 3. else the webpieces built-in ({@link WEBPIECES_DEFAULT_FAILURE_CLASSIFIER}), which never defers.\n * A `?? true` backstop keeps an unexpectedly-deferring built-in fail-safe (treat as failure).\n */\n // webpieces-disable no-function-outside-class -- static global singleton (like HeaderRegistry/LogManager); populated once at startup, never DI-injected\n static classifyFailure(error: Error, methodInfo: ApiMethodInfo): boolean {\n const perClient = ClientRegistry.failureClassifiersByApiClass.get(methodInfo.apiClass);\n if (perClient !== undefined) {\n const verdict = perClient.isFailure(error, methodInfo);\n if (verdict !== undefined) {\n return verdict;\n }\n }\n const verdict = ClientRegistry.appDefaultFailureClassifier.isFailure(error, methodInfo);\n if (verdict !== undefined) {\n return verdict;\n }\n return WEBPIECES_DEFAULT_FAILURE_CLASSIFIER.isFailure(error, methodInfo) ?? true;\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 RESTORES the webpieces defaults rather than emptying the pluggable slots, which is the whole\n * reason it is not called `clear()` any more: `getErrorTranslator()` and the default failure\n * classifier are non-optional now, so \"cleared\" is not a state they have. A spec that wants\n * webpieces' own behaviour back asks for 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.errorTranslator = WEBPIECES_DEFAULT_ERROR_TRANSLATOR;\n ClientRegistry.appDefaultFailureClassifier = WEBPIECES_DEFAULT_FAILURE_CLASSIFIER;\n ClientRegistry.failureClassifiersByApiClass.clear();\n }\n}\n"]}
|
|
1
|
+
{"version":3,"file":"ClientRegistry.js","sourceRoot":"","sources":["../../../../../../packages/core/core-util/src/http/ClientRegistry.ts"],"names":[],"mappings":";;;AAEA,uFAAuF;AACvF,uFAAuF;AAiBvF;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAiCG;AACH,MAAa,cAAc;IACvB,0FAA0F;IAClF,MAAM,CAAU,QAAQ,GAAG,IAAI,GAAG,EAAkB,CAAC;IAE7D,gGAAgG;IACxF,MAAM,CAAC,OAAO,CAAgC;IAEtD;;;;;;;;;;;;;OAaG;IACK,MAAM,CAAC,eAAe,GAAoB,oEAAkC,CAAC;IAErF;;;;;;OAMG;IACK,MAAM,CAAC,yBAAyB,GAAoB,oEAAkC,CAAC;IAE/F;;;;;OAKG;IACK,MAAM,CAAU,0BAA0B,GAAG,IAAI,GAAG,EAA2B,CAAC;IAExF,uDAAuD;IACvD,wJAAwJ;IACxJ,MAAM,CAAC,UAAU,CAAC,OAAe,EAAE,IAAY;QAC3C,cAAc,CAAC,QAAQ,CAAC,GAAG,CAAC,OAAO,EAAE,oBAAoB,IAAI,EAAE,CAAC,CAAC;IACrE,CAAC;IAED;;;;;OAKG;IACH,wJAAwJ;IACxJ,MAAM,CAAC,aAAa,CAAC,OAAe,EAAE,GAAW;QAC7C,cAAc,CAAC,QAAQ,CAAC,GAAG,CAAC,OAAO,EAAE,GAAG,CAAC,CAAC;IAC9C,CAAC;IAED;;;OAGG;IACH,wJAAwJ;IACxJ,MAAM,CAAC,UAAU,CAAC,EAAqB;QACnC,cAAc,CAAC,OAAO,GAAG,EAAE,CAAC;IAChC,CAAC;IAED,kGAAkG;IAClG,wJAAwJ;IACxJ,MAAM,CAAC,SAAS,CAAC,OAAe;QAC5B,OAAO,cAAc,CAAC,QAAQ,CAAC,GAAG,CAAC,OAAO,CAAC,CAAC;IAChD,CAAC;IAED;;;OAGG;IACH,wJAAwJ;IACxJ,MAAM,CAAC,MAAM,CAAC,OAAe;QACzB,MAAM,GAAG,GAAG,cAAc,CAAC,SAAS,CAAC,OAAO,CAAC,CAAC;QAC9C,IAAI,GAAG,KAAK,SAAS,EAAE,CAAC;YACpB,MAAM,IAAI,KAAK,CACX,kCAAkC,OAAO,6BAA6B;gBAClE,oEAAoE;gBACpE,iEAAiE,CACxE,CAAC;QACN,CAAC;QACD,OAAO,GAAG,CAAC;IACf,CAAC;IAED;;;;;;;OAOG;IACH,wJAAwJ;IACxJ,MAAM,CAAC,UAAU,CAAC,OAAe;QAC7B,MAAM,QAAQ,GAAG,cAAc,CAAC,SAAS,CAAC,OAAO,CAAC,CAAC;QACnD,IAAI,QAAQ,KAAK,SAAS,EAAE,CAAC;YACzB,OAAO,OAAO,CAAC,OAAO,CAAC,QAAQ,CAAC,CAAC;QACrC,CAAC;QACD,IAAI,CAAC,cAAc,CAAC,OAAO,EAAE,CAAC;YAC1B,OAAO,OAAO,CAAC,OAAO,CAAC,SAAS,CAAC,CAAC;QACtC,CAAC;QACD,OAAO,cAAc,CAAC,OAAO,CAAC,OAAO,CAAC,CAAC;IAC3C,CAAC;IAED;;;;OAIG;IACH,wJAAwJ;IACxJ,MAAM,CAAC,KAAK,CAAC,OAAO,CAAC,OAAe;QAChC,MAAM,GAAG,GAAG,MAAM,cAAc,CAAC,UAAU,CAAC,OAAO,CAAC,CAAC;QACrD,IAAI,GAAG,KAAK,SAAS,EAAE,CAAC;YACpB,MAAM,IAAI,KAAK,CACX,uBAAuB,OAAO,MAAM;gBAChC,iDAAiD,OAAO,YAAY;gBACpE,oDAAoD,OAAO,qBAAqB;gBAChF,gFAAgF;gBAChF,2FAA2F;gBAC3F,yFAAyF;gBACzF,2FAA2F,CAClG,CAAC;QACN,CAAC;QACD,OAAO,GAAG,CAAC;IACf,CAAC;IAED;;;;;;;OAOG;IACH,wJAAwJ;IACxJ,MAAM,CAAC,kBAAkB,CAAC,UAA2B;QACjD,cAAc,CAAC,eAAe,GAAG,UAAU,CAAC;IAChD,CAAC;IAED;;;;;;;OAOG;IACH,wJAAwJ;IACxJ,MAAM,CAAC,kBAAkB;QACrB,OAAO,cAAc,CAAC,eAAe,CAAC;IAC1C,CAAC;IAED;;;;;OAKG;IACH,wJAAwJ;IACxJ,MAAM,CAAC,yBAAyB,CAAC,UAA2B;QACxD,cAAc,CAAC,yBAAyB,GAAG,UAAU,CAAC;IAC1D,CAAC;IAED;;;;OAIG;IACH,wJAAwJ;IACxJ,MAAM,CAAC,kBAAkB,CAAC,KAA2B;QACjD,cAAc,CAAC,0BAA0B,CAAC,GAAG,CAAC,KAAK,CAAC,QAAQ,EAAE,KAAK,CAAC,UAAU,CAAC,CAAC;IACpF,CAAC;IAED;;;;;;;;;OASG;IACH,wJAAwJ;IACxJ,MAAM,CAAC,QAAQ,CAAC,KAAY,EAAE,UAAyB;QACnD,MAAM,SAAS,GAAG,cAAc,CAAC,0BAA0B,CAAC,GAAG,CAAC,UAAU,CAAC,QAAQ,CAAC,CAAC;QACrF,IAAI,SAAS,KAAK,SAAS,EAAE,CAAC;YAC1B,MAAM,OAAO,GAAG,SAAS,CAAC,QAAQ,CAAC,KAAK,EAAE,UAAU,CAAC,CAAC;YACtD,IAAI,OAAO,KAAK,SAAS,EAAE,CAAC;gBACxB,OAAO,OAAO,CAAC;YACnB,CAAC;QACL,CAAC;QACD,MAAM,OAAO,GAAG,cAAc,CAAC,yBAAyB,CAAC,QAAQ,CAAC,KAAK,EAAE,UAAU,CAAC,CAAC;QACrF,IAAI,OAAO,KAAK,SAAS,EAAE,CAAC;YACxB,OAAO,OAAO,CAAC;QACnB,CAAC;QACD,OAAO,oEAAkC,CAAC,QAAQ,CAAC,KAAK,EAAE,UAAU,CAAC,CAAC;IAC1E,CAAC;IAED;;;;;;;;OAQG;IACH,wJAAwJ;IACxJ,MAAM,CAAC,aAAa;QAChB,cAAc,CAAC,QAAQ,CAAC,KAAK,EAAE,CAAC;QAChC,cAAc,CAAC,OAAO,GAAG,SAAS,CAAC;QACnC,cAAc,CAAC,eAAe,GAAG,oEAAkC,CAAC;QACpE,cAAc,CAAC,yBAAyB,GAAG,oEAAkC,CAAC;QAC9E,cAAc,CAAC,0BAA0B,CAAC,KAAK,EAAE,CAAC;IACtD,CAAC;;AA5NL,wCA6NC","sourcesContent":["import { ErrorTranslator } from './ErrorTranslator';\nimport { CallOutcome, ErrorClassifier, KeyedErrorClassifier } from './ErrorClassifier';\nimport { WEBPIECES_DEFAULT_ERROR_CLASSIFIER } from './WebpiecesDefaultErrorClassifier';\nimport { WEBPIECES_DEFAULT_ERROR_TRANSLATOR } from './WebpiecesDefaultErrorTranslator';\nimport { ApiMethodInfo } from './ApiMethodInfo';\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 process's ONE {@link ErrorTranslator}. NEVER undefined — it starts life holding\n * {@link WEBPIECES_DEFAULT_ERROR_TRANSLATOR}, and {@link ClientRegistry.setErrorTranslator}\n * REPLACES it. Process-global, set once at startup on the SERVER and in the BROWSER — the same\n * no-DI pattern as {@link ClientRegistry.mappings} above.\n *\n * There is no \"did anyone register one\" question anywhere in the framework because this field\n * cannot be empty: every caller makes ONE unconditional call, and an app translator declines an\n * error by DELEGATING to {@link WebpiecesDefaultErrorTranslator}.\n *\n * ONE, not a list, on purpose: an app that has several layers of error policy composes them\n * INSIDE its own `toWire`, where the precedence is written down, instead of leaving it implicit\n * in the order two unrelated startup paths happened to register.\n */\n private static errorTranslator: ErrorTranslator = WEBPIECES_DEFAULT_ERROR_TRANSLATOR;\n\n /**\n * The app/company DEFAULT {@link ErrorClassifier} — ONE per process, reads {@link ApiMethodInfo.side}\n * so a single classifier covers the server router AND all internal clients. NEVER undefined: it\n * starts life holding the webpieces built-in ({@link WEBPIECES_DEFAULT_ERROR_CLASSIFIER}) and\n * {@link ClientRegistry.setDefaultErrorClassifier} replaces it. Populated once at startup on the\n * SERVER and in the BROWSER — same no-DI pattern as {@link ClientRegistry.mappings}.\n */\n private static appDefaultErrorClassifier: ErrorClassifier = WEBPIECES_DEFAULT_ERROR_CLASSIFIER;\n\n /**\n * Per-EXTERNAL-client {@link ErrorClassifier}s, keyed by {@link ApiMethodInfo.apiClass}\n * ('FirestoreAdminClient', 'ClaudeApi', ...). Consulted BEFORE the default tier, so an external\n * client overrides the default for its own apiClass only. Internal clients / the server register\n * nothing here and fall through to the default. See {@link ClientRegistry.classify}.\n */\n private static readonly errorClassifiersByApiClass = new Map<string, ErrorClassifier>();\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 the app's {@link ErrorTranslator} — the ONE symmetric owner of error translation for\n * this process, server side AND every client side. Consulted BEFORE webpieces' built-in mapping\n * in both directions. Call ONCE at startup, on the server AND in the browser, mirroring\n * {@link ClientRegistry.addMapping}.\n *\n * `set`, not `add`: see the field doc above for why a registry LIST is not wanted.\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): void {\n ClientRegistry.errorTranslator = translator;\n }\n\n /**\n * THE {@link ErrorTranslator} for this process — never `undefined`, so a caller never branches.\n *\n * Until an app calls {@link ClientRegistry.setErrorTranslator} this is\n * {@link WEBPIECES_DEFAULT_ERROR_TRANSLATOR}. A registered translator answers EVERY error and\n * EVERY response, declining by delegating to the webpieces default (see {@link ErrorTranslator}),\n * so every call site in the framework is one unconditional line.\n */\n // webpieces-disable no-function-outside-class -- static global singleton (like HeaderRegistry/LogManager); populated once at startup, never DI-injected\n static getErrorTranslator(): ErrorTranslator {\n return ClientRegistry.errorTranslator;\n }\n\n /**\n * Set the app/company DEFAULT error classifier — ONE per process, covering the server router and\n * all internal clients (it branches on {@link ApiMethodInfo.side}). Call ONCE at startup, on the\n * server AND in the browser. Overrides the webpieces built-in for calls no per-apiClass classifier\n * claims. 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 setDefaultErrorClassifier(classifier: ErrorClassifier): void {\n ClientRegistry.appDefaultErrorClassifier = classifier;\n }\n\n /**\n * Register a per-EXTERNAL-client error classifier, keyed by its\n * {@link KeyedErrorClassifier.apiClass} (e.g. 'FirestoreAdminClient'). Consulted BEFORE the\n * default tier for calls with that apiClass. Call ONCE at startup. 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 addErrorClassifier(keyed: KeyedErrorClassifier): void {\n ClientRegistry.errorClassifiersByApiClass.set(keyed.apiClass, keyed.classifier);\n }\n\n /**\n * The {@link CallOutcome} of a thrown API-call error. Resolved most-specific-first, and ALWAYS\n * definitive:\n * 1. the per-apiClass classifier, if one is registered for `methodInfo.apiClass` — unless it defers;\n * 2. else the app default classifier — unless it defers;\n * 3. 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 perClient = ClientRegistry.errorClassifiersByApiClass.get(methodInfo.apiClass);\n if (perClient !== undefined) {\n const verdict = perClient.classify(error, methodInfo);\n if (verdict !== undefined) {\n return verdict;\n }\n }\n const verdict = ClientRegistry.appDefaultErrorClassifier.classify(error, methodInfo);\n if (verdict !== undefined) {\n return verdict;\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 RESTORES the webpieces defaults rather than emptying the pluggable slots, which is the whole\n * reason it is not called `clear()` any more: `getErrorTranslator()` and the default error\n * classifier are non-optional now, so \"cleared\" is not a state they have. A spec that wants\n * webpieces' own behaviour back asks for 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.errorTranslator = WEBPIECES_DEFAULT_ERROR_TRANSLATOR;\n ClientRegistry.appDefaultErrorClassifier = WEBPIECES_DEFAULT_ERROR_CLASSIFIER;\n ClientRegistry.errorClassifiersByApiClass.clear();\n }\n}\n"]}
|