@webpieces/core-util 0.4.790 → 0.4.791

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.
Files changed (50) hide show
  1. package/README.md +1 -1
  2. package/package.json +1 -1
  3. package/src/errors/ReceivedApiErrorRule.d.ts +43 -0
  4. package/src/errors/ReceivedApiErrorRule.js +65 -0
  5. package/src/errors/ReceivedApiErrorRule.js.map +1 -0
  6. package/src/errors/index.d.ts +1 -0
  7. package/src/errors/index.js +3 -1
  8. package/src/errors/index.js.map +1 -1
  9. package/src/http/ClientRegistry.d.ts +29 -18
  10. package/src/http/ClientRegistry.js +37 -27
  11. package/src/http/ClientRegistry.js.map +1 -1
  12. package/src/http/ErrorTranslator.d.ts +115 -0
  13. package/src/http/{ErrorTranslators.js → ErrorTranslator.js} +1 -1
  14. package/src/http/ErrorTranslator.js.map +1 -0
  15. package/src/http/FailureClassifier.d.ts +1 -1
  16. package/src/http/FailureClassifier.js.map +1 -1
  17. package/src/http/HttpResponseDto.d.ts +1 -1
  18. package/src/http/HttpResponseDto.js +1 -1
  19. package/src/http/HttpResponseDto.js.map +1 -1
  20. package/src/http/ServiceInfo.d.ts +1 -1
  21. package/src/http/ServiceInfo.js +1 -1
  22. package/src/http/ServiceInfo.js.map +1 -1
  23. package/src/http/Surface.d.ts +39 -0
  24. package/src/http/Surface.js +28 -0
  25. package/src/http/Surface.js.map +1 -0
  26. package/src/http/WebpiecesCoreHeaders.d.ts +18 -0
  27. package/src/http/WebpiecesCoreHeaders.js +18 -0
  28. package/src/http/WebpiecesCoreHeaders.js.map +1 -1
  29. package/src/http/WebpiecesDefaultErrorTranslator.d.ts +70 -0
  30. package/src/http/WebpiecesDefaultErrorTranslator.js +136 -0
  31. package/src/http/WebpiecesDefaultErrorTranslator.js.map +1 -0
  32. package/src/index.d.ts +5 -1
  33. package/src/index.js +15 -3
  34. package/src/index.js.map +1 -1
  35. package/src/ipc/IpcConnection.js +6 -8
  36. package/src/ipc/IpcConnection.js.map +1 -1
  37. package/src/ipc/IpcErrorTranslator.d.ts +39 -0
  38. package/src/ipc/IpcErrorTranslator.js +3 -0
  39. package/src/ipc/IpcErrorTranslator.js.map +1 -0
  40. package/src/ipc/IpcRegistry.d.ts +55 -0
  41. package/src/ipc/IpcRegistry.js +73 -0
  42. package/src/ipc/IpcRegistry.js.map +1 -0
  43. package/src/ipc/WebpiecesDefaultIpcErrorTranslator.d.ts +28 -0
  44. package/src/ipc/WebpiecesDefaultIpcErrorTranslator.js +49 -0
  45. package/src/ipc/WebpiecesDefaultIpcErrorTranslator.js.map +1 -0
  46. package/src/ipc/index.d.ts +4 -0
  47. package/src/ipc/index.js +9 -1
  48. package/src/ipc/index.js.map +1 -1
  49. package/src/http/ErrorTranslators.d.ts +0 -106
  50. package/src/http/ErrorTranslators.js.map +0 -1
package/README.md CHANGED
@@ -6,7 +6,7 @@ Utility functions for WebPieces applications. Works in both browser and Node.js
6
6
 
7
7
  React Native consumers use `@webpieces/core-util/errors` for canonical semantic errors (`ApiEndUserError`, `ApiNotFoundError`, and the other API categories) and `@webpieces/core-util/ipc` for shared contracts and connection types. These two subpaths are checked with public declaration fixtures, Metro Android/iOS bundles, and Hermes compilation; the broad root barrel is not certified for React Native.
8
8
 
9
- Use `@webpieces/ipc-bridge` for generated clients and typed receivers. HTTP preserves status 266 for `ApiEndUserError`: monitoring succeeds, while generated clients throw it for GUI display. Its optional `edgeHttpStatus` (400/404/409/422, the third constructor argument before `cause`) survives every hop and is what a partner-facing edge in `setEndUserStatus('edge')` mode answers instead. All API errors are transport-neutral, use standard `cause`, and have no HTTP `.code`. See [the taxonomy and verification guide](https://github.com/deanhiller/webpieces-ts/blob/main/docs/portable-ipc-and-errors.md).
9
+ Use `@webpieces/ipc-bridge` for generated clients and typed receivers. HTTP preserves status 266 for `ApiEndUserError`: monitoring succeeds, while generated clients throw it for GUI display. Its optional `edgeHttpStatus` (400/404/409/422, the third constructor argument before `cause`) survives every hop and is what a server answers instead when the CALLER's `SURFACE` is `public-api` (see `SurfaceEndUserStatus`). All API errors are transport-neutral, use standard `cause`, and have no HTTP `.code`. See [the taxonomy and verification guide](https://github.com/deanhiller/webpieces-ts/blob/main/docs/portable-ipc-and-errors.md).
10
10
 
11
11
  ## Installation
12
12
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@webpieces/core-util",
3
- "version": "0.4.790",
3
+ "version": "0.4.791",
4
4
  "description": "Utility functions for WebPieces - works in browser and Node.js",
5
5
  "type": "commonjs",
6
6
  "main": "./src/index.js",
@@ -0,0 +1,43 @@
1
+ import { ApiError } from './ApiError';
2
+ /**
3
+ * THE rule for an error this process RECEIVED from another one — identical over HTTP and over IPC,
4
+ * which is why it lives here and not in either protocol's translator.
5
+ *
6
+ * A status a peer answered describes OUR request to it. It is never, by itself, an answer for our
7
+ * own caller, and the two ways it can be wrong have OPPOSITE owners:
8
+ *
9
+ * - **caller-error status (4xx-equivalent) -> {@link ApiImplementationError}.** WE sent a bad request,
10
+ * called a path that does not exist, or presented the wrong credentials. MY bug. The peer that
11
+ * answered 404 is correct.
12
+ * - **server-error status (5xx-equivalent) -> {@link ApiDependencyError}.** THEY broke. Not my bug,
13
+ * so this process's failure metrics stay clean and page the right team.
14
+ * - **an {@link ApiDependencyError} coming back -> rethrown AS-IS.** The fault is already attributed,
15
+ * somewhere further downstream; this hop adds nothing by re-wrapping it, and re-wrapping would bury
16
+ * the original message one `cause` deeper on every hop of a chain.
17
+ * - **266 / `end-user` -> {@link ApiEndUserError}**, the intended user-facing message, published
18
+ * verbatim. It is the one message the taxonomy lets a peer write for a human.
19
+ *
20
+ * # Why this is UNIFORM, in a browser as much as in a server
21
+ *
22
+ * An earlier design made the browser pass a received status through unchanged on the theory that
23
+ * "the browser IS the user's agent, so a 404 is the user's answer". That is wrong. If a browser
24
+ * received a 404 the browser called the wrong path — that is the BROWSER's bug, and the code that
25
+ * catches it wants to know that, not to show a user a spinner. Read in a browser this reads exactly
26
+ * as intended: `ApiImplementationError` means the client has a bug, `ApiDependencyError` means the
27
+ * server has one.
28
+ *
29
+ * An app that genuinely wants a peer's status relayed as its OWN typed error says so out loud, in one
30
+ * greppable place, by throwing it from its registered translator's `fromWire`.
31
+ */
32
+ export declare class ReceivedApiErrorRule {
33
+ /**
34
+ * @param statusCode - the HTTP status the peer published. IPC has no wire status, so it derives
35
+ * the equivalent from the payload's `kind` through the same `ApiErrorHttpStatus` table — one
36
+ * table, so the two protocols cannot drift apart.
37
+ * @param message - the text to carry forward: the decoded payload's message, else whatever the
38
+ * transport could salvage from the body.
39
+ * @param decoded - the peer's own typed error when the body was a webpieces `ApiErrorPayload`.
40
+ * Absent for a foreign responder.
41
+ */
42
+ static adapt(statusCode: number, message: string, decoded?: ApiError): Error;
43
+ }
@@ -0,0 +1,65 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.ReceivedApiErrorRule = void 0;
4
+ const ApiError_1 = require("./ApiError");
5
+ /**
6
+ * THE rule for an error this process RECEIVED from another one — identical over HTTP and over IPC,
7
+ * which is why it lives here and not in either protocol's translator.
8
+ *
9
+ * A status a peer answered describes OUR request to it. It is never, by itself, an answer for our
10
+ * own caller, and the two ways it can be wrong have OPPOSITE owners:
11
+ *
12
+ * - **caller-error status (4xx-equivalent) -> {@link ApiImplementationError}.** WE sent a bad request,
13
+ * called a path that does not exist, or presented the wrong credentials. MY bug. The peer that
14
+ * answered 404 is correct.
15
+ * - **server-error status (5xx-equivalent) -> {@link ApiDependencyError}.** THEY broke. Not my bug,
16
+ * so this process's failure metrics stay clean and page the right team.
17
+ * - **an {@link ApiDependencyError} coming back -> rethrown AS-IS.** The fault is already attributed,
18
+ * somewhere further downstream; this hop adds nothing by re-wrapping it, and re-wrapping would bury
19
+ * the original message one `cause` deeper on every hop of a chain.
20
+ * - **266 / `end-user` -> {@link ApiEndUserError}**, the intended user-facing message, published
21
+ * verbatim. It is the one message the taxonomy lets a peer write for a human.
22
+ *
23
+ * # Why this is UNIFORM, in a browser as much as in a server
24
+ *
25
+ * An earlier design made the browser pass a received status through unchanged on the theory that
26
+ * "the browser IS the user's agent, so a 404 is the user's answer". That is wrong. If a browser
27
+ * received a 404 the browser called the wrong path — that is the BROWSER's bug, and the code that
28
+ * catches it wants to know that, not to show a user a spinner. Read in a browser this reads exactly
29
+ * as intended: `ApiImplementationError` means the client has a bug, `ApiDependencyError` means the
30
+ * server has one.
31
+ *
32
+ * An app that genuinely wants a peer's status relayed as its OWN typed error says so out loud, in one
33
+ * greppable place, by throwing it from its registered translator's `fromWire`.
34
+ */
35
+ class ReceivedApiErrorRule {
36
+ /**
37
+ * @param statusCode - the HTTP status the peer published. IPC has no wire status, so it derives
38
+ * the equivalent from the payload's `kind` through the same `ApiErrorHttpStatus` table — one
39
+ * table, so the two protocols cannot drift apart.
40
+ * @param message - the text to carry forward: the decoded payload's message, else whatever the
41
+ * transport could salvage from the body.
42
+ * @param decoded - the peer's own typed error when the body was a webpieces `ApiErrorPayload`.
43
+ * Absent for a foreign responder.
44
+ */
45
+ // webpieces-disable no-function-outside-class -- stateless shared rule, called by both protocol translators
46
+ static adapt(statusCode, message, decoded) {
47
+ // 266 is protocol SUCCESS carrying the actor's own answer — the one message published verbatim.
48
+ if (statusCode === 266) {
49
+ return decoded instanceof ApiError_1.ApiEndUserError ? decoded : new ApiError_1.ApiEndUserError(message);
50
+ }
51
+ if (decoded instanceof ApiError_1.ApiDependencyError) {
52
+ return decoded;
53
+ }
54
+ if (statusCode >= 500) {
55
+ return new ApiError_1.ApiDependencyError(`dependency answered ${statusCode} — it is broken, not this service. ` +
56
+ `Downstream said: ${message}`, decoded);
57
+ }
58
+ return new ApiError_1.ApiImplementationError(`dependency answered ${statusCode}. That status describes OUR request to it, not an ` +
59
+ `answer for our caller, so this service owns it — check the path, the base URL, ` +
60
+ `whether the dependency is deployed, and our service credentials. ` +
61
+ `Downstream said: ${message}`, decoded);
62
+ }
63
+ }
64
+ exports.ReceivedApiErrorRule = ReceivedApiErrorRule;
65
+ //# sourceMappingURL=ReceivedApiErrorRule.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"ReceivedApiErrorRule.js","sourceRoot":"","sources":["../../../../../../packages/core/core-util/src/errors/ReceivedApiErrorRule.ts"],"names":[],"mappings":";;;AAAA,yCAKoB;AAEpB;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA6BG;AACH,MAAa,oBAAoB;IAC7B;;;;;;;;OAQG;IACH,4GAA4G;IAC5G,MAAM,CAAC,KAAK,CAAC,UAAkB,EAAE,OAAe,EAAE,OAAkB;QAChE,gGAAgG;QAChG,IAAI,UAAU,KAAK,GAAG,EAAE,CAAC;YACrB,OAAO,OAAO,YAAY,0BAAe,CAAC,CAAC,CAAC,OAAO,CAAC,CAAC,CAAC,IAAI,0BAAe,CAAC,OAAO,CAAC,CAAC;QACvF,CAAC;QACD,IAAI,OAAO,YAAY,6BAAkB,EAAE,CAAC;YACxC,OAAO,OAAO,CAAC;QACnB,CAAC;QACD,IAAI,UAAU,IAAI,GAAG,EAAE,CAAC;YACpB,OAAO,IAAI,6BAAkB,CACzB,uBAAuB,UAAU,qCAAqC;gBAClE,oBAAoB,OAAO,EAAE,EACjC,OAAO,CACV,CAAC;QACN,CAAC;QACD,OAAO,IAAI,iCAAsB,CAC7B,uBAAuB,UAAU,oDAAoD;YACjF,iFAAiF;YACjF,mEAAmE;YACnE,oBAAoB,OAAO,EAAE,EACjC,OAAO,CACV,CAAC;IACN,CAAC;CACJ;AAlCD,oDAkCC","sourcesContent":["import {\n ApiDependencyError,\n ApiEndUserError,\n ApiError,\n ApiImplementationError,\n} from './ApiError';\n\n/**\n * THE rule for an error this process RECEIVED from another one — identical over HTTP and over IPC,\n * which is why it lives here and not in either protocol's translator.\n *\n * A status a peer answered describes OUR request to it. It is never, by itself, an answer for our\n * own caller, and the two ways it can be wrong have OPPOSITE owners:\n *\n * - **caller-error status (4xx-equivalent) -> {@link ApiImplementationError}.** WE sent a bad request,\n * called a path that does not exist, or presented the wrong credentials. MY bug. The peer that\n * answered 404 is correct.\n * - **server-error status (5xx-equivalent) -> {@link ApiDependencyError}.** THEY broke. Not my bug,\n * so this process's failure metrics stay clean and page the right team.\n * - **an {@link ApiDependencyError} coming back -> rethrown AS-IS.** The fault is already attributed,\n * somewhere further downstream; this hop adds nothing by re-wrapping it, and re-wrapping would bury\n * the original message one `cause` deeper on every hop of a chain.\n * - **266 / `end-user` -> {@link ApiEndUserError}**, the intended user-facing message, published\n * verbatim. It is the one message the taxonomy lets a peer write for a human.\n *\n * # Why this is UNIFORM, in a browser as much as in a server\n *\n * An earlier design made the browser pass a received status through unchanged on the theory that\n * \"the browser IS the user's agent, so a 404 is the user's answer\". That is wrong. If a browser\n * received a 404 the browser called the wrong path — that is the BROWSER's bug, and the code that\n * catches it wants to know that, not to show a user a spinner. Read in a browser this reads exactly\n * as intended: `ApiImplementationError` means the client has a bug, `ApiDependencyError` means the\n * server has one.\n *\n * An app that genuinely wants a peer's status relayed as its OWN typed error says so out loud, in one\n * greppable place, by throwing it from its registered translator's `fromWire`.\n */\nexport class ReceivedApiErrorRule {\n /**\n * @param statusCode - the HTTP status the peer published. IPC has no wire status, so it derives\n * the equivalent from the payload's `kind` through the same `ApiErrorHttpStatus` table — one\n * table, so the two protocols cannot drift apart.\n * @param message - the text to carry forward: the decoded payload's message, else whatever the\n * transport could salvage from the body.\n * @param decoded - the peer's own typed error when the body was a webpieces `ApiErrorPayload`.\n * Absent for a foreign responder.\n */\n // webpieces-disable no-function-outside-class -- stateless shared rule, called by both protocol translators\n static adapt(statusCode: number, message: string, decoded?: ApiError): Error {\n // 266 is protocol SUCCESS carrying the actor's own answer — the one message published verbatim.\n if (statusCode === 266) {\n return decoded instanceof ApiEndUserError ? decoded : new ApiEndUserError(message);\n }\n if (decoded instanceof ApiDependencyError) {\n return decoded;\n }\n if (statusCode >= 500) {\n return new ApiDependencyError(\n `dependency answered ${statusCode} — it is broken, not this service. ` +\n `Downstream said: ${message}`,\n decoded,\n );\n }\n return new ApiImplementationError(\n `dependency answered ${statusCode}. That status describes OUR request to it, not an ` +\n `answer for our caller, so this service owns it — check the path, the base URL, ` +\n `whether the dependency is deployed, and our service credentials. ` +\n `Downstream said: ${message}`,\n decoded,\n );\n }\n}\n"]}
@@ -2,4 +2,5 @@ export { ApiError, ApiNotFoundError, ApiDependencyError, ApiUnavailableError, Ap
2
2
  export type { ApiErrorKind, ApiStatusCode, EdgeHttpStatus } from './ApiError';
3
3
  export { ApiErrorCodec, ApiErrorPayload } from './ApiErrorCodec';
4
4
  export { ApiErrorBoundary } from './ApiErrorBoundary';
5
+ export { ReceivedApiErrorRule } from './ReceivedApiErrorRule';
5
6
  export { ENTITY_NOT_FOUND, WRONG_LOGIN_TYPE, WRONG_LOGIN, NOT_APPROVED, EMAIL_NOT_CONFIRMED, WRONG_DOMAIN, WRONG_COMPANY, NO_REG_CODE, } from './AuthErrorSubtypes';
@@ -1,6 +1,6 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.NO_REG_CODE = exports.WRONG_COMPANY = exports.WRONG_DOMAIN = exports.EMAIL_NOT_CONFIRMED = exports.NOT_APPROVED = exports.WRONG_LOGIN = exports.WRONG_LOGIN_TYPE = exports.ENTITY_NOT_FOUND = exports.ApiErrorBoundary = exports.ApiErrorPayload = exports.ApiErrorCodec = exports.ApiCodedError = exports.ApiNotImplementedError = exports.ApiUnsupportedMediaTypeError = exports.ApiPreconditionFailedError = exports.ApiUnprocessableError = exports.ApiConflictError = exports.ApiDependencyBackoffError = exports.ApiUnauthorizedError = exports.ApiBadRequestError = exports.ApiEndUserError = exports.ApiEndpointNotFoundError = exports.ApiConnectionError = exports.ApiRequestTimeoutError = exports.ApiForbiddenError = exports.ApiRateLimitedError = exports.ApiImplementationError = exports.ApiDependencyTimeoutError = exports.ApiUnavailableError = exports.ApiDependencyError = exports.ApiNotFoundError = exports.ApiError = void 0;
3
+ exports.NO_REG_CODE = exports.WRONG_COMPANY = exports.WRONG_DOMAIN = exports.EMAIL_NOT_CONFIRMED = exports.NOT_APPROVED = exports.WRONG_LOGIN = exports.WRONG_LOGIN_TYPE = exports.ENTITY_NOT_FOUND = exports.ReceivedApiErrorRule = exports.ApiErrorBoundary = exports.ApiErrorPayload = exports.ApiErrorCodec = exports.ApiCodedError = exports.ApiNotImplementedError = exports.ApiUnsupportedMediaTypeError = exports.ApiPreconditionFailedError = exports.ApiUnprocessableError = exports.ApiConflictError = exports.ApiDependencyBackoffError = exports.ApiUnauthorizedError = exports.ApiBadRequestError = exports.ApiEndUserError = exports.ApiEndpointNotFoundError = exports.ApiConnectionError = exports.ApiRequestTimeoutError = exports.ApiForbiddenError = exports.ApiRateLimitedError = exports.ApiImplementationError = exports.ApiDependencyTimeoutError = exports.ApiUnavailableError = exports.ApiDependencyError = exports.ApiNotFoundError = exports.ApiError = void 0;
4
4
  var ApiError_1 = require("./ApiError");
5
5
  Object.defineProperty(exports, "ApiError", { enumerable: true, get: function () { return ApiError_1.ApiError; } });
6
6
  Object.defineProperty(exports, "ApiNotFoundError", { enumerable: true, get: function () { return ApiError_1.ApiNotFoundError; } });
@@ -28,6 +28,8 @@ Object.defineProperty(exports, "ApiErrorCodec", { enumerable: true, get: functio
28
28
  Object.defineProperty(exports, "ApiErrorPayload", { enumerable: true, get: function () { return ApiErrorCodec_1.ApiErrorPayload; } });
29
29
  var ApiErrorBoundary_1 = require("./ApiErrorBoundary");
30
30
  Object.defineProperty(exports, "ApiErrorBoundary", { enumerable: true, get: function () { return ApiErrorBoundary_1.ApiErrorBoundary; } });
31
+ var ReceivedApiErrorRule_1 = require("./ReceivedApiErrorRule");
32
+ Object.defineProperty(exports, "ReceivedApiErrorRule", { enumerable: true, get: function () { return ReceivedApiErrorRule_1.ReceivedApiErrorRule; } });
31
33
  var AuthErrorSubtypes_1 = require("./AuthErrorSubtypes");
32
34
  Object.defineProperty(exports, "ENTITY_NOT_FOUND", { enumerable: true, get: function () { return AuthErrorSubtypes_1.ENTITY_NOT_FOUND; } });
33
35
  Object.defineProperty(exports, "WRONG_LOGIN_TYPE", { enumerable: true, get: function () { return AuthErrorSubtypes_1.WRONG_LOGIN_TYPE; } });
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","sourceRoot":"","sources":["../../../../../../packages/core/core-util/src/errors/index.ts"],"names":[],"mappings":";;;AAAA,uCAsBoB;AArBhB,oGAAA,QAAQ,OAAA;AACR,4GAAA,gBAAgB,OAAA;AAChB,8GAAA,kBAAkB,OAAA;AAClB,+GAAA,mBAAmB,OAAA;AACnB,qHAAA,yBAAyB,OAAA;AACzB,kHAAA,sBAAsB,OAAA;AACtB,+GAAA,mBAAmB,OAAA;AACnB,6GAAA,iBAAiB,OAAA;AACjB,kHAAA,sBAAsB,OAAA;AACtB,8GAAA,kBAAkB,OAAA;AAClB,oHAAA,wBAAwB,OAAA;AACxB,2GAAA,eAAe,OAAA;AACf,8GAAA,kBAAkB,OAAA;AAClB,gHAAA,oBAAoB,OAAA;AACpB,qHAAA,yBAAyB,OAAA;AACzB,4GAAA,gBAAgB,OAAA;AAChB,iHAAA,qBAAqB,OAAA;AACrB,sHAAA,0BAA0B,OAAA;AAC1B,wHAAA,4BAA4B,OAAA;AAC5B,kHAAA,sBAAsB,OAAA;AACtB,yGAAA,aAAa,OAAA;AAGjB,iDAAiE;AAAxD,8GAAA,aAAa,OAAA;AAAE,gHAAA,eAAe,OAAA;AACvC,uDAAsD;AAA7C,oHAAA,gBAAgB,OAAA;AACzB,yDAS6B;AARzB,qHAAA,gBAAgB,OAAA;AAChB,qHAAA,gBAAgB,OAAA;AAChB,gHAAA,WAAW,OAAA;AACX,iHAAA,YAAY,OAAA;AACZ,wHAAA,mBAAmB,OAAA;AACnB,iHAAA,YAAY,OAAA;AACZ,kHAAA,aAAa,OAAA;AACb,gHAAA,WAAW,OAAA","sourcesContent":["export {\n ApiError,\n ApiNotFoundError,\n ApiDependencyError,\n ApiUnavailableError,\n ApiDependencyTimeoutError,\n ApiImplementationError,\n ApiRateLimitedError,\n ApiForbiddenError,\n ApiRequestTimeoutError,\n ApiConnectionError,\n ApiEndpointNotFoundError,\n ApiEndUserError,\n ApiBadRequestError,\n ApiUnauthorizedError,\n ApiDependencyBackoffError,\n ApiConflictError,\n ApiUnprocessableError,\n ApiPreconditionFailedError,\n ApiUnsupportedMediaTypeError,\n ApiNotImplementedError,\n ApiCodedError,\n} from './ApiError';\nexport type { ApiErrorKind, ApiStatusCode, EdgeHttpStatus } from './ApiError';\nexport { ApiErrorCodec, ApiErrorPayload } from './ApiErrorCodec';\nexport { ApiErrorBoundary } from './ApiErrorBoundary';\nexport {\n ENTITY_NOT_FOUND,\n WRONG_LOGIN_TYPE,\n WRONG_LOGIN,\n NOT_APPROVED,\n EMAIL_NOT_CONFIRMED,\n WRONG_DOMAIN,\n WRONG_COMPANY,\n NO_REG_CODE,\n} from './AuthErrorSubtypes';\n"]}
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../../../../../../packages/core/core-util/src/errors/index.ts"],"names":[],"mappings":";;;AAAA,uCAsBoB;AArBhB,oGAAA,QAAQ,OAAA;AACR,4GAAA,gBAAgB,OAAA;AAChB,8GAAA,kBAAkB,OAAA;AAClB,+GAAA,mBAAmB,OAAA;AACnB,qHAAA,yBAAyB,OAAA;AACzB,kHAAA,sBAAsB,OAAA;AACtB,+GAAA,mBAAmB,OAAA;AACnB,6GAAA,iBAAiB,OAAA;AACjB,kHAAA,sBAAsB,OAAA;AACtB,8GAAA,kBAAkB,OAAA;AAClB,oHAAA,wBAAwB,OAAA;AACxB,2GAAA,eAAe,OAAA;AACf,8GAAA,kBAAkB,OAAA;AAClB,gHAAA,oBAAoB,OAAA;AACpB,qHAAA,yBAAyB,OAAA;AACzB,4GAAA,gBAAgB,OAAA;AAChB,iHAAA,qBAAqB,OAAA;AACrB,sHAAA,0BAA0B,OAAA;AAC1B,wHAAA,4BAA4B,OAAA;AAC5B,kHAAA,sBAAsB,OAAA;AACtB,yGAAA,aAAa,OAAA;AAGjB,iDAAiE;AAAxD,8GAAA,aAAa,OAAA;AAAE,gHAAA,eAAe,OAAA;AACvC,uDAAsD;AAA7C,oHAAA,gBAAgB,OAAA;AACzB,+DAA8D;AAArD,4HAAA,oBAAoB,OAAA;AAC7B,yDAS6B;AARzB,qHAAA,gBAAgB,OAAA;AAChB,qHAAA,gBAAgB,OAAA;AAChB,gHAAA,WAAW,OAAA;AACX,iHAAA,YAAY,OAAA;AACZ,wHAAA,mBAAmB,OAAA;AACnB,iHAAA,YAAY,OAAA;AACZ,kHAAA,aAAa,OAAA;AACb,gHAAA,WAAW,OAAA","sourcesContent":["export {\n ApiError,\n ApiNotFoundError,\n ApiDependencyError,\n ApiUnavailableError,\n ApiDependencyTimeoutError,\n ApiImplementationError,\n ApiRateLimitedError,\n ApiForbiddenError,\n ApiRequestTimeoutError,\n ApiConnectionError,\n ApiEndpointNotFoundError,\n ApiEndUserError,\n ApiBadRequestError,\n ApiUnauthorizedError,\n ApiDependencyBackoffError,\n ApiConflictError,\n ApiUnprocessableError,\n ApiPreconditionFailedError,\n ApiUnsupportedMediaTypeError,\n ApiNotImplementedError,\n ApiCodedError,\n} from './ApiError';\nexport type { ApiErrorKind, ApiStatusCode, EdgeHttpStatus } from './ApiError';\nexport { ApiErrorCodec, ApiErrorPayload } from './ApiErrorCodec';\nexport { ApiErrorBoundary } from './ApiErrorBoundary';\nexport { ReceivedApiErrorRule } from './ReceivedApiErrorRule';\nexport {\n ENTITY_NOT_FOUND,\n WRONG_LOGIN_TYPE,\n WRONG_LOGIN,\n NOT_APPROVED,\n EMAIL_NOT_CONFIRMED,\n WRONG_DOMAIN,\n WRONG_COMPANY,\n NO_REG_CODE,\n} from './AuthErrorSubtypes';\n"]}
@@ -1,4 +1,4 @@
1
- import { ErrorTranslators } from './ErrorTranslators';
1
+ import { ErrorTranslator } from './ErrorTranslator';
2
2
  import { FailureClassifier } from './FailureClassifier';
3
3
  import { ApiMethodInfo } from './ApiMethodInfo';
4
4
  /**
@@ -54,19 +54,25 @@ export declare class ClientRegistry {
54
54
  /** The fallback for svcNames with no mapping. Undefined = no derivation in this environment. */
55
55
  private static deriver;
56
56
  /**
57
- * The app's ONE {@link ErrorTranslators}, consulted BEFORE webpieces' built-in error mapping in
58
- * BOTH directions. Process-global, set once at startup on the SERVER and in the BROWSER — the
59
- * same no-DI pattern as {@link ClientRegistry.mappings} above.
57
+ * The process's ONE {@link ErrorTranslator}. NEVER undefined — it starts life holding
58
+ * {@link WEBPIECES_DEFAULT_ERROR_TRANSLATOR}, and {@link ClientRegistry.setErrorTranslator}
59
+ * REPLACES it. Process-global, set once at startup on the SERVER and in the BROWSER — the same
60
+ * no-DI pattern as {@link ClientRegistry.mappings} above.
61
+ *
62
+ * There is no "did anyone register one" question anywhere in the framework because this field
63
+ * cannot be empty: every caller makes ONE unconditional call, and an app translator declines an
64
+ * error by DELEGATING to {@link WebpiecesDefaultErrorTranslator}.
60
65
  *
61
66
  * ONE, not a list, on purpose: an app that has several layers of error policy composes them
62
67
  * INSIDE its own `toWire`, where the precedence is written down, instead of leaving it implicit
63
68
  * in the order two unrelated startup paths happened to register.
64
69
  */
65
- private static errorTranslators;
70
+ private static errorTranslator;
66
71
  /**
67
72
  * The app/company DEFAULT {@link FailureClassifier} — ONE per process, reads {@link ApiMethodInfo.side}
68
- * so a single strategy covers the server router AND all internal clients. Undefined = use the
69
- * webpieces built-in ({@link WEBPIECES_DEFAULT_FAILURE_CLASSIFIER}). Populated once at startup on the
73
+ * so a single strategy covers the server router AND all internal clients. NEVER undefined: it
74
+ * starts life holding the webpieces built-in ({@link WEBPIECES_DEFAULT_FAILURE_CLASSIFIER}) and
75
+ * {@link ClientRegistry.setDefaultFailureClassifier} replaces it. Populated once at startup on the
70
76
  * SERVER and in the BROWSER — same no-DI pattern as {@link ClientRegistry.mappings}.
71
77
  */
72
78
  private static appDefaultFailureClassifier;
@@ -114,23 +120,23 @@ export declare class ClientRegistry {
114
120
  */
115
121
  static resolve(svcName: string): Promise<string>;
116
122
  /**
117
- * Install the app's {@link ErrorTranslators} — the ONE symmetric owner of error translation for
123
+ * Install the app's {@link ErrorTranslator} — the ONE symmetric owner of error translation for
118
124
  * this process, server side AND every client side. Consulted BEFORE webpieces' built-in mapping
119
125
  * in both directions. Call ONCE at startup, on the server AND in the browser, mirroring
120
126
  * {@link ClientRegistry.addMapping}.
121
127
  *
122
128
  * `set`, not `add`: see the field doc above for why a registry LIST is not wanted.
123
129
  */
124
- static setErrorTranslators(translators: ErrorTranslators): void;
130
+ static setErrorTranslator(translator: ErrorTranslator): void;
125
131
  /**
126
- * The app's installed {@link ErrorTranslators}, or `undefined` when this process installed none.
132
+ * THE {@link ErrorTranslator} for this process — never `undefined`, so a caller never branches.
127
133
  *
128
- * The `undefined` here is "nobody registered", which is a fact about the PROCESS — it is not a
129
- * per-error "not mine". A registered translator answers EVERY error and EVERY response, declining
130
- * by delegating to the webpieces default (see {@link ErrorTranslators}), so a caller asks this
131
- * question exactly once and then makes one unconditional call.
134
+ * Until an app calls {@link ClientRegistry.setErrorTranslator} this is
135
+ * {@link WEBPIECES_DEFAULT_ERROR_TRANSLATOR}. A registered translator answers EVERY error and
136
+ * EVERY response, declining by delegating to the webpieces default (see {@link ErrorTranslator}),
137
+ * so every call site in the framework is one unconditional line.
132
138
  */
133
- static getErrorTranslators(): ErrorTranslators | undefined;
139
+ static getErrorTranslator(): ErrorTranslator;
134
140
  /**
135
141
  * Set the app/company DEFAULT failure classifier — ONE per process, covering the server router and
136
142
  * all internal clients (it branches on {@link ApiMethodInfo.side}). Call ONCE at startup, on the
@@ -154,8 +160,13 @@ export declare class ClientRegistry {
154
160
  */
155
161
  static classifyFailure(error: Error, methodInfo: ApiMethodInfo): boolean;
156
162
  /**
157
- * Reset mappings, the deriver, the error translators, AND failure classifiers. For tests, so
158
- * the process-globals do not leak across specs.
163
+ * Put the process-globals back to the state a fresh process starts in. For TESTS, so one spec
164
+ * cannot leak its registrations into the next.
165
+ *
166
+ * It RESTORES the webpieces defaults rather than emptying the pluggable slots, which is the whole
167
+ * reason it is not called `clear()` any more: `getErrorTranslator()` and the default failure
168
+ * classifier are non-optional now, so "cleared" is not a state they have. A spec that wants
169
+ * webpieces' own behaviour back asks for exactly that.
159
170
  */
160
- static clear(): void;
171
+ static resetForTests(): void;
161
172
  }
@@ -2,6 +2,7 @@
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.ClientRegistry = void 0;
4
4
  const WebpiecesDefaultFailureClassifier_1 = require("./WebpiecesDefaultFailureClassifier");
5
+ const WebpiecesDefaultErrorTranslator_1 = require("./WebpiecesDefaultErrorTranslator");
5
6
  /**
6
7
  * ClientRegistry - the ONE place a `svcName` becomes a base URL, for every outbound client in every
7
8
  * environment. A client is built once but a URL is per-environment, so the URL belongs here rather
@@ -42,22 +43,28 @@ class ClientRegistry {
42
43
  /** The fallback for svcNames with no mapping. Undefined = no derivation in this environment. */
43
44
  static deriver;
44
45
  /**
45
- * The app's ONE {@link ErrorTranslators}, consulted BEFORE webpieces' built-in error mapping in
46
- * BOTH directions. Process-global, set once at startup on the SERVER and in the BROWSER — the
47
- * same no-DI pattern as {@link ClientRegistry.mappings} above.
46
+ * The process's ONE {@link ErrorTranslator}. NEVER undefined — it starts life holding
47
+ * {@link WEBPIECES_DEFAULT_ERROR_TRANSLATOR}, and {@link ClientRegistry.setErrorTranslator}
48
+ * REPLACES it. Process-global, set once at startup on the SERVER and in the BROWSER — the same
49
+ * no-DI pattern as {@link ClientRegistry.mappings} above.
50
+ *
51
+ * There is no "did anyone register one" question anywhere in the framework because this field
52
+ * cannot be empty: every caller makes ONE unconditional call, and an app translator declines an
53
+ * error by DELEGATING to {@link WebpiecesDefaultErrorTranslator}.
48
54
  *
49
55
  * ONE, not a list, on purpose: an app that has several layers of error policy composes them
50
56
  * INSIDE its own `toWire`, where the precedence is written down, instead of leaving it implicit
51
57
  * in the order two unrelated startup paths happened to register.
52
58
  */
53
- static errorTranslators;
59
+ static errorTranslator = WebpiecesDefaultErrorTranslator_1.WEBPIECES_DEFAULT_ERROR_TRANSLATOR;
54
60
  /**
55
61
  * The app/company DEFAULT {@link FailureClassifier} — ONE per process, reads {@link ApiMethodInfo.side}
56
- * so a single strategy covers the server router AND all internal clients. Undefined = use the
57
- * webpieces built-in ({@link WEBPIECES_DEFAULT_FAILURE_CLASSIFIER}). Populated once at startup on the
62
+ * so a single strategy covers the server router AND all internal clients. NEVER undefined: it
63
+ * starts life holding the webpieces built-in ({@link WEBPIECES_DEFAULT_FAILURE_CLASSIFIER}) and
64
+ * {@link ClientRegistry.setDefaultFailureClassifier} replaces it. Populated once at startup on the
58
65
  * SERVER and in the BROWSER — same no-DI pattern as {@link ClientRegistry.mappings}.
59
66
  */
60
- static appDefaultFailureClassifier;
67
+ static appDefaultFailureClassifier = WebpiecesDefaultFailureClassifier_1.WEBPIECES_DEFAULT_FAILURE_CLASSIFIER;
61
68
  /**
62
69
  * Per-EXTERNAL-client {@link FailureClassifier}s, keyed by {@link ApiMethodInfo.apiClass}
63
70
  * ('FirestoreAdminClient', 'ClaudeApi', ...). Consulted BEFORE the default tier, so an external
@@ -146,7 +153,7 @@ class ClientRegistry {
146
153
  return url;
147
154
  }
148
155
  /**
149
- * Install the app's {@link ErrorTranslators} — the ONE symmetric owner of error translation for
156
+ * Install the app's {@link ErrorTranslator} — the ONE symmetric owner of error translation for
150
157
  * this process, server side AND every client side. Consulted BEFORE webpieces' built-in mapping
151
158
  * in both directions. Call ONCE at startup, on the server AND in the browser, mirroring
152
159
  * {@link ClientRegistry.addMapping}.
@@ -154,20 +161,20 @@ class ClientRegistry {
154
161
  * `set`, not `add`: see the field doc above for why a registry LIST is not wanted.
155
162
  */
156
163
  // webpieces-disable no-function-outside-class -- static global singleton (like HeaderRegistry/LogManager); populated once at startup, never DI-injected
157
- static setErrorTranslators(translators) {
158
- ClientRegistry.errorTranslators = translators;
164
+ static setErrorTranslator(translator) {
165
+ ClientRegistry.errorTranslator = translator;
159
166
  }
160
167
  /**
161
- * The app's installed {@link ErrorTranslators}, or `undefined` when this process installed none.
168
+ * THE {@link ErrorTranslator} for this process — never `undefined`, so a caller never branches.
162
169
  *
163
- * The `undefined` here is "nobody registered", which is a fact about the PROCESS — it is not a
164
- * per-error "not mine". A registered translator answers EVERY error and EVERY response, declining
165
- * by delegating to the webpieces default (see {@link ErrorTranslators}), so a caller asks this
166
- * question exactly once and then makes one unconditional call.
170
+ * Until an app calls {@link ClientRegistry.setErrorTranslator} this is
171
+ * {@link WEBPIECES_DEFAULT_ERROR_TRANSLATOR}. A registered translator answers EVERY error and
172
+ * EVERY response, declining by delegating to the webpieces default (see {@link ErrorTranslator}),
173
+ * so every call site in the framework is one unconditional line.
167
174
  */
168
175
  // webpieces-disable no-function-outside-class -- static global singleton (like HeaderRegistry/LogManager); populated once at startup, never DI-injected
169
- static getErrorTranslators() {
170
- return ClientRegistry.errorTranslators;
176
+ static getErrorTranslator() {
177
+ return ClientRegistry.errorTranslator;
171
178
  }
172
179
  /**
173
180
  * Set the app/company DEFAULT failure classifier — ONE per process, covering the server router and
@@ -205,24 +212,27 @@ class ClientRegistry {
205
212
  return verdict;
206
213
  }
207
214
  }
208
- if (ClientRegistry.appDefaultFailureClassifier !== undefined) {
209
- const verdict = ClientRegistry.appDefaultFailureClassifier.isFailure(error, methodInfo);
210
- if (verdict !== undefined) {
211
- return verdict;
212
- }
215
+ const verdict = ClientRegistry.appDefaultFailureClassifier.isFailure(error, methodInfo);
216
+ if (verdict !== undefined) {
217
+ return verdict;
213
218
  }
214
219
  return WebpiecesDefaultFailureClassifier_1.WEBPIECES_DEFAULT_FAILURE_CLASSIFIER.isFailure(error, methodInfo) ?? true;
215
220
  }
216
221
  /**
217
- * Reset mappings, the deriver, the error translators, AND failure classifiers. For tests, so
218
- * the process-globals do not leak across specs.
222
+ * Put the process-globals back to the state a fresh process starts in. For TESTS, so one spec
223
+ * cannot leak its registrations into the next.
224
+ *
225
+ * It RESTORES the webpieces defaults rather than emptying the pluggable slots, which is the whole
226
+ * reason it is not called `clear()` any more: `getErrorTranslator()` and the default failure
227
+ * classifier are non-optional now, so "cleared" is not a state they have. A spec that wants
228
+ * webpieces' own behaviour back asks for exactly that.
219
229
  */
220
230
  // webpieces-disable no-function-outside-class -- static global singleton (like HeaderRegistry/LogManager); populated once at startup, never DI-injected
221
- static clear() {
231
+ static resetForTests() {
222
232
  ClientRegistry.mappings.clear();
223
233
  ClientRegistry.deriver = undefined;
224
- ClientRegistry.errorTranslators = undefined;
225
- ClientRegistry.appDefaultFailureClassifier = undefined;
234
+ ClientRegistry.errorTranslator = WebpiecesDefaultErrorTranslator_1.WEBPIECES_DEFAULT_ERROR_TRANSLATOR;
235
+ ClientRegistry.appDefaultFailureClassifier = WebpiecesDefaultFailureClassifier_1.WEBPIECES_DEFAULT_FAILURE_CLASSIFIER;
226
236
  ClientRegistry.failureClassifiersByApiClass.clear();
227
237
  }
228
238
  }
@@ -1 +1 @@
1
- {"version":3,"file":"ClientRegistry.js","sourceRoot":"","sources":["../../../../../../packages/core/core-util/src/http/ClientRegistry.ts"],"names":[],"mappings":"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","sourcesContent":["import { ErrorTranslators } from './ErrorTranslators';\nimport { FailureClassifier } from './FailureClassifier';\nimport { WEBPIECES_DEFAULT_FAILURE_CLASSIFIER } from './WebpiecesDefaultFailureClassifier';\nimport { ApiMethodInfo } from './ApiMethodInfo';\n\n/**\n * Derives a base URL for a service name that has NO registered mapping — the pluggable half of\n * {@link ClientRegistry} resolution. One per environment, installed once at startup:\n *\n * - `gcpCloudRunDeriver()` (@webpieces/gcp-identity) — the Cloud Run formula, read from the metadata\n * server. For code running ON GCP.\n * - `gcpCloudRunDeriver(new GcpCloudRunTarget(projectNumber, region))` — the SAME formula with the\n * values supplied from config, for code running OFF GCP that still calls Cloud Run (a CLI, CI).\n * - {@link templateDeriver} (this package, browser-safe) — pure string substitution, for AWS or\n * anything else with predictable DNS.\n * - none at all — a browser goes same-origin, and localhost/tests hand-register their mappings.\n */\nexport type ServiceUrlDeriver = (svcName: string) => Promise<string>;\n\n/**\n * ClientRegistry - the ONE place a `svcName` becomes a base URL, for every outbound client in every\n * environment. A client is built once but a URL is per-environment, so the URL belongs here rather\n * than on the client: clients carry ONLY a svcName.\n *\n * Resolution is one precedence chain, identical in the browser, in node, and in Cloud Tasks:\n *\n * 1. a registered mapping wins — the localhost port table, AWS, an external API, another region\n * or project, a host that is not Cloud Run at all. Populated at startup from per-env config.\n * 2. else the installed {@link ServiceUrlDeriver}, if any — GCP's built-in, a DNS template, or\n * your own. OPTIONAL on purpose: localhost is inherently a TABLE (helper-fsdb -> :8401,\n * helper-portal -> :8201 have per-service ports), so explicit mappings must stay sufficient on\n * their own.\n * 3. else the caller's fallback: the BROWSER goes relative (same origin — see\n * {@link ClientRegistry.tryResolve}), while node THROWS (see {@link ClientRegistry.resolve}),\n * because a server has no \"own origin\" and an unresolvable peer is a setup bug.\n *\n * Configured like {@link HeaderRegistry} / LogManager — populated once at startup, then globally\n * accessible with NO DI wiring. It is browser-safe (no `process.env`, no node-only deps), which is\n * why it lives in core-util rather than gcp-identity.\n *\n * A `svcName` should be the nx MODULE name of the service you are calling — that is the one name\n * `architecture:validate-runtime-architecture` can verify actually exists, so a typo or a rename\n * fails the build instead of failing in production. When the DEPLOYED name differs (an environment\n * prefix like `tf-`, or a Cloud Run service named unlike its module), translate it in ONE place —\n * the deriver below, or a `serviceName` alias in the module's project.json — never at the call site.\n *\n * ```ts\n * // startup, from the current environment's config:\n * ClientRegistry.addMapping('server2', 8202); // -> http://localhost:8202\n * ClientRegistry.addUrlMapping('email', 'https://email.other-region.example');\n * ClientRegistry.setDeriver(gcpCloudRunDeriver()); // everything else, on GCP\n * ```\n */\nexport class ClientRegistry {\n /** svcName -> resolved base URL. Process-global; populated at startup per environment. */\n private static readonly mappings = new Map<string, string>();\n\n /** The fallback for svcNames with no mapping. Undefined = no derivation in this environment. */\n private static deriver: ServiceUrlDeriver | undefined;\n\n /**\n * The app's ONE {@link ErrorTranslators}, consulted BEFORE webpieces' built-in error mapping in\n * BOTH directions. Process-global, set once at startup on the SERVER and in the BROWSER — the\n * same no-DI pattern as {@link ClientRegistry.mappings} above.\n *\n * ONE, not a list, on purpose: an app that has several layers of error policy composes them\n * INSIDE its own `toWire`, where the precedence is written down, instead of leaving it implicit\n * in the order two unrelated startup paths happened to register.\n */\n private static errorTranslators: ErrorTranslators | undefined;\n\n /**\n * The app/company DEFAULT {@link FailureClassifier} — ONE per process, reads {@link ApiMethodInfo.side}\n * so a single strategy covers the server router AND all internal clients. Undefined = use the\n * webpieces built-in ({@link WEBPIECES_DEFAULT_FAILURE_CLASSIFIER}). Populated once at startup on the\n * SERVER and in the BROWSER — same no-DI pattern as {@link ClientRegistry.mappings}.\n */\n private static appDefaultFailureClassifier: FailureClassifier | undefined;\n\n /**\n * Per-EXTERNAL-client {@link FailureClassifier}s, keyed by {@link ApiMethodInfo.apiClass}\n * ('FirestoreAdminClient', 'ClaudeApi', ...). Consulted BEFORE the default tier, so an external\n * client overrides the default for its own apiClass only. Internal clients / the server register\n * nothing here and fall through to the default. See {@link ClientRegistry.classifyFailure}.\n */\n private static readonly failureClassifiersByApiClass = new Map<string, FailureClassifier>();\n\n /** Map a service name to `http://localhost:<port>`. */\n // webpieces-disable no-function-outside-class -- static global singleton (like HeaderRegistry/LogManager); populated once at startup, never DI-injected\n static addMapping(svcName: string, port: number): void {\n ClientRegistry.mappings.set(svcName, `http://localhost:${port}`);\n }\n\n /**\n * Map a service name to an explicit base URL (any host / any environment).\n *\n * The EMPTY STRING is a legal, meaningful mapping: it makes the service relative, i.e.\n * same-origin, because the client builds its URL as `${baseUrl}${route.path}`.\n */\n // webpieces-disable no-function-outside-class -- static global singleton (like HeaderRegistry/LogManager); populated once at startup, never DI-injected\n static addUrlMapping(svcName: string, url: string): void {\n ClientRegistry.mappings.set(svcName, url);\n }\n\n /**\n * Install the environment's {@link ServiceUrlDeriver} — how to resolve a svcName that has NO\n * mapping. Optional: an environment that registers every svcName it calls needs none.\n */\n // webpieces-disable no-function-outside-class -- static global singleton (like HeaderRegistry/LogManager); populated once at startup, never DI-injected\n static setDeriver(fn: ServiceUrlDeriver): void {\n ClientRegistry.deriver = fn;\n }\n\n /** The registered override for `svcName`, or undefined if none. Registry only — no derivation. */\n // webpieces-disable no-function-outside-class -- static global singleton (like HeaderRegistry/LogManager); populated once at startup, never DI-injected\n static tryLookup(svcName: string): string | undefined {\n return ClientRegistry.mappings.get(svcName);\n }\n\n /**\n * Resolve `svcName` to its registered base URL. THROWS if the service was never registered.\n * Registry only — it does NOT consult the deriver; prefer {@link ClientRegistry.resolve}.\n */\n // webpieces-disable no-function-outside-class -- static global singleton (like HeaderRegistry/LogManager); populated once at startup, never DI-injected\n static lookup(svcName: string): string {\n const url = ClientRegistry.tryLookup(svcName);\n if (url === undefined) {\n throw new Error(\n `No URL registered for service \"${svcName}\". Register it at startup: ` +\n `ClientRegistry.addMapping(svcName, port) for a localhost port, or ` +\n `ClientRegistry.addUrlMapping(svcName, url) for an explicit URL.`,\n );\n }\n return url;\n }\n\n /**\n * Steps 1 + 2 of the chain: the registered mapping, else the installed deriver, else undefined.\n * Non-throwing — the BROWSER uses this and treats undefined as \"\" (relative → same origin),\n * which is what a browser app calling the backend it was served from wants by default.\n *\n * Note the `!== undefined` guard: an empty-string mapping is a legal answer (\"this service is\n * same-origin\"), so it must NOT fall through to derivation.\n */\n // webpieces-disable no-function-outside-class -- static global singleton (like HeaderRegistry/LogManager); populated once at startup, never DI-injected\n static tryResolve(svcName: string): Promise<string | undefined> {\n const override = ClientRegistry.tryLookup(svcName);\n if (override !== undefined) {\n return Promise.resolve(override);\n }\n if (!ClientRegistry.deriver) {\n return Promise.resolve(undefined);\n }\n return ClientRegistry.deriver(svcName);\n }\n\n /**\n * The full chain, with node's fallback: mapping, else deriver, else THROW. A server has no \"own\n * origin\" to go relative to, so an unresolvable peer is a setup bug and must fail loudly — and\n * say how to fix it.\n */\n // webpieces-disable no-function-outside-class -- static global singleton (like HeaderRegistry/LogManager); populated once at startup, never DI-injected\n static async resolve(svcName: string): Promise<string> {\n const url = await ClientRegistry.tryResolve(svcName);\n if (url === undefined) {\n throw new Error(\n `No URL for service \"${svcName}\".\\n` +\n ` - localhost/AWS: ClientRegistry.addMapping('${svcName}', 8401)\\n` +\n ` or ClientRegistry.addUrlMapping('${svcName}', 'https://...')\\n` +\n ` - GCP: install a deriver — ClientRegistry.setDeriver(gcpCloudRunDeriver())\\n` +\n ` - deployed name differs from the module name (e.g. a 'tf-' prefix)? Translate it ONCE\\n` +\n ` in the deriver — setDeriver(s => gcpCloudRunDeriver()('tf-' + s)) — so every call\\n` +\n ` site keeps naming the MODULE, which architecture:validate-runtime-architecture checks`,\n );\n }\n return url;\n }\n\n /**\n * Install the app's {@link ErrorTranslators} — the ONE symmetric owner of error translation for\n * this process, server side AND every client side. Consulted BEFORE webpieces' built-in mapping\n * in both directions. Call ONCE at startup, on the server AND in the browser, mirroring\n * {@link ClientRegistry.addMapping}.\n *\n * `set`, not `add`: see the field doc above for why a registry LIST is not wanted.\n */\n // webpieces-disable no-function-outside-class -- static global singleton (like HeaderRegistry/LogManager); populated once at startup, never DI-injected\n static setErrorTranslators(translators: ErrorTranslators): void {\n ClientRegistry.errorTranslators = translators;\n }\n\n /**\n * The app's installed {@link ErrorTranslators}, or `undefined` when this process installed none.\n *\n * The `undefined` here is \"nobody registered\", which is a fact about the PROCESS — it is not a\n * per-error \"not mine\". A registered translator answers EVERY error and EVERY response, declining\n * by delegating to the webpieces default (see {@link ErrorTranslators}), so a caller asks this\n * question exactly once and then makes one unconditional call.\n */\n // webpieces-disable no-function-outside-class -- static global singleton (like HeaderRegistry/LogManager); populated once at startup, never DI-injected\n static getErrorTranslators(): ErrorTranslators | undefined {\n return ClientRegistry.errorTranslators;\n }\n\n /**\n * Set the app/company DEFAULT failure classifier — ONE per process, covering the server router and\n * all internal clients (it branches on {@link ApiMethodInfo.side}). Call ONCE at startup, on the\n * server AND in the browser. Overrides the webpieces built-in for calls no per-apiClass classifier\n * claims. See {@link FailureClassifier}.\n */\n // webpieces-disable no-function-outside-class -- static global singleton (like HeaderRegistry/LogManager); populated once at startup, never DI-injected\n static setDefaultFailureClassifier(classifier: FailureClassifier): void {\n ClientRegistry.appDefaultFailureClassifier = classifier;\n }\n\n /**\n * Register a per-EXTERNAL-client failure classifier, keyed by {@link ApiMethodInfo.apiClass}\n * (e.g. 'FirestoreAdminClient'). Consulted BEFORE the default tier for calls with that apiClass.\n * Call ONCE at startup. See {@link FailureClassifier}.\n */\n // webpieces-disable no-function-outside-class -- static global singleton (like HeaderRegistry/LogManager); populated once at startup, never DI-injected\n static addFailureClassifier(apiClass: string, classifier: FailureClassifier): void {\n ClientRegistry.failureClassifiersByApiClass.set(apiClass, classifier);\n }\n\n /**\n * Is this thrown API-call error a real FAILURE (true) or an expected non-failure (false)?\n * Resolved most-specific-first, and ALWAYS definitive (returns a boolean):\n * 1. the per-apiClass classifier, if one is registered for `methodInfo.apiClass` — unless it defers;\n * 2. else the app default classifier, if one was set — unless it defers;\n * 3. else the webpieces built-in ({@link WEBPIECES_DEFAULT_FAILURE_CLASSIFIER}), which never defers.\n * A `?? true` backstop keeps an unexpectedly-deferring built-in fail-safe (treat as failure).\n */\n // webpieces-disable no-function-outside-class -- static global singleton (like HeaderRegistry/LogManager); populated once at startup, never DI-injected\n static classifyFailure(error: Error, methodInfo: ApiMethodInfo): boolean {\n const perClient = ClientRegistry.failureClassifiersByApiClass.get(methodInfo.apiClass);\n if (perClient !== undefined) {\n const verdict = perClient.isFailure(error, methodInfo);\n if (verdict !== undefined) {\n return verdict;\n }\n }\n if (ClientRegistry.appDefaultFailureClassifier !== undefined) {\n const verdict = ClientRegistry.appDefaultFailureClassifier.isFailure(error, methodInfo);\n if (verdict !== undefined) {\n return verdict;\n }\n }\n return WEBPIECES_DEFAULT_FAILURE_CLASSIFIER.isFailure(error, methodInfo) ?? true;\n }\n\n /**\n * Reset mappings, the deriver, the error translators, AND failure classifiers. For tests, so\n * the process-globals do not leak across specs.\n */\n // webpieces-disable no-function-outside-class -- static global singleton (like HeaderRegistry/LogManager); populated once at startup, never DI-injected\n static clear(): void {\n ClientRegistry.mappings.clear();\n ClientRegistry.deriver = undefined;\n ClientRegistry.errorTranslators = undefined;\n ClientRegistry.appDefaultFailureClassifier = undefined;\n ClientRegistry.failureClassifiersByApiClass.clear();\n }\n}\n"]}
1
+ {"version":3,"file":"ClientRegistry.js","sourceRoot":"","sources":["../../../../../../packages/core/core-util/src/http/ClientRegistry.ts"],"names":[],"mappings":"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","sourcesContent":["import { ErrorTranslator } from './ErrorTranslator';\nimport { FailureClassifier } from './FailureClassifier';\nimport { WEBPIECES_DEFAULT_FAILURE_CLASSIFIER } from './WebpiecesDefaultFailureClassifier';\nimport { WEBPIECES_DEFAULT_ERROR_TRANSLATOR } from './WebpiecesDefaultErrorTranslator';\nimport { ApiMethodInfo } from './ApiMethodInfo';\n\n/**\n * Derives a base URL for a service name that has NO registered mapping — the pluggable half of\n * {@link ClientRegistry} resolution. One per environment, installed once at startup:\n *\n * - `gcpCloudRunDeriver()` (@webpieces/gcp-identity) — the Cloud Run formula, read from the metadata\n * server. For code running ON GCP.\n * - `gcpCloudRunDeriver(new GcpCloudRunTarget(projectNumber, region))` — the SAME formula with the\n * values supplied from config, for code running OFF GCP that still calls Cloud Run (a CLI, CI).\n * - {@link templateDeriver} (this package, browser-safe) — pure string substitution, for AWS or\n * anything else with predictable DNS.\n * - none at all — a browser goes same-origin, and localhost/tests hand-register their mappings.\n */\nexport type ServiceUrlDeriver = (svcName: string) => Promise<string>;\n\n/**\n * ClientRegistry - the ONE place a `svcName` becomes a base URL, for every outbound client in every\n * environment. A client is built once but a URL is per-environment, so the URL belongs here rather\n * than on the client: clients carry ONLY a svcName.\n *\n * Resolution is one precedence chain, identical in the browser, in node, and in Cloud Tasks:\n *\n * 1. a registered mapping wins — the localhost port table, AWS, an external API, another region\n * or project, a host that is not Cloud Run at all. Populated at startup from per-env config.\n * 2. else the installed {@link ServiceUrlDeriver}, if any — GCP's built-in, a DNS template, or\n * your own. OPTIONAL on purpose: localhost is inherently a TABLE (helper-fsdb -> :8401,\n * helper-portal -> :8201 have per-service ports), so explicit mappings must stay sufficient on\n * their own.\n * 3. else the caller's fallback: the BROWSER goes relative (same origin — see\n * {@link ClientRegistry.tryResolve}), while node THROWS (see {@link ClientRegistry.resolve}),\n * because a server has no \"own origin\" and an unresolvable peer is a setup bug.\n *\n * Configured like {@link HeaderRegistry} / LogManager — populated once at startup, then globally\n * accessible with NO DI wiring. It is browser-safe (no `process.env`, no node-only deps), which is\n * why it lives in core-util rather than gcp-identity.\n *\n * A `svcName` should be the nx MODULE name of the service you are calling — that is the one name\n * `architecture:validate-runtime-architecture` can verify actually exists, so a typo or a rename\n * fails the build instead of failing in production. When the DEPLOYED name differs (an environment\n * prefix like `tf-`, or a Cloud Run service named unlike its module), translate it in ONE place —\n * the deriver below, or a `serviceName` alias in the module's project.json — never at the call site.\n *\n * ```ts\n * // startup, from the current environment's config:\n * ClientRegistry.addMapping('server2', 8202); // -> http://localhost:8202\n * ClientRegistry.addUrlMapping('email', 'https://email.other-region.example');\n * ClientRegistry.setDeriver(gcpCloudRunDeriver()); // everything else, on GCP\n * ```\n */\nexport class ClientRegistry {\n /** svcName -> resolved base URL. Process-global; populated at startup per environment. */\n private static readonly mappings = new Map<string, string>();\n\n /** The fallback for svcNames with no mapping. Undefined = no derivation in this environment. */\n private static deriver: ServiceUrlDeriver | undefined;\n\n /**\n * The process's ONE {@link ErrorTranslator}. NEVER undefined — it starts life holding\n * {@link WEBPIECES_DEFAULT_ERROR_TRANSLATOR}, and {@link ClientRegistry.setErrorTranslator}\n * REPLACES it. Process-global, set once at startup on the SERVER and in the BROWSER — the same\n * no-DI pattern as {@link ClientRegistry.mappings} above.\n *\n * There is no \"did anyone register one\" question anywhere in the framework because this field\n * cannot be empty: every caller makes ONE unconditional call, and an app translator declines an\n * error by DELEGATING to {@link WebpiecesDefaultErrorTranslator}.\n *\n * ONE, not a list, on purpose: an app that has several layers of error policy composes them\n * INSIDE its own `toWire`, where the precedence is written down, instead of leaving it implicit\n * in the order two unrelated startup paths happened to register.\n */\n private static errorTranslator: ErrorTranslator = WEBPIECES_DEFAULT_ERROR_TRANSLATOR;\n\n /**\n * The app/company DEFAULT {@link FailureClassifier} — ONE per process, reads {@link ApiMethodInfo.side}\n * so a single strategy covers the server router AND all internal clients. NEVER undefined: it\n * starts life holding the webpieces built-in ({@link WEBPIECES_DEFAULT_FAILURE_CLASSIFIER}) and\n * {@link ClientRegistry.setDefaultFailureClassifier} replaces it. Populated once at startup on the\n * SERVER and in the BROWSER — same no-DI pattern as {@link ClientRegistry.mappings}.\n */\n private static appDefaultFailureClassifier: FailureClassifier =\n WEBPIECES_DEFAULT_FAILURE_CLASSIFIER;\n\n /**\n * Per-EXTERNAL-client {@link FailureClassifier}s, keyed by {@link ApiMethodInfo.apiClass}\n * ('FirestoreAdminClient', 'ClaudeApi', ...). Consulted BEFORE the default tier, so an external\n * client overrides the default for its own apiClass only. Internal clients / the server register\n * nothing here and fall through to the default. See {@link ClientRegistry.classifyFailure}.\n */\n private static readonly failureClassifiersByApiClass = new Map<string, FailureClassifier>();\n\n /** Map a service name to `http://localhost:<port>`. */\n // webpieces-disable no-function-outside-class -- static global singleton (like HeaderRegistry/LogManager); populated once at startup, never DI-injected\n static addMapping(svcName: string, port: number): void {\n ClientRegistry.mappings.set(svcName, `http://localhost:${port}`);\n }\n\n /**\n * Map a service name to an explicit base URL (any host / any environment).\n *\n * The EMPTY STRING is a legal, meaningful mapping: it makes the service relative, i.e.\n * same-origin, because the client builds its URL as `${baseUrl}${route.path}`.\n */\n // webpieces-disable no-function-outside-class -- static global singleton (like HeaderRegistry/LogManager); populated once at startup, never DI-injected\n static addUrlMapping(svcName: string, url: string): void {\n ClientRegistry.mappings.set(svcName, url);\n }\n\n /**\n * Install the environment's {@link ServiceUrlDeriver} — how to resolve a svcName that has NO\n * mapping. Optional: an environment that registers every svcName it calls needs none.\n */\n // webpieces-disable no-function-outside-class -- static global singleton (like HeaderRegistry/LogManager); populated once at startup, never DI-injected\n static setDeriver(fn: ServiceUrlDeriver): void {\n ClientRegistry.deriver = fn;\n }\n\n /** The registered override for `svcName`, or undefined if none. Registry only — no derivation. */\n // webpieces-disable no-function-outside-class -- static global singleton (like HeaderRegistry/LogManager); populated once at startup, never DI-injected\n static tryLookup(svcName: string): string | undefined {\n return ClientRegistry.mappings.get(svcName);\n }\n\n /**\n * Resolve `svcName` to its registered base URL. THROWS if the service was never registered.\n * Registry only — it does NOT consult the deriver; prefer {@link ClientRegistry.resolve}.\n */\n // webpieces-disable no-function-outside-class -- static global singleton (like HeaderRegistry/LogManager); populated once at startup, never DI-injected\n static lookup(svcName: string): string {\n const url = ClientRegistry.tryLookup(svcName);\n if (url === undefined) {\n throw new Error(\n `No URL registered for service \"${svcName}\". Register it at startup: ` +\n `ClientRegistry.addMapping(svcName, port) for a localhost port, or ` +\n `ClientRegistry.addUrlMapping(svcName, url) for an explicit URL.`,\n );\n }\n return url;\n }\n\n /**\n * Steps 1 + 2 of the chain: the registered mapping, else the installed deriver, else undefined.\n * Non-throwing — the BROWSER uses this and treats undefined as \"\" (relative → same origin),\n * which is what a browser app calling the backend it was served from wants by default.\n *\n * Note the `!== undefined` guard: an empty-string mapping is a legal answer (\"this service is\n * same-origin\"), so it must NOT fall through to derivation.\n */\n // webpieces-disable no-function-outside-class -- static global singleton (like HeaderRegistry/LogManager); populated once at startup, never DI-injected\n static tryResolve(svcName: string): Promise<string | undefined> {\n const override = ClientRegistry.tryLookup(svcName);\n if (override !== undefined) {\n return Promise.resolve(override);\n }\n if (!ClientRegistry.deriver) {\n return Promise.resolve(undefined);\n }\n return ClientRegistry.deriver(svcName);\n }\n\n /**\n * The full chain, with node's fallback: mapping, else deriver, else THROW. A server has no \"own\n * origin\" to go relative to, so an unresolvable peer is a setup bug and must fail loudly — and\n * say how to fix it.\n */\n // webpieces-disable no-function-outside-class -- static global singleton (like HeaderRegistry/LogManager); populated once at startup, never DI-injected\n static async resolve(svcName: string): Promise<string> {\n const url = await ClientRegistry.tryResolve(svcName);\n if (url === undefined) {\n throw new Error(\n `No URL for service \"${svcName}\".\\n` +\n ` - localhost/AWS: ClientRegistry.addMapping('${svcName}', 8401)\\n` +\n ` or ClientRegistry.addUrlMapping('${svcName}', 'https://...')\\n` +\n ` - GCP: install a deriver — ClientRegistry.setDeriver(gcpCloudRunDeriver())\\n` +\n ` - deployed name differs from the module name (e.g. a 'tf-' prefix)? Translate it ONCE\\n` +\n ` in the deriver — setDeriver(s => gcpCloudRunDeriver()('tf-' + s)) — so every call\\n` +\n ` site keeps naming the MODULE, which architecture:validate-runtime-architecture checks`,\n );\n }\n return url;\n }\n\n /**\n * Install the app's {@link ErrorTranslator} — the ONE symmetric owner of error translation for\n * this process, server side AND every client side. Consulted BEFORE webpieces' built-in mapping\n * in both directions. Call ONCE at startup, on the server AND in the browser, mirroring\n * {@link ClientRegistry.addMapping}.\n *\n * `set`, not `add`: see the field doc above for why a registry LIST is not wanted.\n */\n // webpieces-disable no-function-outside-class -- static global singleton (like HeaderRegistry/LogManager); populated once at startup, never DI-injected\n static setErrorTranslator(translator: ErrorTranslator): void {\n ClientRegistry.errorTranslator = translator;\n }\n\n /**\n * THE {@link ErrorTranslator} for this process — never `undefined`, so a caller never branches.\n *\n * Until an app calls {@link ClientRegistry.setErrorTranslator} this is\n * {@link WEBPIECES_DEFAULT_ERROR_TRANSLATOR}. A registered translator answers EVERY error and\n * EVERY response, declining by delegating to the webpieces default (see {@link ErrorTranslator}),\n * so every call site in the framework is one unconditional line.\n */\n // webpieces-disable no-function-outside-class -- static global singleton (like HeaderRegistry/LogManager); populated once at startup, never DI-injected\n static getErrorTranslator(): ErrorTranslator {\n return ClientRegistry.errorTranslator;\n }\n\n /**\n * Set the app/company DEFAULT failure classifier — ONE per process, covering the server router and\n * all internal clients (it branches on {@link ApiMethodInfo.side}). Call ONCE at startup, on the\n * server AND in the browser. Overrides the webpieces built-in for calls no per-apiClass classifier\n * claims. See {@link FailureClassifier}.\n */\n // webpieces-disable no-function-outside-class -- static global singleton (like HeaderRegistry/LogManager); populated once at startup, never DI-injected\n static setDefaultFailureClassifier(classifier: FailureClassifier): void {\n ClientRegistry.appDefaultFailureClassifier = classifier;\n }\n\n /**\n * Register a per-EXTERNAL-client failure classifier, keyed by {@link ApiMethodInfo.apiClass}\n * (e.g. 'FirestoreAdminClient'). Consulted BEFORE the default tier for calls with that apiClass.\n * Call ONCE at startup. See {@link FailureClassifier}.\n */\n // webpieces-disable no-function-outside-class -- static global singleton (like HeaderRegistry/LogManager); populated once at startup, never DI-injected\n static addFailureClassifier(apiClass: string, classifier: FailureClassifier): void {\n ClientRegistry.failureClassifiersByApiClass.set(apiClass, classifier);\n }\n\n /**\n * Is this thrown API-call error a real FAILURE (true) or an expected non-failure (false)?\n * Resolved most-specific-first, and ALWAYS definitive (returns a boolean):\n * 1. the per-apiClass classifier, if one is registered for `methodInfo.apiClass` — unless it defers;\n * 2. else the app default classifier, if one was set — unless it defers;\n * 3. else the webpieces built-in ({@link WEBPIECES_DEFAULT_FAILURE_CLASSIFIER}), which never defers.\n * A `?? true` backstop keeps an unexpectedly-deferring built-in fail-safe (treat as failure).\n */\n // webpieces-disable no-function-outside-class -- static global singleton (like HeaderRegistry/LogManager); populated once at startup, never DI-injected\n static classifyFailure(error: Error, methodInfo: ApiMethodInfo): boolean {\n const perClient = ClientRegistry.failureClassifiersByApiClass.get(methodInfo.apiClass);\n if (perClient !== undefined) {\n const verdict = perClient.isFailure(error, methodInfo);\n if (verdict !== undefined) {\n return verdict;\n }\n }\n const verdict = ClientRegistry.appDefaultFailureClassifier.isFailure(error, methodInfo);\n if (verdict !== undefined) {\n return verdict;\n }\n return WEBPIECES_DEFAULT_FAILURE_CLASSIFIER.isFailure(error, methodInfo) ?? true;\n }\n\n /**\n * Put the process-globals back to the state a fresh process starts in. For TESTS, so one spec\n * cannot leak its registrations into the next.\n *\n * It RESTORES the webpieces defaults rather than emptying the pluggable slots, which is the whole\n * reason it is not called `clear()` any more: `getErrorTranslator()` and the default failure\n * classifier are non-optional now, so \"cleared\" is not a state they have. A spec that wants\n * webpieces' own behaviour back asks for exactly that.\n */\n // webpieces-disable no-function-outside-class -- static global singleton (like HeaderRegistry/LogManager); populated once at startup, never DI-injected\n static resetForTests(): void {\n ClientRegistry.mappings.clear();\n ClientRegistry.deriver = undefined;\n ClientRegistry.errorTranslator = WEBPIECES_DEFAULT_ERROR_TRANSLATOR;\n ClientRegistry.appDefaultFailureClassifier = WEBPIECES_DEFAULT_FAILURE_CLASSIFIER;\n ClientRegistry.failureClassifiersByApiClass.clear();\n }\n}\n"]}
@@ -0,0 +1,115 @@
1
+ import { HttpResponseDto } from './HttpResponseDto';
2
+ /**
3
+ * ErrorTranslator - ONE symmetric place an app owns HTTP error translation, in BOTH directions, over
4
+ * the WHOLE response.
5
+ *
6
+ * An app implements this ONCE and registers it ONCE per process via
7
+ * {@link ClientRegistry.setErrorTranslator} — on the server AND in the browser. Its `toWire` runs on
8
+ * the SERVER (`ExpressWrapper.handleError`) and its `fromWire` runs on every CLIENT in that process
9
+ * (`ClientErrorTranslator.throwIfFailure`, shared by `http-client-node` and `http-client-browser`).
10
+ * The payoff is type symmetry across the wire: the server throws `OrderNotFoundError` and the caller
11
+ * CATCHES `OrderNotFoundError`, instead of decoding a status code by hand at every call site.
12
+ *
13
+ * # THERE IS ALWAYS A TRANSLATOR
14
+ *
15
+ * {@link ClientRegistry.getErrorTranslator} is non-optional and starts life holding
16
+ * {@link WebpiecesDefaultErrorTranslator}. So no caller anywhere asks "did anyone register one" —
17
+ * every call site is ONE unconditional line. A registered app translator REPLACES the default and
18
+ * declines by DELEGATING to it:
19
+ *
20
+ * ```ts
21
+ * private readonly fallback = new WebpiecesDefaultErrorTranslator();
22
+ * ```
23
+ *
24
+ * That is one call, it is visible at the call site, and it composes: an app that wants to DECORATE
25
+ * the default rather than replace it does so by calling it and editing the result. A `next` parameter
26
+ * was considered and rejected — it buys nothing this does not, and it puts the framework back in the
27
+ * business of sequencing somebody else's precedence.
28
+ *
29
+ * # Both halves speak {@link HttpResponseDto}, and that is the point
30
+ *
31
+ * `toWire` PRODUCES exactly what `fromWire` CONSUMES. Reading the two methods against each other in
32
+ * one file is what makes a mistake visible, which is why this is one object rather than two
33
+ * separately-registered functions.
34
+ *
35
+ * # The whole response, not a status plus a body
36
+ *
37
+ * The previous contract could express only `(statusCode, protocolError)`, so an app could not put its
38
+ * own trace id, `Retry-After`, `WWW-Authenticate` or a cookie on an error response. A translator now
39
+ * returns the status code, the reason phrase, the header LIST and the body — everything.
40
+ *
41
+ * # `toWire` RETURNS a response; `fromWire` THROWS
42
+ *
43
+ * They are mirrors, not twins. `toWire` is called inside a catch and its whole job is to produce a
44
+ * value, so it returns one. `fromWire` is called on a response the caller is about to be handed, and
45
+ * its whole job is to stop that from happening, so it THROWS and is typed `void`. Returning an
46
+ * `Error` the caller then has to remember to throw was the previous shape and it is exactly the bug
47
+ * class this deletes: a `void` return cannot be silently dropped.
48
+ *
49
+ * There is exactly ONE registered ErrorTranslator per process (a `set`, not an `add` to a list):
50
+ * precedence between an app's own layers is the APP's to compose explicitly inside its `toWire`,
51
+ * rather than something hidden in registration order inside the framework.
52
+ *
53
+ * # Not covered: 404 / unknown route
54
+ *
55
+ * A request that matches no route has no route context and never reaches this seam. That is
56
+ * deliberate and out of scope.
57
+ *
58
+ * This is a business-logic contract (methods, not data), so it is an interface per the webpieces
59
+ * guidelines.
60
+ *
61
+ * ```ts
62
+ * class OrderErrorPayload {
63
+ * constructor(public message: string, public errorCode: string) {}
64
+ * }
65
+ *
66
+ * export class OrderErrorTranslator implements ErrorTranslator {
67
+ * private readonly fallback = new WebpiecesDefaultErrorTranslator();
68
+ *
69
+ * toWire(error: Error): HttpResponseDto {
70
+ * if (!(error instanceof OrderNotFoundError)) {
71
+ * return this.fallback.toWire(error); // not mine -> webpieces default
72
+ * }
73
+ * const body = new OrderErrorPayload(error.message, 'ORDER_NOT_FOUND');
74
+ * return new HttpResponseDto(
75
+ * new HttpResponseStatus(460, 'Order Not Found'),
76
+ * [new HttpHeader('x-order-trace', error.traceId)],
77
+ * body,
78
+ * );
79
+ * }
80
+ *
81
+ * fromWire(response: HttpResponseDto): void {
82
+ * if (response.status.code !== 460) {
83
+ * this.fallback.fromWire(response); // not mine -> webpieces default
84
+ * return;
85
+ * }
86
+ * throw new OrderNotFoundError(String(response.body));
87
+ * }
88
+ * }
89
+ *
90
+ * // startup, ONCE per process (server AND browser)
91
+ * ClientRegistry.setErrorTranslator(new OrderErrorTranslator());
92
+ * ```
93
+ */
94
+ export interface ErrorTranslator {
95
+ /**
96
+ * SERVER: exception -> the ENTIRE response (status code, reason phrase, headers, body). Always a
97
+ * response, never `undefined`; decline by returning
98
+ * `new WebpiecesDefaultErrorTranslator().toWire(error)`.
99
+ */
100
+ toWire(error: Error): HttpResponseDto;
101
+ /**
102
+ * CLIENT: the ENTIRE response -> a THROW. Receives the SAME shape `toWire` produces, normalised
103
+ * from whichever transport read it.
104
+ *
105
+ * Called for EVERY response, including a 2xx — deliberately, so an app whose 200 body signals
106
+ * failure can turn it into a throw. Returning normally means "let this response through"; the
107
+ * webpieces default returns normally for any 2xx that is not 266 and throws otherwise. Decline by
108
+ * calling `new WebpiecesDefaultErrorTranslator().fromWire(response)`.
109
+ *
110
+ * A non-2xx response can never reach a typed caller even if an app translator forgets to throw:
111
+ * `ClientErrorTranslator.throwIfFailure` applies the webpieces default behind it. That is bug
112
+ * containment, not a "was one registered" branch.
113
+ */
114
+ fromWire(response: HttpResponseDto): void;
115
+ }
@@ -1,3 +1,3 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- //# sourceMappingURL=ErrorTranslators.js.map
3
+ //# sourceMappingURL=ErrorTranslator.js.map