@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 +1 @@
|
|
|
1
|
-
{"version":3,"file":"DeletedAuthCompileAssertions.js","sourceRoot":"","sources":["../../../../../../packages/core/core-util/src/http/DeletedAuthCompileAssertions.ts"],"names":[],"mappings":";;;;AAAA,2DAAqC;AAErC,4FAA4F;AAC5F,MAAa,4BAA4B;IACrC,cAAc;QACV,2DAA2D;QAC3D,KAAK,QAAQ,CAAC,SAAS,CAAC;QACxB,oGAAoG;QACpG,KAAK,QAAQ,CAAC,UAAU,CAAC;QACzB,4HAA4H;QAC5H,KAAK,QAAQ,CAAC,IAAI,CAAC;QACnB,uEAAuE;QACvE,KAAK,QAAQ,CAAC,kBAAkB,CAAC;QACjC,qEAAqE;QACrE,KAAK,QAAQ,CAAC,aAAa,CAAC;QAC5B
|
|
1
|
+
{"version":3,"file":"DeletedAuthCompileAssertions.js","sourceRoot":"","sources":["../../../../../../packages/core/core-util/src/http/DeletedAuthCompileAssertions.ts"],"names":[],"mappings":";;;;AAAA,2DAAqC;AAErC,4FAA4F;AAC5F,MAAa,4BAA4B;IACrC,cAAc;QACV,2DAA2D;QAC3D,KAAK,QAAQ,CAAC,SAAS,CAAC;QACxB,oGAAoG;QACpG,KAAK,QAAQ,CAAC,UAAU,CAAC;QACzB,4HAA4H;QAC5H,KAAK,QAAQ,CAAC,IAAI,CAAC;QACnB,uEAAuE;QACvE,KAAK,QAAQ,CAAC,kBAAkB,CAAC;QACjC,qEAAqE;QACrE,KAAK,QAAQ,CAAC,aAAa,CAAC;QAC5B,4HAA4H;QAC5H,KAAK,QAAQ,CAAC,OAAO,CAAC;QACtB,sHAAsH;QACtH,KAAK,QAAQ,CAAC,QAAQ,CAAC;QACvB,sGAAsG;QACtG,KAAK,QAAQ,CAAC,yCAAyC,CAAC;QACxD,uEAAuE;QACvE,KAAK,QAAQ,CAAC,wCAAwC,CAAC;QACvD,iEAAiE;QACjE,KAAK,QAAQ,CAAC,YAAY,CAAC;QAC3B,8EAA8E;QAC9E,KAAK,QAAQ,CAAC,eAAe,CAAC;QAC9B,iEAAiE;QACjE,KAAK,QAAQ,CAAC,YAAY,CAAC;QAC3B,oEAAoE;QACpE,KAAK,QAAQ,CAAC,WAAW,CAAC;QAC1B,kEAAkE;QAClE,KAAK,QAAQ,CAAC,aAAa,CAAC;IAChC,CAAC;CACJ;AA/BD,oEA+BC","sourcesContent":["import * as CoreUtil from '../index';\n\n/** Each removed spelling has its own assertion so one missing export cannot mask a shim. */\nexport class DeletedAuthCompileAssertions {\n removedExports(): void {\n // @ts-expect-error use WpAuth([jwt()]) and WpAuthorization\n void CoreUtil.WpAuthJwt;\n // @ts-expect-error use WpAuth([oidcSameProject()]) or WpAuth([oidcSvcs([...])]) and WpAuthorization\n void CoreUtil.WpAuthOidc;\n // @ts-expect-error oidc(...) is removed: oidcSameProject() for this GCP project, oidcSvcs([...]) for named service accounts\n void CoreUtil.oidc;\n // @ts-expect-error use WpAuth([sharedSecret(...)]) and WpAuthorization\n void CoreUtil.WpAuthSharedSecret;\n // @ts-expect-error use WpAuth([vendorAuth(...)]) and WpAuthorization\n void CoreUtil.WpAuthWebhook;\n // @ts-expect-error webhook(name) is renamed vendorAuth(name): a VendorAuthMinter sends it, a VendorAuthVerifier receives it\n void CoreUtil.webhook;\n // @ts-expect-error EXTERNAL is split: EXTERNAL_INBOUND (an outside system calls us) or EXTERNAL_OUTBOUND (we call it)\n void CoreUtil.EXTERNAL;\n // @ts-expect-error renamed assertExternalEndpointKinds, which also checks EXTERNAL_OUTBOUND contracts\n void CoreUtil.assertEveryExternalEndpointDeclaresCaller;\n // @ts-expect-error renamed assertEveryVendorAuthEndpointRetainsRawBody\n void CoreUtil.assertEveryWebhookEndpointRetainsRawBody;\n // @ts-expect-error use WpAuth([apiKey(...)]) and WpAuthorization\n void CoreUtil.WpAuthApiKey;\n // @ts-expect-error use separate WpLocalOnly, authentication and authorization\n void CoreUtil.WpAuthLocalOnly;\n // @ts-expect-error MCP shares receiving endpoint WpAuthorization\n void CoreUtil.WpMcpAuthJwt;\n // @ts-expect-error unresolved credentials are read with getAuthMeta\n void CoreUtil.getAuthMode;\n // @ts-expect-error roles belong to common operation authorization\n void CoreUtil.rolesRequired;\n }\n}\n"]}
|
|
@@ -25,18 +25,18 @@ export declare enum CallOutcome {
|
|
|
25
25
|
* This is BEHAVIOR, so it is an interface (per CLAUDE.md), passed as an object with a NAMED method
|
|
26
26
|
* rather than a bare function — so "find usages" in an IDE lands on every implementation.
|
|
27
27
|
*
|
|
28
|
-
* Registered on
|
|
29
|
-
* singleton that owns
|
|
30
|
-
* `
|
|
31
|
-
* `
|
|
28
|
+
* Registered on `ApiRegistry` at startup — the same browser-safe, no-DI, populated-once
|
|
29
|
+
* singleton that owns the {@link ErrorTranslator} — with
|
|
30
|
+
* `ApiRegistry.setErrorClassifier(c, scope)`, on the same `CallScope` ladder as
|
|
31
|
+
* `ClientRegistry`'s strategy and timeout. {@link ApiRegistry.classify} resolves it most specific
|
|
32
32
|
* first:
|
|
33
33
|
*
|
|
34
|
-
* 1. `setErrorClassifier(c, XxxApi, 'method')` — one method of one contract.
|
|
35
|
-
* 2. `setErrorClassifier(c, XxxApi)` — one contract, keyed by its CLASS (never a name). An
|
|
34
|
+
* 1. `setErrorClassifier(c, CallScope.method(XxxApi, 'method'))` — one method of one contract.
|
|
35
|
+
* 2. `setErrorClassifier(c, CallScope.api(XxxApi))` — one contract, keyed by its CLASS (never a name). An
|
|
36
36
|
* SDK-wrapping api-client keys by its abstract `XxxApi`: each external API qualifies its errors
|
|
37
37
|
* differently (a document store's 404 miss is EXPECTED; a messaging vendor's 429 is TRANSIENT),
|
|
38
38
|
* so it overrides the rungs below for that contract only.
|
|
39
|
-
* 3. `setErrorClassifier(c,
|
|
39
|
+
* 3. `setErrorClassifier(c, CallScope.all())` — ONE per app/company. It reads {@link ApiMethodInfo.side}, so a
|
|
40
40
|
* single classifier covers the SERVER router AND every INTERNAL client (webpieces http client,
|
|
41
41
|
* cloud tasks).
|
|
42
42
|
* 4. {@link WebpiecesDefaultErrorClassifier} — the terminal rung, which never defers.
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"ErrorClassifier.js","sourceRoot":"","sources":["../../../../../../packages/core/core-util/src/http/ErrorClassifier.ts"],"names":[],"mappings":";;;AAEA;;;;;;;;;;;GAWG;AACH,IAAY,WAOX;AAPD,WAAY,WAAW;IACnB,qGAAqG;IACrG,oCAAqB,CAAA;IACrB,qGAAqG;IACrG,8CAA+B,CAAA;IAC/B,6GAA6G;IAC7G,kCAAmB,CAAA;AACvB,CAAC,EAPW,WAAW,2BAAX,WAAW,QAOtB","sourcesContent":["import { ApiMethodInfo } from './ApiMethodInfo';\n\n/**\n * The ONE verdict on a thrown API-call error. Three readers share it, so there is exactly one table\n * answering \"is this error expected, and is it worth repeating?\":\n *\n * - `RetryStrategy` retries only {@link CallOutcome.TRANSIENT_FAILURE} (and only on a `READ` /\n * `WRITE_IDEMPOTENT` endpoint — the ENDPOINT's safety is `CallContext.operation`'s question, never\n * the classifier's);\n * - `LogApiCall` logs {@link CallOutcome.EXPECTED} as `[API-*-resp-OTHER]` (`result='success'`) and\n * both failure outcomes as `[API-*-resp-FAIL]` (`result='failure'`);\n * - the MCP tool-call renderer derives its model-visible `retry` advice from the outcome plus the\n * endpoint's operation.\n */\nexport enum CallOutcome {\n /** It threw, but things are WORKING: a 266, a server-side 4xx, an external client's \"404 = miss\". */\n EXPECTED = 'expected',\n /** Broken NOW, may work if repeated: unavailable, connection, timeouts, 429/backoff, bad-gateway. */\n TRANSIENT_FAILURE = 'transient',\n /** Broken, and repeating will not help: a RECEIVED 4xx (our bug), implementation, 501, retries exhausted. */\n FAILURE = 'failure',\n}\n\n/**\n * Classifies a thrown API-call error into a {@link CallOutcome}.\n *\n * This is BEHAVIOR, so it is an interface (per CLAUDE.md), passed as an object with a NAMED method\n * rather than a bare function — so \"find usages\" in an IDE lands on every implementation.\n *\n * Registered on
|
|
1
|
+
{"version":3,"file":"ErrorClassifier.js","sourceRoot":"","sources":["../../../../../../packages/core/core-util/src/http/ErrorClassifier.ts"],"names":[],"mappings":";;;AAEA;;;;;;;;;;;GAWG;AACH,IAAY,WAOX;AAPD,WAAY,WAAW;IACnB,qGAAqG;IACrG,oCAAqB,CAAA;IACrB,qGAAqG;IACrG,8CAA+B,CAAA;IAC/B,6GAA6G;IAC7G,kCAAmB,CAAA;AACvB,CAAC,EAPW,WAAW,2BAAX,WAAW,QAOtB","sourcesContent":["import { ApiMethodInfo } from './ApiMethodInfo';\n\n/**\n * The ONE verdict on a thrown API-call error. Three readers share it, so there is exactly one table\n * answering \"is this error expected, and is it worth repeating?\":\n *\n * - `RetryStrategy` retries only {@link CallOutcome.TRANSIENT_FAILURE} (and only on a `READ` /\n * `WRITE_IDEMPOTENT` endpoint — the ENDPOINT's safety is `CallContext.operation`'s question, never\n * the classifier's);\n * - `LogApiCall` logs {@link CallOutcome.EXPECTED} as `[API-*-resp-OTHER]` (`result='success'`) and\n * both failure outcomes as `[API-*-resp-FAIL]` (`result='failure'`);\n * - the MCP tool-call renderer derives its model-visible `retry` advice from the outcome plus the\n * endpoint's operation.\n */\nexport enum CallOutcome {\n /** It threw, but things are WORKING: a 266, a server-side 4xx, an external client's \"404 = miss\". */\n EXPECTED = 'expected',\n /** Broken NOW, may work if repeated: unavailable, connection, timeouts, 429/backoff, bad-gateway. */\n TRANSIENT_FAILURE = 'transient',\n /** Broken, and repeating will not help: a RECEIVED 4xx (our bug), implementation, 501, retries exhausted. */\n FAILURE = 'failure',\n}\n\n/**\n * Classifies a thrown API-call error into a {@link CallOutcome}.\n *\n * This is BEHAVIOR, so it is an interface (per CLAUDE.md), passed as an object with a NAMED method\n * rather than a bare function — so \"find usages\" in an IDE lands on every implementation.\n *\n * Registered on `ApiRegistry` at startup — the same browser-safe, no-DI, populated-once\n * singleton that owns the {@link ErrorTranslator} — with\n * `ApiRegistry.setErrorClassifier(c, scope)`, on the same `CallScope` ladder as\n * `ClientRegistry`'s strategy and timeout. {@link ApiRegistry.classify} resolves it most specific\n * first:\n *\n * 1. `setErrorClassifier(c, CallScope.method(XxxApi, 'method'))` — one method of one contract.\n * 2. `setErrorClassifier(c, CallScope.api(XxxApi))` — one contract, keyed by its CLASS (never a name). An\n * SDK-wrapping api-client keys by its abstract `XxxApi`: each external API qualifies its errors\n * differently (a document store's 404 miss is EXPECTED; a messaging vendor's 429 is TRANSIENT),\n * so it overrides the rungs below for that contract only.\n * 3. `setErrorClassifier(c, CallScope.all())` — ONE per app/company. It reads {@link ApiMethodInfo.side}, so a\n * single classifier covers the SERVER router AND every INTERNAL client (webpieces http client,\n * cloud tasks).\n * 4. {@link WebpiecesDefaultErrorClassifier} — the terminal rung, which never defers.\n *\n * Internal clients and the server register NOTHING per contract — the ABSENCE of a contract rung is\n * the signal. An external client that forgets to register is FAIL-SAFE: it falls to the rungs below,\n * where every client-side error except a 266 is a failure.\n */\nexport interface ErrorClassifier {\n /**\n * @param error - the already-normalized thrown error (callers pass `toError(err)`)\n * @param methodInfo - the call identity; `side` distinguishes server/client, `contract` the API\n * @returns the outcome, or `undefined` to DEFER to the next rung (method defers to API, API to\n * ALL, ALL to the webpieces built-in). Deferring is what makes classifiers additive AND\n * override-capable.\n */\n classify(error: Error, methodInfo: ApiMethodInfo): CallOutcome | undefined;\n}\n\n"]}
|
|
@@ -1,32 +1,56 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
const ApiRegistry_1 = require("./ApiRegistry");
|
|
3
4
|
const ClientRegistry_1 = require("./ClientRegistry");
|
|
5
|
+
const CallScope_1 = require("./CallScope");
|
|
4
6
|
const ApiMethodInfo_1 = require("./ApiMethodInfo");
|
|
5
7
|
const CallStrategy_1 = require("./CallStrategy");
|
|
6
8
|
const HttpEndpointOptions_1 = require("./HttpEndpointOptions");
|
|
9
|
+
/** A contract class to scope by. */
|
|
10
|
+
class SaveTargetApi {
|
|
11
|
+
save() {
|
|
12
|
+
return Promise.resolve();
|
|
13
|
+
}
|
|
14
|
+
}
|
|
7
15
|
/**
|
|
8
16
|
* Compiled by tsc (spec files are not), never executed. Pins the ONE spelling of the error ladder:
|
|
9
|
-
* every registration states its `CallScope`, and the
|
|
10
|
-
* tsc fails the build with TS2578 if any of these ever starts
|
|
17
|
+
* it lives on `ApiRegistry` (both sides read it), every registration states its `CallScope`, and the
|
|
18
|
+
* spellings it replaced do not compile. tsc fails the build with TS2578 if any of these ever starts
|
|
19
|
+
* compiling again.
|
|
11
20
|
*/
|
|
12
21
|
class ErrorLadderCompileAssertions {
|
|
13
22
|
scopeIsRequired(translator, classifier) {
|
|
14
|
-
// @ts-expect-error setErrorTranslator(t) became setErrorTranslator(t,
|
|
15
|
-
|
|
16
|
-
// @ts-expect-error setDefaultErrorClassifier(c) became setErrorClassifier(c,
|
|
17
|
-
|
|
18
|
-
// @ts-expect-error addErrorClassifier(new KeyedErrorClassifier('XApi', c)) became setErrorClassifier(c, XApi)
|
|
19
|
-
|
|
20
|
-
// @ts-expect-error the scope is required — setErrorClassifier(c,
|
|
21
|
-
|
|
23
|
+
// @ts-expect-error setErrorTranslator(t) became setErrorTranslator(t, CallScope.all()) — the scope is required
|
|
24
|
+
ApiRegistry_1.ApiRegistry.setErrorTranslator(translator);
|
|
25
|
+
// @ts-expect-error setDefaultErrorClassifier(c) became setErrorClassifier(c, CallScope.all())
|
|
26
|
+
ApiRegistry_1.ApiRegistry.setDefaultErrorClassifier(classifier);
|
|
27
|
+
// @ts-expect-error addErrorClassifier(new KeyedErrorClassifier('XApi', c)) became setErrorClassifier(c, CallScope.api(XApi))
|
|
28
|
+
ApiRegistry_1.ApiRegistry.addErrorClassifier(classifier);
|
|
29
|
+
// @ts-expect-error the scope is required — setErrorClassifier(c, CallScope.all())
|
|
30
|
+
ApiRegistry_1.ApiRegistry.setErrorClassifier(classifier);
|
|
22
31
|
// @ts-expect-error getErrorTranslator() became getErrorTranslator(api, methodName)
|
|
23
|
-
|
|
32
|
+
ApiRegistry_1.ApiRegistry.getErrorTranslator();
|
|
33
|
+
}
|
|
34
|
+
scopeIsACallScope(translator, classifier) {
|
|
35
|
+
// @ts-expect-error the 'ALL' string became CallScope.all()
|
|
36
|
+
ApiRegistry_1.ApiRegistry.setErrorTranslator(translator, 'ALL');
|
|
37
|
+
// @ts-expect-error the 'ALL' string became CallScope.all()
|
|
38
|
+
ApiRegistry_1.ApiRegistry.setErrorClassifier(classifier, 'ALL');
|
|
39
|
+
// @ts-expect-error the (Api, 'method') tuple became CallScope.method(Api, 'method')
|
|
40
|
+
ApiRegistry_1.ApiRegistry.setErrorTranslator(translator, SaveTargetApi, 'save');
|
|
41
|
+
// @ts-expect-error a misspelled method does not compile — it would silently match nothing
|
|
42
|
+
ApiRegistry_1.ApiRegistry.setErrorClassifier(classifier, CallScope_1.CallScope.method(SaveTargetApi, 'svae'));
|
|
43
|
+
ApiRegistry_1.ApiRegistry.setErrorClassifier(classifier, CallScope_1.CallScope.method(SaveTargetApi, 'save'));
|
|
24
44
|
}
|
|
25
|
-
|
|
26
|
-
// @ts-expect-error
|
|
27
|
-
ClientRegistry_1.ClientRegistry.setErrorTranslator(translator,
|
|
28
|
-
// @ts-expect-error
|
|
29
|
-
ClientRegistry_1.ClientRegistry.setErrorClassifier(classifier,
|
|
45
|
+
errorPolicyMovedToApiRegistry(translator, classifier) {
|
|
46
|
+
// @ts-expect-error ClientRegistry.setErrorTranslator became ApiRegistry.setErrorTranslator — both sides read it
|
|
47
|
+
ClientRegistry_1.ClientRegistry.setErrorTranslator(translator, CallScope_1.CallScope.all());
|
|
48
|
+
// @ts-expect-error ClientRegistry.setErrorClassifier became ApiRegistry.setErrorClassifier — both sides read it
|
|
49
|
+
ClientRegistry_1.ClientRegistry.setErrorClassifier(classifier, CallScope_1.CallScope.all());
|
|
50
|
+
// @ts-expect-error ClientRegistry.getErrorTranslator became ApiRegistry.getErrorTranslator
|
|
51
|
+
ClientRegistry_1.ClientRegistry.getErrorTranslator(undefined, undefined);
|
|
52
|
+
// @ts-expect-error ClientRegistry.classify became ApiRegistry.classify
|
|
53
|
+
ClientRegistry_1.ClientRegistry.classify(new Error('x'), new ApiMethodInfo_1.ApiMethodInfo('client', 'SaveApi', 'save', SaveTargetApi));
|
|
30
54
|
}
|
|
31
55
|
callIdentityStatesItsContract() {
|
|
32
56
|
// @ts-expect-error ApiMethodInfo's contract CLASS is required
|
|
@@ -40,7 +64,7 @@ class ErrorLadderCompileAssertions {
|
|
|
40
64
|
}
|
|
41
65
|
keyedByClassNeverByName(classifier) {
|
|
42
66
|
// @ts-expect-error the ladder is keyed by the contract CLASS, never its name
|
|
43
|
-
|
|
67
|
+
ApiRegistry_1.ApiRegistry.setErrorClassifier(classifier, 'SaveApi');
|
|
44
68
|
}
|
|
45
69
|
}
|
|
46
70
|
//# sourceMappingURL=ErrorLadderCompileAssertions.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"ErrorLadderCompileAssertions.js","sourceRoot":"","sources":["../../../../../../packages/core/core-util/src/http/ErrorLadderCompileAssertions.ts"],"names":[],"mappings":";;AAAA,qDAAkD;
|
|
1
|
+
{"version":3,"file":"ErrorLadderCompileAssertions.js","sourceRoot":"","sources":["../../../../../../packages/core/core-util/src/http/ErrorLadderCompileAssertions.ts"],"names":[],"mappings":";;AAAA,+CAA4C;AAC5C,qDAAkD;AAClD,2CAAwC;AAGxC,mDAAgD;AAChD,iDAA6C;AAC7C,+DAA8C;AAE9C,oCAAoC;AACpC,MAAM,aAAa;IACf,IAAI;QACA,OAAO,OAAO,CAAC,OAAO,EAAE,CAAC;IAC7B,CAAC;CACJ;AAED;;;;;GAKG;AACH,MAAM,4BAA4B;IAC9B,eAAe,CAAC,UAA2B,EAAE,UAA2B;QACpE,+GAA+G;QAC/G,yBAAW,CAAC,kBAAkB,CAAC,UAAU,CAAC,CAAC;QAC3C,8FAA8F;QAC9F,yBAAW,CAAC,yBAAyB,CAAC,UAAU,CAAC,CAAC;QAClD,6HAA6H;QAC7H,yBAAW,CAAC,kBAAkB,CAAC,UAAU,CAAC,CAAC;QAC3C,kFAAkF;QAClF,yBAAW,CAAC,kBAAkB,CAAC,UAAU,CAAC,CAAC;QAC3C,mFAAmF;QACnF,yBAAW,CAAC,kBAAkB,EAAE,CAAC;IACrC,CAAC;IAED,iBAAiB,CAAC,UAA2B,EAAE,UAA2B;QACtE,2DAA2D;QAC3D,yBAAW,CAAC,kBAAkB,CAAC,UAAU,EAAE,KAAK,CAAC,CAAC;QAClD,2DAA2D;QAC3D,yBAAW,CAAC,kBAAkB,CAAC,UAAU,EAAE,KAAK,CAAC,CAAC;QAClD,oFAAoF;QACpF,yBAAW,CAAC,kBAAkB,CAAC,UAAU,EAAE,aAAa,EAAE,MAAM,CAAC,CAAC;QAClE,0FAA0F;QAC1F,yBAAW,CAAC,kBAAkB,CAAC,UAAU,EAAE,qBAAS,CAAC,MAAM,CAAC,aAAa,EAAE,MAAM,CAAC,CAAC,CAAC;QACpF,yBAAW,CAAC,kBAAkB,CAAC,UAAU,EAAE,qBAAS,CAAC,MAAM,CAAC,aAAa,EAAE,MAAM,CAAC,CAAC,CAAC;IACxF,CAAC;IAED,6BAA6B,CAAC,UAA2B,EAAE,UAA2B;QAClF,gHAAgH;QAChH,+BAAc,CAAC,kBAAkB,CAAC,UAAU,EAAE,qBAAS,CAAC,GAAG,EAAE,CAAC,CAAC;QAC/D,gHAAgH;QAChH,+BAAc,CAAC,kBAAkB,CAAC,UAAU,EAAE,qBAAS,CAAC,GAAG,EAAE,CAAC,CAAC;QAC/D,2FAA2F;QAC3F,+BAAc,CAAC,kBAAkB,CAAC,SAAS,EAAE,SAAS,CAAC,CAAC;QACxD,uEAAuE;QACvE,+BAAc,CAAC,QAAQ,CAAC,IAAI,KAAK,CAAC,GAAG,CAAC,EAAE,IAAI,6BAAa,CAAC,QAAQ,EAAE,SAAS,EAAE,MAAM,EAAE,aAAa,CAAC,CAAC,CAAC;IAC3G,CAAC;IAED,6BAA6B;QACzB,8DAA8D;QAC9D,KAAK,IAAI,6BAAa,CAAC,QAAQ,EAAE,SAAS,EAAE,MAAM,CAAC,CAAC;QACpD,4DAA4D;QAC5D,KAAK,IAAI,0BAAW,CAAC,SAAS,EAAE,MAAM,EAAE,2BAAK,CAAC,CAAC;QAC/C,uGAAuG;QACvG,KAAK,IAAI,6BAAa,CAAC,QAAQ,EAAE,SAAS,EAAE,MAAM,EAAE,SAAS,CAAC,CAAC;QAC/D,qGAAqG;QACrG,KAAK,IAAI,0BAAW,CAAC,SAAS,EAAE,MAAM,EAAE,2BAAK,EAAE,SAAS,CAAC,CAAC;IAC9D,CAAC;IAED,uBAAuB,CAAC,UAA2B;QAC/C,6EAA6E;QAC7E,yBAAW,CAAC,kBAAkB,CAAC,UAAU,EAAE,SAAS,CAAC,CAAC;IAC1D,CAAC;CACJ","sourcesContent":["import { ApiRegistry } from './ApiRegistry';\nimport { ClientRegistry } from './ClientRegistry';\nimport { CallScope } from './CallScope';\nimport { ErrorClassifier } from './ErrorClassifier';\nimport { ErrorTranslator } from './ErrorTranslator';\nimport { ApiMethodInfo } from './ApiMethodInfo';\nimport { CallContext } from './CallStrategy';\nimport { WRITE } from './HttpEndpointOptions';\n\n/** A contract class to scope by. */\nclass SaveTargetApi {\n save(): 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 the error ladder:\n * it lives on `ApiRegistry` (both sides read it), every registration states its `CallScope`, and the\n * spellings it replaced do not compile. tsc fails the build with TS2578 if any of these ever starts\n * compiling again.\n */\nclass ErrorLadderCompileAssertions {\n scopeIsRequired(translator: ErrorTranslator, classifier: ErrorClassifier): void {\n // @ts-expect-error setErrorTranslator(t) became setErrorTranslator(t, CallScope.all()) — the scope is required\n ApiRegistry.setErrorTranslator(translator);\n // @ts-expect-error setDefaultErrorClassifier(c) became setErrorClassifier(c, CallScope.all())\n ApiRegistry.setDefaultErrorClassifier(classifier);\n // @ts-expect-error addErrorClassifier(new KeyedErrorClassifier('XApi', c)) became setErrorClassifier(c, CallScope.api(XApi))\n ApiRegistry.addErrorClassifier(classifier);\n // @ts-expect-error the scope is required — setErrorClassifier(c, CallScope.all())\n ApiRegistry.setErrorClassifier(classifier);\n // @ts-expect-error getErrorTranslator() became getErrorTranslator(api, methodName)\n ApiRegistry.getErrorTranslator();\n }\n\n scopeIsACallScope(translator: ErrorTranslator, classifier: ErrorClassifier): void {\n // @ts-expect-error the 'ALL' string became CallScope.all()\n ApiRegistry.setErrorTranslator(translator, 'ALL');\n // @ts-expect-error the 'ALL' string became CallScope.all()\n ApiRegistry.setErrorClassifier(classifier, 'ALL');\n // @ts-expect-error the (Api, 'method') tuple became CallScope.method(Api, 'method')\n ApiRegistry.setErrorTranslator(translator, SaveTargetApi, 'save');\n // @ts-expect-error a misspelled method does not compile — it would silently match nothing\n ApiRegistry.setErrorClassifier(classifier, CallScope.method(SaveTargetApi, 'svae'));\n ApiRegistry.setErrorClassifier(classifier, CallScope.method(SaveTargetApi, 'save'));\n }\n\n errorPolicyMovedToApiRegistry(translator: ErrorTranslator, classifier: ErrorClassifier): void {\n // @ts-expect-error ClientRegistry.setErrorTranslator became ApiRegistry.setErrorTranslator — both sides read it\n ClientRegistry.setErrorTranslator(translator, CallScope.all());\n // @ts-expect-error ClientRegistry.setErrorClassifier became ApiRegistry.setErrorClassifier — both sides read it\n ClientRegistry.setErrorClassifier(classifier, CallScope.all());\n // @ts-expect-error ClientRegistry.getErrorTranslator became ApiRegistry.getErrorTranslator\n ClientRegistry.getErrorTranslator(undefined, undefined);\n // @ts-expect-error ClientRegistry.classify became ApiRegistry.classify\n ClientRegistry.classify(new Error('x'), new ApiMethodInfo('client', 'SaveApi', 'save', SaveTargetApi));\n }\n\n callIdentityStatesItsContract(): void {\n // @ts-expect-error ApiMethodInfo's contract CLASS is required\n void new ApiMethodInfo('client', 'SaveApi', 'save');\n // @ts-expect-error CallContext's contract CLASS is required\n void new CallContext('SaveApi', 'save', WRITE);\n // @ts-expect-error ApiMethodInfo's contract is a CLASS, never undefined — pass the real contract class\n void new ApiMethodInfo('client', 'SaveApi', 'save', undefined);\n // @ts-expect-error CallContext's contract is a CLASS, never undefined — pass the real contract class\n void new CallContext('SaveApi', 'save', WRITE, undefined);\n }\n\n keyedByClassNeverByName(classifier: ErrorClassifier): void {\n // @ts-expect-error the ladder is keyed by the contract CLASS, never its name\n ApiRegistry.setErrorClassifier(classifier, 'SaveApi');\n }\n}\n"]}
|
|
@@ -5,11 +5,11 @@ import { HttpResponseDto } from './HttpResponseDto';
|
|
|
5
5
|
* the WHOLE response.
|
|
6
6
|
*
|
|
7
7
|
* An app implements this ONCE and registers it per process via
|
|
8
|
-
* `
|
|
8
|
+
* `ApiRegistry.setErrorTranslator(t, CallScope.all())` — on the server AND in the browser. A contract whose
|
|
9
9
|
* errors travel in a DIFFERENT wire format (a vendor's own 4xx body, or the error body an inbound
|
|
10
|
-
* vendor expects back) gets its own translator on the same `CallScope` ladder as `
|
|
11
|
-
* strategy and timeout: `setErrorTranslator(t, VendorApi)` or
|
|
12
|
-
* 'method')`, resolved method -> API -> ALL -> webpieces default. Its `toWire` runs on
|
|
10
|
+
* vendor expects back) gets its own translator on the same `CallScope` ladder as `ClientRegistry`'s
|
|
11
|
+
* strategy and timeout: `setErrorTranslator(t, CallScope.api(VendorApi))` or
|
|
12
|
+
* `setErrorTranslator(t, CallScope.method(VendorApi, 'method'))`, resolved method -> API -> ALL -> webpieces default. Its `toWire` runs on
|
|
13
13
|
* the SERVER (`ExpressWrapper.handleError`) and its `fromWire` runs on every CLIENT in that process
|
|
14
14
|
* (`ClientErrorTranslator.throwIfFailure`, shared by `http-client-node` and `http-client-browser`).
|
|
15
15
|
* The payoff is type symmetry across the wire: the server throws `OrderNotFoundError` and the caller
|
|
@@ -17,7 +17,7 @@ import { HttpResponseDto } from './HttpResponseDto';
|
|
|
17
17
|
*
|
|
18
18
|
* # THERE IS ALWAYS A TRANSLATOR
|
|
19
19
|
*
|
|
20
|
-
* {@link
|
|
20
|
+
* {@link ApiRegistry.getErrorTranslator} is non-optional: below every rung of the ladder sits
|
|
21
21
|
* {@link WebpiecesDefaultErrorTranslator}. So no caller anywhere asks "did anyone register one" —
|
|
22
22
|
* every call site is ONE unconditional line. A registered app translator REPLACES the default and
|
|
23
23
|
* declines by DELEGATING to it:
|
|
@@ -93,7 +93,7 @@ import { HttpResponseDto } from './HttpResponseDto';
|
|
|
93
93
|
* }
|
|
94
94
|
*
|
|
95
95
|
* // startup, ONCE per process (server AND browser)
|
|
96
|
-
*
|
|
96
|
+
* ApiRegistry.setErrorTranslator(new OrderErrorTranslator(), CallScope.all());
|
|
97
97
|
* ```
|
|
98
98
|
*/
|
|
99
99
|
export interface ErrorTranslator {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"ErrorTranslator.js","sourceRoot":"","sources":["../../../../../../packages/core/core-util/src/http/ErrorTranslator.ts"],"names":[],"mappings":"","sourcesContent":["import { ClientRole } from '../errors/ClientRole';\nimport { HttpResponseDto } from './HttpResponseDto';\n\n/**\n * ErrorTranslator - ONE symmetric place an app owns HTTP error translation, in BOTH directions, over\n * the WHOLE response.\n *\n * An app implements this ONCE and registers it per process via\n * `
|
|
1
|
+
{"version":3,"file":"ErrorTranslator.js","sourceRoot":"","sources":["../../../../../../packages/core/core-util/src/http/ErrorTranslator.ts"],"names":[],"mappings":"","sourcesContent":["import { ClientRole } from '../errors/ClientRole';\nimport { HttpResponseDto } from './HttpResponseDto';\n\n/**\n * ErrorTranslator - ONE symmetric place an app owns HTTP error translation, in BOTH directions, over\n * the WHOLE response.\n *\n * An app implements this ONCE and registers it per process via\n * `ApiRegistry.setErrorTranslator(t, CallScope.all())` — on the server AND in the browser. A contract whose\n * errors travel in a DIFFERENT wire format (a vendor's own 4xx body, or the error body an inbound\n * vendor expects back) gets its own translator on the same `CallScope` ladder as `ClientRegistry`'s\n * strategy and timeout: `setErrorTranslator(t, CallScope.api(VendorApi))` or\n * `setErrorTranslator(t, CallScope.method(VendorApi, 'method'))`, resolved method -> API -> ALL -> webpieces default. Its `toWire` runs on\n * the SERVER (`ExpressWrapper.handleError`) and its `fromWire` runs on every CLIENT in that process\n * (`ClientErrorTranslator.throwIfFailure`, shared by `http-client-node` and `http-client-browser`).\n * The payoff is type symmetry across the wire: the server throws `OrderNotFoundError` and the caller\n * CATCHES `OrderNotFoundError`, instead of decoding a status code by hand at every call site.\n *\n * # THERE IS ALWAYS A TRANSLATOR\n *\n * {@link ApiRegistry.getErrorTranslator} is non-optional: below every rung of the ladder sits\n * {@link WebpiecesDefaultErrorTranslator}. So no caller anywhere asks \"did anyone register one\" —\n * every call site is ONE unconditional line. A registered app translator REPLACES the default and\n * declines by DELEGATING to it:\n *\n * ```ts\n * private readonly fallback = new WebpiecesDefaultErrorTranslator();\n * ```\n *\n * That is one call, it is visible at the call site, and it composes: an app that wants to DECORATE\n * the default rather than replace it does so by calling it and editing the result. A `next` parameter\n * was considered and rejected — it buys nothing this does not, and it puts the framework back in the\n * business of sequencing somebody else's precedence.\n *\n * # Both halves speak {@link HttpResponseDto}, and that is the point\n *\n * `toWire` PRODUCES exactly what `fromWire` CONSUMES. Reading the two methods against each other in\n * one file is what makes a mistake visible, which is why this is one object rather than two\n * separately-registered functions.\n *\n * # The whole response, not a status plus a body\n *\n * The previous contract could express only `(statusCode, protocolError)`, so an app could not put its\n * own trace id, `Retry-After`, `WWW-Authenticate` or a cookie on an error response. A translator now\n * returns the status code, the reason phrase, the header LIST and the body — everything.\n *\n * # `toWire` RETURNS a response; `fromWire` THROWS\n *\n * They are mirrors, not twins. `toWire` is called inside a catch and its whole job is to produce a\n * value, so it returns one. `fromWire` is called on a response the caller is about to be handed, and\n * its whole job is to stop that from happening, so it THROWS and is typed `void`. Returning an\n * `Error` the caller then has to remember to throw was the previous shape and it is exactly the bug\n * class this deletes: a `void` return cannot be silently dropped.\n *\n * There is exactly ONE registered ErrorTranslator per process (a `set`, not an `add` to a list):\n * precedence between an app's own layers is the APP's to compose explicitly inside its `toWire`,\n * rather than something hidden in registration order inside the framework.\n *\n * # Not covered: 404 / unknown route\n *\n * A request that matches no route has no route context and never reaches this seam. That is\n * deliberate and out of scope.\n *\n * This is a business-logic contract (methods, not data), so it is an interface per the webpieces\n * guidelines.\n *\n * ```ts\n * class OrderErrorPayload {\n * constructor(public message: string, public errorCode: string) {}\n * }\n *\n * export class OrderErrorTranslator implements ErrorTranslator {\n * private readonly fallback = new WebpiecesDefaultErrorTranslator();\n *\n * toWire(error: Error): HttpResponseDto {\n * if (!(error instanceof OrderNotFoundError)) {\n * return this.fallback.toWire(error); // not mine -> webpieces default\n * }\n * const body = new OrderErrorPayload(error.message, 'ORDER_NOT_FOUND');\n * return new HttpResponseDto(\n * new HttpResponseStatus(460, 'Order Not Found'),\n * [new HttpHeader('x-order-trace', error.traceId)],\n * body,\n * );\n * }\n *\n * fromWire(response: HttpResponseDto, role: ClientRole): void {\n * if (response.status.code !== 460) {\n * this.fallback.fromWire(response, role); // not mine -> webpieces default\n * return;\n * }\n * throw new OrderNotFoundError(String(response.body));\n * }\n * }\n *\n * // startup, ONCE per process (server AND browser)\n * ApiRegistry.setErrorTranslator(new OrderErrorTranslator(), CallScope.all());\n * ```\n */\nexport interface ErrorTranslator {\n /**\n * SERVER: exception -> the ENTIRE response (status code, reason phrase, headers, body). Always a\n * response, never `undefined`; decline by returning\n * `new WebpiecesDefaultErrorTranslator().toWire(error)`.\n */\n toWire(error: Error): HttpResponseDto;\n\n /**\n * CLIENT: the ENTIRE response -> a THROW. Receives the SAME shape `toWire` produces, normalised\n * from whichever transport read it.\n *\n * Called for EVERY response, including a 2xx — deliberately, so an app whose 200 body signals\n * failure can turn it into a throw. Returning normally means \"let this response through\"; the\n * webpieces default returns normally for any 2xx that is not 266 and throws otherwise. Decline by\n * calling `new WebpiecesDefaultErrorTranslator().fromWire(response, role)`.\n *\n * `role` is WHO is receiving ({@link ClientRole}): the end-user client or a server. The client\n * that read the response passes the role ITS setup declared (`ClientConfig`), so one translator\n * registered in a process serves every client there. It decides what a received 401 means; pass\n * it through unchanged when declining.\n *\n * A non-2xx response can never reach a typed caller even if an app translator forgets to throw:\n * `ClientErrorTranslator.throwIfFailure` applies the webpieces default behind it. That is bug\n * containment, not a \"was one registered\" branch.\n */\n fromWire(response: HttpResponseDto, role: ClientRole): void;\n}\n"]}
|
package/src/http/LogApiCall.d.ts
CHANGED
|
@@ -106,7 +106,7 @@ export declare class LogApiCall {
|
|
|
106
106
|
* (request and response bodies are capped at {@link LogApiCall.maxLoggedBodyChars} chars, #1300)
|
|
107
107
|
* - [API-{side}-resp-SUCCESS] ClassName.methodName response={...}
|
|
108
108
|
* - [API-{side}-resp-OTHER] ClassName.methodName errorType={...} error={...} (CallOutcome.EXPECTED)
|
|
109
|
-
* - [API-{side}-resp-FAIL] ClassName.methodName error={...} (TRANSIENT_FAILURE or FAILURE — final by position; each retried attempt is logged by
|
|
109
|
+
* - [API-{side}-resp-FAIL] ClassName.methodName error={...} (TRANSIENT_FAILURE or FAILURE — final by position; each retried attempt is logged by ClientRegistry as [API-client-attempt-TRANSIENT])
|
|
110
110
|
*/
|
|
111
111
|
export declare class LogApiCallImpl {
|
|
112
112
|
private readonly ctx;
|
package/src/http/LogApiCall.js
CHANGED
|
@@ -6,7 +6,7 @@ const LogManager_1 = require("../logging/LogManager");
|
|
|
6
6
|
const ApiCallInfo_1 = require("./ApiCallInfo");
|
|
7
7
|
const WebpiecesCoreHeaders_1 = require("./WebpiecesCoreHeaders");
|
|
8
8
|
const ApiCallLogName_1 = require("./ApiCallLogName");
|
|
9
|
-
const
|
|
9
|
+
const ApiRegistry_1 = require("./ApiRegistry");
|
|
10
10
|
const ErrorClassifier_1 = require("./ErrorClassifier");
|
|
11
11
|
// The console backends special-case THIS logger name into a self-describing [API.{side}.{phase}]
|
|
12
12
|
// bracket (see ApiCallLogName) — so the name here and the name they match are the one constant.
|
|
@@ -162,7 +162,7 @@ exports.LogApiCall = LogApiCall;
|
|
|
162
162
|
* (request and response bodies are capped at {@link LogApiCall.maxLoggedBodyChars} chars, #1300)
|
|
163
163
|
* - [API-{side}-resp-SUCCESS] ClassName.methodName response={...}
|
|
164
164
|
* - [API-{side}-resp-OTHER] ClassName.methodName errorType={...} error={...} (CallOutcome.EXPECTED)
|
|
165
|
-
* - [API-{side}-resp-FAIL] ClassName.methodName error={...} (TRANSIENT_FAILURE or FAILURE — final by position; each retried attempt is logged by
|
|
165
|
+
* - [API-{side}-resp-FAIL] ClassName.methodName error={...} (TRANSIENT_FAILURE or FAILURE — final by position; each retried attempt is logged by ClientRegistry as [API-client-attempt-TRANSIENT])
|
|
166
166
|
*/
|
|
167
167
|
class LogApiCallImpl {
|
|
168
168
|
ctx;
|
|
@@ -347,13 +347,13 @@ class LogApiCallImpl {
|
|
|
347
347
|
// per failure now that `ApiErrorBoundary` writes none. An operator error message is never
|
|
348
348
|
// published to a caller (see `ApiErrorCodec.publicMessage`), so the log is the one place it
|
|
349
349
|
// exists at all — dropping it on the caller-error branch would lose it outright.
|
|
350
|
-
// Pluggable classification (
|
|
350
|
+
// Pluggable classification (ApiRegistry): the CallScope ladder of the call's contract class
|
|
351
351
|
// (method, then API, then ALL), else the webpieces built-in — which is side-dependent (a 4xx the SERVER
|
|
352
352
|
// raised is a handled non-failure; the same 4xx a CLIENT receives means its call FAILED; 266 is
|
|
353
353
|
// never a failure either side). This line is FINAL BY POSITION: it only sees what escaped every
|
|
354
354
|
// retry, so a TRANSIENT_FAILURE here was never retried (no strategy, or a WRITE) or gave up —
|
|
355
355
|
// it logs FAIL exactly like a FAILURE. Only EXPECTED is "OTHER / result:'success'".
|
|
356
|
-
const isUser =
|
|
356
|
+
const isUser = ApiRegistry_1.ApiRegistry.classify(error, methodInfo) === ErrorClassifier_1.CallOutcome.EXPECTED;
|
|
357
357
|
stamp(new ApiCallInfo_1.ApiCallInfo(methodInfo, 'response', isUser ? 'success' : 'failure', durationMs, requestSize, undefined, undefined, http?.facts), () => isUser
|
|
358
358
|
? log.warn(`[API-${side}-resp-OTHER] ${id} errorType=${errorType} error=${error.message}`)
|
|
359
359
|
: log.error(`[API-${side}-resp-FAIL] ${id} errorType=${errorType} error=${error.message}`));
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"LogApiCall.js","sourceRoot":"","sources":["../../../../../../packages/core/core-util/src/http/LogApiCall.ts"],"names":[],"mappings":"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","sourcesContent":["import { toError } from '../lib/errorUtils';\nimport { LogManager } from '../logging/LogManager';\nimport { ApiCallInfo, HttpCallFactsHolder } from './ApiCallInfo';\nimport { ApiMethodInfo } from './ApiMethodInfo';\nimport { ApiCallContext } from './ApiCallContext';\nimport { WebpiecesCoreHeaders } from './WebpiecesCoreHeaders';\nimport { LOG_API_CALL_LOGGER_NAME } from './ApiCallLogName';\nimport { ClientRegistry } from './ClientRegistry';\nimport { CallOutcome } from './ErrorClassifier';\n\n// The console backends special-case THIS logger name into a self-describing [API.{side}.{phase}]\n// bracket (see ApiCallLogName) — so the name here and the name they match are the one constant.\nconst log = LogManager.getLogger(LOG_API_CALL_LOGGER_NAME);\n\n/** The slice of node's `process.versions` that runtime detection reads (core-util has no node types). */\nclass NodeVersionsProbe {\n node?: string;\n}\n\n/** The slice of node's `process` that runtime detection reads. */\nclass NodeProcessProbe {\n versions?: NodeVersionsProbe;\n}\n\n/** `globalThis`, as seen by runtime detection: a node `process` may or may not exist. */\nclass NodeGlobalProbe {\n process?: NodeProcessProbe;\n}\n\n/**\n * The startup configuration for {@link LogApiCall.configure}.\n *\n * A class rather than an object literal (CLAUDE.md: data-only structures are classes), so the one call\n * site reads `LogApiCall.configure(new LogApiCallConfig(2000))`.\n */\nexport class LogApiCallConfig {\n /**\n * @param maxLoggedBodyChars - the most characters of a serialized request/response body that one\n * `[API-*]` line may carry. `undefined` keeps the runtime default\n * ({@link LogApiCall.BROWSER_DEFAULT_MAX_BODY_CHARS} / {@link LogApiCall.NODE_DEFAULT_MAX_BODY_CHARS});\n * `0` or `Infinity` mean NO cap, for local debugging.\n */\n constructor(readonly maxLoggedBodyChars?: number) {}\n}\n\n/**\n * Process-wide settings for the `[API-*]` lines {@link LogApiCallImpl} writes — today, how much of a\n * body one line may carry (#1300).\n *\n * WHY a cap: the size of a body is already on the same line as a structured tag\n * (`api.requestSize` / `api.responseSize` / `api.responseCount`, computed on the FULL body), so the body\n * text adds no measurement, only bytes. Unbounded, it blew a consumer's 4,000-char browser message\n * trimmer (a Sentry report per line) and on a server it risks Cloud Logging's 256 KB per-entry limit,\n * where an oversize entry is LOST, not shortened.\n *\n * WHY the default depends on the RUNTIME, not on `methodInfo.side`: a node server is also a CLIENT when\n * it calls another service, and those lines go to Cloud Logging too. The runtime is detected ONCE, at\n * module load, by `process.versions.node` only — never `typeof window`, which React Native defines and\n * some SSR setups fake.\n *\n * A static, process-wide setting in the style of {@link ClientRegistry} / `HeaderRegistry`: set once at\n * startup (an app's `main.ts`, a server's bootstrap), never DI-injected, because the browser-side\n * {@link LogApiCallImpl} is constructed by a transport with no DI container.\n */\nexport class LogApiCall {\n /**\n * Browser and React Native default: well under a 4,000-char shipped-log message cap, leaving room\n * for the `[API-…] Class.method response=` prefix and the truncation marker.\n */\n static readonly BROWSER_DEFAULT_MAX_BODY_CHARS = 2000;\n\n /**\n * Node default: even at a worst-case 4 UTF-8 bytes per char this is ~200 KB, under Cloud Logging's\n * 256 KB per-entry limit with room for the entry's other fields.\n */\n static readonly NODE_DEFAULT_MAX_BODY_CHARS = 50000;\n\n /** The default for the runtime this module was loaded in, decided once. */\n static readonly RUNTIME_DEFAULT_MAX_BODY_CHARS = LogApiCall.defaultMaxBodyChars(\n LogApiCall.isNodeRuntime(),\n );\n\n private static configuredMaxBodyChars: number | undefined;\n\n /**\n * Override the runtime default, once at startup. A config whose `maxLoggedBodyChars` is\n * `undefined` restores the runtime default; `0` or `Infinity` turn the cap off.\n */\n // webpieces-disable no-function-outside-class -- static global singleton (like HeaderRegistry/ClientRegistry); set once at startup, never DI-injected\n static configure(config: LogApiCallConfig): void {\n const max = config.maxLoggedBodyChars;\n if (max !== undefined && (Number.isNaN(max) || max < 0)) {\n throw new Error(\n `LogApiCall.configure: maxLoggedBodyChars must be a non-negative number, 0 or Infinity (no cap), or undefined (runtime default); got ${max}`,\n );\n }\n LogApiCall.configuredMaxBodyChars = max;\n }\n\n /** The cap in force: the configured value, else the runtime default. 0 or Infinity mean no cap. */\n // webpieces-disable no-function-outside-class -- static global singleton (like HeaderRegistry/ClientRegistry); set once at startup, never DI-injected\n static maxLoggedBodyChars(): number {\n return LogApiCall.configuredMaxBodyChars ?? LogApiCall.RUNTIME_DEFAULT_MAX_BODY_CHARS;\n }\n\n /**\n * The default for a runtime: {@link NODE_DEFAULT_MAX_BODY_CHARS} on node, else\n * {@link BROWSER_DEFAULT_MAX_BODY_CHARS} (browser and React Native alike).\n */\n // webpieces-disable no-function-outside-class -- pure static helper of the static LogApiCall settings\n static defaultMaxBodyChars(isNode: boolean): number {\n return isNode\n ? LogApiCall.NODE_DEFAULT_MAX_BODY_CHARS\n : LogApiCall.BROWSER_DEFAULT_MAX_BODY_CHARS;\n }\n\n /**\n * True only when a node `process` with `versions.node` exists. Read through `globalThis` because\n * this browser-safe package compiles with no node types.\n */\n // webpieces-disable no-function-outside-class -- pure static helper of the static LogApiCall settings\n static isNodeRuntime(): boolean {\n const g = globalThis as NodeGlobalProbe;\n return typeof g.process !== 'undefined' && !!g.process.versions?.node;\n }\n\n /**\n * Cut a serialized body to its first `max` chars and append\n * `…[truncated: logged <kept> of <total> chars]`. Run on the ALREADY-MASKED string, so a cut can\n * never reveal a masked field. Never splits a surrogate pair: when the cut would land between the\n * two halves of one code point, that code point is dropped whole. A body within the cap, an\n * undefined body, and a cap of 0 / Infinity are returned unchanged.\n */\n // webpieces-disable no-function-outside-class -- pure static helper of the static LogApiCall settings\n static truncateForLog(body: string | undefined, max: number): string | undefined {\n if (body === undefined || max === 0 || !Number.isFinite(max) || body.length <= max) {\n return body;\n }\n let end = Math.floor(max);\n const last = body.charCodeAt(end - 1);\n if (last >= 0xd800 && last <= 0xdbff) {\n end -= 1; // a high surrogate whose low half would be cut off — drop the pair\n }\n return `${body.slice(0, end)}…[truncated: logged ${end} of ${body.length} chars]`;\n }\n}\n\n/**\n * LogApiCallImpl - Generic API call logging utility, used by BOTH server-side (LogApiFilter) and\n * client-side (ProxyClient) for one consistent logging shape across the framework.\n *\n * TWO things happen around each call:\n * 1. Text lines are emitted (the human-readable `[API-...]` patterns below).\n * 2. A structured {@link ApiCallInfo} tag is stamped into the ambient request context via the\n * {@link ApiCallContext} seam, so EVERY log line emitted during the call (not just the\n * req/resp lines) inherits a filterable `api` object — surfacing in GCP as\n * `jsonPayload.api.{method.{side,apiClass,methodName,controllerName},type,result}`.\n *\n * BROWSER-SAFE: this lives in core-util and runs in the browser bundle (via ProxyClient →\n * BrowserProxyClient), so it MUST NOT import `RequestContext` (Node async_hooks, and a circular dep).\n * It stamps through the {@link ApiCallContext} seam instead, and takes that seam as a REQUIRED\n * CONSTRUCTOR ARGUMENT — there is no process-global holder to install and none to forget. Each\n * environment-specific package constructs its own:\n *\n * LogApiFilter (@webpieces/http-routing) -> new LogApiCallImpl(new RequestContextApiCallContext())\n * NodeProxyClient (@webpieces/http-client-node) -> new LogApiCallImpl(new RequestContextApiCallContext())\n * TaskProxyClient (@webpieces/cloudtasks-client) -> new LogApiCallImpl(new RequestContextApiCallContext())\n * BrowserProxyClient (@webpieces/http-client-browser) -> new LogApiCallImpl(new BrowserApiCallContext())\n *\n * NOT a singleton, deliberately: a shared instance would need a shared context, which is the global\n * this constructor replaced. Construct one where you know which environment you are in.\n *\n * Logging format patterns:\n * - [API-{side}-req] ClassName.methodName request={...}\n * (request and response bodies are capped at {@link LogApiCall.maxLoggedBodyChars} chars, #1300)\n * - [API-{side}-resp-SUCCESS] ClassName.methodName response={...}\n * - [API-{side}-resp-OTHER] ClassName.methodName errorType={...} error={...} (CallOutcome.EXPECTED)\n * - [API-{side}-resp-FAIL] ClassName.methodName error={...} (TRANSIENT_FAILURE or FAILURE — final by position; each retried attempt is logged by CallRegistry as [API-client-attempt-TRANSIENT])\n */\nexport class LogApiCallImpl {\n /**\n * @param ctx - the environment's {@link ApiCallContext}. REQUIRED, with no default: that is what\n * turns \"nobody bootstrapped the context\" into a compile error instead of a throw on the first\n * real call in production.\n */\n constructor(private readonly ctx: ApiCallContext) {}\n\n /**\n * Execute an API call with logging + `api` context-tagging around it.\n *\n * @param methodInfo - The transport-neutral call identity (side, apiClass, methodName,\n * controllerName?). `apiClass` is what matches a client call to its server handler in the logs.\n * @param requestDto - The request DTO (external multi-param callers synthesize a small object)\n * @param method - The method to execute\n *\n * Correlation fields (requestId, tenantId, ...) are NOT stamped here — a logging BACKEND owns that,\n * reading RequestContext on every record. What IS stamped here is the per-call `api` tag, and only\n * for the SYNCHRONOUS span of each log line: set → log → remove. Because the tag is never held across\n * `await method(...)`, a concurrent browser call (single-threaded, one global slot) can never clobber\n * it. Cost: only the `[API-*]` req/resp lines carry `api`, not lines emitted mid-call — which is\n * exactly what the GCP filters (`jsonPayload.api.*`) want.\n */\n public async execute<Q, R>(\n methodInfo: ApiMethodInfo,\n requestDto: Q,\n method: (dto: Q) => Promise<R>,\n responseCount?: (response: R) => number | undefined,\n /** The HTTP client's per-call slot — see {@link HttpCallFactsHolder}. Absent everywhere else. */\n http?: HttpCallFactsHolder,\n ): Promise<R> {\n if (methodInfo.noLogging) {\n return this.executeUnlogged(methodInfo, requestDto, method);\n }\n const side = methodInfo.side;\n const id = `${methodInfo.apiClass}.${methodInfo.methodName}`;\n const stamp = this.stamper(this.activeContext());\n\n // Stringify ONCE and reuse for both the log text and the size — a second JSON.stringify of a\n // large DTO purely to measure it would double the cost of the thing we are measuring.\n // Only take the field-masking hit when this call declared sensitive fields; otherwise the plain\n // JSON.stringify fast path, unchanged for every existing caller.\n const requestBody = this.serialize(requestDto, methodInfo);\n const requestSize = this.byteSize(requestBody);\n // The size above is on the FULL body; only the log TEXT is capped (#1300), and only after\n // serialize() has masked it.\n const maxBodyChars = LogApiCall.maxLoggedBodyChars();\n // Declared out here so the catch below can read it too. Reassigned just before the call, so\n // the number times ONLY the call and not our own request-logging.\n let startMs = Date.now();\n\n // eslint-disable-next-line @webpieces/no-unmanaged-exceptions -- LogApiCall logs errors before re-throwing to caller\n try {\n stamp(new ApiCallInfo(methodInfo, 'request', undefined, undefined, requestSize), () =>\n log.info(\n `[API-${side}-req] ${id} request=${LogApiCall.truncateForLog(requestBody, maxBodyChars)}`,\n ),\n );\n\n if (!requestDto) throw new Error(`Request cannot be null and was from ${id}`);\n\n startMs = Date.now();\n const response = await method(requestDto);\n const durationMs = Date.now() - startMs;\n\n const responseBody = this.serialize(response, methodInfo);\n const count = this.selectResponseCount(response, responseCount, side, id);\n stamp(\n new ApiCallInfo(\n methodInfo,\n 'response',\n 'success',\n durationMs,\n requestSize,\n this.byteSize(responseBody),\n count,\n http?.facts,\n ),\n () =>\n log.info(\n `[API-${side}-resp-SUCCESS] ${id} response=${LogApiCall.truncateForLog(responseBody, maxBodyChars)}`,\n ),\n );\n\n return response;\n } catch (err: unknown) {\n const error = toError(err);\n // Duration comes off the SAME start as the success path, so a slow failure (a timeout, a\n // hung dependency) reports its real cost rather than nothing.\n this.logFailure(error, methodInfo, Date.now() - startMs, requestSize, stamp, http);\n throw err;\n }\n }\n\n /**\n * Build the tag-and-emit span used around every `[API-*]` line.\n *\n * set → emit → remove, as ONE synchronous span: the tag is live only while the logger reads it,\n * never across an await, so a single browser global slot can never be clobbered by a concurrent\n * call. `ctx` is passed in (rather than read from `this`) so the caller has already proven it is\n * ACTIVE before anything is stamped.\n */\n private stamper(ctx: ApiCallContext): (info: ApiCallInfo, emit: () => void) => void {\n const key = WebpiecesCoreHeaders.API_CALL_INFO;\n return (info: ApiCallInfo, emit: () => void): void => {\n ctx.set(key, info);\n // webpieces-disable no-unmanaged-exceptions -- cleanup must run when a logging backend throws\n try {\n emit();\n } finally {\n ctx.remove(key);\n }\n };\n }\n\n /**\n * The `@Endpoint(..., { noLogging: true })` path: run the call and say NOTHING about it.\n *\n * No `[API-*]` line, no {@link ApiCallInfo} stamp, and no serialization of either DTO — a\n * noLogging route is a log shipper or a log-ingest endpoint, so a line describing the call IS the\n * thing being shipped, and emitting it feeds the next batch its own payload (#976).\n *\n * It deliberately does NOT call {@link activeContext}: that guard exists because a call with\n * nowhere to STAMP a tag is a bug, and this path stamps nothing. A noLogging call is therefore\n * legal outside a RequestContext scope, which is exactly where a flusher tends to run.\n *\n * The null-request check is kept, because it is a contract error and not observability — a\n * noLogging route deserves the same error as every other route, and the throw propagates to the caller\n * unlogged here, exactly as the logged path re-throws.\n *\n * `responseCount` is not taken: it feeds only the `api.responseCount` tag, which is not emitted.\n */\n private async executeUnlogged<Q, R>(\n methodInfo: ApiMethodInfo,\n requestDto: Q,\n method: (dto: Q) => Promise<R>,\n ): Promise<R> {\n if (!requestDto) {\n throw new Error(\n `Request cannot be null and was from ${methodInfo.apiClass}.${methodInfo.methodName}`,\n );\n }\n return method(requestDto);\n }\n\n /**\n * Evaluate an opt-in logical-item count without allowing observability code to affect the call.\n * Warnings deliberately contain neither request nor response bodies (nor the invalid value).\n */\n private selectResponseCount<R>(\n response: R,\n selector: ((response: R) => number | undefined) | undefined,\n side: string,\n id: string,\n ): number | undefined {\n if (!selector) {\n return undefined;\n }\n\n // eslint-disable-next-line @webpieces/no-unmanaged-exceptions -- an observability callback must not fail the API call\n try {\n const count = selector(response);\n if (\n count === undefined ||\n (Number.isFinite(count) && Number.isInteger(count) && count >= 0)\n ) {\n return count;\n }\n this.warnInvalidResponseCount(side, id, 'returned an invalid value');\n } catch (err: unknown) {\n const error = toError(err);\n void error;\n this.warnInvalidResponseCount(side, id, 'threw');\n }\n return undefined;\n }\n\n private warnInvalidResponseCount(side: string, id: string, reason: string): void {\n // eslint-disable-next-line @webpieces/no-unmanaged-exceptions -- a warning backend must not replace a successful API response\n try {\n log.warn(\n `[API-${side}-resp-COUNT-WARN] ${id} responseCount selector ${reason}; omitting responseCount`,\n );\n } catch (err: unknown) {\n const error = toError(err);\n void error;\n // The API response is authoritative; observability failures are intentionally ignored here.\n }\n }\n\n /**\n * Serialize a DTO for the LOG LINE ONLY. With no mask on the call, this is a plain JSON.stringify\n * (byte-for-byte the old behavior, no walk) so existing callers pay nothing. With a mask, it runs\n * {@link MaskSpec.stringify}, which produces a masked STRING without ever mutating the DTO — so the\n * object handed to the transport, and thus the value ON THE WIRE, is unchanged.\n */\n private serialize(\n // webpieces-disable no-any-unknown -- DTO types are erased at the api/proxy boundary (matches execute)\n dto: unknown,\n methodInfo: ApiMethodInfo,\n ): string | undefined {\n return methodInfo.mask ? methodInfo.mask.stringify(dto) : JSON.stringify(dto);\n }\n\n /**\n * The ApiCallContext to stamp into. It cannot be MISSING (it is a constructor argument), but it\n * can be INACTIVE — a Node context used outside any `RequestContext.run(...)` scope. That throws:\n * an api call with nowhere to tag is a bug.\n */\n private activeContext(): ApiCallContext {\n const ctx = this.ctx;\n if (!ctx.isActive()) {\n throw new Error(\n 'LogApiCall requires an ACTIVE ApiCallContext. On a Node server, run inside a ' +\n 'RequestContext.run(...) scope — a server filter opens one per request, and a ' +\n 'non-webpieces host must open one around the work that calls a webpieces client. ' +\n '(A BrowserApiCallContext is always active, so this can only be the Node side.)',\n );\n }\n return ctx;\n }\n\n /**\n * Tag + log a thrown call. There is no responseSize — a throw produced no response body to measure.\n */\n private logFailure(\n error: Error,\n methodInfo: ApiMethodInfo,\n durationMs: number,\n requestSize: number | undefined,\n stamp: (info: ApiCallInfo, emit: () => void) => void,\n http: HttpCallFactsHolder | undefined,\n ): void {\n const side = methodInfo.side;\n const id = `${methodInfo.apiClass}.${methodInfo.methodName}`;\n const errorType = error.constructor.name;\n // The MESSAGE is on BOTH branches, and that is load-bearing: this is the ONLY operator line\n // per failure now that `ApiErrorBoundary` writes none. An operator error message is never\n // published to a caller (see `ApiErrorCodec.publicMessage`), so the log is the one place it\n // exists at all — dropping it on the caller-error branch would lose it outright.\n // Pluggable classification (ClientRegistry): the CallScope ladder of the call's contract class\n // (method, then API, then ALL), else the webpieces built-in — which is side-dependent (a 4xx the SERVER\n // raised is a handled non-failure; the same 4xx a CLIENT receives means its call FAILED; 266 is\n // never a failure either side). This line is FINAL BY POSITION: it only sees what escaped every\n // retry, so a TRANSIENT_FAILURE here was never retried (no strategy, or a WRITE) or gave up —\n // it logs FAIL exactly like a FAILURE. Only EXPECTED is \"OTHER / result:'success'\".\n const isUser = ClientRegistry.classify(error, methodInfo) === CallOutcome.EXPECTED;\n\n stamp(\n new ApiCallInfo(\n methodInfo,\n 'response',\n isUser ? 'success' : 'failure',\n durationMs,\n requestSize,\n undefined,\n undefined,\n http?.facts,\n ),\n () =>\n isUser\n ? log.warn(\n `[API-${side}-resp-OTHER] ${id} errorType=${errorType} error=${error.message}`,\n )\n : log.error(\n `[API-${side}-resp-FAIL] ${id} errorType=${errorType} error=${error.message}`,\n ),\n );\n }\n\n /**\n * UTF-8 byte size without platform-specific encoding globals: LogApiCall runs in the\n * browser bundle. Undefined in, undefined out — a `Promise<void>` method has no body to measure,\n * and a 0 there would be a lie (JSON.stringify(undefined) returns undefined, not '').\n */\n private byteSize(serialized: string | undefined): number | undefined {\n if (serialized === undefined) {\n return undefined;\n }\n let bytes = 0;\n for (const character of serialized) {\n const code = character.codePointAt(0)!;\n bytes += code <= 0x7f ? 1 : code <= 0x7ff ? 2 : code <= 0xffff ? 3 : 4;\n }\n return bytes;\n }\n}\n"]}
|
|
1
|
+
{"version":3,"file":"LogApiCall.js","sourceRoot":"","sources":["../../../../../../packages/core/core-util/src/http/LogApiCall.ts"],"names":[],"mappings":";;;AAAA,kDAA4C;AAC5C,sDAAmD;AACnD,+CAAiE;AAGjE,iEAA8D;AAC9D,qDAA4D;AAC5D,+CAA4C;AAC5C,uDAAgD;AAEhD,iGAAiG;AACjG,gGAAgG;AAChG,MAAM,GAAG,GAAG,uBAAU,CAAC,SAAS,CAAC,yCAAwB,CAAC,CAAC;AAE3D,yGAAyG;AACzG,MAAM,iBAAiB;IACnB,IAAI,CAAU;CACjB;AAED,kEAAkE;AAClE,MAAM,gBAAgB;IAClB,QAAQ,CAAqB;CAChC;AAED,yFAAyF;AACzF,MAAM,eAAe;IACjB,OAAO,CAAoB;CAC9B;AAED;;;;;GAKG;AACH,MAAa,gBAAgB;IAOJ;IANrB;;;;;OAKG;IACH,YAAqB,kBAA2B;QAA3B,uBAAkB,GAAlB,kBAAkB,CAAS;IAAG,CAAC;CACvD;AARD,4CAQC;AAED;;;;;;;;;;;;;;;;;;GAkBG;AACH,MAAa,UAAU;IACnB;;;OAGG;IACH,MAAM,CAAU,8BAA8B,GAAG,IAAI,CAAC;IAEtD;;;OAGG;IACH,MAAM,CAAU,2BAA2B,GAAG,KAAK,CAAC;IAEpD,2EAA2E;IAC3E,MAAM,CAAU,8BAA8B,GAAG,UAAU,CAAC,mBAAmB,CAC3E,UAAU,CAAC,aAAa,EAAE,CAC7B,CAAC;IAEM,MAAM,CAAC,sBAAsB,CAAqB;IAE1D;;;OAGG;IACH,sJAAsJ;IACtJ,MAAM,CAAC,SAAS,CAAC,MAAwB;QACrC,MAAM,GAAG,GAAG,MAAM,CAAC,kBAAkB,CAAC;QACtC,IAAI,GAAG,KAAK,SAAS,IAAI,CAAC,MAAM,CAAC,KAAK,CAAC,GAAG,CAAC,IAAI,GAAG,GAAG,CAAC,CAAC,EAAE,CAAC;YACtD,MAAM,IAAI,KAAK,CACX,uIAAuI,GAAG,EAAE,CAC/I,CAAC;QACN,CAAC;QACD,UAAU,CAAC,sBAAsB,GAAG,GAAG,CAAC;IAC5C,CAAC;IAED,mGAAmG;IACnG,sJAAsJ;IACtJ,MAAM,CAAC,kBAAkB;QACrB,OAAO,UAAU,CAAC,sBAAsB,IAAI,UAAU,CAAC,8BAA8B,CAAC;IAC1F,CAAC;IAED;;;OAGG;IACH,sGAAsG;IACtG,MAAM,CAAC,mBAAmB,CAAC,MAAe;QACtC,OAAO,MAAM;YACT,CAAC,CAAC,UAAU,CAAC,2BAA2B;YACxC,CAAC,CAAC,UAAU,CAAC,8BAA8B,CAAC;IACpD,CAAC;IAED;;;OAGG;IACH,sGAAsG;IACtG,MAAM,CAAC,aAAa;QAChB,MAAM,CAAC,GAAG,UAA6B,CAAC;QACxC,OAAO,OAAO,CAAC,CAAC,OAAO,KAAK,WAAW,IAAI,CAAC,CAAC,CAAC,CAAC,OAAO,CAAC,QAAQ,EAAE,IAAI,CAAC;IAC1E,CAAC;IAED;;;;;;OAMG;IACH,sGAAsG;IACtG,MAAM,CAAC,cAAc,CAAC,IAAwB,EAAE,GAAW;QACvD,IAAI,IAAI,KAAK,SAAS,IAAI,GAAG,KAAK,CAAC,IAAI,CAAC,MAAM,CAAC,QAAQ,CAAC,GAAG,CAAC,IAAI,IAAI,CAAC,MAAM,IAAI,GAAG,EAAE,CAAC;YACjF,OAAO,IAAI,CAAC;QAChB,CAAC;QACD,IAAI,GAAG,GAAG,IAAI,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC;QAC1B,MAAM,IAAI,GAAG,IAAI,CAAC,UAAU,CAAC,GAAG,GAAG,CAAC,CAAC,CAAC;QACtC,IAAI,IAAI,IAAI,MAAM,IAAI,IAAI,IAAI,MAAM,EAAE,CAAC;YACnC,GAAG,IAAI,CAAC,CAAC,CAAC,mEAAmE;QACjF,CAAC;QACD,OAAO,GAAG,IAAI,CAAC,KAAK,CAAC,CAAC,EAAE,GAAG,CAAC,uBAAuB,GAAG,OAAO,IAAI,CAAC,MAAM,SAAS,CAAC;IACtF,CAAC;;AAhFL,gCAiFC;AAED;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA+BG;AACH,MAAa,cAAc;IAMM;IAL7B;;;;OAIG;IACH,YAA6B,GAAmB;QAAnB,QAAG,GAAH,GAAG,CAAgB;IAAG,CAAC;IAEpD;;;;;;;;;;;;;;OAcG;IACI,KAAK,CAAC,OAAO,CAChB,UAAyB,EACzB,UAAa,EACb,MAA8B,EAC9B,aAAmD;IACnD,iGAAiG;IACjG,IAA0B;QAE1B,IAAI,UAAU,CAAC,SAAS,EAAE,CAAC;YACvB,OAAO,IAAI,CAAC,eAAe,CAAC,UAAU,EAAE,UAAU,EAAE,MAAM,CAAC,CAAC;QAChE,CAAC;QACD,MAAM,IAAI,GAAG,UAAU,CAAC,IAAI,CAAC;QAC7B,MAAM,EAAE,GAAG,GAAG,UAAU,CAAC,QAAQ,IAAI,UAAU,CAAC,UAAU,EAAE,CAAC;QAC7D,MAAM,KAAK,GAAG,IAAI,CAAC,OAAO,CAAC,IAAI,CAAC,aAAa,EAAE,CAAC,CAAC;QAEjD,6FAA6F;QAC7F,sFAAsF;QACtF,gGAAgG;QAChG,iEAAiE;QACjE,MAAM,WAAW,GAAG,IAAI,CAAC,SAAS,CAAC,UAAU,EAAE,UAAU,CAAC,CAAC;QAC3D,MAAM,WAAW,GAAG,IAAI,CAAC,QAAQ,CAAC,WAAW,CAAC,CAAC;QAC/C,0FAA0F;QAC1F,6BAA6B;QAC7B,MAAM,YAAY,GAAG,UAAU,CAAC,kBAAkB,EAAE,CAAC;QACrD,4FAA4F;QAC5F,kEAAkE;QAClE,IAAI,OAAO,GAAG,IAAI,CAAC,GAAG,EAAE,CAAC;QAEzB,qHAAqH;QACrH,IAAI,CAAC;YACD,KAAK,CAAC,IAAI,yBAAW,CAAC,UAAU,EAAE,SAAS,EAAE,SAAS,EAAE,SAAS,EAAE,WAAW,CAAC,EAAE,GAAG,EAAE,CAClF,GAAG,CAAC,IAAI,CACJ,QAAQ,IAAI,SAAS,EAAE,YAAY,UAAU,CAAC,cAAc,CAAC,WAAW,EAAE,YAAY,CAAC,EAAE,CAC5F,CACJ,CAAC;YAEF,IAAI,CAAC,UAAU;gBAAE,MAAM,IAAI,KAAK,CAAC,uCAAuC,EAAE,EAAE,CAAC,CAAC;YAE9E,OAAO,GAAG,IAAI,CAAC,GAAG,EAAE,CAAC;YACrB,MAAM,QAAQ,GAAG,MAAM,MAAM,CAAC,UAAU,CAAC,CAAC;YAC1C,MAAM,UAAU,GAAG,IAAI,CAAC,GAAG,EAAE,GAAG,OAAO,CAAC;YAExC,MAAM,YAAY,GAAG,IAAI,CAAC,SAAS,CAAC,QAAQ,EAAE,UAAU,CAAC,CAAC;YAC1D,MAAM,KAAK,GAAG,IAAI,CAAC,mBAAmB,CAAC,QAAQ,EAAE,aAAa,EAAE,IAAI,EAAE,EAAE,CAAC,CAAC;YAC1E,KAAK,CACD,IAAI,yBAAW,CACX,UAAU,EACV,UAAU,EACV,SAAS,EACT,UAAU,EACV,WAAW,EACX,IAAI,CAAC,QAAQ,CAAC,YAAY,CAAC,EAC3B,KAAK,EACL,IAAI,EAAE,KAAK,CACd,EACD,GAAG,EAAE,CACD,GAAG,CAAC,IAAI,CACJ,QAAQ,IAAI,kBAAkB,EAAE,aAAa,UAAU,CAAC,cAAc,CAAC,YAAY,EAAE,YAAY,CAAC,EAAE,CACvG,CACR,CAAC;YAEF,OAAO,QAAQ,CAAC;QACpB,CAAC;QAAC,OAAO,GAAY,EAAE,CAAC;YACpB,MAAM,KAAK,GAAG,IAAA,oBAAO,EAAC,GAAG,CAAC,CAAC;YAC3B,yFAAyF;YACzF,8DAA8D;YAC9D,IAAI,CAAC,UAAU,CAAC,KAAK,EAAE,UAAU,EAAE,IAAI,CAAC,GAAG,EAAE,GAAG,OAAO,EAAE,WAAW,EAAE,KAAK,EAAE,IAAI,CAAC,CAAC;YACnF,MAAM,GAAG,CAAC;QACd,CAAC;IACL,CAAC;IAED;;;;;;;OAOG;IACK,OAAO,CAAC,GAAmB;QAC/B,MAAM,GAAG,GAAG,2CAAoB,CAAC,aAAa,CAAC;QAC/C,OAAO,CAAC,IAAiB,EAAE,IAAgB,EAAQ,EAAE;YACjD,GAAG,CAAC,GAAG,CAAC,GAAG,EAAE,IAAI,CAAC,CAAC;YACnB,8FAA8F;YAC9F,IAAI,CAAC;gBACD,IAAI,EAAE,CAAC;YACX,CAAC;oBAAS,CAAC;gBACP,GAAG,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC;YACpB,CAAC;QACL,CAAC,CAAC;IACN,CAAC;IAED;;;;;;;;;;;;;;;;OAgBG;IACK,KAAK,CAAC,eAAe,CACzB,UAAyB,EACzB,UAAa,EACb,MAA8B;QAE9B,IAAI,CAAC,UAAU,EAAE,CAAC;YACd,MAAM,IAAI,KAAK,CACX,uCAAuC,UAAU,CAAC,QAAQ,IAAI,UAAU,CAAC,UAAU,EAAE,CACxF,CAAC;QACN,CAAC;QACD,OAAO,MAAM,CAAC,UAAU,CAAC,CAAC;IAC9B,CAAC;IAED;;;OAGG;IACK,mBAAmB,CACvB,QAAW,EACX,QAA2D,EAC3D,IAAY,EACZ,EAAU;QAEV,IAAI,CAAC,QAAQ,EAAE,CAAC;YACZ,OAAO,SAAS,CAAC;QACrB,CAAC;QAED,sHAAsH;QACtH,IAAI,CAAC;YACD,MAAM,KAAK,GAAG,QAAQ,CAAC,QAAQ,CAAC,CAAC;YACjC,IACI,KAAK,KAAK,SAAS;gBACnB,CAAC,MAAM,CAAC,QAAQ,CAAC,KAAK,CAAC,IAAI,MAAM,CAAC,SAAS,CAAC,KAAK,CAAC,IAAI,KAAK,IAAI,CAAC,CAAC,EACnE,CAAC;gBACC,OAAO,KAAK,CAAC;YACjB,CAAC;YACD,IAAI,CAAC,wBAAwB,CAAC,IAAI,EAAE,EAAE,EAAE,2BAA2B,CAAC,CAAC;QACzE,CAAC;QAAC,OAAO,GAAY,EAAE,CAAC;YACpB,MAAM,KAAK,GAAG,IAAA,oBAAO,EAAC,GAAG,CAAC,CAAC;YAC3B,KAAK,KAAK,CAAC;YACX,IAAI,CAAC,wBAAwB,CAAC,IAAI,EAAE,EAAE,EAAE,OAAO,CAAC,CAAC;QACrD,CAAC;QACD,OAAO,SAAS,CAAC;IACrB,CAAC;IAEO,wBAAwB,CAAC,IAAY,EAAE,EAAU,EAAE,MAAc;QACrE,8HAA8H;QAC9H,IAAI,CAAC;YACD,GAAG,CAAC,IAAI,CACJ,QAAQ,IAAI,qBAAqB,EAAE,2BAA2B,MAAM,0BAA0B,CACjG,CAAC;QACN,CAAC;QAAC,OAAO,GAAY,EAAE,CAAC;YACpB,MAAM,KAAK,GAAG,IAAA,oBAAO,EAAC,GAAG,CAAC,CAAC;YAC3B,KAAK,KAAK,CAAC;YACX,4FAA4F;QAChG,CAAC;IACL,CAAC;IAED;;;;;OAKG;IACK,SAAS;IACb,uGAAuG;IACvG,GAAY,EACZ,UAAyB;QAEzB,OAAO,UAAU,CAAC,IAAI,CAAC,CAAC,CAAC,UAAU,CAAC,IAAI,CAAC,SAAS,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC,SAAS,CAAC,GAAG,CAAC,CAAC;IAClF,CAAC;IAED;;;;OAIG;IACK,aAAa;QACjB,MAAM,GAAG,GAAG,IAAI,CAAC,GAAG,CAAC;QACrB,IAAI,CAAC,GAAG,CAAC,QAAQ,EAAE,EAAE,CAAC;YAClB,MAAM,IAAI,KAAK,CACX,+EAA+E;gBAC3E,+EAA+E;gBAC/E,kFAAkF;gBAClF,gFAAgF,CACvF,CAAC;QACN,CAAC;QACD,OAAO,GAAG,CAAC;IACf,CAAC;IAED;;OAEG;IACK,UAAU,CACd,KAAY,EACZ,UAAyB,EACzB,UAAkB,EAClB,WAA+B,EAC/B,KAAoD,EACpD,IAAqC;QAErC,MAAM,IAAI,GAAG,UAAU,CAAC,IAAI,CAAC;QAC7B,MAAM,EAAE,GAAG,GAAG,UAAU,CAAC,QAAQ,IAAI,UAAU,CAAC,UAAU,EAAE,CAAC;QAC7D,MAAM,SAAS,GAAG,KAAK,CAAC,WAAW,CAAC,IAAI,CAAC;QACzC,4FAA4F;QAC5F,0FAA0F;QAC1F,4FAA4F;QAC5F,iFAAiF;QACjF,4FAA4F;QAC5F,wGAAwG;QACxG,gGAAgG;QAChG,gGAAgG;QAChG,8FAA8F;QAC9F,oFAAoF;QACpF,MAAM,MAAM,GAAG,yBAAW,CAAC,QAAQ,CAAC,KAAK,EAAE,UAAU,CAAC,KAAK,6BAAW,CAAC,QAAQ,CAAC;QAEhF,KAAK,CACD,IAAI,yBAAW,CACX,UAAU,EACV,UAAU,EACV,MAAM,CAAC,CAAC,CAAC,SAAS,CAAC,CAAC,CAAC,SAAS,EAC9B,UAAU,EACV,WAAW,EACX,SAAS,EACT,SAAS,EACT,IAAI,EAAE,KAAK,CACd,EACD,GAAG,EAAE,CACD,MAAM;YACF,CAAC,CAAC,GAAG,CAAC,IAAI,CACJ,QAAQ,IAAI,gBAAgB,EAAE,cAAc,SAAS,UAAU,KAAK,CAAC,OAAO,EAAE,CACjF;YACH,CAAC,CAAC,GAAG,CAAC,KAAK,CACL,QAAQ,IAAI,eAAe,EAAE,cAAc,SAAS,UAAU,KAAK,CAAC,OAAO,EAAE,CAChF,CACd,CAAC;IACN,CAAC;IAED;;;;OAIG;IACK,QAAQ,CAAC,UAA8B;QAC3C,IAAI,UAAU,KAAK,SAAS,EAAE,CAAC;YAC3B,OAAO,SAAS,CAAC;QACrB,CAAC;QACD,IAAI,KAAK,GAAG,CAAC,CAAC;QACd,KAAK,MAAM,SAAS,IAAI,UAAU,EAAE,CAAC;YACjC,MAAM,IAAI,GAAG,SAAS,CAAC,WAAW,CAAC,CAAC,CAAE,CAAC;YACvC,KAAK,IAAI,IAAI,IAAI,IAAI,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,IAAI,IAAI,KAAK,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,IAAI,IAAI,MAAM,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC;QAC3E,CAAC;QACD,OAAO,KAAK,CAAC;IACjB,CAAC;CACJ;AA9RD,wCA8RC","sourcesContent":["import { toError } from '../lib/errorUtils';\nimport { LogManager } from '../logging/LogManager';\nimport { ApiCallInfo, HttpCallFactsHolder } from './ApiCallInfo';\nimport { ApiMethodInfo } from './ApiMethodInfo';\nimport { ApiCallContext } from './ApiCallContext';\nimport { WebpiecesCoreHeaders } from './WebpiecesCoreHeaders';\nimport { LOG_API_CALL_LOGGER_NAME } from './ApiCallLogName';\nimport { ApiRegistry } from './ApiRegistry';\nimport { CallOutcome } from './ErrorClassifier';\n\n// The console backends special-case THIS logger name into a self-describing [API.{side}.{phase}]\n// bracket (see ApiCallLogName) — so the name here and the name they match are the one constant.\nconst log = LogManager.getLogger(LOG_API_CALL_LOGGER_NAME);\n\n/** The slice of node's `process.versions` that runtime detection reads (core-util has no node types). */\nclass NodeVersionsProbe {\n node?: string;\n}\n\n/** The slice of node's `process` that runtime detection reads. */\nclass NodeProcessProbe {\n versions?: NodeVersionsProbe;\n}\n\n/** `globalThis`, as seen by runtime detection: a node `process` may or may not exist. */\nclass NodeGlobalProbe {\n process?: NodeProcessProbe;\n}\n\n/**\n * The startup configuration for {@link LogApiCall.configure}.\n *\n * A class rather than an object literal (CLAUDE.md: data-only structures are classes), so the one call\n * site reads `LogApiCall.configure(new LogApiCallConfig(2000))`.\n */\nexport class LogApiCallConfig {\n /**\n * @param maxLoggedBodyChars - the most characters of a serialized request/response body that one\n * `[API-*]` line may carry. `undefined` keeps the runtime default\n * ({@link LogApiCall.BROWSER_DEFAULT_MAX_BODY_CHARS} / {@link LogApiCall.NODE_DEFAULT_MAX_BODY_CHARS});\n * `0` or `Infinity` mean NO cap, for local debugging.\n */\n constructor(readonly maxLoggedBodyChars?: number) {}\n}\n\n/**\n * Process-wide settings for the `[API-*]` lines {@link LogApiCallImpl} writes — today, how much of a\n * body one line may carry (#1300).\n *\n * WHY a cap: the size of a body is already on the same line as a structured tag\n * (`api.requestSize` / `api.responseSize` / `api.responseCount`, computed on the FULL body), so the body\n * text adds no measurement, only bytes. Unbounded, it blew a consumer's 4,000-char browser message\n * trimmer (a Sentry report per line) and on a server it risks Cloud Logging's 256 KB per-entry limit,\n * where an oversize entry is LOST, not shortened.\n *\n * WHY the default depends on the RUNTIME, not on `methodInfo.side`: a node server is also a CLIENT when\n * it calls another service, and those lines go to Cloud Logging too. The runtime is detected ONCE, at\n * module load, by `process.versions.node` only — never `typeof window`, which React Native defines and\n * some SSR setups fake.\n *\n * A static, process-wide setting in the style of {@link ClientRegistry} / `HeaderRegistry`: set once at\n * startup (an app's `main.ts`, a server's bootstrap), never DI-injected, because the browser-side\n * {@link LogApiCallImpl} is constructed by a transport with no DI container.\n */\nexport class LogApiCall {\n /**\n * Browser and React Native default: well under a 4,000-char shipped-log message cap, leaving room\n * for the `[API-…] Class.method response=` prefix and the truncation marker.\n */\n static readonly BROWSER_DEFAULT_MAX_BODY_CHARS = 2000;\n\n /**\n * Node default: even at a worst-case 4 UTF-8 bytes per char this is ~200 KB, under Cloud Logging's\n * 256 KB per-entry limit with room for the entry's other fields.\n */\n static readonly NODE_DEFAULT_MAX_BODY_CHARS = 50000;\n\n /** The default for the runtime this module was loaded in, decided once. */\n static readonly RUNTIME_DEFAULT_MAX_BODY_CHARS = LogApiCall.defaultMaxBodyChars(\n LogApiCall.isNodeRuntime(),\n );\n\n private static configuredMaxBodyChars: number | undefined;\n\n /**\n * Override the runtime default, once at startup. A config whose `maxLoggedBodyChars` is\n * `undefined` restores the runtime default; `0` or `Infinity` turn the cap off.\n */\n // webpieces-disable no-function-outside-class -- static global singleton (like HeaderRegistry/ClientRegistry); set once at startup, never DI-injected\n static configure(config: LogApiCallConfig): void {\n const max = config.maxLoggedBodyChars;\n if (max !== undefined && (Number.isNaN(max) || max < 0)) {\n throw new Error(\n `LogApiCall.configure: maxLoggedBodyChars must be a non-negative number, 0 or Infinity (no cap), or undefined (runtime default); got ${max}`,\n );\n }\n LogApiCall.configuredMaxBodyChars = max;\n }\n\n /** The cap in force: the configured value, else the runtime default. 0 or Infinity mean no cap. */\n // webpieces-disable no-function-outside-class -- static global singleton (like HeaderRegistry/ClientRegistry); set once at startup, never DI-injected\n static maxLoggedBodyChars(): number {\n return LogApiCall.configuredMaxBodyChars ?? LogApiCall.RUNTIME_DEFAULT_MAX_BODY_CHARS;\n }\n\n /**\n * The default for a runtime: {@link NODE_DEFAULT_MAX_BODY_CHARS} on node, else\n * {@link BROWSER_DEFAULT_MAX_BODY_CHARS} (browser and React Native alike).\n */\n // webpieces-disable no-function-outside-class -- pure static helper of the static LogApiCall settings\n static defaultMaxBodyChars(isNode: boolean): number {\n return isNode\n ? LogApiCall.NODE_DEFAULT_MAX_BODY_CHARS\n : LogApiCall.BROWSER_DEFAULT_MAX_BODY_CHARS;\n }\n\n /**\n * True only when a node `process` with `versions.node` exists. Read through `globalThis` because\n * this browser-safe package compiles with no node types.\n */\n // webpieces-disable no-function-outside-class -- pure static helper of the static LogApiCall settings\n static isNodeRuntime(): boolean {\n const g = globalThis as NodeGlobalProbe;\n return typeof g.process !== 'undefined' && !!g.process.versions?.node;\n }\n\n /**\n * Cut a serialized body to its first `max` chars and append\n * `…[truncated: logged <kept> of <total> chars]`. Run on the ALREADY-MASKED string, so a cut can\n * never reveal a masked field. Never splits a surrogate pair: when the cut would land between the\n * two halves of one code point, that code point is dropped whole. A body within the cap, an\n * undefined body, and a cap of 0 / Infinity are returned unchanged.\n */\n // webpieces-disable no-function-outside-class -- pure static helper of the static LogApiCall settings\n static truncateForLog(body: string | undefined, max: number): string | undefined {\n if (body === undefined || max === 0 || !Number.isFinite(max) || body.length <= max) {\n return body;\n }\n let end = Math.floor(max);\n const last = body.charCodeAt(end - 1);\n if (last >= 0xd800 && last <= 0xdbff) {\n end -= 1; // a high surrogate whose low half would be cut off — drop the pair\n }\n return `${body.slice(0, end)}…[truncated: logged ${end} of ${body.length} chars]`;\n }\n}\n\n/**\n * LogApiCallImpl - Generic API call logging utility, used by BOTH server-side (LogApiFilter) and\n * client-side (ProxyClient) for one consistent logging shape across the framework.\n *\n * TWO things happen around each call:\n * 1. Text lines are emitted (the human-readable `[API-...]` patterns below).\n * 2. A structured {@link ApiCallInfo} tag is stamped into the ambient request context via the\n * {@link ApiCallContext} seam, so EVERY log line emitted during the call (not just the\n * req/resp lines) inherits a filterable `api` object — surfacing in GCP as\n * `jsonPayload.api.{method.{side,apiClass,methodName,controllerName},type,result}`.\n *\n * BROWSER-SAFE: this lives in core-util and runs in the browser bundle (via ProxyClient →\n * BrowserProxyClient), so it MUST NOT import `RequestContext` (Node async_hooks, and a circular dep).\n * It stamps through the {@link ApiCallContext} seam instead, and takes that seam as a REQUIRED\n * CONSTRUCTOR ARGUMENT — there is no process-global holder to install and none to forget. Each\n * environment-specific package constructs its own:\n *\n * LogApiFilter (@webpieces/http-routing) -> new LogApiCallImpl(new RequestContextApiCallContext())\n * NodeProxyClient (@webpieces/http-client-node) -> new LogApiCallImpl(new RequestContextApiCallContext())\n * TaskProxyClient (@webpieces/cloudtasks-client) -> new LogApiCallImpl(new RequestContextApiCallContext())\n * BrowserProxyClient (@webpieces/http-client-browser) -> new LogApiCallImpl(new BrowserApiCallContext())\n *\n * NOT a singleton, deliberately: a shared instance would need a shared context, which is the global\n * this constructor replaced. Construct one where you know which environment you are in.\n *\n * Logging format patterns:\n * - [API-{side}-req] ClassName.methodName request={...}\n * (request and response bodies are capped at {@link LogApiCall.maxLoggedBodyChars} chars, #1300)\n * - [API-{side}-resp-SUCCESS] ClassName.methodName response={...}\n * - [API-{side}-resp-OTHER] ClassName.methodName errorType={...} error={...} (CallOutcome.EXPECTED)\n * - [API-{side}-resp-FAIL] ClassName.methodName error={...} (TRANSIENT_FAILURE or FAILURE — final by position; each retried attempt is logged by ClientRegistry as [API-client-attempt-TRANSIENT])\n */\nexport class LogApiCallImpl {\n /**\n * @param ctx - the environment's {@link ApiCallContext}. REQUIRED, with no default: that is what\n * turns \"nobody bootstrapped the context\" into a compile error instead of a throw on the first\n * real call in production.\n */\n constructor(private readonly ctx: ApiCallContext) {}\n\n /**\n * Execute an API call with logging + `api` context-tagging around it.\n *\n * @param methodInfo - The transport-neutral call identity (side, apiClass, methodName,\n * controllerName?). `apiClass` is what matches a client call to its server handler in the logs.\n * @param requestDto - The request DTO (external multi-param callers synthesize a small object)\n * @param method - The method to execute\n *\n * Correlation fields (requestId, tenantId, ...) are NOT stamped here — a logging BACKEND owns that,\n * reading RequestContext on every record. What IS stamped here is the per-call `api` tag, and only\n * for the SYNCHRONOUS span of each log line: set → log → remove. Because the tag is never held across\n * `await method(...)`, a concurrent browser call (single-threaded, one global slot) can never clobber\n * it. Cost: only the `[API-*]` req/resp lines carry `api`, not lines emitted mid-call — which is\n * exactly what the GCP filters (`jsonPayload.api.*`) want.\n */\n public async execute<Q, R>(\n methodInfo: ApiMethodInfo,\n requestDto: Q,\n method: (dto: Q) => Promise<R>,\n responseCount?: (response: R) => number | undefined,\n /** The HTTP client's per-call slot — see {@link HttpCallFactsHolder}. Absent everywhere else. */\n http?: HttpCallFactsHolder,\n ): Promise<R> {\n if (methodInfo.noLogging) {\n return this.executeUnlogged(methodInfo, requestDto, method);\n }\n const side = methodInfo.side;\n const id = `${methodInfo.apiClass}.${methodInfo.methodName}`;\n const stamp = this.stamper(this.activeContext());\n\n // Stringify ONCE and reuse for both the log text and the size — a second JSON.stringify of a\n // large DTO purely to measure it would double the cost of the thing we are measuring.\n // Only take the field-masking hit when this call declared sensitive fields; otherwise the plain\n // JSON.stringify fast path, unchanged for every existing caller.\n const requestBody = this.serialize(requestDto, methodInfo);\n const requestSize = this.byteSize(requestBody);\n // The size above is on the FULL body; only the log TEXT is capped (#1300), and only after\n // serialize() has masked it.\n const maxBodyChars = LogApiCall.maxLoggedBodyChars();\n // Declared out here so the catch below can read it too. Reassigned just before the call, so\n // the number times ONLY the call and not our own request-logging.\n let startMs = Date.now();\n\n // eslint-disable-next-line @webpieces/no-unmanaged-exceptions -- LogApiCall logs errors before re-throwing to caller\n try {\n stamp(new ApiCallInfo(methodInfo, 'request', undefined, undefined, requestSize), () =>\n log.info(\n `[API-${side}-req] ${id} request=${LogApiCall.truncateForLog(requestBody, maxBodyChars)}`,\n ),\n );\n\n if (!requestDto) throw new Error(`Request cannot be null and was from ${id}`);\n\n startMs = Date.now();\n const response = await method(requestDto);\n const durationMs = Date.now() - startMs;\n\n const responseBody = this.serialize(response, methodInfo);\n const count = this.selectResponseCount(response, responseCount, side, id);\n stamp(\n new ApiCallInfo(\n methodInfo,\n 'response',\n 'success',\n durationMs,\n requestSize,\n this.byteSize(responseBody),\n count,\n http?.facts,\n ),\n () =>\n log.info(\n `[API-${side}-resp-SUCCESS] ${id} response=${LogApiCall.truncateForLog(responseBody, maxBodyChars)}`,\n ),\n );\n\n return response;\n } catch (err: unknown) {\n const error = toError(err);\n // Duration comes off the SAME start as the success path, so a slow failure (a timeout, a\n // hung dependency) reports its real cost rather than nothing.\n this.logFailure(error, methodInfo, Date.now() - startMs, requestSize, stamp, http);\n throw err;\n }\n }\n\n /**\n * Build the tag-and-emit span used around every `[API-*]` line.\n *\n * set → emit → remove, as ONE synchronous span: the tag is live only while the logger reads it,\n * never across an await, so a single browser global slot can never be clobbered by a concurrent\n * call. `ctx` is passed in (rather than read from `this`) so the caller has already proven it is\n * ACTIVE before anything is stamped.\n */\n private stamper(ctx: ApiCallContext): (info: ApiCallInfo, emit: () => void) => void {\n const key = WebpiecesCoreHeaders.API_CALL_INFO;\n return (info: ApiCallInfo, emit: () => void): void => {\n ctx.set(key, info);\n // webpieces-disable no-unmanaged-exceptions -- cleanup must run when a logging backend throws\n try {\n emit();\n } finally {\n ctx.remove(key);\n }\n };\n }\n\n /**\n * The `@Endpoint(..., { noLogging: true })` path: run the call and say NOTHING about it.\n *\n * No `[API-*]` line, no {@link ApiCallInfo} stamp, and no serialization of either DTO — a\n * noLogging route is a log shipper or a log-ingest endpoint, so a line describing the call IS the\n * thing being shipped, and emitting it feeds the next batch its own payload (#976).\n *\n * It deliberately does NOT call {@link activeContext}: that guard exists because a call with\n * nowhere to STAMP a tag is a bug, and this path stamps nothing. A noLogging call is therefore\n * legal outside a RequestContext scope, which is exactly where a flusher tends to run.\n *\n * The null-request check is kept, because it is a contract error and not observability — a\n * noLogging route deserves the same error as every other route, and the throw propagates to the caller\n * unlogged here, exactly as the logged path re-throws.\n *\n * `responseCount` is not taken: it feeds only the `api.responseCount` tag, which is not emitted.\n */\n private async executeUnlogged<Q, R>(\n methodInfo: ApiMethodInfo,\n requestDto: Q,\n method: (dto: Q) => Promise<R>,\n ): Promise<R> {\n if (!requestDto) {\n throw new Error(\n `Request cannot be null and was from ${methodInfo.apiClass}.${methodInfo.methodName}`,\n );\n }\n return method(requestDto);\n }\n\n /**\n * Evaluate an opt-in logical-item count without allowing observability code to affect the call.\n * Warnings deliberately contain neither request nor response bodies (nor the invalid value).\n */\n private selectResponseCount<R>(\n response: R,\n selector: ((response: R) => number | undefined) | undefined,\n side: string,\n id: string,\n ): number | undefined {\n if (!selector) {\n return undefined;\n }\n\n // eslint-disable-next-line @webpieces/no-unmanaged-exceptions -- an observability callback must not fail the API call\n try {\n const count = selector(response);\n if (\n count === undefined ||\n (Number.isFinite(count) && Number.isInteger(count) && count >= 0)\n ) {\n return count;\n }\n this.warnInvalidResponseCount(side, id, 'returned an invalid value');\n } catch (err: unknown) {\n const error = toError(err);\n void error;\n this.warnInvalidResponseCount(side, id, 'threw');\n }\n return undefined;\n }\n\n private warnInvalidResponseCount(side: string, id: string, reason: string): void {\n // eslint-disable-next-line @webpieces/no-unmanaged-exceptions -- a warning backend must not replace a successful API response\n try {\n log.warn(\n `[API-${side}-resp-COUNT-WARN] ${id} responseCount selector ${reason}; omitting responseCount`,\n );\n } catch (err: unknown) {\n const error = toError(err);\n void error;\n // The API response is authoritative; observability failures are intentionally ignored here.\n }\n }\n\n /**\n * Serialize a DTO for the LOG LINE ONLY. With no mask on the call, this is a plain JSON.stringify\n * (byte-for-byte the old behavior, no walk) so existing callers pay nothing. With a mask, it runs\n * {@link MaskSpec.stringify}, which produces a masked STRING without ever mutating the DTO — so the\n * object handed to the transport, and thus the value ON THE WIRE, is unchanged.\n */\n private serialize(\n // webpieces-disable no-any-unknown -- DTO types are erased at the api/proxy boundary (matches execute)\n dto: unknown,\n methodInfo: ApiMethodInfo,\n ): string | undefined {\n return methodInfo.mask ? methodInfo.mask.stringify(dto) : JSON.stringify(dto);\n }\n\n /**\n * The ApiCallContext to stamp into. It cannot be MISSING (it is a constructor argument), but it\n * can be INACTIVE — a Node context used outside any `RequestContext.run(...)` scope. That throws:\n * an api call with nowhere to tag is a bug.\n */\n private activeContext(): ApiCallContext {\n const ctx = this.ctx;\n if (!ctx.isActive()) {\n throw new Error(\n 'LogApiCall requires an ACTIVE ApiCallContext. On a Node server, run inside a ' +\n 'RequestContext.run(...) scope — a server filter opens one per request, and a ' +\n 'non-webpieces host must open one around the work that calls a webpieces client. ' +\n '(A BrowserApiCallContext is always active, so this can only be the Node side.)',\n );\n }\n return ctx;\n }\n\n /**\n * Tag + log a thrown call. There is no responseSize — a throw produced no response body to measure.\n */\n private logFailure(\n error: Error,\n methodInfo: ApiMethodInfo,\n durationMs: number,\n requestSize: number | undefined,\n stamp: (info: ApiCallInfo, emit: () => void) => void,\n http: HttpCallFactsHolder | undefined,\n ): void {\n const side = methodInfo.side;\n const id = `${methodInfo.apiClass}.${methodInfo.methodName}`;\n const errorType = error.constructor.name;\n // The MESSAGE is on BOTH branches, and that is load-bearing: this is the ONLY operator line\n // per failure now that `ApiErrorBoundary` writes none. An operator error message is never\n // published to a caller (see `ApiErrorCodec.publicMessage`), so the log is the one place it\n // exists at all — dropping it on the caller-error branch would lose it outright.\n // Pluggable classification (ApiRegistry): the CallScope ladder of the call's contract class\n // (method, then API, then ALL), else the webpieces built-in — which is side-dependent (a 4xx the SERVER\n // raised is a handled non-failure; the same 4xx a CLIENT receives means its call FAILED; 266 is\n // never a failure either side). This line is FINAL BY POSITION: it only sees what escaped every\n // retry, so a TRANSIENT_FAILURE here was never retried (no strategy, or a WRITE) or gave up —\n // it logs FAIL exactly like a FAILURE. Only EXPECTED is \"OTHER / result:'success'\".\n const isUser = ApiRegistry.classify(error, methodInfo) === CallOutcome.EXPECTED;\n\n stamp(\n new ApiCallInfo(\n methodInfo,\n 'response',\n isUser ? 'success' : 'failure',\n durationMs,\n requestSize,\n undefined,\n undefined,\n http?.facts,\n ),\n () =>\n isUser\n ? log.warn(\n `[API-${side}-resp-OTHER] ${id} errorType=${errorType} error=${error.message}`,\n )\n : log.error(\n `[API-${side}-resp-FAIL] ${id} errorType=${errorType} error=${error.message}`,\n ),\n );\n }\n\n /**\n * UTF-8 byte size without platform-specific encoding globals: LogApiCall runs in the\n * browser bundle. Undefined in, undefined out — a `Promise<void>` method has no body to measure,\n * and a 0 there would be a lie (JSON.stringify(undefined) returns undefined, not '').\n */\n private byteSize(serialized: string | undefined): number | undefined {\n if (serialized === undefined) {\n return undefined;\n }\n let bytes = 0;\n for (const character of serialized) {\n const code = character.codePointAt(0)!;\n bytes += code <= 0x7f ? 1 : code <= 0x7ff ? 2 : code <= 0xffff ? 3 : 4;\n }\n return bytes;\n }\n}\n"]}
|
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
import { CallStrategy } from './CallStrategy';
|
|
2
2
|
/**
|
|
3
3
|
* THE webpieces retry strategy, shared by every transport — register its {@link RetryStrategy.run}
|
|
4
|
-
* with `
|
|
4
|
+
* with `ClientRegistry.setStrategy(...)`, or hand it to a `TransportDefault`.
|
|
5
5
|
*
|
|
6
6
|
* Two questions, two owners, and it asks both:
|
|
7
7
|
*
|
|
8
|
-
* - **the ERROR** — `
|
|
8
|
+
* - **the ERROR** — `ApiRegistry.classify(error, …)`. Only `CallOutcome.TRANSIENT_FAILURE` is
|
|
9
9
|
* retried; an EXPECTED or FAILURE outcome is a definitive answer and is rethrown unchanged.
|
|
10
10
|
* - **the ENDPOINT** — `CallContext.operation`. A `WRITE` is NEVER retried, even when this strategy is
|
|
11
11
|
* registered at ALL: a transient error on a non-idempotent write may already have happened.
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.RetryStrategy = void 0;
|
|
4
4
|
const CallDeadline_1 = require("./CallDeadline");
|
|
5
|
-
const
|
|
5
|
+
const ApiRegistry_1 = require("./ApiRegistry");
|
|
6
6
|
const ErrorClassifier_1 = require("./ErrorClassifier");
|
|
7
7
|
const ApiMethodInfo_1 = require("./ApiMethodInfo");
|
|
8
8
|
const HttpEndpointOptions_1 = require("./HttpEndpointOptions");
|
|
@@ -10,11 +10,11 @@ const ApiError_1 = require("../errors/ApiError");
|
|
|
10
10
|
const errorUtils_1 = require("../lib/errorUtils");
|
|
11
11
|
/**
|
|
12
12
|
* THE webpieces retry strategy, shared by every transport — register its {@link RetryStrategy.run}
|
|
13
|
-
* with `
|
|
13
|
+
* with `ClientRegistry.setStrategy(...)`, or hand it to a `TransportDefault`.
|
|
14
14
|
*
|
|
15
15
|
* Two questions, two owners, and it asks both:
|
|
16
16
|
*
|
|
17
|
-
* - **the ERROR** — `
|
|
17
|
+
* - **the ERROR** — `ApiRegistry.classify(error, …)`. Only `CallOutcome.TRANSIENT_FAILURE` is
|
|
18
18
|
* retried; an EXPECTED or FAILURE outcome is a definitive answer and is rethrown unchanged.
|
|
19
19
|
* - **the ENDPOINT** — `CallContext.operation`. A `WRITE` is NEVER retried, even when this strategy is
|
|
20
20
|
* registered at ALL: a transient error on a non-idempotent write may already have happened.
|
|
@@ -64,7 +64,7 @@ class RetryStrategy {
|
|
|
64
64
|
catch (err) {
|
|
65
65
|
const error = (0, errorUtils_1.toError)(err);
|
|
66
66
|
const retryable = ctx.operation !== HttpEndpointOptions_1.WRITE &&
|
|
67
|
-
|
|
67
|
+
ApiRegistry_1.ApiRegistry.classify(error, methodInfo) === ErrorClassifier_1.CallOutcome.TRANSIENT_FAILURE;
|
|
68
68
|
if (!retryable) {
|
|
69
69
|
// eslint-disable-next-line @webpieces/no-unmanaged-exceptions -- a definitive answer is rethrown unchanged
|
|
70
70
|
throw err;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"RetryStrategy.js","sourceRoot":"","sources":["../../../../../../packages/core/core-util/src/http/RetryStrategy.ts"],"names":[],"mappings":";;;AACA,iDAA8C;AAC9C
|
|
1
|
+
{"version":3,"file":"RetryStrategy.js","sourceRoot":"","sources":["../../../../../../packages/core/core-util/src/http/RetryStrategy.ts"],"names":[],"mappings":"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","sourcesContent":["import { Attempt, CallContext, CallStrategy } from './CallStrategy';\nimport { CallDeadline } from './CallDeadline';\nimport { ApiRegistry } from './ApiRegistry';\nimport { CallOutcome } from './ErrorClassifier';\nimport { ApiMethodInfo } from './ApiMethodInfo';\nimport { WRITE } from './HttpEndpointOptions';\nimport { ApiRetriesExhaustedError } from '../errors/ApiError';\nimport { toError } from '../lib/errorUtils';\n\n/**\n * THE webpieces retry strategy, shared by every transport — register its {@link RetryStrategy.run}\n * with `ClientRegistry.setStrategy(...)`, or hand it to a `TransportDefault`.\n *\n * Two questions, two owners, and it asks both:\n *\n * - **the ERROR** — `ApiRegistry.classify(error, …)`. Only `CallOutcome.TRANSIENT_FAILURE` is\n * retried; an EXPECTED or FAILURE outcome is a definitive answer and is rethrown unchanged.\n * - **the ENDPOINT** — `CallContext.operation`. A `WRITE` is NEVER retried, even when this strategy is\n * registered at ALL: a transient error on a non-idempotent write may already have happened.\n *\n * Each attempt gets its own `attemptTimeoutMs` deadline. Between attempts it waits\n * `initialDelayMs`, then multiplies by `delayMultiplier`, capped at `maxDelayMs`.\n *\n * When it RETRIED and ran out, it throws {@link ApiRetriesExhaustedError} (`cause` = the last\n * attempt's error), which is classified FAILURE so no layer above retries again. A call that made a\n * single attempt — `maxRetries` 0, a `WRITE`, a non-transient error — rethrows its own error unchanged.\n */\nexport class RetryStrategy {\n /** The {@link CallStrategy} to register. A bound field, so `strategy.run` can be passed around. */\n // webpieces-disable no-any-unknown -- registry strategies span heterogeneous response DTOs\n readonly run: CallStrategy<unknown>;\n\n constructor(\n readonly attemptTimeoutMs: number,\n readonly maxRetries: number,\n readonly initialDelayMs: number,\n readonly delayMultiplier: number,\n readonly maxDelayMs: number,\n ) {\n CallDeadline.validate(attemptTimeoutMs);\n RetryStrategy.requireWhole('maxRetries', maxRetries);\n RetryStrategy.requireWhole('initialDelayMs', initialDelayMs);\n RetryStrategy.requireWhole('maxDelayMs', maxDelayMs);\n if (!Number.isFinite(delayMultiplier) || delayMultiplier < 1) {\n throw new RangeError('delayMultiplier must be finite and at least 1');\n }\n // webpieces-disable no-any-unknown -- registry strategies span heterogeneous response DTOs\n this.run = (call: Attempt<unknown>, ctx: CallContext): Promise<unknown> => this.execute(call, ctx);\n }\n\n // webpieces-disable no-any-unknown -- registry strategies span heterogeneous response DTOs\n private async execute(call: Attempt<unknown>, ctx: CallContext): Promise<unknown> {\n const methodInfo = new ApiMethodInfo(\n 'client',\n ctx.apiName,\n ctx.methodName,\n ctx.contract,\n );\n let delayMs = this.initialDelayMs;\n for (let attempt = 1; ; attempt++) {\n // webpieces-disable no-unmanaged-exceptions -- a strategy decides per failure whether to retry\n // eslint-disable-next-line @webpieces/no-unmanaged-exceptions\n try {\n return await call(this.attemptTimeoutMs);\n } catch (err: unknown) {\n const error = toError(err);\n const retryable =\n ctx.operation !== WRITE &&\n ApiRegistry.classify(error, methodInfo) === CallOutcome.TRANSIENT_FAILURE;\n if (!retryable) {\n // eslint-disable-next-line @webpieces/no-unmanaged-exceptions -- a definitive answer is rethrown unchanged\n throw err;\n }\n if (attempt > this.maxRetries) {\n if (attempt === 1) {\n // eslint-disable-next-line @webpieces/no-unmanaged-exceptions -- never retried, so nothing was exhausted\n throw err;\n }\n throw new ApiRetriesExhaustedError(attempt, error);\n }\n await RetryStrategy.sleep(delayMs);\n delayMs = Math.min(delayMs * this.delayMultiplier, this.maxDelayMs);\n }\n }\n }\n\n // webpieces-disable no-function-outside-class -- constructor argument guard\n private static requireWhole(name: string, value: number): void {\n if (!Number.isInteger(value) || value < 0) {\n throw new RangeError(`${name} must be a non-negative integer`);\n }\n }\n\n // webpieces-disable no-function-outside-class -- backoff timer\n private static sleep(ms: number): Promise<void> {\n return new Promise<void>((resolve: () => void) => setTimeout(resolve, ms));\n }\n}\n"]}
|
|
@@ -70,7 +70,7 @@ export declare class RouteMetadata {
|
|
|
70
70
|
readonly mask?: MaskSpec;
|
|
71
71
|
/**
|
|
72
72
|
* True when @Endpoint(..., { rawBody: true }): the transport must retain the verbatim bytes +
|
|
73
|
-
* absolute url for `
|
|
73
|
+
* absolute url for `VendorAuthVerifier.verify` to check a vendor credential over. Rides the route
|
|
74
74
|
* metadata for the same reason {@link formPost} does — the transport adapter decides how to read
|
|
75
75
|
* the body from the ROUTE, without knowing the apiClass/methodName.
|
|
76
76
|
*/
|
|
@@ -81,7 +81,7 @@ class RouteMetadata {
|
|
|
81
81
|
mask;
|
|
82
82
|
/**
|
|
83
83
|
* True when @Endpoint(..., { rawBody: true }): the transport must retain the verbatim bytes +
|
|
84
|
-
* absolute url for `
|
|
84
|
+
* absolute url for `VendorAuthVerifier.verify` to check a vendor credential over. Rides the route
|
|
85
85
|
* metadata for the same reason {@link formPost} does — the transport adapter decides how to read
|
|
86
86
|
* the body from the ROUTE, without knowing the apiClass/methodName.
|
|
87
87
|
*/
|