@webpieces/core-util 0.4.962 → 0.4.964
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/EndpointCompileAssertions.js +14 -0
- package/src/http/EndpointCompileAssertions.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/HttpEndpointOptions.d.ts +19 -3
- package/src/http/HttpEndpointOptions.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 +2 -1
- package/src/http/RouteMetadata.js +2 -1
- package/src/http/RouteMetadata.js.map +1 -1
- package/src/http/RouteMetadataFactory.d.ts +8 -1
- package/src/http/RouteMetadataFactory.js +28 -7
- package/src/http/RouteMetadataFactory.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/decorators.d.ts +4 -3
- package/src/http/decorators.js.map +1 -1
- package/src/index.d.ts +3 -4
- 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":"EndpointCompileAssertions.js","sourceRoot":"","sources":["../../../../../../packages/core/core-util/src/http/EndpointCompileAssertions.ts"],"names":[],"mappings":";;AAAA,
|
|
1
|
+
{"version":3,"file":"EndpointCompileAssertions.js","sourceRoot":"","sources":["../../../../../../packages/core/core-util/src/http/EndpointCompileAssertions.ts"],"names":[],"mappings":"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","sourcesContent":["import { NONE } from './HttpEndpointOptions';\nimport { CLOUDTASKS, CRON, DELETE, GET, READ, Endpoint, EXTERNAL_INBOUND, EXTERNAL_OUTBOUND, PATCH, POST, PUT, RPC, WRITE, WRITE_IDEMPOTENT } from './decorators';\n\nEndpoint(POST, '/valid-rpc', WRITE, RPC);\nEndpoint(PUT, '/valid-put', WRITE_IDEMPOTENT, RPC);\nEndpoint(PATCH, '/valid-patch', WRITE, RPC);\nEndpoint(DELETE, '/valid-delete', WRITE_IDEMPOTENT, RPC);\nEndpoint(POST, '/valid-external', WRITE, EXTERNAL_INBOUND, { calledBy: 'twilio' });\nEndpoint(GET, '/valid-outbound', READ, EXTERNAL_OUTBOUND);\n\n// @ts-expect-error inbound external endpoints require options containing calledBy\nEndpoint(POST, '/missing-external-options', WRITE, EXTERNAL_INBOUND);\n\n// @ts-expect-error inbound external endpoint options require calledBy\nEndpoint(POST, '/missing-external-caller', WRITE, EXTERNAL_INBOUND, { formPost: true });\n\n// @ts-expect-error callerKind is a closed enum-backed union rather than free text\nEndpoint(POST, '/invalid-caller-kind', WRITE, EXTERNAL_INBOUND, {\n calledBy: 'twilio',\n callerKind: 'vendor',\n});\n\n// @ts-expect-error callers must import the enum-backed operation constant\nEndpoint(POST, '/raw-operation', 'write', RPC);\n\n// @ts-expect-error callers must import the enum-backed HTTP method constant\nEndpoint('POST', '/raw-method', WRITE, RPC);\n\n// @ts-expect-error callers must import the enum-backed trigger-kind constant\nEndpoint(POST, '/raw-trigger', WRITE, 'rpc');\n\n// @ts-expect-error callers must import the enum-backed HTTP method constant, for the new verbs too\nEndpoint('PUT', '/raw-put', WRITE, RPC);\n\n// A CLOUDTASKS / CRON endpoint has no response body BY KIND, an EXTERNAL_OUTBOUND one is shaped by the\n// vendor (#1348); NONE stays a declaration only where OUR server answers (RPC, EXTERNAL_INBOUND).\nEndpoint(POST, '/valid-task', WRITE, CLOUDTASKS);\nEndpoint(POST, '/valid-cron', WRITE, CRON, { noLogging: true });\nEndpoint(POST, '/valid-rpc-void', WRITE, RPC, { responseBody: NONE });\nEndpoint(DELETE, '/valid-outbound-void', WRITE_IDEMPOTENT, EXTERNAL_OUTBOUND);\nEndpoint(POST, '/valid-inbound-void', WRITE, EXTERNAL_INBOUND, { calledBy: 'twilio', responseBody: NONE });\n\n// @ts-expect-error CLOUDTASKS already implies no response body; responseBody: NONE is a second spelling\nEndpoint(POST, '/redundant-task-none', WRITE, CLOUDTASKS, { responseBody: NONE });\n\n// @ts-expect-error CRON already implies no response body; responseBody: NONE is a second spelling\nEndpoint(POST, '/redundant-cron-none', WRITE, CRON, { responseBody: NONE });\n\n// @ts-expect-error the vendor shapes an EXTERNAL_OUTBOUND response; responseBody: NONE describes OUR server\nEndpoint(DELETE, '/outbound-none', WRITE_IDEMPOTENT, EXTERNAL_OUTBOUND, { responseBody: NONE });\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"]}
|
|
@@ -25,14 +25,21 @@ export interface EndpointOptions {
|
|
|
25
25
|
/** Return only a JSON body (default), or the full transport-neutral status/header/body value. */
|
|
26
26
|
responseType?: EndpointResponseType;
|
|
27
27
|
/**
|
|
28
|
-
* `NONE`: this method returns `Promise<void>` and
|
|
28
|
+
* `NONE`: this method returns `Promise<void>` and OUR SERVER's success response is 0 BYTES: it
|
|
29
29
|
* answers `204 No Content` with no body and no Content-Type, and every client (node, browser,
|
|
30
|
-
* in-process) resolves `undefined` — while still ACCEPTING `{}`, an empty `200` or a `204
|
|
31
|
-
* because a vendor whose spec documents no body may still send one.
|
|
30
|
+
* in-process) resolves `undefined` — while still ACCEPTING `{}`, an empty `200` or a `204`.
|
|
32
31
|
*
|
|
33
32
|
* WHY a declaration: `design:returntype` of an async method is just `Promise`, so the runtime
|
|
34
33
|
* cannot see `void`. The architecture scan checks this option agrees with the declared
|
|
35
34
|
* `Promise<void>` in both directions, so it cannot drift from the signature.
|
|
35
|
+
*
|
|
36
|
+
* Only for `RPC` and `EXTERNAL_INBOUND`, where our server answers and a body is a real
|
|
37
|
+
* question (#1348). Every other kind refuses it (see {@link NoResponseBodyOptionEndpointOptions}):
|
|
38
|
+
* - `CLOUDTASKS` / `CRON` are fire-and-forget BY KIND — the caller reads nothing but the status —
|
|
39
|
+
* so the kind itself implies no response body; such a method must return `Promise<void>`.
|
|
40
|
+
* - `EXTERNAL_OUTBOUND` is served by the VENDOR, which shapes its own responses; no server of
|
|
41
|
+
* ours implements it. Its client accepts any 2xx: 204 / empty -> `undefined`, a JSON body is
|
|
42
|
+
* parsed, a non-JSON body -> `undefined`.
|
|
36
43
|
*/
|
|
37
44
|
responseBody?: ResponseBodyKind;
|
|
38
45
|
/**
|
|
@@ -160,4 +167,13 @@ export interface ExternalEndpointOptions extends EndpointOptions {
|
|
|
160
167
|
/** What that caller is; picks its graph shape. Defaults to `'saas'`. */
|
|
161
168
|
callerKind?: ExternalSystemKind;
|
|
162
169
|
}
|
|
170
|
+
/**
|
|
171
|
+
* The options of a `CLOUDTASKS`, `CRON` or `EXTERNAL_OUTBOUND` endpoint, where `responseBody: NONE`
|
|
172
|
+
* cannot be written (#1348): the CLOUDTASKS / CRON kind already says the response has no body (one
|
|
173
|
+
* spelling per decision), and an EXTERNAL_OUTBOUND response is shaped by the vendor, not by us.
|
|
174
|
+
*/
|
|
175
|
+
export interface NoResponseBodyOptionEndpointOptions extends EndpointOptions {
|
|
176
|
+
/** Implied by CLOUDTASKS / CRON, the vendor's to shape on EXTERNAL_OUTBOUND: `NONE` does not compile. */
|
|
177
|
+
responseBody?: never;
|
|
178
|
+
}
|
|
163
179
|
export {};
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"HttpEndpointOptions.js","sourceRoot":"","sources":["../../../../../../packages/core/core-util/src/http/HttpEndpointOptions.ts"],"names":[],"mappings":";;;AAGA,8EAA8E;AAC9E,IAAK,sBAIJ;AAJD,WAAK,sBAAsB;IACvB,uCAAa,CAAA;IACb,+DAAqC,CAAA;IACrC,yCAAe,CAAA;AACnB,CAAC,EAJI,sBAAsB,KAAtB,sBAAsB,QAI1B;AAED,wDAAwD;AAC3C,QAAA,IAAI,GAAG,sBAAsB,CAAC,IAAI,CAAC;AACnC,QAAA,gBAAgB,GAAG,sBAAsB,CAAC,gBAAgB,CAAC;AAC3D,QAAA,KAAK,GAAG,sBAAsB,CAAC,KAAK,CAAC;AAKlD,gEAAgE;AAChE,IAAK,iBAEJ;AAFD,WAAK,iBAAiB;IAClB,kCAAa,CAAA;AACjB,CAAC,EAFI,iBAAiB,KAAjB,iBAAiB,QAErB;AAED,uFAAuF;AAC1E,QAAA,IAAI,GAAG,iBAAiB,CAAC,IAAI,CAAC","sourcesContent":["import { ExternalSystemKind } from './external-caller';\nimport { EndpointResponseType } from './HttpContract';\n\n/** Nominal backing keeps raw string literals out of endpoint declarations. */\nenum EndpointOperationValue {\n READ = 'read',\n WRITE_IDEMPOTENT = 'write-idempotent',\n WRITE = 'write',\n}\n\n/** Short, statically importable decorator arguments. */\nexport const READ = EndpointOperationValue.READ;\nexport const WRITE_IDEMPOTENT = EndpointOperationValue.WRITE_IDEMPOTENT;\nexport const WRITE = EndpointOperationValue.WRITE;\n\n/** The side-effect contract callers use to decide whether repeating an operation is safe. */\nexport type EndpointOperation = typeof READ | typeof WRITE_IDEMPOTENT | typeof WRITE;\n\n/** Nominal backing for {@link EndpointOptions.responseBody}. */\nenum ResponseBodyValue {\n NONE = 'none',\n}\n\n/** `@Endpoint(..., { responseBody: NONE })` — the success response carries 0 bytes. */\nexport const NONE = ResponseBodyValue.NONE;\n\n/** The one response-body declaration an endpoint can make; absent means a JSON body. */\nexport type ResponseBodyKind = typeof NONE;\n\n/** Options for one `@Endpoint`, stored parallel to the method-to-path metadata. */\nexport interface EndpointOptions {\n /** Return only a JSON body (default), or the full transport-neutral status/header/body value. */\n responseType?: EndpointResponseType;\n /**\n * `NONE`: this method returns `Promise<void>` and its success response is 0 BYTES. Our server\n * answers `204 No Content` with no body and no Content-Type, and every client (node, browser,\n * in-process) resolves `undefined` — while still ACCEPTING `{}`, an empty `200` or a `204`,\n * because a vendor whose spec documents no body may still send one.\n *\n * WHY a declaration: `design:returntype` of an async method is just `Promise`, so the runtime\n * cannot see `void`. The architecture scan checks this option agrees with the declared\n * `Promise<void>` in both directions, so it cannot drift from the signature.\n */\n responseBody?: ResponseBodyKind;\n /**\n * Parse the request body as application/x-www-form-urlencoded instead of JSON. The request\n * DTO must be flat; urlencoded has no nesting. Default false = JSON.\n */\n formPost?: boolean;\n /**\n * Retain the verbatim request bytes and absolute URL for a `vendorAuth(...)` hook to verify.\n * This is retention, not new buffering: the Express adapter already accumulates the body.\n *\n * A served `vendorAuth(...)` endpoint requires this option at wiring time. It combines with `formPost` for vendors\n * that sign flat form bodies, e.g. `{ formPost: true, rawBody: true }`.\n */\n rawBody?: boolean;\n /**\n * The app should show NO progress UI for this call — it is not something the USER is waiting on\n * (a background download, a log shipper, a heartbeat, a telemetry flush).\n *\n * webpieces draws no progress bar itself; it DECLARES the fact and carries it on\n * `RouteMetadata.hideProgress`, which an app's `RequestLifecycleListener` reads to keep the call\n * out of its bar. WHY a declaration on the endpoint: the alternative is the app matching path\n * strings in its listener, which silently drifts the first time a route is renamed. The ROUTE is\n * the stable fact, so the decision belongs to the route's author.\n *\n * Independent of {@link noLogging} and {@link allowUpgradeInFlight}: a background download hides\n * its progress yet still wants its `[API-*]` lines (exactly what you need when a sync is slow)\n * and still must finish before an app upgrades under it.\n *\n * Default false: absent means the call drives the app's progress UI, like any ordinary route.\n * `grep -rn hideProgress` lists every endpoint the user is not shown waiting on.\n */\n hideProgress?: boolean;\n /**\n * webpieces writes NO `[API-*-req]` / `[API-*-resp-*]` line for this call, on either side —\n * {@link LogApiCallImpl} reads it via `ApiMethodInfo.noLogging`, handed in by both the client\n * proxy and the server's `LogApiFilter` — and stamps no per-call `api` tag.\n *\n * WHY a declaration and not a suppression switch: shipping a log over a logged transport logs\n * about shipping logs — the line describing the call IS the thing being shipped, and emitting it\n * feeds the next batch its own payload. The two mechanisms that do not know the endpoint both\n * failed in prod (see #976) — a wall-clock \"we are shipping\" boolean swallowed unrelated lines\n * including an error alarm, and a logger-name allowlist was incomplete by construction the moment\n * anyone added a logger, producing a steady-state 4-lines-per-second request loop on an idle page.\n * The ENDPOINT is the only stable, declarative fact, so it is where the decision belongs.\n *\n * Rides the route metadata for the same reason {@link formPost} and {@link rawBody} do: the\n * consumer branches on the ROUTE, without knowing the apiClass/methodName.\n *\n * Default false: absent means the ordinary, fully-logged route. The quiet path is the one you\n * have to say out loud, so `grep -rn noLogging` lists every endpoint webpieces stops logging.\n */\n noLogging?: boolean;\n /**\n * An app MAY upgrade (reload onto a newer deployed bundle) while this call is in flight, and the\n * call neither blocks nor arms that upgrade.\n *\n * By default every RPC participates in the app's upgrade decision: the app must not upgrade while\n * it is in flight, and it counts toward the \"finished with a SUCCESS result\" boundary at which an\n * app typically arms its seamless upgrade. That default is the safe one — reloading under an\n * in-flight write or download loses it. A call nobody asked for, and that is harmless to cut off\n * (a log shipper, a heartbeat), declares `true` so it can neither hold an upgrade hostage nor be\n * the success that triggers one.\n *\n * WHY separate from {@link hideProgress}: apps commonly arm an upgrade off their progress bar's\n * request group, so hiding a call from the bar used to drop it out of the upgrade decision as a\n * side effect. A hidden background download must still block an upgrade until it succeeds; only\n * this flag, said out loud, lets a call stop participating.\n *\n * webpieces only declares and carries it (`RouteMetadata.allowUpgradeInFlight`); the app's\n * `RequestLifecycleListener` / upgrade logic decides what to do with it. Default false.\n * `grep -rn allowUpgradeInFlight` lists every endpoint an upgrade may cut off.\n *\n * THE SERVER'S HALF of the upgrade story is `ApiClientTooOldError` (HTTP 426,\n * `errorCode: 'client-too-old'`). This flag is the CLIENT deciding WHEN it may upgrade; that\n * error is a SERVER saying the client MUST. Who throws it: a server gate that compares the build\n * the caller declared (a header, a version in the request) against the minimum it still\n * supports. Receivers decode it back into `ApiClientTooOldError` on every hop, server-to-server\n * and browser, HTTP and IPC, and treat it as an upgrade, never as a bug: the app's upgrade logic\n * reloads or installs the new build, typically honouring this flag for the calls still in flight.\n */\n allowUpgradeInFlight?: boolean;\n /**\n * This route is NOT part of the CUSTOMER-facing contract. `public-openapi.json` — the document a\n * customer gets, and the only one a publish path uploads — contains no path, no operation, no\n * schema and no prose for it. It is not \"rendered with a flag set\", it is absent.\n *\n * It is still in `full-private-openapi.json`, and still in `mcp-openapi.json` when it carries\n * `@WpMcpTool`. The reason it is withheld belongs in the method's JSDoc, which is where every\n * other piece of its documentation already lives.\n *\n * WHY A BOOLEAN HERE IS NOT SHIM SHAPE #5, given that `@ApiType` exists precisely to avoid a\n * falsy default meaning \"publish\": the two operate at different levels and the default here is\n * NOT the permissive one. A contract reaches customers only by declaring\n * `@ApiType(..., EXTERNAL_CUSTOMER)` — one deliberate token per contract — and `hidden` only ever\n * SUBTRACTS from what that token already granted. Omitting it on a contract nobody published to\n * customers publishes nothing. The per-method case it exists for is real and cannot be said at\n * class level: a NEW endpoint on an already-published API, built on main before it is announced.\n *\n * Default false, and `grep -rn \"hidden: true\"` lists every method withheld from customers.\n *\n * HIDING IS A DOCUMENTATION DECISION AND NEVER AN ACCESS-CONTROL ONE. The route is served exactly\n * as before and is still reachable by anything holding its credential — including an agent, if it\n * also carries `@WpMcpTool`. Declare its authorization as loudly as a published one.\n */\n hidden?: boolean;\n /**\n * This operation may touch systems OUTSIDE this service — a payment processor, a partner's API,\n * the physical world. It feeds MCP's `openWorldHint`, and it is rendered as a sentence in the\n * operation's description so a human integrating against the document sees it too.\n *\n * DEFAULT FALSE, deliberately unlike the MCP specification's own `true` default. MCP defaults to\n * \"open world\" because it cannot know; webpieces CAN know, because the author of the endpoint is\n * right here. Defaulting to `true` would make the interesting case the silent one and the boring\n * case the one you have to type, which is backwards — and an agent told that every endpoint might\n * reach the outside world has been told nothing. `grep -rn \"openWorld: true\"` lists the ones that\n * genuinely do.\n */\n openWorld?: boolean;\n}\n\n/** An EXTERNAL_INBOUND endpoint must also identify the outside system that calls it. */\nexport interface ExternalEndpointOptions extends EndpointOptions {\n /** The outside system (`'twilio'`) — graph identity, not display text. */\n calledBy: string;\n /** What that caller is; picks its graph shape. Defaults to `'saas'`. */\n callerKind?: ExternalSystemKind;\n}\n"]}
|
|
1
|
+
{"version":3,"file":"HttpEndpointOptions.js","sourceRoot":"","sources":["../../../../../../packages/core/core-util/src/http/HttpEndpointOptions.ts"],"names":[],"mappings":";;;AAGA,8EAA8E;AAC9E,IAAK,sBAIJ;AAJD,WAAK,sBAAsB;IACvB,uCAAa,CAAA;IACb,+DAAqC,CAAA;IACrC,yCAAe,CAAA;AACnB,CAAC,EAJI,sBAAsB,KAAtB,sBAAsB,QAI1B;AAED,wDAAwD;AAC3C,QAAA,IAAI,GAAG,sBAAsB,CAAC,IAAI,CAAC;AACnC,QAAA,gBAAgB,GAAG,sBAAsB,CAAC,gBAAgB,CAAC;AAC3D,QAAA,KAAK,GAAG,sBAAsB,CAAC,KAAK,CAAC;AAKlD,gEAAgE;AAChE,IAAK,iBAEJ;AAFD,WAAK,iBAAiB;IAClB,kCAAa,CAAA;AACjB,CAAC,EAFI,iBAAiB,KAAjB,iBAAiB,QAErB;AAED,uFAAuF;AAC1E,QAAA,IAAI,GAAG,iBAAiB,CAAC,IAAI,CAAC","sourcesContent":["import { ExternalSystemKind } from './external-caller';\nimport { EndpointResponseType } from './HttpContract';\n\n/** Nominal backing keeps raw string literals out of endpoint declarations. */\nenum EndpointOperationValue {\n READ = 'read',\n WRITE_IDEMPOTENT = 'write-idempotent',\n WRITE = 'write',\n}\n\n/** Short, statically importable decorator arguments. */\nexport const READ = EndpointOperationValue.READ;\nexport const WRITE_IDEMPOTENT = EndpointOperationValue.WRITE_IDEMPOTENT;\nexport const WRITE = EndpointOperationValue.WRITE;\n\n/** The side-effect contract callers use to decide whether repeating an operation is safe. */\nexport type EndpointOperation = typeof READ | typeof WRITE_IDEMPOTENT | typeof WRITE;\n\n/** Nominal backing for {@link EndpointOptions.responseBody}. */\nenum ResponseBodyValue {\n NONE = 'none',\n}\n\n/** `@Endpoint(..., { responseBody: NONE })` — the success response carries 0 bytes. */\nexport const NONE = ResponseBodyValue.NONE;\n\n/** The one response-body declaration an endpoint can make; absent means a JSON body. */\nexport type ResponseBodyKind = typeof NONE;\n\n/** Options for one `@Endpoint`, stored parallel to the method-to-path metadata. */\nexport interface EndpointOptions {\n /** Return only a JSON body (default), or the full transport-neutral status/header/body value. */\n responseType?: EndpointResponseType;\n /**\n * `NONE`: this method returns `Promise<void>` and OUR SERVER's success response is 0 BYTES: it\n * answers `204 No Content` with no body and no Content-Type, and every client (node, browser,\n * in-process) resolves `undefined` — while still ACCEPTING `{}`, an empty `200` or a `204`.\n *\n * WHY a declaration: `design:returntype` of an async method is just `Promise`, so the runtime\n * cannot see `void`. The architecture scan checks this option agrees with the declared\n * `Promise<void>` in both directions, so it cannot drift from the signature.\n *\n * Only for `RPC` and `EXTERNAL_INBOUND`, where our server answers and a body is a real\n * question (#1348). Every other kind refuses it (see {@link NoResponseBodyOptionEndpointOptions}):\n * - `CLOUDTASKS` / `CRON` are fire-and-forget BY KIND — the caller reads nothing but the status —\n * so the kind itself implies no response body; such a method must return `Promise<void>`.\n * - `EXTERNAL_OUTBOUND` is served by the VENDOR, which shapes its own responses; no server of\n * ours implements it. Its client accepts any 2xx: 204 / empty -> `undefined`, a JSON body is\n * parsed, a non-JSON body -> `undefined`.\n */\n responseBody?: ResponseBodyKind;\n /**\n * Parse the request body as application/x-www-form-urlencoded instead of JSON. The request\n * DTO must be flat; urlencoded has no nesting. Default false = JSON.\n */\n formPost?: boolean;\n /**\n * Retain the verbatim request bytes and absolute URL for a `vendorAuth(...)` hook to verify.\n * This is retention, not new buffering: the Express adapter already accumulates the body.\n *\n * A served `vendorAuth(...)` endpoint requires this option at wiring time. It combines with `formPost` for vendors\n * that sign flat form bodies, e.g. `{ formPost: true, rawBody: true }`.\n */\n rawBody?: boolean;\n /**\n * The app should show NO progress UI for this call — it is not something the USER is waiting on\n * (a background download, a log shipper, a heartbeat, a telemetry flush).\n *\n * webpieces draws no progress bar itself; it DECLARES the fact and carries it on\n * `RouteMetadata.hideProgress`, which an app's `RequestLifecycleListener` reads to keep the call\n * out of its bar. WHY a declaration on the endpoint: the alternative is the app matching path\n * strings in its listener, which silently drifts the first time a route is renamed. The ROUTE is\n * the stable fact, so the decision belongs to the route's author.\n *\n * Independent of {@link noLogging} and {@link allowUpgradeInFlight}: a background download hides\n * its progress yet still wants its `[API-*]` lines (exactly what you need when a sync is slow)\n * and still must finish before an app upgrades under it.\n *\n * Default false: absent means the call drives the app's progress UI, like any ordinary route.\n * `grep -rn hideProgress` lists every endpoint the user is not shown waiting on.\n */\n hideProgress?: boolean;\n /**\n * webpieces writes NO `[API-*-req]` / `[API-*-resp-*]` line for this call, on either side —\n * {@link LogApiCallImpl} reads it via `ApiMethodInfo.noLogging`, handed in by both the client\n * proxy and the server's `LogApiFilter` — and stamps no per-call `api` tag.\n *\n * WHY a declaration and not a suppression switch: shipping a log over a logged transport logs\n * about shipping logs — the line describing the call IS the thing being shipped, and emitting it\n * feeds the next batch its own payload. The two mechanisms that do not know the endpoint both\n * failed in prod (see #976) — a wall-clock \"we are shipping\" boolean swallowed unrelated lines\n * including an error alarm, and a logger-name allowlist was incomplete by construction the moment\n * anyone added a logger, producing a steady-state 4-lines-per-second request loop on an idle page.\n * The ENDPOINT is the only stable, declarative fact, so it is where the decision belongs.\n *\n * Rides the route metadata for the same reason {@link formPost} and {@link rawBody} do: the\n * consumer branches on the ROUTE, without knowing the apiClass/methodName.\n *\n * Default false: absent means the ordinary, fully-logged route. The quiet path is the one you\n * have to say out loud, so `grep -rn noLogging` lists every endpoint webpieces stops logging.\n */\n noLogging?: boolean;\n /**\n * An app MAY upgrade (reload onto a newer deployed bundle) while this call is in flight, and the\n * call neither blocks nor arms that upgrade.\n *\n * By default every RPC participates in the app's upgrade decision: the app must not upgrade while\n * it is in flight, and it counts toward the \"finished with a SUCCESS result\" boundary at which an\n * app typically arms its seamless upgrade. That default is the safe one — reloading under an\n * in-flight write or download loses it. A call nobody asked for, and that is harmless to cut off\n * (a log shipper, a heartbeat), declares `true` so it can neither hold an upgrade hostage nor be\n * the success that triggers one.\n *\n * WHY separate from {@link hideProgress}: apps commonly arm an upgrade off their progress bar's\n * request group, so hiding a call from the bar used to drop it out of the upgrade decision as a\n * side effect. A hidden background download must still block an upgrade until it succeeds; only\n * this flag, said out loud, lets a call stop participating.\n *\n * webpieces only declares and carries it (`RouteMetadata.allowUpgradeInFlight`); the app's\n * `RequestLifecycleListener` / upgrade logic decides what to do with it. Default false.\n * `grep -rn allowUpgradeInFlight` lists every endpoint an upgrade may cut off.\n *\n * THE SERVER'S HALF of the upgrade story is `ApiClientTooOldError` (HTTP 426,\n * `errorCode: 'client-too-old'`). This flag is the CLIENT deciding WHEN it may upgrade; that\n * error is a SERVER saying the client MUST. Who throws it: a server gate that compares the build\n * the caller declared (a header, a version in the request) against the minimum it still\n * supports. Receivers decode it back into `ApiClientTooOldError` on every hop, server-to-server\n * and browser, HTTP and IPC, and treat it as an upgrade, never as a bug: the app's upgrade logic\n * reloads or installs the new build, typically honouring this flag for the calls still in flight.\n */\n allowUpgradeInFlight?: boolean;\n /**\n * This route is NOT part of the CUSTOMER-facing contract. `public-openapi.json` — the document a\n * customer gets, and the only one a publish path uploads — contains no path, no operation, no\n * schema and no prose for it. It is not \"rendered with a flag set\", it is absent.\n *\n * It is still in `full-private-openapi.json`, and still in `mcp-openapi.json` when it carries\n * `@WpMcpTool`. The reason it is withheld belongs in the method's JSDoc, which is where every\n * other piece of its documentation already lives.\n *\n * WHY A BOOLEAN HERE IS NOT SHIM SHAPE #5, given that `@ApiType` exists precisely to avoid a\n * falsy default meaning \"publish\": the two operate at different levels and the default here is\n * NOT the permissive one. A contract reaches customers only by declaring\n * `@ApiType(..., EXTERNAL_CUSTOMER)` — one deliberate token per contract — and `hidden` only ever\n * SUBTRACTS from what that token already granted. Omitting it on a contract nobody published to\n * customers publishes nothing. The per-method case it exists for is real and cannot be said at\n * class level: a NEW endpoint on an already-published API, built on main before it is announced.\n *\n * Default false, and `grep -rn \"hidden: true\"` lists every method withheld from customers.\n *\n * HIDING IS A DOCUMENTATION DECISION AND NEVER AN ACCESS-CONTROL ONE. The route is served exactly\n * as before and is still reachable by anything holding its credential — including an agent, if it\n * also carries `@WpMcpTool`. Declare its authorization as loudly as a published one.\n */\n hidden?: boolean;\n /**\n * This operation may touch systems OUTSIDE this service — a payment processor, a partner's API,\n * the physical world. It feeds MCP's `openWorldHint`, and it is rendered as a sentence in the\n * operation's description so a human integrating against the document sees it too.\n *\n * DEFAULT FALSE, deliberately unlike the MCP specification's own `true` default. MCP defaults to\n * \"open world\" because it cannot know; webpieces CAN know, because the author of the endpoint is\n * right here. Defaulting to `true` would make the interesting case the silent one and the boring\n * case the one you have to type, which is backwards — and an agent told that every endpoint might\n * reach the outside world has been told nothing. `grep -rn \"openWorld: true\"` lists the ones that\n * genuinely do.\n */\n openWorld?: boolean;\n}\n\n/** An EXTERNAL_INBOUND endpoint must also identify the outside system that calls it. */\nexport interface ExternalEndpointOptions extends EndpointOptions {\n /** The outside system (`'twilio'`) — graph identity, not display text. */\n calledBy: string;\n /** What that caller is; picks its graph shape. Defaults to `'saas'`. */\n callerKind?: ExternalSystemKind;\n}\n\n/**\n * The options of a `CLOUDTASKS`, `CRON` or `EXTERNAL_OUTBOUND` endpoint, where `responseBody: NONE`\n * cannot be written (#1348): the CLOUDTASKS / CRON kind already says the response has no body (one\n * spelling per decision), and an EXTERNAL_OUTBOUND response is shaped by the vendor, not by us.\n */\nexport interface NoResponseBodyOptionEndpointOptions extends EndpointOptions {\n /** Implied by CLOUDTASKS / CRON, the vendor's to shape on EXTERNAL_OUTBOUND: `NONE` does not compile. */\n responseBody?: never;\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":"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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 { 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.
|