@webpieces/core-util 0.4.963 → 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/ErrorClassifier.d.ts +7 -7
- package/src/http/ErrorClassifier.js.map +1 -1
- package/src/http/ErrorLadderCompileAssertions.js +41 -17
- package/src/http/ErrorLadderCompileAssertions.js.map +1 -1
- package/src/http/ErrorTranslator.d.ts +6 -6
- package/src/http/ErrorTranslator.js.map +1 -1
- package/src/http/LogApiCall.d.ts +1 -1
- package/src/http/LogApiCall.js +4 -4
- package/src/http/LogApiCall.js.map +1 -1
- package/src/http/RetryStrategy.d.ts +2 -2
- package/src/http/RetryStrategy.js +4 -4
- package/src/http/RetryStrategy.js.map +1 -1
- package/src/http/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/index.d.ts +2 -3
- package/src/index.js +7 -5
- package/src/index.js.map +1 -1
- package/src/ipc/IpcErrorTranslator.d.ts +1 -1
- package/src/ipc/IpcErrorTranslator.js.map +1 -1
- package/src/ipc/IpcRegistry.d.ts +1 -1
- package/src/ipc/IpcRegistry.js +1 -1
- package/src/ipc/IpcRegistry.js.map +1 -1
- package/src/http/CallRegistry.d.ts +0 -42
- package/src/http/CallRegistry.js +0 -143
- package/src/http/CallRegistry.js.map +0 -1
- package/src/http/CallRegistryCompileAssertions.js +0 -27
- package/src/http/CallRegistryCompileAssertions.js.map +0 -1
- /package/src/http/{CallRegistryCompileAssertions.d.ts → ClientRegistryCompileAssertions.d.ts} +0 -0
package/package.json
CHANGED
|
@@ -44,7 +44,7 @@ import { MaskSpec } from './LogFieldMask';
|
|
|
44
44
|
export type ApiCallDirection = 'request' | 'response';
|
|
45
45
|
/**
|
|
46
46
|
* Response outcome. 'success' covers 2xx AND user errors (400/401/403/404/266 — a successfully
|
|
47
|
-
* handled "you made a mistake"); 'failure' is a genuine server error. `success` = {@link CallOutcome.EXPECTED} from {@link
|
|
47
|
+
* handled "you made a mistake"); 'failure' is a genuine server error. `success` = {@link CallOutcome.EXPECTED} from {@link ApiRegistry.classify}.
|
|
48
48
|
*/
|
|
49
49
|
export type ApiResult = 'success' | 'failure';
|
|
50
50
|
/** Re-exported from {@link ApiMethodInfo} (its true home) so existing `ApiSide` imports keep working. */
|
|
@@ -104,7 +104,7 @@ export declare class ApiCallInfo {
|
|
|
104
104
|
* There is deliberately no top-level `statusCode` beside this. LogApiCall runs deep in the
|
|
105
105
|
* stack over in-process calls, pubsub handlers, and cloud-task enqueues — none of which have
|
|
106
106
|
* an HTTP status — and business logic must not know about HTTP. `result` (see {@link ApiResult})
|
|
107
|
-
* is the transport-neutral outcome, exactly as {@link
|
|
107
|
+
* is the transport-neutral outcome, exactly as {@link ApiRegistry.classify} (a {@link CallOutcome}) classifies by
|
|
108
108
|
* portable Error TYPE rather than by status code. That decision STANDS (#1339): the HTTP
|
|
109
109
|
* client's status lives in the optional, nested {@link http} — present only where there IS an
|
|
110
110
|
* HTTP exchange, so nothing transport-neutral ever reads it.
|
|
@@ -146,7 +146,7 @@ export declare class ApiCallInfo {
|
|
|
146
146
|
* There is deliberately no top-level `statusCode` beside this. LogApiCall runs deep in the
|
|
147
147
|
* stack over in-process calls, pubsub handlers, and cloud-task enqueues — none of which have
|
|
148
148
|
* an HTTP status — and business logic must not know about HTTP. `result` (see {@link ApiResult})
|
|
149
|
-
* is the transport-neutral outcome, exactly as {@link
|
|
149
|
+
* is the transport-neutral outcome, exactly as {@link ApiRegistry.classify} (a {@link CallOutcome}) classifies by
|
|
150
150
|
* portable Error TYPE rather than by status code. That decision STANDS (#1339): the HTTP
|
|
151
151
|
* client's status lives in the optional, nested {@link http} — present only where there IS an
|
|
152
152
|
* HTTP exchange, so nothing transport-neutral ever reads it.
|
package/src/http/ApiCallInfo.js
CHANGED
|
@@ -87,7 +87,7 @@ class ApiCallInfo {
|
|
|
87
87
|
* There is deliberately no top-level `statusCode` beside this. LogApiCall runs deep in the
|
|
88
88
|
* stack over in-process calls, pubsub handlers, and cloud-task enqueues — none of which have
|
|
89
89
|
* an HTTP status — and business logic must not know about HTTP. `result` (see {@link ApiResult})
|
|
90
|
-
* is the transport-neutral outcome, exactly as {@link
|
|
90
|
+
* is the transport-neutral outcome, exactly as {@link ApiRegistry.classify} (a {@link CallOutcome}) classifies by
|
|
91
91
|
* portable Error TYPE rather than by status code. That decision STANDS (#1339): the HTTP
|
|
92
92
|
* client's status lives in the optional, nested {@link http} — present only where there IS an
|
|
93
93
|
* HTTP exchange, so nothing transport-neutral ever reads it.
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"ApiCallInfo.js","sourceRoot":"","sources":["../../../../../../packages/core/core-util/src/http/ApiCallInfo.ts"],"names":[],"mappings":";;;AAwDA;;;;;;;;GAQG;AACH,MAAa,aAAa;IAGT;IAEA;IAEA;IANb;IACI,iCAAiC;IACxB,MAAc;IACvB,sGAAsG;IAC7F,GAAW;IACpB,gDAAgD;IACvC,MAAc;QAJd,WAAM,GAAN,MAAM,CAAQ;QAEd,QAAG,GAAH,GAAG,CAAQ;QAEX,WAAM,GAAN,MAAM,CAAQ;IACxB,CAAC;IAEJ;;;;OAIG;IACH,8FAA8F;IAC9F,MAAM,CAAC,OAAO,CAAC,GAAW,EAAE,IAA0B;QAClD,MAAM,eAAe,GAAG,GAAG,CAAC,OAAO,CAAC,qCAAqC,EAAE,IAAI,CAAC,CAAC;QACjF,MAAM,OAAO,GAAG,eAAe,CAAC,OAAO,CAAC,GAAG,CAAC,CAAC;QAC7C,IAAI,OAAO,GAAG,CAAC,IAAI,IAAI,KAAK,SAAS;YAAE,OAAO,eAAe,CAAC;QAC9D,MAAM,KAAK,GAAG,eAAe;aACxB,KAAK,CAAC,OAAO,GAAG,CAAC,CAAC;aAClB,KAAK,CAAC,GAAG,CAAC;aACV,GAAG,CAAC,CAAC,IAAY,EAAE,EAAE;YAClB,MAAM,MAAM,GAAG,IAAI,CAAC,OAAO,CAAC,GAAG,CAAC,CAAC;YACjC,MAAM,GAAG,GAAG,MAAM,GAAG,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC,EAAE,MAAM,CAAC,CAAC;YACtD,OAAO,IAAI,CAAC,OAAO,CAAC,aAAa,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC,KAAK,SAAS,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,GAAG,GAAG,QAAQ,CAAC;QACzF,CAAC,CAAC;aACD,IAAI,CAAC,GAAG,CAAC,CAAC;QACf,OAAO,GAAG,eAAe,CAAC,KAAK,CAAC,CAAC,EAAE,OAAO,CAAC,IAAI,KAAK,EAAE,CAAC;IAC3D,CAAC;IAED,8FAA8F;IACtF,MAAM,CAAC,MAAM,CAAC,GAAW;QAC7B,uFAAuF;QACvF,OAAO,mBAAmB,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,kBAAkB,CAAC,GAAG,CAAC,CAAC;IACzE,CAAC;CACJ;AArCD,sCAqCC;AAED;;;;;GAKG;AACH,MAAa,mBAAmB;IAC5B,KAAK,GAA8B,SAAS,CAAC;CAChD;AAFD,kDAEC;AAED,MAAa,WAAW;IAIP;IACA;IAEA;IAcA;IAUA;IAGA;IAEA;IAOA;IA1Cb;IACI;oCACgC;IACvB,MAAqB,EACrB,IAAsB;IAC/B,sDAAsD;IAC7C,MAAkB;IAC3B;;;;;;;;;;;;OAYG;IACM,UAAmB;IAC5B;;;;;;;;OAQG;IACM,WAAoB;IAC7B;yFACqF;IAC5E,YAAqB;IAC9B,4FAA4F;IACnF,aAAsB;IAC/B;;;;;OAKG;IACM,IAAoB;QAvCpB,WAAM,GAAN,MAAM,CAAe;QACrB,SAAI,GAAJ,IAAI,CAAkB;QAEtB,WAAM,GAAN,MAAM,CAAY;QAclB,eAAU,GAAV,UAAU,CAAS;QAUnB,gBAAW,GAAX,WAAW,CAAS;QAGpB,iBAAY,GAAZ,YAAY,CAAS;QAErB,kBAAa,GAAb,aAAa,CAAS;QAOtB,SAAI,GAAJ,IAAI,CAAgB;IAC9B,CAAC;CACP;AA7CD,kCA6CC","sourcesContent":["import { ApiMethodInfo } from './ApiMethodInfo';\nimport { MaskSpec } from './LogFieldMask';\n\n/**\n * ApiCallInfo - the structured tag stamped into RequestContext around every API call\n * (by {@link LogApiCallImpl}), so ANY log line emitted during the call inherits a filterable\n * `api` object rather than only the req/resp text lines.\n *\n * The node logging backends (winston/bunyan) read this struct out of context via\n * `RequestContext.buildStructuredLogFields()` and emit it AS AN OBJECT under `jsonPayload.api`,\n * which unlocks GCP Cloud Logging filters like:\n * - `jsonPayload.api.method.side=\"client\"` — every outbound call this process made\n * - `jsonPayload.api.method.side=\"server\"` — every inbound call it handled\n * - `jsonPayload.api.method.apiClass=\"SaveApi\"` — one logical method, BOTH sides (client + server)\n * - `jsonPayload.api.result=\"failure\"` — failed exchanges only\n * - `jsonPayload.api.durationMs>1000` — slow calls, either side\n * - `jsonPayload.api.responseSize>100000` — the fat responses (the ones that get chunked)\n * - `jsonPayload.api.responseCount>100` — responses containing many logical items\n * - `jsonPayload.api:*` — \"API traffic only\" (tracing + the recorder)\n * - `jsonPayload.api.http.status=404` — an HTTP client call the vendor answered 404 (#1339)\n *\n * IMPORTANT: the field names here (and on the nested {@link ApiMethodInfo}) ARE the GCP field names —\n * rename a field and the filter renames with it. The identity lives NESTED under `api.method`\n * (`api.method.{side,apiClass,methodName,controllerName}`); `api.type` and `api.result` sit at the top.\n *\n * NOTE: the request `httpMethod`/`path` are NOT here — an inbound request stamps them as the separate\n * top-level logged keys `jsonPayload.httpMethod` / `jsonPayload.requestPath` (see\n * {@link WebpiecesCoreHeaders} + `RequestContextHeaders.fillFromRequest`). Outbound client calls have\n * no inbound path, so they carry only the `api` identity.\n *\n * Per-hop only: the underlying `API_CALL_INFO` ContextKey is NOT transferred over the wire, so a\n * downstream server stamps its own `side:'server'` rather than inheriting the caller's `side:'client'`.\n *\n * Per CLAUDE.md: data-only structures are classes, not interfaces.\n */\n\n/** Which half of the exchange this tag describes: the outgoing 'request' or the returning 'response'. */\n/**\n * Which HALF of one api call a log line describes.\n *\n * Renamed from `ApiType`, which collided with the `@ApiType(...)` CONTRACT decorator and read as if\n * it were about the kind of API rather than the direction of one line. Per\n * `.claude/rules/no-backwards-compat.md` the old spelling is deleted rather than aliased, so a caller\n * on the old name stops compiling and the error names this one.\n */\nexport type ApiCallDirection = 'request' | 'response';\n\n/**\n * Response outcome. 'success' covers 2xx AND user errors (400/401/403/404/266 — a successfully\n * handled \"you made a mistake\"); 'failure' is a genuine server error. `success` = {@link CallOutcome.EXPECTED} from {@link ClientRegistry.classify}.\n */\nexport type ApiResult = 'success' | 'failure';\n\n/** Re-exported from {@link ApiMethodInfo} (its true home) so existing `ApiSide` imports keep working. */\nexport type { ApiSide } from './ApiMethodInfo';\n\n/**\n * The HTTP facts of ONE client call's last transmission — surfaces as `jsonPayload.api.http.*`.\n * Filled ONLY by the HTTP client (node and browser), which is the one layer that has a verb, a url\n * and a status; in-process, pubsub and cloud-task calls leave {@link ApiCallInfo.http} unset. It\n * exists because a vendor's 404 and its 500 were indistinguishable in the logs when only the DTOs\n * were recorded.\n *\n * Never headers or tokens: the url is MASKED ({@link maskUrl}) before it gets here.\n */\nexport class HttpCallFacts {\n constructor(\n /** The HTTP verb, e.g. `GET`. */\n readonly method: string,\n /** The final absolute url, masked: no userinfo, and `@MaskLog` field names' query values redacted. */\n readonly url: string,\n /** The status of the last response received. */\n readonly status: number,\n ) {}\n\n /**\n * The url as it may be logged: any `user:password@` userinfo is dropped, and the value of every\n * query key the endpoint's `@MaskLog` names is replaced with `*****` (a `?token=` the DTO mask\n * already hides must not reappear in the url).\n */\n // webpieces-disable no-function-outside-class -- pure static helper beside the data it builds\n static maskUrl(url: string, mask: MaskSpec | undefined): string {\n const withoutUserInfo = url.replace(/^([a-z][a-z0-9+.-]*:\\/\\/)[^/@?#]*@/i, '$1');\n const queryAt = withoutUserInfo.indexOf('?');\n if (queryAt < 0 || mask === undefined) return withoutUserInfo;\n const query = withoutUserInfo\n .slice(queryAt + 1)\n .split('&')\n .map((pair: string) => {\n const equals = pair.indexOf('=');\n const key = equals < 0 ? pair : pair.slice(0, equals);\n return mask.modeFor(HttpCallFacts.decode(key)) === undefined ? pair : `${key}=*****`;\n })\n .join('&');\n return `${withoutUserInfo.slice(0, queryAt)}?${query}`;\n }\n\n // webpieces-disable no-function-outside-class -- pure static helper beside the data it builds\n private static decode(key: string): string {\n // A malformed escape is logged as written; it can never be a masked field name anyway.\n return /%(?![0-9a-f]{2})/i.test(key) ? key : decodeURIComponent(key);\n }\n}\n\n/**\n * The per-call slot the HTTP client fills with {@link HttpCallFacts} as each response arrives, and\n * {@link LogApiCallImpl} reads when it stamps the response tag. A holder rather than a return value\n * because the facts are known DEEP in the transport while the tag is written at the top of the call;\n * the last transmission (after redirects, a 401 refresh or a retry) wins.\n */\nexport class HttpCallFactsHolder {\n facts: HttpCallFacts | undefined = undefined;\n}\n\nexport class ApiCallInfo {\n constructor(\n /** The call identity (side, apiClass, methodName, controllerName) — surfaces nested under\n * `jsonPayload.api.method`. */\n readonly method: ApiMethodInfo,\n readonly type: ApiCallDirection,\n /** Response only — undefined on the 'request' tag. */\n readonly result?: ApiResult,\n /**\n * Wall-clock milliseconds the call took. RESPONSE tag only (undefined on 'request') — a\n * request has no duration yet. Present on BOTH the success and failure paths, so\n * `jsonPayload.api.durationMs>1000 AND api.result=\"failure\"` finds slow failures.\n *\n * There is deliberately no top-level `statusCode` beside this. LogApiCall runs deep in the\n * stack over in-process calls, pubsub handlers, and cloud-task enqueues — none of which have\n * an HTTP status — and business logic must not know about HTTP. `result` (see {@link ApiResult})\n * is the transport-neutral outcome, exactly as {@link ClientRegistry.classify} (a {@link CallOutcome}) classifies by\n * portable Error TYPE rather than by status code. That decision STANDS (#1339): the HTTP\n * client's status lives in the optional, nested {@link http} — present only where there IS an\n * HTTP exchange, so nothing transport-neutral ever reads it.\n */\n readonly durationMs?: number,\n /**\n * Bytes of the serialized request DTO. Stamped on BOTH tags: the 'request' tag reports it as\n * soon as it is known, and the 'response' tag repeats it so one record shows the whole\n * exchange (`api.requestSize` + `api.responseSize` without a join).\n *\n * This is the TOTAL size of the body, measured BEFORE any log chunking — chunking is a\n * transport concern handled by the GCP backends, and a body split across 3 records still\n * reports its one true size here.\n */\n readonly requestSize?: number,\n /** Bytes of the serialized response. RESPONSE tag only, and only when the call succeeded —\n * a thrown error produced no response body to measure. Total size, pre-chunking. */\n readonly responseSize?: number,\n /** Number of logical items in a successful response. RESPONSE tag only, opt-in per call. */\n readonly responseCount?: number,\n /**\n * The HTTP verb, masked url and status of the call's last transmission — `jsonPayload.api.http.*`.\n * RESPONSE tag only, and only for a call made by the HTTP client (success AND failure, so\n * `api.result=\"failure\" AND api.http.status=429` finds a vendor throttling us). Unset for\n * in-process, pubsub and cloud-task calls. See {@link HttpCallFacts}.\n */\n readonly http?: HttpCallFacts,\n ) {}\n}\n"]}
|
|
1
|
+
{"version":3,"file":"ApiCallInfo.js","sourceRoot":"","sources":["../../../../../../packages/core/core-util/src/http/ApiCallInfo.ts"],"names":[],"mappings":";;;AAwDA;;;;;;;;GAQG;AACH,MAAa,aAAa;IAGT;IAEA;IAEA;IANb;IACI,iCAAiC;IACxB,MAAc;IACvB,sGAAsG;IAC7F,GAAW;IACpB,gDAAgD;IACvC,MAAc;QAJd,WAAM,GAAN,MAAM,CAAQ;QAEd,QAAG,GAAH,GAAG,CAAQ;QAEX,WAAM,GAAN,MAAM,CAAQ;IACxB,CAAC;IAEJ;;;;OAIG;IACH,8FAA8F;IAC9F,MAAM,CAAC,OAAO,CAAC,GAAW,EAAE,IAA0B;QAClD,MAAM,eAAe,GAAG,GAAG,CAAC,OAAO,CAAC,qCAAqC,EAAE,IAAI,CAAC,CAAC;QACjF,MAAM,OAAO,GAAG,eAAe,CAAC,OAAO,CAAC,GAAG,CAAC,CAAC;QAC7C,IAAI,OAAO,GAAG,CAAC,IAAI,IAAI,KAAK,SAAS;YAAE,OAAO,eAAe,CAAC;QAC9D,MAAM,KAAK,GAAG,eAAe;aACxB,KAAK,CAAC,OAAO,GAAG,CAAC,CAAC;aAClB,KAAK,CAAC,GAAG,CAAC;aACV,GAAG,CAAC,CAAC,IAAY,EAAE,EAAE;YAClB,MAAM,MAAM,GAAG,IAAI,CAAC,OAAO,CAAC,GAAG,CAAC,CAAC;YACjC,MAAM,GAAG,GAAG,MAAM,GAAG,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC,EAAE,MAAM,CAAC,CAAC;YACtD,OAAO,IAAI,CAAC,OAAO,CAAC,aAAa,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC,KAAK,SAAS,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,GAAG,GAAG,QAAQ,CAAC;QACzF,CAAC,CAAC;aACD,IAAI,CAAC,GAAG,CAAC,CAAC;QACf,OAAO,GAAG,eAAe,CAAC,KAAK,CAAC,CAAC,EAAE,OAAO,CAAC,IAAI,KAAK,EAAE,CAAC;IAC3D,CAAC;IAED,8FAA8F;IACtF,MAAM,CAAC,MAAM,CAAC,GAAW;QAC7B,uFAAuF;QACvF,OAAO,mBAAmB,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,kBAAkB,CAAC,GAAG,CAAC,CAAC;IACzE,CAAC;CACJ;AArCD,sCAqCC;AAED;;;;;GAKG;AACH,MAAa,mBAAmB;IAC5B,KAAK,GAA8B,SAAS,CAAC;CAChD;AAFD,kDAEC;AAED,MAAa,WAAW;IAIP;IACA;IAEA;IAcA;IAUA;IAGA;IAEA;IAOA;IA1Cb;IACI;oCACgC;IACvB,MAAqB,EACrB,IAAsB;IAC/B,sDAAsD;IAC7C,MAAkB;IAC3B;;;;;;;;;;;;OAYG;IACM,UAAmB;IAC5B;;;;;;;;OAQG;IACM,WAAoB;IAC7B;yFACqF;IAC5E,YAAqB;IAC9B,4FAA4F;IACnF,aAAsB;IAC/B;;;;;OAKG;IACM,IAAoB;QAvCpB,WAAM,GAAN,MAAM,CAAe;QACrB,SAAI,GAAJ,IAAI,CAAkB;QAEtB,WAAM,GAAN,MAAM,CAAY;QAclB,eAAU,GAAV,UAAU,CAAS;QAUnB,gBAAW,GAAX,WAAW,CAAS;QAGpB,iBAAY,GAAZ,YAAY,CAAS;QAErB,kBAAa,GAAb,aAAa,CAAS;QAOtB,SAAI,GAAJ,IAAI,CAAgB;IAC9B,CAAC;CACP;AA7CD,kCA6CC","sourcesContent":["import { ApiMethodInfo } from './ApiMethodInfo';\nimport { MaskSpec } from './LogFieldMask';\n\n/**\n * ApiCallInfo - the structured tag stamped into RequestContext around every API call\n * (by {@link LogApiCallImpl}), so ANY log line emitted during the call inherits a filterable\n * `api` object rather than only the req/resp text lines.\n *\n * The node logging backends (winston/bunyan) read this struct out of context via\n * `RequestContext.buildStructuredLogFields()` and emit it AS AN OBJECT under `jsonPayload.api`,\n * which unlocks GCP Cloud Logging filters like:\n * - `jsonPayload.api.method.side=\"client\"` — every outbound call this process made\n * - `jsonPayload.api.method.side=\"server\"` — every inbound call it handled\n * - `jsonPayload.api.method.apiClass=\"SaveApi\"` — one logical method, BOTH sides (client + server)\n * - `jsonPayload.api.result=\"failure\"` — failed exchanges only\n * - `jsonPayload.api.durationMs>1000` — slow calls, either side\n * - `jsonPayload.api.responseSize>100000` — the fat responses (the ones that get chunked)\n * - `jsonPayload.api.responseCount>100` — responses containing many logical items\n * - `jsonPayload.api:*` — \"API traffic only\" (tracing + the recorder)\n * - `jsonPayload.api.http.status=404` — an HTTP client call the vendor answered 404 (#1339)\n *\n * IMPORTANT: the field names here (and on the nested {@link ApiMethodInfo}) ARE the GCP field names —\n * rename a field and the filter renames with it. The identity lives NESTED under `api.method`\n * (`api.method.{side,apiClass,methodName,controllerName}`); `api.type` and `api.result` sit at the top.\n *\n * NOTE: the request `httpMethod`/`path` are NOT here — an inbound request stamps them as the separate\n * top-level logged keys `jsonPayload.httpMethod` / `jsonPayload.requestPath` (see\n * {@link WebpiecesCoreHeaders} + `RequestContextHeaders.fillFromRequest`). Outbound client calls have\n * no inbound path, so they carry only the `api` identity.\n *\n * Per-hop only: the underlying `API_CALL_INFO` ContextKey is NOT transferred over the wire, so a\n * downstream server stamps its own `side:'server'` rather than inheriting the caller's `side:'client'`.\n *\n * Per CLAUDE.md: data-only structures are classes, not interfaces.\n */\n\n/** Which half of the exchange this tag describes: the outgoing 'request' or the returning 'response'. */\n/**\n * Which HALF of one api call a log line describes.\n *\n * Renamed from `ApiType`, which collided with the `@ApiType(...)` CONTRACT decorator and read as if\n * it were about the kind of API rather than the direction of one line. Per\n * `.claude/rules/no-backwards-compat.md` the old spelling is deleted rather than aliased, so a caller\n * on the old name stops compiling and the error names this one.\n */\nexport type ApiCallDirection = 'request' | 'response';\n\n/**\n * Response outcome. 'success' covers 2xx AND user errors (400/401/403/404/266 — a successfully\n * handled \"you made a mistake\"); 'failure' is a genuine server error. `success` = {@link CallOutcome.EXPECTED} from {@link ApiRegistry.classify}.\n */\nexport type ApiResult = 'success' | 'failure';\n\n/** Re-exported from {@link ApiMethodInfo} (its true home) so existing `ApiSide` imports keep working. */\nexport type { ApiSide } from './ApiMethodInfo';\n\n/**\n * The HTTP facts of ONE client call's last transmission — surfaces as `jsonPayload.api.http.*`.\n * Filled ONLY by the HTTP client (node and browser), which is the one layer that has a verb, a url\n * and a status; in-process, pubsub and cloud-task calls leave {@link ApiCallInfo.http} unset. It\n * exists because a vendor's 404 and its 500 were indistinguishable in the logs when only the DTOs\n * were recorded.\n *\n * Never headers or tokens: the url is MASKED ({@link maskUrl}) before it gets here.\n */\nexport class HttpCallFacts {\n constructor(\n /** The HTTP verb, e.g. `GET`. */\n readonly method: string,\n /** The final absolute url, masked: no userinfo, and `@MaskLog` field names' query values redacted. */\n readonly url: string,\n /** The status of the last response received. */\n readonly status: number,\n ) {}\n\n /**\n * The url as it may be logged: any `user:password@` userinfo is dropped, and the value of every\n * query key the endpoint's `@MaskLog` names is replaced with `*****` (a `?token=` the DTO mask\n * already hides must not reappear in the url).\n */\n // webpieces-disable no-function-outside-class -- pure static helper beside the data it builds\n static maskUrl(url: string, mask: MaskSpec | undefined): string {\n const withoutUserInfo = url.replace(/^([a-z][a-z0-9+.-]*:\\/\\/)[^/@?#]*@/i, '$1');\n const queryAt = withoutUserInfo.indexOf('?');\n if (queryAt < 0 || mask === undefined) return withoutUserInfo;\n const query = withoutUserInfo\n .slice(queryAt + 1)\n .split('&')\n .map((pair: string) => {\n const equals = pair.indexOf('=');\n const key = equals < 0 ? pair : pair.slice(0, equals);\n return mask.modeFor(HttpCallFacts.decode(key)) === undefined ? pair : `${key}=*****`;\n })\n .join('&');\n return `${withoutUserInfo.slice(0, queryAt)}?${query}`;\n }\n\n // webpieces-disable no-function-outside-class -- pure static helper beside the data it builds\n private static decode(key: string): string {\n // A malformed escape is logged as written; it can never be a masked field name anyway.\n return /%(?![0-9a-f]{2})/i.test(key) ? key : decodeURIComponent(key);\n }\n}\n\n/**\n * The per-call slot the HTTP client fills with {@link HttpCallFacts} as each response arrives, and\n * {@link LogApiCallImpl} reads when it stamps the response tag. A holder rather than a return value\n * because the facts are known DEEP in the transport while the tag is written at the top of the call;\n * the last transmission (after redirects, a 401 refresh or a retry) wins.\n */\nexport class HttpCallFactsHolder {\n facts: HttpCallFacts | undefined = undefined;\n}\n\nexport class ApiCallInfo {\n constructor(\n /** The call identity (side, apiClass, methodName, controllerName) — surfaces nested under\n * `jsonPayload.api.method`. */\n readonly method: ApiMethodInfo,\n readonly type: ApiCallDirection,\n /** Response only — undefined on the 'request' tag. */\n readonly result?: ApiResult,\n /**\n * Wall-clock milliseconds the call took. RESPONSE tag only (undefined on 'request') — a\n * request has no duration yet. Present on BOTH the success and failure paths, so\n * `jsonPayload.api.durationMs>1000 AND api.result=\"failure\"` finds slow failures.\n *\n * There is deliberately no top-level `statusCode` beside this. LogApiCall runs deep in the\n * stack over in-process calls, pubsub handlers, and cloud-task enqueues — none of which have\n * an HTTP status — and business logic must not know about HTTP. `result` (see {@link ApiResult})\n * is the transport-neutral outcome, exactly as {@link ApiRegistry.classify} (a {@link CallOutcome}) classifies by\n * portable Error TYPE rather than by status code. That decision STANDS (#1339): the HTTP\n * client's status lives in the optional, nested {@link http} — present only where there IS an\n * HTTP exchange, so nothing transport-neutral ever reads it.\n */\n readonly durationMs?: number,\n /**\n * Bytes of the serialized request DTO. Stamped on BOTH tags: the 'request' tag reports it as\n * soon as it is known, and the 'response' tag repeats it so one record shows the whole\n * exchange (`api.requestSize` + `api.responseSize` without a join).\n *\n * This is the TOTAL size of the body, measured BEFORE any log chunking — chunking is a\n * transport concern handled by the GCP backends, and a body split across 3 records still\n * reports its one true size here.\n */\n readonly requestSize?: number,\n /** Bytes of the serialized response. RESPONSE tag only, and only when the call succeeded —\n * a thrown error produced no response body to measure. Total size, pre-chunking. */\n readonly responseSize?: number,\n /** Number of logical items in a successful response. RESPONSE tag only, opt-in per call. */\n readonly responseCount?: number,\n /**\n * The HTTP verb, masked url and status of the call's last transmission — `jsonPayload.api.http.*`.\n * RESPONSE tag only, and only for a call made by the HTTP client (success AND failure, so\n * `api.result=\"failure\" AND api.http.status=429` finds a vendor throttling us). Unset for\n * in-process, pubsub and cloud-task calls. See {@link HttpCallFacts}.\n */\n readonly http?: HttpCallFacts,\n ) {}\n}\n"]}
|
|
@@ -50,7 +50,7 @@ export declare class ApiMethodInfo {
|
|
|
50
50
|
readonly allowUpgradeInFlight: boolean;
|
|
51
51
|
/**
|
|
52
52
|
* The contract CLASS (the `@ApiPath` contract, or an SDK-wrapping api-client's abstract
|
|
53
|
-
* `XxxApi`) — the key {@link
|
|
53
|
+
* `XxxApi`) — the key {@link ApiRegistry.classify} resolves the `CallScope` ladder on
|
|
54
54
|
* (method -> API -> ALL). {@link apiClass} stays the LOGGED name. REQUIRED as a constructor
|
|
55
55
|
* argument so every caller states it, and never `undefined`: an SDK-wrapping client passes its
|
|
56
56
|
* abstract `XxxApi`, an IPC hop its `@WpInternal` class, and an MCP protocol step (which has no
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"ApiMethodInfo.js","sourceRoot":"","sources":["../../../../../../packages/core/core-util/src/http/ApiMethodInfo.ts"],"names":[],"mappings":";AAAA;;;;;;;;;;;;;;;;;GAiBG;;;AAOH,MAAa,aAAa;IAgBT;IAGA;IAEA;IAKA;IAMA;IAKA;IAIA;IAIA;IA9Bb,YACa,IAAa;IACtB;2FACuF;IAC9E,QAAgB;IACzB,2DAA2D;IAClD,UAAkB;IAC3B,yEAAyE;IACzE,QAAkB;IAClB;gGAC4F;IACnF,cAAuB;IAChC;;;;wGAIoG;IAC3F,IAAe;IACxB;;;sGAGkG;IACzF,YAAqB,KAAK;IACnC;;+BAE2B;IAClB,eAAwB,KAAK;IACtC;;qCAEiC;IACxB,uBAAgC,KAAK;QA7BrC,SAAI,GAAJ,IAAI,CAAS;QAGb,aAAQ,GAAR,QAAQ,CAAQ;QAEhB,eAAU,GAAV,UAAU,CAAQ;QAKlB,mBAAc,GAAd,cAAc,CAAS;QAMvB,SAAI,GAAJ,IAAI,CAAW;QAKf,cAAS,GAAT,SAAS,CAAiB;QAI1B,iBAAY,GAAZ,YAAY,CAAiB;QAI7B,yBAAoB,GAApB,oBAAoB,CAAiB;QAE9C,MAAM,CAAC,cAAc,CAAC,IAAI,EAAE,UAAU,EAAE,EAAE,KAAK,EAAE,QAAQ,EAAE,UAAU,EAAE,KAAK,EAAE,CAAC,CAAC;IACpF,CAAC;CACJ;AAjDD,sCAiDC","sourcesContent":["/**\n * ApiMethodInfo - the transport-neutral identity of a single API method call, handed to\n * {@link LogApiCallImpl.execute} by every caller (server inbound, http/in-process clients, cloud tasks,\n * and external wrapped clients like the firestore admin client).\n *\n * WHY generic (not RouteMetadata): LogApiCall runs deep in the stack over MANY shapes — HTTP routes,\n * pubsub/queue enqueues, and multi-param external clients returning Promise<void>/Promise<unknown>.\n * None of those own an `httpMethod`/`path`, so LogApiCall must not depend on the HTTP-shaped\n * `RouteMetadata`. This carries only what identifies the call.\n *\n * MATCHING is the point of {@link apiClass}: a CLIENT call and the SERVER handler for the same logical\n * method must log the SAME identity so `jsonPayload.api.method.apiClass=\"SaveApi\"` filters both sides\n * together. The API CONTRACT class name (e.g. 'SaveApi') is available on both sides — the client only\n * ever knows it, and the server carries it alongside its impl name — so it, not the server's impl\n * class, is the required key.\n *\n * Per CLAUDE.md: data-only structures are classes, not interfaces.\n */\n\nimport {MaskSpec} from \"./LogFieldMask\";\n\n/** Which end of the exchange this process is: the caller ('client') or the handler ('server'). */\nexport type ApiSide = 'client' | 'server';\n\nexport class ApiMethodInfo {\n /**\n * The contract CLASS (the `@ApiPath` contract, or an SDK-wrapping api-client's abstract\n * `XxxApi`) — the key {@link
|
|
1
|
+
{"version":3,"file":"ApiMethodInfo.js","sourceRoot":"","sources":["../../../../../../packages/core/core-util/src/http/ApiMethodInfo.ts"],"names":[],"mappings":";AAAA;;;;;;;;;;;;;;;;;GAiBG;;;AAOH,MAAa,aAAa;IAgBT;IAGA;IAEA;IAKA;IAMA;IAKA;IAIA;IAIA;IA9Bb,YACa,IAAa;IACtB;2FACuF;IAC9E,QAAgB;IACzB,2DAA2D;IAClD,UAAkB;IAC3B,yEAAyE;IACzE,QAAkB;IAClB;gGAC4F;IACnF,cAAuB;IAChC;;;;wGAIoG;IAC3F,IAAe;IACxB;;;sGAGkG;IACzF,YAAqB,KAAK;IACnC;;+BAE2B;IAClB,eAAwB,KAAK;IACtC;;qCAEiC;IACxB,uBAAgC,KAAK;QA7BrC,SAAI,GAAJ,IAAI,CAAS;QAGb,aAAQ,GAAR,QAAQ,CAAQ;QAEhB,eAAU,GAAV,UAAU,CAAQ;QAKlB,mBAAc,GAAd,cAAc,CAAS;QAMvB,SAAI,GAAJ,IAAI,CAAW;QAKf,cAAS,GAAT,SAAS,CAAiB;QAI1B,iBAAY,GAAZ,YAAY,CAAiB;QAI7B,yBAAoB,GAApB,oBAAoB,CAAiB;QAE9C,MAAM,CAAC,cAAc,CAAC,IAAI,EAAE,UAAU,EAAE,EAAE,KAAK,EAAE,QAAQ,EAAE,UAAU,EAAE,KAAK,EAAE,CAAC,CAAC;IACpF,CAAC;CACJ;AAjDD,sCAiDC","sourcesContent":["/**\n * ApiMethodInfo - the transport-neutral identity of a single API method call, handed to\n * {@link LogApiCallImpl.execute} by every caller (server inbound, http/in-process clients, cloud tasks,\n * and external wrapped clients like the firestore admin client).\n *\n * WHY generic (not RouteMetadata): LogApiCall runs deep in the stack over MANY shapes — HTTP routes,\n * pubsub/queue enqueues, and multi-param external clients returning Promise<void>/Promise<unknown>.\n * None of those own an `httpMethod`/`path`, so LogApiCall must not depend on the HTTP-shaped\n * `RouteMetadata`. This carries only what identifies the call.\n *\n * MATCHING is the point of {@link apiClass}: a CLIENT call and the SERVER handler for the same logical\n * method must log the SAME identity so `jsonPayload.api.method.apiClass=\"SaveApi\"` filters both sides\n * together. The API CONTRACT class name (e.g. 'SaveApi') is available on both sides — the client only\n * ever knows it, and the server carries it alongside its impl name — so it, not the server's impl\n * class, is the required key.\n *\n * Per CLAUDE.md: data-only structures are classes, not interfaces.\n */\n\nimport {MaskSpec} from \"./LogFieldMask\";\n\n/** Which end of the exchange this process is: the caller ('client') or the handler ('server'). */\nexport type ApiSide = 'client' | 'server';\n\nexport class ApiMethodInfo {\n /**\n * The contract CLASS (the `@ApiPath` contract, or an SDK-wrapping api-client's abstract\n * `XxxApi`) — the key {@link ApiRegistry.classify} resolves the `CallScope` ladder on\n * (method -> API -> ALL). {@link apiClass} stays the LOGGED name. REQUIRED as a constructor\n * argument so every caller states it, and never `undefined`: an SDK-wrapping client passes its\n * abstract `XxxApi`, an IPC hop its `@WpInternal` class, and an MCP protocol step (which has no\n * user contract) the framework-owned `McpProtocol` class — which reaches the ALL rung unless\n * somebody registers for that class.\n *\n * NON-enumerable on purpose: this object is nested into every `[API-*]` log record as\n * `jsonPayload.api.method`, and a class reference is not log data.\n */\n declare readonly contract: Function;\n\n constructor(\n readonly side: ApiSide,\n /** REQUIRED — the API CONTRACT class name (e.g. 'SaveApi'). Matches client + server so both\n * sides of one logical call filter together via `jsonPayload.api.method.apiClass`. */\n readonly apiClass: string,\n /** REQUIRED — the method on the contract (e.g. 'save'). */\n readonly methodName: string,\n /** REQUIRED — the contract CLASS; see {@link ApiMethodInfo.contract}. */\n contract: Function,\n /** OPTIONAL — server-side impl class (e.g. 'SaveController'). Absent on clients, which have no\n * impl. Surfaces as `jsonPayload.api.method.controllerName` for server-only drill-down. */\n readonly controllerName?: string,\n /** OPTIONAL — which DTO fields to mask in the log lines, and how. Absent = log the DTO\n * verbatim exactly as before (the fast path, no per-field walk). Present = {@link LogApiCallImpl}\n * runs {@link maskedStringify} so a declared-sensitive field (an OAuth refresh token, an\n * id-token JWT) is masked in the log while the REAL value still travels on the wire untouched.\n * Rides the call identity so the spec travels with the api definition, not a global app config. */\n readonly mask?: MaskSpec,\n /** OPTIONAL — true when the route declared `@Endpoint(..., { noLogging: true })` (a log\n * shipper, a log-ingest endpoint): {@link LogApiCallImpl} emits NO `[API-*-req]`/`[API-*-resp-*]`\n * line and stamps no {@link ApiCallInfo} tag for it. Read once at route-build time and handed\n * here, exactly as {@link mask} is. Default (absent/false) = the ordinary, fully-logged call. */\n readonly noLogging: boolean = false,\n /** OPTIONAL — `@Endpoint(..., { hideProgress: true })`, carried from the route so the whole\n * call identity travels together. webpieces itself does not act on it; an app's progress UI\n * does. Default false. */\n readonly hideProgress: boolean = false,\n /** OPTIONAL — `@Endpoint(..., { allowUpgradeInFlight: true })`, carried from the route so the\n * whole call identity travels together. webpieces itself does not act on it; an app's upgrade\n * logic does. Default false. */\n readonly allowUpgradeInFlight: boolean = false,\n ) {\n Object.defineProperty(this, 'contract', { value: contract, enumerable: false });\n }\n}\n"]}
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
import { ErrorTranslator } from './ErrorTranslator';
|
|
2
|
+
import { CallOutcome, ErrorClassifier } from './ErrorClassifier';
|
|
3
|
+
import { CallScope } from './CallScope';
|
|
4
|
+
import { ApiMethodInfo } from './ApiMethodInfo';
|
|
5
|
+
/**
|
|
6
|
+
* ApiRegistry - the startup settings BOTH sides of an API call read: the {@link ErrorTranslator}
|
|
7
|
+
* (the server's `toWire`, every client's `fromWire`, IPC and MCP) and the {@link ErrorClassifier}
|
|
8
|
+
* (the server's `LogApiFilter`, `LogApiCall`, `RetryStrategy`, the MCP renderer, Cloud Tasks). What
|
|
9
|
+
* only an OUTBOUND call reads — URLs, timeouts, strategies — lives on `ClientRegistry` instead.
|
|
10
|
+
*
|
|
11
|
+
* Every setting is scoped by a {@link CallScope} and resolved method -> API -> ALL -> the webpieces
|
|
12
|
+
* default. Process-global, populated once at startup on the SERVER and in the BROWSER, with no DI
|
|
13
|
+
* wiring (like `HeaderRegistry` / `LogManager`).
|
|
14
|
+
*
|
|
15
|
+
* ```ts
|
|
16
|
+
* ApiRegistry.setErrorTranslator(new AppErrorTranslator(), CallScope.all());
|
|
17
|
+
* ApiRegistry.setErrorTranslator(new VendorErrorTranslator(), CallScope.api(VendorApi));
|
|
18
|
+
* ApiRegistry.setErrorClassifier(new CompanyClassifier(), CallScope.all());
|
|
19
|
+
* ```
|
|
20
|
+
*/
|
|
21
|
+
export declare class ApiRegistry {
|
|
22
|
+
/**
|
|
23
|
+
* The {@link ErrorTranslator} per {@link CallScope} — method -> API -> ALL, and below every rung
|
|
24
|
+
* {@link WEBPIECES_DEFAULT_ERROR_TRANSLATOR}, so {@link ApiRegistry.getErrorTranslator} is never
|
|
25
|
+
* undefined.
|
|
26
|
+
*
|
|
27
|
+
* ONE translator per scope, not a list, on purpose: an app that has several layers of error
|
|
28
|
+
* policy for one scope composes them INSIDE its own `toWire`, where the precedence is written
|
|
29
|
+
* down. The ladder exists because vendors put their errors on the wire in their OWN formats — a
|
|
30
|
+
* per-API translator reads one vendor's 4xx body (`fromWire`), or writes the error body an
|
|
31
|
+
* inbound vendor expects back (`toWire`), while every other contract keeps the app's ALL one.
|
|
32
|
+
*/
|
|
33
|
+
private static readonly errorTranslators;
|
|
34
|
+
/**
|
|
35
|
+
* The {@link ErrorClassifier} per {@link CallScope} — method -> API -> ALL, and below every rung
|
|
36
|
+
* {@link WEBPIECES_DEFAULT_ERROR_CLASSIFIER}. A classifier that DEFERS (`undefined`) falls through
|
|
37
|
+
* to the next rung. The ALL classifier reads {@link ApiMethodInfo.side}, so one classifier covers
|
|
38
|
+
* the server router AND every internal client; an SDK-wrapping api-client registers its own at
|
|
39
|
+
* its abstract `XxxApi` class. See {@link ApiRegistry.classify}.
|
|
40
|
+
*/
|
|
41
|
+
private static readonly errorClassifiers;
|
|
42
|
+
/**
|
|
43
|
+
* Install an {@link ErrorTranslator} for one {@link CallScope}: `CallScope.all()` (the app's
|
|
44
|
+
* translator, server side AND every client side), one contract (`CallScope.api(VendorApi)`), or
|
|
45
|
+
* one method of one contract (`CallScope.method(VendorApi, 'sync')`). The scope is REQUIRED —
|
|
46
|
+
* there is no unscoped spelling. Call at startup, on the server AND in the browser.
|
|
47
|
+
*
|
|
48
|
+
* `set`, not `add`: see the field doc above for why a per-scope LIST is not wanted. A translator
|
|
49
|
+
* declines an error by DELEGATING to {@link WebpiecesDefaultErrorTranslator} — not to the next
|
|
50
|
+
* rung.
|
|
51
|
+
*/
|
|
52
|
+
static setErrorTranslator(translator: ErrorTranslator, scope: CallScope): void;
|
|
53
|
+
/**
|
|
54
|
+
* THE {@link ErrorTranslator} for one call — method -> API -> ALL ->
|
|
55
|
+
* {@link WEBPIECES_DEFAULT_ERROR_TRANSLATOR}, never `undefined`, so a caller never branches.
|
|
56
|
+
*
|
|
57
|
+
* Both arguments are required so every call site states what it knows: the client and the server
|
|
58
|
+
* pass the route's contract class and method; a failure with no route (an unmatched request, a
|
|
59
|
+
* failure before routing) passes `undefined, undefined` and reaches the ALL rung alone.
|
|
60
|
+
*/
|
|
61
|
+
static getErrorTranslator(api: Function | undefined, methodName: string | undefined): ErrorTranslator;
|
|
62
|
+
/**
|
|
63
|
+
* Install an {@link ErrorClassifier} for one {@link CallScope}: `CallScope.all()` (the
|
|
64
|
+
* app/company classifier — it reads {@link ApiMethodInfo.side}, so it covers the server router
|
|
65
|
+
* and every internal client), one contract (an SDK-wrapping api-client keys by its abstract
|
|
66
|
+
* `XxxApi`), or one method of one contract. The scope is REQUIRED. Call at startup, on the server
|
|
67
|
+
* AND in the browser. See {@link ErrorClassifier}.
|
|
68
|
+
*/
|
|
69
|
+
static setErrorClassifier(classifier: ErrorClassifier, scope: CallScope): void;
|
|
70
|
+
/**
|
|
71
|
+
* The {@link CallOutcome} of a thrown API-call error. Resolved on the {@link CallScope} ladder of
|
|
72
|
+
* `methodInfo.contract` / `methodInfo.methodName`, most specific first, and ALWAYS definitive:
|
|
73
|
+
* 1. the method classifier — unless it defers;
|
|
74
|
+
* 2. else the API classifier — unless it defers;
|
|
75
|
+
* 3. else the ALL classifier — unless it defers;
|
|
76
|
+
* 4. else the webpieces built-in ({@link WEBPIECES_DEFAULT_ERROR_CLASSIFIER}), which never defers.
|
|
77
|
+
*
|
|
78
|
+
* The ONE verdict read by `RetryStrategy` (retry only TRANSIENT), `ClientRegistry`'s per-attempt
|
|
79
|
+
* log line, `LogApiCall` (EXPECTED → OTHER, else FAIL) and the MCP renderer's retry advice.
|
|
80
|
+
*/
|
|
81
|
+
static classify(error: Error, methodInfo: ApiMethodInfo): CallOutcome;
|
|
82
|
+
/**
|
|
83
|
+
* Put the process-globals back to the state a fresh process starts in. For TESTS, so one spec
|
|
84
|
+
* cannot leak its registrations into the next.
|
|
85
|
+
*
|
|
86
|
+
* It empties every translator and classifier rung, which RESTORES the webpieces defaults:
|
|
87
|
+
* `getErrorTranslator(...)` and `classify(...)` fall through to them below every rung, so
|
|
88
|
+
* "cleared" is not a state they have.
|
|
89
|
+
*/
|
|
90
|
+
static resetForTests(): void;
|
|
91
|
+
}
|
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.ApiRegistry = void 0;
|
|
4
|
+
const CallScope_1 = require("./CallScope");
|
|
5
|
+
const WebpiecesDefaultErrorClassifier_1 = require("./WebpiecesDefaultErrorClassifier");
|
|
6
|
+
const WebpiecesDefaultErrorTranslator_1 = require("./WebpiecesDefaultErrorTranslator");
|
|
7
|
+
/** One {@link CallScope} rung of the error-translator ladder. Data only, so a class. */
|
|
8
|
+
class TranslatorSlot {
|
|
9
|
+
translator;
|
|
10
|
+
}
|
|
11
|
+
/** One {@link CallScope} rung of the error-classifier ladder. Data only, so a class. */
|
|
12
|
+
class ClassifierSlot {
|
|
13
|
+
classifier;
|
|
14
|
+
}
|
|
15
|
+
/**
|
|
16
|
+
* ApiRegistry - the startup settings BOTH sides of an API call read: the {@link ErrorTranslator}
|
|
17
|
+
* (the server's `toWire`, every client's `fromWire`, IPC and MCP) and the {@link ErrorClassifier}
|
|
18
|
+
* (the server's `LogApiFilter`, `LogApiCall`, `RetryStrategy`, the MCP renderer, Cloud Tasks). What
|
|
19
|
+
* only an OUTBOUND call reads — URLs, timeouts, strategies — lives on `ClientRegistry` instead.
|
|
20
|
+
*
|
|
21
|
+
* Every setting is scoped by a {@link CallScope} and resolved method -> API -> ALL -> the webpieces
|
|
22
|
+
* default. Process-global, populated once at startup on the SERVER and in the BROWSER, with no DI
|
|
23
|
+
* wiring (like `HeaderRegistry` / `LogManager`).
|
|
24
|
+
*
|
|
25
|
+
* ```ts
|
|
26
|
+
* ApiRegistry.setErrorTranslator(new AppErrorTranslator(), CallScope.all());
|
|
27
|
+
* ApiRegistry.setErrorTranslator(new VendorErrorTranslator(), CallScope.api(VendorApi));
|
|
28
|
+
* ApiRegistry.setErrorClassifier(new CompanyClassifier(), CallScope.all());
|
|
29
|
+
* ```
|
|
30
|
+
*/
|
|
31
|
+
class ApiRegistry {
|
|
32
|
+
/**
|
|
33
|
+
* The {@link ErrorTranslator} per {@link CallScope} — method -> API -> ALL, and below every rung
|
|
34
|
+
* {@link WEBPIECES_DEFAULT_ERROR_TRANSLATOR}, so {@link ApiRegistry.getErrorTranslator} is never
|
|
35
|
+
* undefined.
|
|
36
|
+
*
|
|
37
|
+
* ONE translator per scope, not a list, on purpose: an app that has several layers of error
|
|
38
|
+
* policy for one scope composes them INSIDE its own `toWire`, where the precedence is written
|
|
39
|
+
* down. The ladder exists because vendors put their errors on the wire in their OWN formats — a
|
|
40
|
+
* per-API translator reads one vendor's 4xx body (`fromWire`), or writes the error body an
|
|
41
|
+
* inbound vendor expects back (`toWire`), while every other contract keeps the app's ALL one.
|
|
42
|
+
*/
|
|
43
|
+
static errorTranslators = new CallScope_1.CallScopeLadder(() => new TranslatorSlot());
|
|
44
|
+
/**
|
|
45
|
+
* The {@link ErrorClassifier} per {@link CallScope} — method -> API -> ALL, and below every rung
|
|
46
|
+
* {@link WEBPIECES_DEFAULT_ERROR_CLASSIFIER}. A classifier that DEFERS (`undefined`) falls through
|
|
47
|
+
* to the next rung. The ALL classifier reads {@link ApiMethodInfo.side}, so one classifier covers
|
|
48
|
+
* the server router AND every internal client; an SDK-wrapping api-client registers its own at
|
|
49
|
+
* its abstract `XxxApi` class. See {@link ApiRegistry.classify}.
|
|
50
|
+
*/
|
|
51
|
+
static errorClassifiers = new CallScope_1.CallScopeLadder(() => new ClassifierSlot());
|
|
52
|
+
/**
|
|
53
|
+
* Install an {@link ErrorTranslator} for one {@link CallScope}: `CallScope.all()` (the app's
|
|
54
|
+
* translator, server side AND every client side), one contract (`CallScope.api(VendorApi)`), or
|
|
55
|
+
* one method of one contract (`CallScope.method(VendorApi, 'sync')`). The scope is REQUIRED —
|
|
56
|
+
* there is no unscoped spelling. Call at startup, on the server AND in the browser.
|
|
57
|
+
*
|
|
58
|
+
* `set`, not `add`: see the field doc above for why a per-scope LIST is not wanted. A translator
|
|
59
|
+
* declines an error by DELEGATING to {@link WebpiecesDefaultErrorTranslator} — not to the next
|
|
60
|
+
* rung.
|
|
61
|
+
*/
|
|
62
|
+
// webpieces-disable no-function-outside-class -- static global singleton (like HeaderRegistry/LogManager); populated once at startup, never DI-injected
|
|
63
|
+
static setErrorTranslator(translator, scope) {
|
|
64
|
+
ApiRegistry.errorTranslators.at(scope).translator = translator;
|
|
65
|
+
}
|
|
66
|
+
/**
|
|
67
|
+
* THE {@link ErrorTranslator} for one call — method -> API -> ALL ->
|
|
68
|
+
* {@link WEBPIECES_DEFAULT_ERROR_TRANSLATOR}, never `undefined`, so a caller never branches.
|
|
69
|
+
*
|
|
70
|
+
* Both arguments are required so every call site states what it knows: the client and the server
|
|
71
|
+
* pass the route's contract class and method; a failure with no route (an unmatched request, a
|
|
72
|
+
* failure before routing) passes `undefined, undefined` and reaches the ALL rung alone.
|
|
73
|
+
*/
|
|
74
|
+
// webpieces-disable no-function-outside-class -- static global singleton (like HeaderRegistry/LogManager); populated once at startup, never DI-injected
|
|
75
|
+
static getErrorTranslator(api, methodName) {
|
|
76
|
+
const registered = ApiRegistry.errorTranslators
|
|
77
|
+
.rungs(api, methodName)
|
|
78
|
+
.find((slot) => slot.translator !== undefined)?.translator;
|
|
79
|
+
return registered ?? WebpiecesDefaultErrorTranslator_1.WEBPIECES_DEFAULT_ERROR_TRANSLATOR;
|
|
80
|
+
}
|
|
81
|
+
/**
|
|
82
|
+
* Install an {@link ErrorClassifier} for one {@link CallScope}: `CallScope.all()` (the
|
|
83
|
+
* app/company classifier — it reads {@link ApiMethodInfo.side}, so it covers the server router
|
|
84
|
+
* and every internal client), one contract (an SDK-wrapping api-client keys by its abstract
|
|
85
|
+
* `XxxApi`), or one method of one contract. The scope is REQUIRED. Call at startup, on the server
|
|
86
|
+
* AND in the browser. See {@link ErrorClassifier}.
|
|
87
|
+
*/
|
|
88
|
+
// webpieces-disable no-function-outside-class -- static global singleton (like HeaderRegistry/LogManager); populated once at startup, never DI-injected
|
|
89
|
+
static setErrorClassifier(classifier, scope) {
|
|
90
|
+
ApiRegistry.errorClassifiers.at(scope).classifier = classifier;
|
|
91
|
+
}
|
|
92
|
+
/**
|
|
93
|
+
* The {@link CallOutcome} of a thrown API-call error. Resolved on the {@link CallScope} ladder of
|
|
94
|
+
* `methodInfo.contract` / `methodInfo.methodName`, most specific first, and ALWAYS definitive:
|
|
95
|
+
* 1. the method classifier — unless it defers;
|
|
96
|
+
* 2. else the API classifier — unless it defers;
|
|
97
|
+
* 3. else the ALL classifier — unless it defers;
|
|
98
|
+
* 4. else the webpieces built-in ({@link WEBPIECES_DEFAULT_ERROR_CLASSIFIER}), which never defers.
|
|
99
|
+
*
|
|
100
|
+
* The ONE verdict read by `RetryStrategy` (retry only TRANSIENT), `ClientRegistry`'s per-attempt
|
|
101
|
+
* log line, `LogApiCall` (EXPECTED → OTHER, else FAIL) and the MCP renderer's retry advice.
|
|
102
|
+
*/
|
|
103
|
+
// webpieces-disable no-function-outside-class -- static global singleton (like HeaderRegistry/LogManager); populated once at startup, never DI-injected
|
|
104
|
+
static classify(error, methodInfo) {
|
|
105
|
+
const rungs = ApiRegistry.errorClassifiers.rungs(methodInfo.contract, methodInfo.methodName);
|
|
106
|
+
for (const slot of rungs) {
|
|
107
|
+
const verdict = slot.classifier?.classify(error, methodInfo);
|
|
108
|
+
if (verdict !== undefined) {
|
|
109
|
+
return verdict;
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
return WebpiecesDefaultErrorClassifier_1.WEBPIECES_DEFAULT_ERROR_CLASSIFIER.classify(error, methodInfo);
|
|
113
|
+
}
|
|
114
|
+
/**
|
|
115
|
+
* Put the process-globals back to the state a fresh process starts in. For TESTS, so one spec
|
|
116
|
+
* cannot leak its registrations into the next.
|
|
117
|
+
*
|
|
118
|
+
* It empties every translator and classifier rung, which RESTORES the webpieces defaults:
|
|
119
|
+
* `getErrorTranslator(...)` and `classify(...)` fall through to them below every rung, so
|
|
120
|
+
* "cleared" is not a state they have.
|
|
121
|
+
*/
|
|
122
|
+
// webpieces-disable no-function-outside-class -- static global singleton (like HeaderRegistry/LogManager); populated once at startup, never DI-injected
|
|
123
|
+
static resetForTests() {
|
|
124
|
+
ApiRegistry.errorTranslators.clear();
|
|
125
|
+
ApiRegistry.errorClassifiers.clear();
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
exports.ApiRegistry = ApiRegistry;
|
|
129
|
+
//# sourceMappingURL=ApiRegistry.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"ApiRegistry.js","sourceRoot":"","sources":["../../../../../../packages/core/core-util/src/http/ApiRegistry.ts"],"names":[],"mappings":";;;AAEA,2CAAyD;AACzD,uFAAuF;AACvF,uFAAuF;AAGvF,wFAAwF;AACxF,MAAM,cAAc;IAChB,UAAU,CAAmB;CAChC;AAED,wFAAwF;AACxF,MAAM,cAAc;IAChB,UAAU,CAAmB;CAChC;AAED;;;;;;;;;;;;;;;GAeG;AACH,MAAa,WAAW;IACpB;;;;;;;;;;OAUG;IACK,MAAM,CAAU,gBAAgB,GAAG,IAAI,2BAAe,CAC1D,GAAG,EAAE,CAAC,IAAI,cAAc,EAAE,CAC7B,CAAC;IAEF;;;;;;OAMG;IACK,MAAM,CAAU,gBAAgB,GAAG,IAAI,2BAAe,CAC1D,GAAG,EAAE,CAAC,IAAI,cAAc,EAAE,CAC7B,CAAC;IAEF;;;;;;;;;OASG;IACH,wJAAwJ;IACxJ,MAAM,CAAC,kBAAkB,CAAC,UAA2B,EAAE,KAAgB;QACnE,WAAW,CAAC,gBAAgB,CAAC,EAAE,CAAC,KAAK,CAAC,CAAC,UAAU,GAAG,UAAU,CAAC;IACnE,CAAC;IAED;;;;;;;OAOG;IACH,wJAAwJ;IACxJ,MAAM,CAAC,kBAAkB,CAAC,GAAyB,EAAE,UAA8B;QAC/E,MAAM,UAAU,GAAG,WAAW,CAAC,gBAAgB;aAC1C,KAAK,CAAC,GAAG,EAAE,UAAU,CAAC;aACtB,IAAI,CAAC,CAAC,IAAoB,EAAE,EAAE,CAAC,IAAI,CAAC,UAAU,KAAK,SAAS,CAAC,EAAE,UAAU,CAAC;QAC/E,OAAO,UAAU,IAAI,oEAAkC,CAAC;IAC5D,CAAC;IAED;;;;;;OAMG;IACH,wJAAwJ;IACxJ,MAAM,CAAC,kBAAkB,CAAC,UAA2B,EAAE,KAAgB;QACnE,WAAW,CAAC,gBAAgB,CAAC,EAAE,CAAC,KAAK,CAAC,CAAC,UAAU,GAAG,UAAU,CAAC;IACnE,CAAC;IAED;;;;;;;;;;OAUG;IACH,wJAAwJ;IACxJ,MAAM,CAAC,QAAQ,CAAC,KAAY,EAAE,UAAyB;QACnD,MAAM,KAAK,GAAG,WAAW,CAAC,gBAAgB,CAAC,KAAK,CAAC,UAAU,CAAC,QAAQ,EAAE,UAAU,CAAC,UAAU,CAAC,CAAC;QAC7F,KAAK,MAAM,IAAI,IAAI,KAAK,EAAE,CAAC;YACvB,MAAM,OAAO,GAAG,IAAI,CAAC,UAAU,EAAE,QAAQ,CAAC,KAAK,EAAE,UAAU,CAAC,CAAC;YAC7D,IAAI,OAAO,KAAK,SAAS,EAAE,CAAC;gBACxB,OAAO,OAAO,CAAC;YACnB,CAAC;QACL,CAAC;QACD,OAAO,oEAAkC,CAAC,QAAQ,CAAC,KAAK,EAAE,UAAU,CAAC,CAAC;IAC1E,CAAC;IAED;;;;;;;OAOG;IACH,wJAAwJ;IACxJ,MAAM,CAAC,aAAa;QAChB,WAAW,CAAC,gBAAgB,CAAC,KAAK,EAAE,CAAC;QACrC,WAAW,CAAC,gBAAgB,CAAC,KAAK,EAAE,CAAC;IACzC,CAAC;;AAzGL,kCA0GC","sourcesContent":["import { ErrorTranslator } from './ErrorTranslator';\nimport { CallOutcome, ErrorClassifier } from './ErrorClassifier';\nimport { CallScope, CallScopeLadder } from './CallScope';\nimport { WEBPIECES_DEFAULT_ERROR_CLASSIFIER } from './WebpiecesDefaultErrorClassifier';\nimport { WEBPIECES_DEFAULT_ERROR_TRANSLATOR } from './WebpiecesDefaultErrorTranslator';\nimport { ApiMethodInfo } from './ApiMethodInfo';\n\n/** One {@link CallScope} rung of the error-translator ladder. Data only, so a class. */\nclass TranslatorSlot {\n translator?: ErrorTranslator;\n}\n\n/** One {@link CallScope} rung of the error-classifier ladder. Data only, so a class. */\nclass ClassifierSlot {\n classifier?: ErrorClassifier;\n}\n\n/**\n * ApiRegistry - the startup settings BOTH sides of an API call read: the {@link ErrorTranslator}\n * (the server's `toWire`, every client's `fromWire`, IPC and MCP) and the {@link ErrorClassifier}\n * (the server's `LogApiFilter`, `LogApiCall`, `RetryStrategy`, the MCP renderer, Cloud Tasks). What\n * only an OUTBOUND call reads — URLs, timeouts, strategies — lives on `ClientRegistry` instead.\n *\n * Every setting is scoped by a {@link CallScope} and resolved method -> API -> ALL -> the webpieces\n * default. Process-global, populated once at startup on the SERVER and in the BROWSER, with no DI\n * wiring (like `HeaderRegistry` / `LogManager`).\n *\n * ```ts\n * ApiRegistry.setErrorTranslator(new AppErrorTranslator(), CallScope.all());\n * ApiRegistry.setErrorTranslator(new VendorErrorTranslator(), CallScope.api(VendorApi));\n * ApiRegistry.setErrorClassifier(new CompanyClassifier(), CallScope.all());\n * ```\n */\nexport class ApiRegistry {\n /**\n * The {@link ErrorTranslator} per {@link CallScope} — method -> API -> ALL, and below every rung\n * {@link WEBPIECES_DEFAULT_ERROR_TRANSLATOR}, so {@link ApiRegistry.getErrorTranslator} is never\n * undefined.\n *\n * ONE translator per scope, not a list, on purpose: an app that has several layers of error\n * policy for one scope composes them INSIDE its own `toWire`, where the precedence is written\n * down. The ladder exists because vendors put their errors on the wire in their OWN formats — a\n * per-API translator reads one vendor's 4xx body (`fromWire`), or writes the error body an\n * inbound vendor expects back (`toWire`), while every other contract keeps the app's ALL one.\n */\n private static readonly errorTranslators = new CallScopeLadder<TranslatorSlot>(\n () => new TranslatorSlot(),\n );\n\n /**\n * The {@link ErrorClassifier} per {@link CallScope} — method -> API -> ALL, and below every rung\n * {@link WEBPIECES_DEFAULT_ERROR_CLASSIFIER}. A classifier that DEFERS (`undefined`) falls through\n * to the next rung. The ALL classifier reads {@link ApiMethodInfo.side}, so one classifier covers\n * the server router AND every internal client; an SDK-wrapping api-client registers its own at\n * its abstract `XxxApi` class. See {@link ApiRegistry.classify}.\n */\n private static readonly errorClassifiers = new CallScopeLadder<ClassifierSlot>(\n () => new ClassifierSlot(),\n );\n\n /**\n * Install an {@link ErrorTranslator} for one {@link CallScope}: `CallScope.all()` (the app's\n * translator, server side AND every client side), one contract (`CallScope.api(VendorApi)`), or\n * one method of one contract (`CallScope.method(VendorApi, 'sync')`). The scope is REQUIRED —\n * there is no unscoped spelling. Call at startup, on the server AND in the browser.\n *\n * `set`, not `add`: see the field doc above for why a per-scope LIST is not wanted. A translator\n * declines an error by DELEGATING to {@link WebpiecesDefaultErrorTranslator} — not to the next\n * rung.\n */\n // webpieces-disable no-function-outside-class -- static global singleton (like HeaderRegistry/LogManager); populated once at startup, never DI-injected\n static setErrorTranslator(translator: ErrorTranslator, scope: CallScope): void {\n ApiRegistry.errorTranslators.at(scope).translator = translator;\n }\n\n /**\n * THE {@link ErrorTranslator} for one call — method -> API -> ALL ->\n * {@link WEBPIECES_DEFAULT_ERROR_TRANSLATOR}, never `undefined`, so a caller never branches.\n *\n * Both arguments are required so every call site states what it knows: the client and the server\n * pass the route's contract class and method; a failure with no route (an unmatched request, a\n * failure before routing) passes `undefined, undefined` and reaches the ALL rung alone.\n */\n // webpieces-disable no-function-outside-class -- static global singleton (like HeaderRegistry/LogManager); populated once at startup, never DI-injected\n static getErrorTranslator(api: Function | undefined, methodName: string | undefined): ErrorTranslator {\n const registered = ApiRegistry.errorTranslators\n .rungs(api, methodName)\n .find((slot: TranslatorSlot) => slot.translator !== undefined)?.translator;\n return registered ?? WEBPIECES_DEFAULT_ERROR_TRANSLATOR;\n }\n\n /**\n * Install an {@link ErrorClassifier} for one {@link CallScope}: `CallScope.all()` (the\n * app/company classifier — it reads {@link ApiMethodInfo.side}, so it covers the server router\n * and every internal client), one contract (an SDK-wrapping api-client keys by its abstract\n * `XxxApi`), or one method of one contract. The scope is REQUIRED. Call at startup, on the server\n * AND in the browser. See {@link ErrorClassifier}.\n */\n // webpieces-disable no-function-outside-class -- static global singleton (like HeaderRegistry/LogManager); populated once at startup, never DI-injected\n static setErrorClassifier(classifier: ErrorClassifier, scope: CallScope): void {\n ApiRegistry.errorClassifiers.at(scope).classifier = classifier;\n }\n\n /**\n * The {@link CallOutcome} of a thrown API-call error. Resolved on the {@link CallScope} ladder of\n * `methodInfo.contract` / `methodInfo.methodName`, most specific first, and ALWAYS definitive:\n * 1. the method classifier — unless it defers;\n * 2. else the API classifier — unless it defers;\n * 3. else the ALL classifier — unless it defers;\n * 4. else the webpieces built-in ({@link WEBPIECES_DEFAULT_ERROR_CLASSIFIER}), which never defers.\n *\n * The ONE verdict read by `RetryStrategy` (retry only TRANSIENT), `ClientRegistry`'s per-attempt\n * log line, `LogApiCall` (EXPECTED → OTHER, else FAIL) and the MCP renderer's retry advice.\n */\n // webpieces-disable no-function-outside-class -- static global singleton (like HeaderRegistry/LogManager); populated once at startup, never DI-injected\n static classify(error: Error, methodInfo: ApiMethodInfo): CallOutcome {\n const rungs = ApiRegistry.errorClassifiers.rungs(methodInfo.contract, methodInfo.methodName);\n for (const slot of rungs) {\n const verdict = slot.classifier?.classify(error, methodInfo);\n if (verdict !== undefined) {\n return verdict;\n }\n }\n return WEBPIECES_DEFAULT_ERROR_CLASSIFIER.classify(error, methodInfo);\n }\n\n /**\n * Put the process-globals back to the state a fresh process starts in. For TESTS, so one spec\n * cannot leak its registrations into the next.\n *\n * It empties every translator and classifier rung, which RESTORES the webpieces defaults:\n * `getErrorTranslator(...)` and `classify(...)` fall through to them below every rung, so\n * \"cleared\" is not a state they have.\n */\n // webpieces-disable no-function-outside-class -- static global singleton (like HeaderRegistry/LogManager); populated once at startup, never DI-injected\n static resetForTests(): void {\n ApiRegistry.errorTranslators.clear();\n ApiRegistry.errorClassifiers.clear();\n }\n}\n"]}
|
package/src/http/CallScope.d.ts
CHANGED
|
@@ -1,15 +1,42 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Where a policy applies: every contract (
|
|
3
|
-
* of one contract. The contract is always the CLASS (the
|
|
4
|
-
* api-client's abstract `XxxApi`), never its name — a string
|
|
5
|
-
* matching nothing
|
|
2
|
+
* Where a policy applies: every contract ({@link CallScope.all}), one contract ({@link CallScope.api}),
|
|
3
|
+
* or one method of one contract ({@link CallScope.method}). The contract is always the CLASS (the
|
|
4
|
+
* `@ApiPath` contract, or an SDK-wrapping api-client's abstract `XxxApi`), never its name — a string
|
|
5
|
+
* key is one typo away from silently matching nothing — and `method` checks the method name against
|
|
6
|
+
* that class, so a misspelled method does not compile either.
|
|
6
7
|
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
8
|
+
* The constructor is private: the three factories are the ONLY way to get one, so a string, `{}` or a
|
|
9
|
+
* bare class is not a scope. `grep -rn 'CallScope.all('` lists every app-wide policy.
|
|
10
|
+
*
|
|
11
|
+
* ```ts
|
|
12
|
+
* ClientRegistry.setTimeout(5_000, CallScope.api(VendorApi));
|
|
13
|
+
* ClientRegistry.setStrategy(retry, CallScope.method(VendorApi, 'sync'));
|
|
14
|
+
* ApiRegistry.setErrorTranslator(new AppErrorTranslator(), CallScope.all());
|
|
15
|
+
* ```
|
|
16
|
+
*
|
|
17
|
+
* Every per-call startup registry scopes its settings with this ONE ladder: `ClientRegistry`
|
|
18
|
+
* (strategy, timeout), `ApiRegistry` (error translator, error classifier) and cloudtasks-client's
|
|
9
19
|
* `GcpTaskRegistry` (customizer, gax). Resolution is always method -> API -> ALL -> the registry's
|
|
10
20
|
* own webpieces default.
|
|
11
21
|
*/
|
|
12
|
-
export
|
|
22
|
+
export declare class CallScope {
|
|
23
|
+
/** The contract CLASS, or `undefined` for every contract. */
|
|
24
|
+
readonly api: Function | undefined;
|
|
25
|
+
/** One method of {@link CallScope.api}, or `undefined` for the whole contract. */
|
|
26
|
+
readonly methodName: string | undefined;
|
|
27
|
+
/**
|
|
28
|
+
* A private member makes the type NOMINAL: without it an object literal `{ api, methodName }`
|
|
29
|
+
* would satisfy `CallScope` structurally and skip the factories.
|
|
30
|
+
*/
|
|
31
|
+
private readonly nominal;
|
|
32
|
+
private constructor();
|
|
33
|
+
/** Every contract — the app's (or company's) one policy. Has no method. */
|
|
34
|
+
static all(): CallScope;
|
|
35
|
+
/** One contract, keyed by its CLASS. */
|
|
36
|
+
static api(api: Function): CallScope;
|
|
37
|
+
/** One method of one contract; `methodName` must be a member the contract declares. */
|
|
38
|
+
static method<T>(api: abstract new (...args: never[]) => T, methodName: keyof T & string): CallScope;
|
|
39
|
+
}
|
|
13
40
|
/**
|
|
14
41
|
* The ONE implementation of the {@link CallScope} ladder. It stores one slot per scope (created on
|
|
15
42
|
* first use by `create`) and answers a call's rungs most-specific-first; each registry decides what a
|
|
@@ -22,7 +49,7 @@ export declare class CallScopeLadder<T> {
|
|
|
22
49
|
private readonly apis;
|
|
23
50
|
constructor(create: () => T);
|
|
24
51
|
/** The slot for `scope`, created on first use. Registries write their setting into it. */
|
|
25
|
-
at(
|
|
52
|
+
at(scope: CallScope): T;
|
|
26
53
|
/**
|
|
27
54
|
* The registered slots for one call, most specific first: method, then API, then ALL. A rung with
|
|
28
55
|
* no slot is skipped. `api` undefined (a failure before routing, which has no route and so no
|
package/src/http/CallScope.js
CHANGED
|
@@ -1,6 +1,60 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.CallScopeLadder = void 0;
|
|
3
|
+
exports.CallScopeLadder = exports.CallScope = void 0;
|
|
4
|
+
/**
|
|
5
|
+
* Where a policy applies: every contract ({@link CallScope.all}), one contract ({@link CallScope.api}),
|
|
6
|
+
* or one method of one contract ({@link CallScope.method}). The contract is always the CLASS (the
|
|
7
|
+
* `@ApiPath` contract, or an SDK-wrapping api-client's abstract `XxxApi`), never its name — a string
|
|
8
|
+
* key is one typo away from silently matching nothing — and `method` checks the method name against
|
|
9
|
+
* that class, so a misspelled method does not compile either.
|
|
10
|
+
*
|
|
11
|
+
* The constructor is private: the three factories are the ONLY way to get one, so a string, `{}` or a
|
|
12
|
+
* bare class is not a scope. `grep -rn 'CallScope.all('` lists every app-wide policy.
|
|
13
|
+
*
|
|
14
|
+
* ```ts
|
|
15
|
+
* ClientRegistry.setTimeout(5_000, CallScope.api(VendorApi));
|
|
16
|
+
* ClientRegistry.setStrategy(retry, CallScope.method(VendorApi, 'sync'));
|
|
17
|
+
* ApiRegistry.setErrorTranslator(new AppErrorTranslator(), CallScope.all());
|
|
18
|
+
* ```
|
|
19
|
+
*
|
|
20
|
+
* Every per-call startup registry scopes its settings with this ONE ladder: `ClientRegistry`
|
|
21
|
+
* (strategy, timeout), `ApiRegistry` (error translator, error classifier) and cloudtasks-client's
|
|
22
|
+
* `GcpTaskRegistry` (customizer, gax). Resolution is always method -> API -> ALL -> the registry's
|
|
23
|
+
* own webpieces default.
|
|
24
|
+
*/
|
|
25
|
+
class CallScope {
|
|
26
|
+
api;
|
|
27
|
+
methodName;
|
|
28
|
+
/**
|
|
29
|
+
* A private member makes the type NOMINAL: without it an object literal `{ api, methodName }`
|
|
30
|
+
* would satisfy `CallScope` structurally and skip the factories.
|
|
31
|
+
*/
|
|
32
|
+
nominal = true;
|
|
33
|
+
constructor(
|
|
34
|
+
/** The contract CLASS, or `undefined` for every contract. */
|
|
35
|
+
api,
|
|
36
|
+
/** One method of {@link CallScope.api}, or `undefined` for the whole contract. */
|
|
37
|
+
methodName) {
|
|
38
|
+
this.api = api;
|
|
39
|
+
this.methodName = methodName;
|
|
40
|
+
}
|
|
41
|
+
/** Every contract — the app's (or company's) one policy. Has no method. */
|
|
42
|
+
// webpieces-disable no-function-outside-class -- static factory of the scope value class
|
|
43
|
+
static all() {
|
|
44
|
+
return new CallScope(undefined, undefined);
|
|
45
|
+
}
|
|
46
|
+
/** One contract, keyed by its CLASS. */
|
|
47
|
+
// webpieces-disable no-function-outside-class -- static factory of the scope value class
|
|
48
|
+
static api(api) {
|
|
49
|
+
return new CallScope(api, undefined);
|
|
50
|
+
}
|
|
51
|
+
/** One method of one contract; `methodName` must be a member the contract declares. */
|
|
52
|
+
// webpieces-disable no-function-outside-class -- static factory of the scope value class
|
|
53
|
+
static method(api, methodName) {
|
|
54
|
+
return new CallScope(api, methodName);
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
exports.CallScope = CallScope;
|
|
4
58
|
class ApiRungs {
|
|
5
59
|
api;
|
|
6
60
|
methods = new Map();
|
|
@@ -19,9 +73,9 @@ class CallScopeLadder {
|
|
|
19
73
|
this.create = create;
|
|
20
74
|
}
|
|
21
75
|
/** The slot for `scope`, created on first use. Registries write their setting into it. */
|
|
22
|
-
at(
|
|
23
|
-
const api = scope
|
|
24
|
-
if (api ===
|
|
76
|
+
at(scope) {
|
|
77
|
+
const api = scope.api;
|
|
78
|
+
if (api === undefined) {
|
|
25
79
|
if (this.all === undefined)
|
|
26
80
|
this.all = this.create();
|
|
27
81
|
return this.all;
|
|
@@ -31,7 +85,7 @@ class CallScopeLadder {
|
|
|
31
85
|
rungs = new ApiRungs();
|
|
32
86
|
this.apis.set(api, rungs);
|
|
33
87
|
}
|
|
34
|
-
const methodName = scope
|
|
88
|
+
const methodName = scope.methodName;
|
|
35
89
|
if (methodName === undefined) {
|
|
36
90
|
if (rungs.api === undefined)
|
|
37
91
|
rungs.api = this.create();
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"CallScope.js","sourceRoot":"","sources":["../../../../../../packages/core/core-util/src/http/CallScope.ts"],"names":[],"mappings":";;;
|
|
1
|
+
{"version":3,"file":"CallScope.js","sourceRoot":"","sources":["../../../../../../packages/core/core-util/src/http/CallScope.ts"],"names":[],"mappings":";;;AAAA;;;;;;;;;;;;;;;;;;;;GAoBG;AACH,MAAa,SAAS;IASL;IAEA;IAVb;;;OAGG;IACc,OAAO,GAAG,IAAI,CAAC;IAEhC;IACI,6DAA6D;IACpD,GAAyB;IAClC,kFAAkF;IACzE,UAA8B;QAF9B,QAAG,GAAH,GAAG,CAAsB;QAEzB,eAAU,GAAV,UAAU,CAAoB;IACxC,CAAC;IAEJ,2EAA2E;IAC3E,yFAAyF;IACzF,MAAM,CAAC,GAAG;QACN,OAAO,IAAI,SAAS,CAAC,SAAS,EAAE,SAAS,CAAC,CAAC;IAC/C,CAAC;IAED,wCAAwC;IACxC,yFAAyF;IACzF,MAAM,CAAC,GAAG,CAAC,GAAa;QACpB,OAAO,IAAI,SAAS,CAAC,GAAG,EAAE,SAAS,CAAC,CAAC;IACzC,CAAC;IAED,uFAAuF;IACvF,yFAAyF;IACzF,MAAM,CAAC,MAAM,CAAI,GAAyC,EAAE,UAA4B;QACpF,OAAO,IAAI,SAAS,CAAC,GAAG,EAAE,UAAU,CAAC,CAAC;IAC1C,CAAC;CACJ;AA/BD,8BA+BC;AAED,MAAM,QAAQ;IACV,GAAG,CAAK;IACC,OAAO,GAAG,IAAI,GAAG,EAAa,CAAC;CAC3C;AAED;;;;;GAKG;AACH,MAAa,eAAe;IAIK;IAHrB,GAAG,CAAgB;IACV,IAAI,GAAG,IAAI,GAAG,EAAyB,CAAC;IAEzD,YAA6B,MAAe;QAAf,WAAM,GAAN,MAAM,CAAS;IAAG,CAAC;IAEhD,0FAA0F;IAC1F,EAAE,CAAC,KAAgB;QACf,MAAM,GAAG,GAAG,KAAK,CAAC,GAAG,CAAC;QACtB,IAAI,GAAG,KAAK,SAAS,EAAE,CAAC;YACpB,IAAI,IAAI,CAAC,GAAG,KAAK,SAAS;gBAAE,IAAI,CAAC,GAAG,GAAG,IAAI,CAAC,MAAM,EAAE,CAAC;YACrD,OAAO,IAAI,CAAC,GAAG,CAAC;QACpB,CAAC;QACD,IAAI,KAAK,GAAG,IAAI,CAAC,IAAI,CAAC,GAAG,CAAC,GAAG,CAAC,CAAC;QAC/B,IAAI,CAAC,KAAK,EAAE,CAAC;YACT,KAAK,GAAG,IAAI,QAAQ,EAAK,CAAC;YAC1B,IAAI,CAAC,IAAI,CAAC,GAAG,CAAC,GAAG,EAAE,KAAK,CAAC,CAAC;QAC9B,CAAC;QACD,MAAM,UAAU,GAAG,KAAK,CAAC,UAAU,CAAC;QACpC,IAAI,UAAU,KAAK,SAAS,EAAE,CAAC;YAC3B,IAAI,KAAK,CAAC,GAAG,KAAK,SAAS;gBAAE,KAAK,CAAC,GAAG,GAAG,IAAI,CAAC,MAAM,EAAE,CAAC;YACvD,OAAO,KAAK,CAAC,GAAG,CAAC;QACrB,CAAC;QACD,IAAI,IAAI,GAAG,KAAK,CAAC,OAAO,CAAC,GAAG,CAAC,UAAU,CAAC,CAAC;QACzC,IAAI,IAAI,KAAK,SAAS,EAAE,CAAC;YACrB,IAAI,GAAG,IAAI,CAAC,MAAM,EAAE,CAAC;YACrB,KAAK,CAAC,OAAO,CAAC,GAAG,CAAC,UAAU,EAAE,IAAI,CAAC,CAAC;QACxC,CAAC;QACD,OAAO,IAAI,CAAC;IAChB,CAAC;IAED;;;;OAIG;IACH,KAAK,CAAC,GAAyB,EAAE,UAA8B;QAC3D,MAAM,KAAK,GAAQ,EAAE,CAAC;QACtB,MAAM,KAAK,GAAG,GAAG,KAAK,SAAS,CAAC,CAAC,CAAC,SAAS,CAAC,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,GAAG,CAAC,GAAG,CAAC,CAAC;QACjE,MAAM,MAAM,GAAG,UAAU,KAAK,SAAS,CAAC,CAAC,CAAC,SAAS,CAAC,CAAC,CAAC,KAAK,EAAE,OAAO,CAAC,GAAG,CAAC,UAAU,CAAC,CAAC;QACrF,IAAI,MAAM,KAAK,SAAS;YAAE,KAAK,CAAC,IAAI,CAAC,MAAM,CAAC,CAAC;QAC7C,IAAI,KAAK,EAAE,GAAG,KAAK,SAAS;YAAE,KAAK,CAAC,IAAI,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC;QACpD,IAAI,IAAI,CAAC,GAAG,KAAK,SAAS;YAAE,KAAK,CAAC,IAAI,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC;QACjD,OAAO,KAAK,CAAC;IACjB,CAAC;IAED,KAAK;QACD,IAAI,CAAC,GAAG,GAAG,SAAS,CAAC;QACrB,IAAI,CAAC,IAAI,CAAC,KAAK,EAAE,CAAC;IACtB,CAAC;CACJ;AAlDD,0CAkDC","sourcesContent":["/**\n * Where a policy applies: every contract ({@link CallScope.all}), one contract ({@link CallScope.api}),\n * or one method of one contract ({@link CallScope.method}). The contract is always the CLASS (the\n * `@ApiPath` contract, or an SDK-wrapping api-client's abstract `XxxApi`), never its name — a string\n * key is one typo away from silently matching nothing — and `method` checks the method name against\n * that class, so a misspelled method does not compile either.\n *\n * The constructor is private: the three factories are the ONLY way to get one, so a string, `{}` or a\n * bare class is not a scope. `grep -rn 'CallScope.all('` lists every app-wide policy.\n *\n * ```ts\n * ClientRegistry.setTimeout(5_000, CallScope.api(VendorApi));\n * ClientRegistry.setStrategy(retry, CallScope.method(VendorApi, 'sync'));\n * ApiRegistry.setErrorTranslator(new AppErrorTranslator(), CallScope.all());\n * ```\n *\n * Every per-call startup registry scopes its settings with this ONE ladder: `ClientRegistry`\n * (strategy, timeout), `ApiRegistry` (error translator, error classifier) and cloudtasks-client's\n * `GcpTaskRegistry` (customizer, gax). Resolution is always method -> API -> ALL -> the registry's\n * own webpieces default.\n */\nexport class CallScope {\n /**\n * A private member makes the type NOMINAL: without it an object literal `{ api, methodName }`\n * would satisfy `CallScope` structurally and skip the factories.\n */\n private readonly nominal = true;\n\n private constructor(\n /** The contract CLASS, or `undefined` for every contract. */\n readonly api: Function | undefined,\n /** One method of {@link CallScope.api}, or `undefined` for the whole contract. */\n readonly methodName: string | undefined,\n ) {}\n\n /** Every contract — the app's (or company's) one policy. Has no method. */\n // webpieces-disable no-function-outside-class -- static factory of the scope value class\n static all(): CallScope {\n return new CallScope(undefined, undefined);\n }\n\n /** One contract, keyed by its CLASS. */\n // webpieces-disable no-function-outside-class -- static factory of the scope value class\n static api(api: Function): CallScope {\n return new CallScope(api, undefined);\n }\n\n /** One method of one contract; `methodName` must be a member the contract declares. */\n // webpieces-disable no-function-outside-class -- static factory of the scope value class\n static method<T>(api: abstract new (...args: never[]) => T, methodName: keyof T & string): CallScope {\n return new CallScope(api, methodName);\n }\n}\n\nclass ApiRungs<T> {\n api?: T;\n readonly methods = new Map<string, T>();\n}\n\n/**\n * The ONE implementation of the {@link CallScope} ladder. It stores one slot per scope (created on\n * first use by `create`) and answers a call's rungs most-specific-first; each registry decides what a\n * rung's slot holds and how a rung \"answers\" (first value present, or first verdict that does not\n * defer).\n */\nexport class CallScopeLadder<T> {\n private all: T | undefined;\n private readonly apis = new Map<Function, ApiRungs<T>>();\n\n constructor(private readonly create: () => T) {}\n\n /** The slot for `scope`, created on first use. Registries write their setting into it. */\n at(scope: CallScope): T {\n const api = scope.api;\n if (api === undefined) {\n if (this.all === undefined) this.all = this.create();\n return this.all;\n }\n let rungs = this.apis.get(api);\n if (!rungs) {\n rungs = new ApiRungs<T>();\n this.apis.set(api, rungs);\n }\n const methodName = scope.methodName;\n if (methodName === undefined) {\n if (rungs.api === undefined) rungs.api = this.create();\n return rungs.api;\n }\n let slot = rungs.methods.get(methodName);\n if (slot === undefined) {\n slot = this.create();\n rungs.methods.set(methodName, slot);\n }\n return slot;\n }\n\n /**\n * The registered slots for one call, most specific first: method, then API, then ALL. A rung with\n * no slot is skipped. `api` undefined (a failure before routing, which has no route and so no\n * contract class) reaches only the ALL rung.\n */\n rungs(api: Function | undefined, methodName: string | undefined): T[] {\n const found: T[] = [];\n const rungs = api === undefined ? undefined : this.apis.get(api);\n const method = methodName === undefined ? undefined : rungs?.methods.get(methodName);\n if (method !== undefined) found.push(method);\n if (rungs?.api !== undefined) found.push(rungs.api);\n if (this.all !== undefined) found.push(this.all);\n return found;\n }\n\n clear(): void {\n this.all = undefined;\n this.apis.clear();\n }\n}\n"]}
|
|
@@ -19,7 +19,7 @@ export declare class CallContext {
|
|
|
19
19
|
* per-attempt error classification resolves the classifier registered for THIS contract.
|
|
20
20
|
* REQUIRED and never `undefined`: every transport knows its contract class (HTTP and Cloud Tasks
|
|
21
21
|
* clients hold it, the IPC client proxy holds its `@WpInternal` class), and
|
|
22
|
-
* `
|
|
22
|
+
* `ClientRegistry.execute` re-keys the context a strategy sees onto the class it was given.
|
|
23
23
|
*
|
|
24
24
|
* NON-enumerable on purpose: this context rides on `ApiCallTimeoutError`, which is logged and
|
|
25
25
|
* compared as data, and a class reference is not data.
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"CallStrategy.js","sourceRoot":"","sources":["../../../../../../packages/core/core-util/src/http/CallStrategy.ts"],"names":[],"mappings":";;;AAKA;;;;;;;;GAQG;AACH,MAAa,WAAW;IAcA;IACA;IACA;IAHpB,YACoB,OAAe,EACf,UAAkB,EAClB,SAA4B;IAC5C,uEAAuE;IACvE,QAAkB;QAJF,YAAO,GAAP,OAAO,CAAQ;QACf,eAAU,GAAV,UAAU,CAAQ;QAClB,cAAS,GAAT,SAAS,CAAmB;QAI5C,MAAM,CAAC,cAAc,CAAC,IAAI,EAAE,UAAU,EAAE,EAAE,KAAK,EAAE,QAAQ,EAAE,UAAU,EAAE,KAAK,EAAE,CAAC,CAAC;IACpF,CAAC;CACJ;AAtBD,kCAsBC","sourcesContent":["import { EndpointOperation } from './HttpEndpointOptions';\n\n/** One bounded attempt. Only an explicitly registered strategy may retry it. */\nexport type Attempt<T> = (timeoutMs: number) => Promise<T>;\n\n/**\n * Contract identity for diagnostics; registry keys use the class itself.\n *\n * `operation` is the endpoint's declared `@Endpoint` side-effect contract (`READ`,\n * `WRITE_IDEMPOTENT` or `WRITE`). A {@link CallStrategy} reads it to decide whether a retry is safe,\n * and `ApiCallTimeoutError` reads it to choose the text it shows an end user. A transport with\n * no declared operation (IPC contracts declare none) passes `WRITE`: the conservative answer, since\n * it never invites a blind retry of something that may already have happened.\n */\nexport class CallContext {\n /**\n * The contract CLASS the call belongs to — the key of the `CallScope` ladder, so a strategy's\n * per-attempt error classification resolves the classifier registered for THIS contract.\n * REQUIRED and never `undefined`: every transport knows its contract class (HTTP and Cloud Tasks\n * clients hold it, the IPC client proxy holds its `@WpInternal` class), and\n * `
|
|
1
|
+
{"version":3,"file":"CallStrategy.js","sourceRoot":"","sources":["../../../../../../packages/core/core-util/src/http/CallStrategy.ts"],"names":[],"mappings":";;;AAKA;;;;;;;;GAQG;AACH,MAAa,WAAW;IAcA;IACA;IACA;IAHpB,YACoB,OAAe,EACf,UAAkB,EAClB,SAA4B;IAC5C,uEAAuE;IACvE,QAAkB;QAJF,YAAO,GAAP,OAAO,CAAQ;QACf,eAAU,GAAV,UAAU,CAAQ;QAClB,cAAS,GAAT,SAAS,CAAmB;QAI5C,MAAM,CAAC,cAAc,CAAC,IAAI,EAAE,UAAU,EAAE,EAAE,KAAK,EAAE,QAAQ,EAAE,UAAU,EAAE,KAAK,EAAE,CAAC,CAAC;IACpF,CAAC;CACJ;AAtBD,kCAsBC","sourcesContent":["import { EndpointOperation } from './HttpEndpointOptions';\n\n/** One bounded attempt. Only an explicitly registered strategy may retry it. */\nexport type Attempt<T> = (timeoutMs: number) => Promise<T>;\n\n/**\n * Contract identity for diagnostics; registry keys use the class itself.\n *\n * `operation` is the endpoint's declared `@Endpoint` side-effect contract (`READ`,\n * `WRITE_IDEMPOTENT` or `WRITE`). A {@link CallStrategy} reads it to decide whether a retry is safe,\n * and `ApiCallTimeoutError` reads it to choose the text it shows an end user. A transport with\n * no declared operation (IPC contracts declare none) passes `WRITE`: the conservative answer, since\n * it never invites a blind retry of something that may already have happened.\n */\nexport class CallContext {\n /**\n * The contract CLASS the call belongs to — the key of the `CallScope` ladder, so a strategy's\n * per-attempt error classification resolves the classifier registered for THIS contract.\n * REQUIRED and never `undefined`: every transport knows its contract class (HTTP and Cloud Tasks\n * clients hold it, the IPC client proxy holds its `@WpInternal` class), and\n * `ClientRegistry.execute` re-keys the context a strategy sees onto the class it was given.\n *\n * NON-enumerable on purpose: this context rides on `ApiCallTimeoutError`, which is logged and\n * compared as data, and a class reference is not data.\n */\n declare readonly contract: Function;\n\n constructor(\n public readonly apiName: string,\n public readonly methodName: string,\n public readonly operation: EndpointOperation,\n /** REQUIRED — the contract CLASS; see {@link CallContext.contract}. */\n contract: Function,\n ) {\n Object.defineProperty(this, 'contract', { value: contract, enumerable: false });\n }\n}\n\n/** Owns all timing and retry decisions. No public cancellation signal. */\nexport type CallStrategy<T> = (call: Attempt<T>, ctx: CallContext) => Promise<T>;\n"]}
|