@webpieces/core-util 0.4.877 → 0.4.879
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +1 -1
- package/src/errors/ApiError.d.ts +10 -1
- package/src/errors/ApiError.js +10 -1
- package/src/errors/ApiError.js.map +1 -1
- package/src/http/ApiCallTimeoutError.d.ts +22 -1
- package/src/http/ApiCallTimeoutError.js +29 -1
- package/src/http/ApiCallTimeoutError.js.map +1 -1
- package/src/http/CallRegistry.d.ts +7 -2
- package/src/http/CallRegistry.js +8 -4
- package/src/http/CallRegistry.js.map +1 -1
- package/src/http/CallStrategy.d.ts +12 -2
- package/src/http/CallStrategy.js +12 -2
- package/src/http/CallStrategy.js.map +1 -1
- package/src/http/WebpiecesDefaultErrorTranslator.d.ts +9 -0
- package/src/http/WebpiecesDefaultErrorTranslator.js +16 -1
- package/src/http/WebpiecesDefaultErrorTranslator.js.map +1 -1
- package/src/ipc/IpcConnection.js +5 -1
- package/src/ipc/IpcConnection.js.map +1 -1
package/package.json
CHANGED
package/src/errors/ApiError.d.ts
CHANGED
|
@@ -36,9 +36,18 @@ export declare class ApiForbiddenError extends ApiError {
|
|
|
36
36
|
export declare class ApiRequestTimeoutError extends ApiError {
|
|
37
37
|
readonly kind: "request-timeout";
|
|
38
38
|
}
|
|
39
|
-
/**
|
|
39
|
+
/**
|
|
40
|
+
* A caller-local connection failure. HTTP servers normalize this to ApiImplementationError.
|
|
41
|
+
*
|
|
42
|
+
* `message` is the developer text (it names the url and the transport's own reason); `userMessage`
|
|
43
|
+
* is the stable text an app renders verbatim to an end user, matching `ApiCallTimeoutError.userMessage`
|
|
44
|
+
* so the two transport failures read consistently.
|
|
45
|
+
*/
|
|
40
46
|
export declare class ApiConnectionError extends ApiError {
|
|
47
|
+
static readonly USER_MESSAGE = "We couldn't reach the server. Please check your network connection and try again.";
|
|
41
48
|
readonly kind: "connection";
|
|
49
|
+
/** End-user text; `message` stays the developer text. */
|
|
50
|
+
readonly userMessage: string;
|
|
42
51
|
}
|
|
43
52
|
/** The API operation is missing, distinct from an absent domain entity. */
|
|
44
53
|
export declare class ApiEndpointNotFoundError extends ApiNotFoundError {
|
package/src/errors/ApiError.js
CHANGED
|
@@ -53,9 +53,18 @@ class ApiRequestTimeoutError extends ApiError {
|
|
|
53
53
|
kind = 'request-timeout';
|
|
54
54
|
}
|
|
55
55
|
exports.ApiRequestTimeoutError = ApiRequestTimeoutError;
|
|
56
|
-
/**
|
|
56
|
+
/**
|
|
57
|
+
* A caller-local connection failure. HTTP servers normalize this to ApiImplementationError.
|
|
58
|
+
*
|
|
59
|
+
* `message` is the developer text (it names the url and the transport's own reason); `userMessage`
|
|
60
|
+
* is the stable text an app renders verbatim to an end user, matching `ApiCallTimeoutError.userMessage`
|
|
61
|
+
* so the two transport failures read consistently.
|
|
62
|
+
*/
|
|
57
63
|
class ApiConnectionError extends ApiError {
|
|
64
|
+
static USER_MESSAGE = "We couldn't reach the server. Please check your network connection and try again.";
|
|
58
65
|
kind = 'connection';
|
|
66
|
+
/** End-user text; `message` stays the developer text. */
|
|
67
|
+
userMessage = ApiConnectionError.USER_MESSAGE;
|
|
59
68
|
}
|
|
60
69
|
exports.ApiConnectionError = ApiConnectionError;
|
|
61
70
|
/** The API operation is missing, distinct from an absent domain entity. */
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"ApiError.js","sourceRoot":"","sources":["../../../../../../packages/core/core-util/src/errors/ApiError.ts"],"names":[],"mappings":"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","sourcesContent":["export type ApiErrorKind =\n | 'end-user'\n | 'bad-request'\n | 'unauthorized'\n | 'forbidden'\n | 'not-found'\n | 'endpoint-not-found'\n | 'request-timeout'\n | 'rate-limited'\n | 'conflict'\n | 'unprocessable'\n | 'precondition-failed'\n | 'unsupported-media-type'\n | 'not-implemented'\n | 'coded'\n | 'implementation'\n | 'dependency'\n | 'bad-gateway'\n | 'unavailable'\n | 'dependency-timeout'\n | 'dependency-backoff'\n | 'connection';\n\n/** Category for portable API failures. Concrete subclasses alone define protocol mappings. */\nexport abstract class ApiError extends Error {\n abstract readonly kind: ApiErrorKind;\n public subType?: string;\n\n constructor(message: string, cause?: Error) {\n super(message, { cause });\n this.name = new.target.name;\n }\n}\n\nexport class ApiNotFoundError extends ApiError {\n override readonly kind: ApiErrorKind = 'not-found';\n}\nexport class ApiDependencyError extends ApiError {\n override readonly kind: ApiErrorKind = 'dependency';\n}\n/** A retry-eligible HTTP 502/proxy failure, distinct from a generic dependency defect. */\nexport class ApiBadGatewayError extends ApiDependencyError {\n override readonly kind = 'bad-gateway' as const;\n}\nexport class ApiUnavailableError extends ApiDependencyError {\n override readonly kind = 'unavailable' as const;\n}\nexport class ApiDependencyTimeoutError extends ApiDependencyError {\n override readonly kind = 'dependency-timeout' as const;\n}\n\n/** A bug or invalid implementation state. Local errors become server errors only after a remote decode. */\nexport class ApiImplementationError extends ApiError {\n override readonly kind = 'implementation' as const;\n\n constructor(\n message: string,\n cause?: Error,\n public readonly serverError = false,\n ) {\n super(message, cause);\n }\n}\n\nexport class ApiRateLimitedError extends ApiError {\n override readonly kind = 'rate-limited' as const;\n}\nexport class ApiForbiddenError extends ApiError {\n override readonly kind = 'forbidden' as const;\n}\nexport class ApiRequestTimeoutError extends ApiError {\n override readonly kind = 'request-timeout' as const;\n}\n\n/** A caller-local connection failure. HTTP servers normalize this to ApiImplementationError. */\nexport class ApiConnectionError extends ApiError {\n override readonly kind = 'connection' as const;\n}\n\n/** The API operation is missing, distinct from an absent domain entity. */\nexport class ApiEndpointNotFoundError extends ApiNotFoundError {\n override readonly kind = 'endpoint-not-found' as const;\n}\n\n/**\n * The HTTP status an API EDGE answers for an {@link ApiEndUserError}. End-user outcomes only, so\n * nobody tunnels a 5xx/401/403/429 through the end-user channel (auth, rate limiting and dependency\n * failure keep their own types). Extend deliberately.\n */\nexport type EdgeHttpStatus = 400 | 404 | 409 | 422;\n\n/**\n * Expected mistake by the ACTOR driving the call: the input was well-formed but wrong in a way the actor\n * must fix (for example \"the two passwords you entered do not match\"). The actor may be a human user in\n * a GUI OR an LLM calling an MCP tool; the same class serves both. Its message is deliberately\n * caller-safe and is the only ApiError message published verbatim: `ApiErrorCodec` keeps it (and\n * `errorCode`) across every remote hop, so a downstream service's text reaches the GUI or the model\n * byte-for-byte.\n *\n * `edgeHttpStatus` travels the same way. It is what a partner-facing API edge (an http-server router\n * in `'edge'` end-user-status mode) answers instead of 266, so one throw site serves both a GUI and a\n * published REST contract: `throw new ApiEndUserError('That report does not exist', 'report_not_found', 404)`.\n * GUI edges ignore it and keep answering 266.\n */\nexport class ApiEndUserError extends ApiError {\n override readonly kind = 'end-user' as const;\n\n constructor(\n message: string,\n public errorCode?: string,\n public edgeHttpStatus?: EdgeHttpStatus,\n cause?: Error,\n ) {\n super(message, cause);\n this.subType = 'USER_ERROR';\n }\n\n /** Narrows a dynamic value (for example one read off the wire) to a legal {@link EdgeHttpStatus}. */\n // webpieces-disable no-function-outside-class -- stateless status guard; webpieces-disable no-any-unknown -- wire values are narrowed here\n static isEdgeHttpStatus(value: unknown): value is EdgeHttpStatus {\n return value === 400 || value === 404 || value === 409 || value === 422;\n }\n}\n\n/**\n * Invalid API caller input (malformed, or failed validation). `message` is operator-only diagnostic\n * text and may name internals; it is never published. `callerMessage` (plus `field`) is what a GUI OR\n * an MCP model is shown so it can correct its input, so throw sites that want the caller to\n * self-correct MUST set it.\n */\nexport class ApiBadRequestError extends ApiError {\n override readonly kind = 'bad-request' as const;\n\n constructor(\n message: string,\n public field?: string,\n public callerMessage?: string,\n cause?: Error,\n ) {\n super(message, cause);\n }\n}\n\nexport class ApiUnauthorizedError extends ApiError {\n override readonly kind = 'unauthorized' as const;\n\n constructor(message: string, subType?: string, cause?: Error) {\n super(message, cause);\n this.subType = subType;\n }\n}\n\nexport class ApiDependencyBackoffError extends ApiDependencyError {\n override readonly kind = 'dependency-backoff' as const;\n\n constructor(\n message: string,\n public retryAfterSeconds = 30,\n cause?: Error,\n ) {\n super(message, cause);\n }\n}\n\n/** The request conflicts with the current state of the target resource (HTTP 409). */\nexport class ApiConflictError extends ApiError {\n override readonly kind = 'conflict' as const;\n}\n\n/** The request was well-formed but semantically unprocessable (HTTP 422). */\nexport class ApiUnprocessableError extends ApiError {\n override readonly kind = 'unprocessable' as const;\n}\n\n/** A caller-supplied precondition (for example If-Match) did not hold (HTTP 412). */\nexport class ApiPreconditionFailedError extends ApiError {\n override readonly kind = 'precondition-failed' as const;\n}\n\n/** The request body is in a media type this operation does not accept (HTTP 415). */\nexport class ApiUnsupportedMediaTypeError extends ApiError {\n override readonly kind = 'unsupported-media-type' as const;\n}\n\n/** The operation exists in the contract but this server does not implement it (HTTP 501). */\nexport class ApiNotImplementedError extends ApiError {\n override readonly kind = 'not-implemented' as const;\n}\n\n/** Builds the tuple [0, 1, ..., N-1]; tail-recursive so TypeScript can reach 600. */\ntype StatusRange<N extends number, Acc extends number[] = []> = Acc['length'] extends N\n ? Acc\n : StatusRange<N, [...Acc, Acc['length']]>;\n\n/**\n * Every integer from 100 through 599 as a literal union. `new ApiCodedError(msg, 600)`,\n * `new ApiCodedError(msg, 99)` and `new ApiCodedError(msg, 404.5)` do not compile; a dynamic\n * `number` must be narrowed with {@link ApiCodedError.isStatusCode} first.\n */\nexport type ApiStatusCode = Exclude<StatusRange<600>[number], StatusRange<100>[number]>;\n\n/**\n * Catch-all carrying an explicit protocol status for outcomes no named ApiError covers. Any code in\n * 100-599 is accepted, including one a named class already owns. `statusCode` and `errorCode` survive\n * every remote hop through `ApiErrorCodec`; `message` stays operator-only like every other non\n * end-user kind. A status below 500 is a caller outcome (except 408 and 429, which mirror\n * `ApiRequestTimeoutError` and `ApiRateLimitedError`); 500 and above is a server/dependency fault.\n */\nexport class ApiCodedError extends ApiError {\n override readonly kind = 'coded' as const;\n\n constructor(\n message: string,\n public readonly statusCode: ApiStatusCode,\n public errorCode?: string,\n cause?: Error,\n ) {\n super(message, cause);\n }\n\n /** Narrows a dynamic number (for example one read off the wire) to a legal status code. */\n // webpieces-disable no-function-outside-class -- stateless status guard; webpieces-disable no-any-unknown -- wire and app values are narrowed here\n static isStatusCode(value: unknown): value is ApiStatusCode {\n return typeof value === 'number' && Number.isInteger(value) && value >= 100 && value <= 599;\n }\n\n /** True when the status describes the caller's mistake rather than a fault on the serving side. */\n isCallerError(): boolean {\n return this.statusCode < 500 && this.statusCode !== 408 && this.statusCode !== 429;\n }\n}\n"]}
|
|
1
|
+
{"version":3,"file":"ApiError.js","sourceRoot":"","sources":["../../../../../../packages/core/core-util/src/errors/ApiError.ts"],"names":[],"mappings":"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","sourcesContent":["export type ApiErrorKind =\n | 'end-user'\n | 'bad-request'\n | 'unauthorized'\n | 'forbidden'\n | 'not-found'\n | 'endpoint-not-found'\n | 'request-timeout'\n | 'rate-limited'\n | 'conflict'\n | 'unprocessable'\n | 'precondition-failed'\n | 'unsupported-media-type'\n | 'not-implemented'\n | 'coded'\n | 'implementation'\n | 'dependency'\n | 'bad-gateway'\n | 'unavailable'\n | 'dependency-timeout'\n | 'dependency-backoff'\n | 'connection';\n\n/** Category for portable API failures. Concrete subclasses alone define protocol mappings. */\nexport abstract class ApiError extends Error {\n abstract readonly kind: ApiErrorKind;\n public subType?: string;\n\n constructor(message: string, cause?: Error) {\n super(message, { cause });\n this.name = new.target.name;\n }\n}\n\nexport class ApiNotFoundError extends ApiError {\n override readonly kind: ApiErrorKind = 'not-found';\n}\nexport class ApiDependencyError extends ApiError {\n override readonly kind: ApiErrorKind = 'dependency';\n}\n/** A retry-eligible HTTP 502/proxy failure, distinct from a generic dependency defect. */\nexport class ApiBadGatewayError extends ApiDependencyError {\n override readonly kind = 'bad-gateway' as const;\n}\nexport class ApiUnavailableError extends ApiDependencyError {\n override readonly kind = 'unavailable' as const;\n}\nexport class ApiDependencyTimeoutError extends ApiDependencyError {\n override readonly kind = 'dependency-timeout' as const;\n}\n\n/** A bug or invalid implementation state. Local errors become server errors only after a remote decode. */\nexport class ApiImplementationError extends ApiError {\n override readonly kind = 'implementation' as const;\n\n constructor(\n message: string,\n cause?: Error,\n public readonly serverError = false,\n ) {\n super(message, cause);\n }\n}\n\nexport class ApiRateLimitedError extends ApiError {\n override readonly kind = 'rate-limited' as const;\n}\nexport class ApiForbiddenError extends ApiError {\n override readonly kind = 'forbidden' as const;\n}\nexport class ApiRequestTimeoutError extends ApiError {\n override readonly kind = 'request-timeout' as const;\n}\n\n/**\n * A caller-local connection failure. HTTP servers normalize this to ApiImplementationError.\n *\n * `message` is the developer text (it names the url and the transport's own reason); `userMessage`\n * is the stable text an app renders verbatim to an end user, matching `ApiCallTimeoutError.userMessage`\n * so the two transport failures read consistently.\n */\nexport class ApiConnectionError extends ApiError {\n static readonly USER_MESSAGE =\n \"We couldn't reach the server. Please check your network connection and try again.\";\n\n override readonly kind = 'connection' as const;\n /** End-user text; `message` stays the developer text. */\n readonly userMessage: string = ApiConnectionError.USER_MESSAGE;\n}\n\n/** The API operation is missing, distinct from an absent domain entity. */\nexport class ApiEndpointNotFoundError extends ApiNotFoundError {\n override readonly kind = 'endpoint-not-found' as const;\n}\n\n/**\n * The HTTP status an API EDGE answers for an {@link ApiEndUserError}. End-user outcomes only, so\n * nobody tunnels a 5xx/401/403/429 through the end-user channel (auth, rate limiting and dependency\n * failure keep their own types). Extend deliberately.\n */\nexport type EdgeHttpStatus = 400 | 404 | 409 | 422;\n\n/**\n * Expected mistake by the ACTOR driving the call: the input was well-formed but wrong in a way the actor\n * must fix (for example \"the two passwords you entered do not match\"). The actor may be a human user in\n * a GUI OR an LLM calling an MCP tool; the same class serves both. Its message is deliberately\n * caller-safe and is the only ApiError message published verbatim: `ApiErrorCodec` keeps it (and\n * `errorCode`) across every remote hop, so a downstream service's text reaches the GUI or the model\n * byte-for-byte.\n *\n * `edgeHttpStatus` travels the same way. It is what a partner-facing API edge (an http-server router\n * in `'edge'` end-user-status mode) answers instead of 266, so one throw site serves both a GUI and a\n * published REST contract: `throw new ApiEndUserError('That report does not exist', 'report_not_found', 404)`.\n * GUI edges ignore it and keep answering 266.\n */\nexport class ApiEndUserError extends ApiError {\n override readonly kind = 'end-user' as const;\n\n constructor(\n message: string,\n public errorCode?: string,\n public edgeHttpStatus?: EdgeHttpStatus,\n cause?: Error,\n ) {\n super(message, cause);\n this.subType = 'USER_ERROR';\n }\n\n /** Narrows a dynamic value (for example one read off the wire) to a legal {@link EdgeHttpStatus}. */\n // webpieces-disable no-function-outside-class -- stateless status guard; webpieces-disable no-any-unknown -- wire values are narrowed here\n static isEdgeHttpStatus(value: unknown): value is EdgeHttpStatus {\n return value === 400 || value === 404 || value === 409 || value === 422;\n }\n}\n\n/**\n * Invalid API caller input (malformed, or failed validation). `message` is operator-only diagnostic\n * text and may name internals; it is never published. `callerMessage` (plus `field`) is what a GUI OR\n * an MCP model is shown so it can correct its input, so throw sites that want the caller to\n * self-correct MUST set it.\n */\nexport class ApiBadRequestError extends ApiError {\n override readonly kind = 'bad-request' as const;\n\n constructor(\n message: string,\n public field?: string,\n public callerMessage?: string,\n cause?: Error,\n ) {\n super(message, cause);\n }\n}\n\nexport class ApiUnauthorizedError extends ApiError {\n override readonly kind = 'unauthorized' as const;\n\n constructor(message: string, subType?: string, cause?: Error) {\n super(message, cause);\n this.subType = subType;\n }\n}\n\nexport class ApiDependencyBackoffError extends ApiDependencyError {\n override readonly kind = 'dependency-backoff' as const;\n\n constructor(\n message: string,\n public retryAfterSeconds = 30,\n cause?: Error,\n ) {\n super(message, cause);\n }\n}\n\n/** The request conflicts with the current state of the target resource (HTTP 409). */\nexport class ApiConflictError extends ApiError {\n override readonly kind = 'conflict' as const;\n}\n\n/** The request was well-formed but semantically unprocessable (HTTP 422). */\nexport class ApiUnprocessableError extends ApiError {\n override readonly kind = 'unprocessable' as const;\n}\n\n/** A caller-supplied precondition (for example If-Match) did not hold (HTTP 412). */\nexport class ApiPreconditionFailedError extends ApiError {\n override readonly kind = 'precondition-failed' as const;\n}\n\n/** The request body is in a media type this operation does not accept (HTTP 415). */\nexport class ApiUnsupportedMediaTypeError extends ApiError {\n override readonly kind = 'unsupported-media-type' as const;\n}\n\n/** The operation exists in the contract but this server does not implement it (HTTP 501). */\nexport class ApiNotImplementedError extends ApiError {\n override readonly kind = 'not-implemented' as const;\n}\n\n/** Builds the tuple [0, 1, ..., N-1]; tail-recursive so TypeScript can reach 600. */\ntype StatusRange<N extends number, Acc extends number[] = []> = Acc['length'] extends N\n ? Acc\n : StatusRange<N, [...Acc, Acc['length']]>;\n\n/**\n * Every integer from 100 through 599 as a literal union. `new ApiCodedError(msg, 600)`,\n * `new ApiCodedError(msg, 99)` and `new ApiCodedError(msg, 404.5)` do not compile; a dynamic\n * `number` must be narrowed with {@link ApiCodedError.isStatusCode} first.\n */\nexport type ApiStatusCode = Exclude<StatusRange<600>[number], StatusRange<100>[number]>;\n\n/**\n * Catch-all carrying an explicit protocol status for outcomes no named ApiError covers. Any code in\n * 100-599 is accepted, including one a named class already owns. `statusCode` and `errorCode` survive\n * every remote hop through `ApiErrorCodec`; `message` stays operator-only like every other non\n * end-user kind. A status below 500 is a caller outcome (except 408 and 429, which mirror\n * `ApiRequestTimeoutError` and `ApiRateLimitedError`); 500 and above is a server/dependency fault.\n */\nexport class ApiCodedError extends ApiError {\n override readonly kind = 'coded' as const;\n\n constructor(\n message: string,\n public readonly statusCode: ApiStatusCode,\n public errorCode?: string,\n cause?: Error,\n ) {\n super(message, cause);\n }\n\n /** Narrows a dynamic number (for example one read off the wire) to a legal status code. */\n // webpieces-disable no-function-outside-class -- stateless status guard; webpieces-disable no-any-unknown -- wire and app values are narrowed here\n static isStatusCode(value: unknown): value is ApiStatusCode {\n return typeof value === 'number' && Number.isInteger(value) && value >= 100 && value <= 599;\n }\n\n /** True when the status describes the caller's mistake rather than a fault on the serving side. */\n isCallerError(): boolean {\n return this.statusCode < 500 && this.statusCode !== 408 && this.statusCode !== 429;\n }\n}\n"]}
|
|
@@ -1,8 +1,29 @@
|
|
|
1
1
|
import { CallContext } from './CallStrategy';
|
|
2
2
|
import { ApiDependencyTimeoutError } from '../errors/ApiError';
|
|
3
|
-
/**
|
|
3
|
+
/**
|
|
4
|
+
* Stopped waiting; this does not prove the remote operation did not run.
|
|
5
|
+
*
|
|
6
|
+
* Two audiences, two fields:
|
|
7
|
+
* - `message` is the DEVELOPER text (`SaveApi.save timed out after 30000ms`) for logs and error
|
|
8
|
+
* reporting. Keep reporting it: a deadline that fires in production is a bug for somebody.
|
|
9
|
+
* - `userMessage` is the stable, calm text an app renders VERBATIM to an end user, in the same
|
|
10
|
+
* spirit as `ApiBadRequestError.callerMessage`. It is chosen by the endpoint's declared
|
|
11
|
+
* operation (`context.operation`), because the safe advice differs: a `READ` or
|
|
12
|
+
* `WRITE_IDEMPOTENT` call is safe to repeat, while a `WRITE` may already have been applied and a
|
|
13
|
+
* blind retry can apply it twice.
|
|
14
|
+
*
|
|
15
|
+
* A client deadline is not a gateway failure, so an app should never title it "Gateway Timeout";
|
|
16
|
+
* check `instanceof ApiCallTimeoutError` BEFORE the broader `ApiDependencyTimeoutError`.
|
|
17
|
+
*/
|
|
4
18
|
export declare class ApiCallTimeoutError extends ApiDependencyTimeoutError {
|
|
5
19
|
readonly timeoutMs: number;
|
|
6
20
|
readonly context: CallContext;
|
|
21
|
+
/** Shown for `READ` and `WRITE_IDEMPOTENT`: repeating the call is safe. */
|
|
22
|
+
static readonly RETRY_SAFE_USER_MESSAGE = "The network timed out. It probably just flaked. Please try your request again.";
|
|
23
|
+
/** Shown for `WRITE`: the change may already have been applied, so check before repeating it. */
|
|
24
|
+
static readonly WRITE_USER_MESSAGE = "The network timed out. Your change may or may not have been saved \u2014 please check before trying again.";
|
|
25
|
+
/** End-user text, chosen from `context.operation`. `message` stays the developer text. */
|
|
26
|
+
readonly userMessage: string;
|
|
7
27
|
constructor(timeoutMs: number, context: CallContext);
|
|
28
|
+
private static userMessageFor;
|
|
8
29
|
}
|
|
@@ -2,15 +2,43 @@
|
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.ApiCallTimeoutError = void 0;
|
|
4
4
|
const ApiError_1 = require("../errors/ApiError");
|
|
5
|
-
|
|
5
|
+
const HttpEndpointOptions_1 = require("./HttpEndpointOptions");
|
|
6
|
+
/**
|
|
7
|
+
* Stopped waiting; this does not prove the remote operation did not run.
|
|
8
|
+
*
|
|
9
|
+
* Two audiences, two fields:
|
|
10
|
+
* - `message` is the DEVELOPER text (`SaveApi.save timed out after 30000ms`) for logs and error
|
|
11
|
+
* reporting. Keep reporting it: a deadline that fires in production is a bug for somebody.
|
|
12
|
+
* - `userMessage` is the stable, calm text an app renders VERBATIM to an end user, in the same
|
|
13
|
+
* spirit as `ApiBadRequestError.callerMessage`. It is chosen by the endpoint's declared
|
|
14
|
+
* operation (`context.operation`), because the safe advice differs: a `READ` or
|
|
15
|
+
* `WRITE_IDEMPOTENT` call is safe to repeat, while a `WRITE` may already have been applied and a
|
|
16
|
+
* blind retry can apply it twice.
|
|
17
|
+
*
|
|
18
|
+
* A client deadline is not a gateway failure, so an app should never title it "Gateway Timeout";
|
|
19
|
+
* check `instanceof ApiCallTimeoutError` BEFORE the broader `ApiDependencyTimeoutError`.
|
|
20
|
+
*/
|
|
6
21
|
class ApiCallTimeoutError extends ApiError_1.ApiDependencyTimeoutError {
|
|
7
22
|
timeoutMs;
|
|
8
23
|
context;
|
|
24
|
+
/** Shown for `READ` and `WRITE_IDEMPOTENT`: repeating the call is safe. */
|
|
25
|
+
static RETRY_SAFE_USER_MESSAGE = 'The network timed out. It probably just flaked. Please try your request again.';
|
|
26
|
+
/** Shown for `WRITE`: the change may already have been applied, so check before repeating it. */
|
|
27
|
+
static WRITE_USER_MESSAGE = 'The network timed out. Your change may or may not have been saved — please check before trying again.';
|
|
28
|
+
/** End-user text, chosen from `context.operation`. `message` stays the developer text. */
|
|
29
|
+
userMessage;
|
|
9
30
|
constructor(timeoutMs, context) {
|
|
10
31
|
super(`${context.apiName}.${context.methodName} timed out after ${timeoutMs}ms`);
|
|
11
32
|
this.timeoutMs = timeoutMs;
|
|
12
33
|
this.context = context;
|
|
13
34
|
this.name = 'ApiCallTimeoutError';
|
|
35
|
+
this.userMessage = ApiCallTimeoutError.userMessageFor(context.operation);
|
|
36
|
+
}
|
|
37
|
+
// webpieces-disable no-function-outside-class -- pure operation -> text mapping owned by this error
|
|
38
|
+
static userMessageFor(operation) {
|
|
39
|
+
return operation === HttpEndpointOptions_1.WRITE
|
|
40
|
+
? ApiCallTimeoutError.WRITE_USER_MESSAGE
|
|
41
|
+
: ApiCallTimeoutError.RETRY_SAFE_USER_MESSAGE;
|
|
14
42
|
}
|
|
15
43
|
}
|
|
16
44
|
exports.ApiCallTimeoutError = ApiCallTimeoutError;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"ApiCallTimeoutError.js","sourceRoot":"","sources":["../../../../../../packages/core/core-util/src/http/ApiCallTimeoutError.ts"],"names":[],"mappings":";;;AACA,iDAA+D;
|
|
1
|
+
{"version":3,"file":"ApiCallTimeoutError.js","sourceRoot":"","sources":["../../../../../../packages/core/core-util/src/http/ApiCallTimeoutError.ts"],"names":[],"mappings":";;;AACA,iDAA+D;AAC/D,+DAAiE;AAEjE;;;;;;;;;;;;;;GAcG;AACH,MAAa,mBAAoB,SAAQ,oCAAyB;IAY1C;IACA;IAZpB,2EAA2E;IAC3E,MAAM,CAAU,uBAAuB,GACnC,gFAAgF,CAAC;IACrF,iGAAiG;IACjG,MAAM,CAAU,kBAAkB,GAC9B,uGAAuG,CAAC;IAE5G,0FAA0F;IACjF,WAAW,CAAS;IAE7B,YACoB,SAAiB,EACjB,OAAoB;QAEpC,KAAK,CAAC,GAAG,OAAO,CAAC,OAAO,IAAI,OAAO,CAAC,UAAU,oBAAoB,SAAS,IAAI,CAAC,CAAC;QAHjE,cAAS,GAAT,SAAS,CAAQ;QACjB,YAAO,GAAP,OAAO,CAAa;QAGpC,IAAI,CAAC,IAAI,GAAG,qBAAqB,CAAC;QAClC,IAAI,CAAC,WAAW,GAAG,mBAAmB,CAAC,cAAc,CAAC,OAAO,CAAC,SAAS,CAAC,CAAC;IAC7E,CAAC;IAED,oGAAoG;IAC5F,MAAM,CAAC,cAAc,CAAC,SAA4B;QACtD,OAAO,SAAS,KAAK,2BAAK;YACtB,CAAC,CAAC,mBAAmB,CAAC,kBAAkB;YACxC,CAAC,CAAC,mBAAmB,CAAC,uBAAuB,CAAC;IACtD,CAAC;;AAzBL,kDA0BC","sourcesContent":["import { CallContext } from './CallStrategy';\nimport { ApiDependencyTimeoutError } from '../errors/ApiError';\nimport { EndpointOperation, WRITE } from './HttpEndpointOptions';\n\n/**\n * Stopped waiting; this does not prove the remote operation did not run.\n *\n * Two audiences, two fields:\n * - `message` is the DEVELOPER text (`SaveApi.save timed out after 30000ms`) for logs and error\n * reporting. Keep reporting it: a deadline that fires in production is a bug for somebody.\n * - `userMessage` is the stable, calm text an app renders VERBATIM to an end user, in the same\n * spirit as `ApiBadRequestError.callerMessage`. It is chosen by the endpoint's declared\n * operation (`context.operation`), because the safe advice differs: a `READ` or\n * `WRITE_IDEMPOTENT` call is safe to repeat, while a `WRITE` may already have been applied and a\n * blind retry can apply it twice.\n *\n * A client deadline is not a gateway failure, so an app should never title it \"Gateway Timeout\";\n * check `instanceof ApiCallTimeoutError` BEFORE the broader `ApiDependencyTimeoutError`.\n */\nexport class ApiCallTimeoutError extends ApiDependencyTimeoutError {\n /** Shown for `READ` and `WRITE_IDEMPOTENT`: repeating the call is safe. */\n static readonly RETRY_SAFE_USER_MESSAGE =\n 'The network timed out. It probably just flaked. Please try your request again.';\n /** Shown for `WRITE`: the change may already have been applied, so check before repeating it. */\n static readonly WRITE_USER_MESSAGE =\n 'The network timed out. Your change may or may not have been saved — please check before trying again.';\n\n /** End-user text, chosen from `context.operation`. `message` stays the developer text. */\n readonly userMessage: string;\n\n constructor(\n public readonly timeoutMs: number,\n public readonly context: CallContext,\n ) {\n super(`${context.apiName}.${context.methodName} timed out after ${timeoutMs}ms`);\n this.name = 'ApiCallTimeoutError';\n this.userMessage = ApiCallTimeoutError.userMessageFor(context.operation);\n }\n\n // webpieces-disable no-function-outside-class -- pure operation -> text mapping owned by this error\n private static userMessageFor(operation: EndpointOperation): string {\n return operation === WRITE\n ? ApiCallTimeoutError.WRITE_USER_MESSAGE\n : ApiCallTimeoutError.RETRY_SAFE_USER_MESSAGE;\n }\n}\n"]}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { Attempt, CallStrategy } from './CallStrategy';
|
|
1
|
+
import { Attempt, CallContext, CallStrategy } from './CallStrategy';
|
|
2
2
|
/** ALL has no method; a contract can optionally select one method. */
|
|
3
3
|
type CallScope = [api: 'ALL'] | [api: Function, methodName?: string];
|
|
4
4
|
/**
|
|
@@ -12,7 +12,12 @@ export declare class CallRegistry {
|
|
|
12
12
|
private static readonly apis;
|
|
13
13
|
static setTimeout(timeoutMs: number | undefined, ...scope: CallScope): void;
|
|
14
14
|
static setStrategy(strategy: CallStrategy<unknown> | undefined, ...scope: CallScope): void;
|
|
15
|
-
|
|
15
|
+
/**
|
|
16
|
+
* Run one call under its registered policy. `context` names the method (its `methodName` selects
|
|
17
|
+
* the per-method policy) and carries the endpoint's declared operation, so the SAME context a
|
|
18
|
+
* strategy sees is the one the transport's deadline puts on an `ApiCallTimeoutError`.
|
|
19
|
+
*/
|
|
20
|
+
static execute<T>(api: Function, context: CallContext, attempt: Attempt<T>, defaultMs: number): Promise<T>;
|
|
16
21
|
static clear(): void;
|
|
17
22
|
private static policy;
|
|
18
23
|
}
|
package/src/http/CallRegistry.js
CHANGED
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.CallRegistry = void 0;
|
|
4
|
-
const CallStrategy_1 = require("./CallStrategy");
|
|
5
4
|
const CallDeadline_1 = require("./CallDeadline");
|
|
6
5
|
class CallPolicy {
|
|
7
6
|
timeoutMs;
|
|
@@ -33,13 +32,18 @@ class CallRegistry {
|
|
|
33
32
|
strategy, ...scope) {
|
|
34
33
|
CallRegistry.policy(scope[0], scope[1]).strategy = strategy;
|
|
35
34
|
}
|
|
35
|
+
/**
|
|
36
|
+
* Run one call under its registered policy. `context` names the method (its `methodName` selects
|
|
37
|
+
* the per-method policy) and carries the endpoint's declared operation, so the SAME context a
|
|
38
|
+
* strategy sees is the one the transport's deadline puts on an `ApiCallTimeoutError`.
|
|
39
|
+
*/
|
|
36
40
|
// webpieces-disable no-function-outside-class -- transport-independent policy execution
|
|
37
|
-
static async execute(api,
|
|
41
|
+
static async execute(api, context, attempt, defaultMs) {
|
|
38
42
|
const policies = CallRegistry.apis.get(api);
|
|
39
|
-
const method = policies?.methods.get(methodName);
|
|
43
|
+
const method = policies?.methods.get(context.methodName);
|
|
40
44
|
const strategy = method?.strategy ?? policies?.policy.strategy ?? CallRegistry.all.strategy;
|
|
41
45
|
if (strategy !== undefined) {
|
|
42
|
-
return (await strategy(attempt,
|
|
46
|
+
return (await strategy(attempt, context));
|
|
43
47
|
}
|
|
44
48
|
return attempt(method?.timeoutMs ??
|
|
45
49
|
policies?.policy.timeoutMs ??
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"CallRegistry.js","sourceRoot":"","sources":["../../../../../../packages/core/core-util/src/http/CallRegistry.ts"],"names":[],"mappings":";;;
|
|
1
|
+
{"version":3,"file":"CallRegistry.js","sourceRoot":"","sources":["../../../../../../packages/core/core-util/src/http/CallRegistry.ts"],"names":[],"mappings":"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","sourcesContent":["import { Attempt, CallContext, CallStrategy } from './CallStrategy';\nimport { CallDeadline } from './CallDeadline';\n\n/** ALL has no method; a contract can optionally select one method. */\ntype CallScope = [api: 'ALL'] | [api: Function, methodName?: string];\n\nclass CallPolicy {\n timeoutMs?: number;\n // webpieces-disable no-any-unknown -- global policy spans heterogeneous response DTOs\n strategy?: CallStrategy<unknown>;\n}\n\nclass ApiCallPolicies {\n readonly policy = new CallPolicy();\n readonly methods = new Map<string, CallPolicy>();\n}\n\n/**\n * Shared startup registry for browser RPC, node RPC, and Cloud Tasks ENQUEUE.\n * Strategy method -> API -> ALL first; ANY strategy replaces the ENTIRE timeout ladder.\n * Otherwise timeout method -> API -> ALL -> transport default (30s today).\n * No default retry: only the application knows whether repeating a POST is safe.\n */\nexport class CallRegistry {\n private static all = new CallPolicy();\n private static readonly apis = new Map<Function, ApiCallPolicies>();\n\n // webpieces-disable no-function-outside-class -- process-global startup registry\n static setTimeout(timeoutMs: number | undefined, ...scope: CallScope): void {\n if (timeoutMs !== undefined) CallDeadline.validate(timeoutMs);\n CallRegistry.policy(scope[0], scope[1]).timeoutMs = timeoutMs;\n }\n\n // webpieces-disable no-function-outside-class -- process-global startup registry\n static setStrategy(\n // webpieces-disable no-any-unknown -- registry strategies span heterogeneous response DTOs\n strategy: CallStrategy<unknown> | undefined,\n ...scope: CallScope\n ): void {\n CallRegistry.policy(scope[0], scope[1]).strategy = strategy;\n }\n\n /**\n * Run one call under its registered policy. `context` names the method (its `methodName` selects\n * the per-method policy) and carries the endpoint's declared operation, so the SAME context a\n * strategy sees is the one the transport's deadline puts on an `ApiCallTimeoutError`.\n */\n // webpieces-disable no-function-outside-class -- transport-independent policy execution\n static async execute<T>(\n api: Function,\n context: CallContext,\n attempt: Attempt<T>,\n defaultMs: number,\n ): Promise<T> {\n const policies = CallRegistry.apis.get(api);\n const method = policies?.methods.get(context.methodName);\n const strategy = method?.strategy ?? policies?.policy.strategy ?? CallRegistry.all.strategy;\n if (strategy !== undefined) {\n return (await strategy(attempt, context)) as T;\n }\n return attempt(\n method?.timeoutMs ??\n policies?.policy.timeoutMs ??\n CallRegistry.all.timeoutMs ??\n defaultMs,\n );\n }\n\n // webpieces-disable no-function-outside-class -- process-global registry reset for tests\n static clear(): void {\n CallRegistry.all = new CallPolicy();\n CallRegistry.apis.clear();\n }\n\n // webpieces-disable no-function-outside-class -- process-global registry storage\n private static policy(api: Function | 'ALL', methodName?: string): CallPolicy {\n if (api === 'ALL') {\n return CallRegistry.all;\n }\n let policies = CallRegistry.apis.get(api);\n if (!policies) {\n policies = new ApiCallPolicies();\n CallRegistry.apis.set(api, policies);\n }\n if (methodName === undefined) return policies.policy;\n let policy = policies.methods.get(methodName);\n if (!policy) {\n policy = new CallPolicy();\n policies.methods.set(methodName, policy);\n }\n return policy;\n }\n}\n"]}
|
|
@@ -1,10 +1,20 @@
|
|
|
1
|
+
import { EndpointOperation } from './HttpEndpointOptions';
|
|
1
2
|
/** One bounded attempt. Only an explicitly registered strategy may retry it. */
|
|
2
3
|
export type Attempt<T> = (timeoutMs: number) => Promise<T>;
|
|
3
|
-
/**
|
|
4
|
+
/**
|
|
5
|
+
* Contract identity for diagnostics; registry keys use the class itself.
|
|
6
|
+
*
|
|
7
|
+
* `operation` is the endpoint's declared `@Endpoint` side-effect contract (`READ`,
|
|
8
|
+
* `WRITE_IDEMPOTENT` or `WRITE`). A {@link CallStrategy} reads it to decide whether a retry is safe,
|
|
9
|
+
* and `ApiCallTimeoutError` reads it to choose the text it shows an end user. A transport with
|
|
10
|
+
* no declared operation (IPC contracts declare none) passes `WRITE`: the conservative answer, since
|
|
11
|
+
* it never invites a blind retry of something that may already have happened.
|
|
12
|
+
*/
|
|
4
13
|
export declare class CallContext {
|
|
5
14
|
readonly apiName: string;
|
|
6
15
|
readonly methodName: string;
|
|
7
|
-
|
|
16
|
+
readonly operation: EndpointOperation;
|
|
17
|
+
constructor(apiName: string, methodName: string, operation: EndpointOperation);
|
|
8
18
|
}
|
|
9
19
|
/** Owns all timing and retry decisions. No public cancellation signal. */
|
|
10
20
|
export type CallStrategy<T> = (call: Attempt<T>, ctx: CallContext) => Promise<T>;
|
package/src/http/CallStrategy.js
CHANGED
|
@@ -1,13 +1,23 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.CallContext = void 0;
|
|
4
|
-
/**
|
|
4
|
+
/**
|
|
5
|
+
* Contract identity for diagnostics; registry keys use the class itself.
|
|
6
|
+
*
|
|
7
|
+
* `operation` is the endpoint's declared `@Endpoint` side-effect contract (`READ`,
|
|
8
|
+
* `WRITE_IDEMPOTENT` or `WRITE`). A {@link CallStrategy} reads it to decide whether a retry is safe,
|
|
9
|
+
* and `ApiCallTimeoutError` reads it to choose the text it shows an end user. A transport with
|
|
10
|
+
* no declared operation (IPC contracts declare none) passes `WRITE`: the conservative answer, since
|
|
11
|
+
* it never invites a blind retry of something that may already have happened.
|
|
12
|
+
*/
|
|
5
13
|
class CallContext {
|
|
6
14
|
apiName;
|
|
7
15
|
methodName;
|
|
8
|
-
|
|
16
|
+
operation;
|
|
17
|
+
constructor(apiName, methodName, operation) {
|
|
9
18
|
this.apiName = apiName;
|
|
10
19
|
this.methodName = methodName;
|
|
20
|
+
this.operation = operation;
|
|
11
21
|
}
|
|
12
22
|
}
|
|
13
23
|
exports.CallContext = CallContext;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"CallStrategy.js","sourceRoot":"","sources":["../../../../../../packages/core/core-util/src/http/CallStrategy.ts"],"names":[],"mappings":";;;
|
|
1
|
+
{"version":3,"file":"CallStrategy.js","sourceRoot":"","sources":["../../../../../../packages/core/core-util/src/http/CallStrategy.ts"],"names":[],"mappings":";;;AAKA;;;;;;;;GAQG;AACH,MAAa,WAAW;IAEA;IACA;IACA;IAHpB,YACoB,OAAe,EACf,UAAkB,EAClB,SAA4B;QAF5B,YAAO,GAAP,OAAO,CAAQ;QACf,eAAU,GAAV,UAAU,CAAQ;QAClB,cAAS,GAAT,SAAS,CAAmB;IAC7C,CAAC;CACP;AAND,kCAMC","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 constructor(\n public readonly apiName: string,\n public readonly methodName: string,\n public readonly operation: EndpointOperation,\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"]}
|
|
@@ -22,6 +22,8 @@ import { HttpResponseDto } from './HttpResponseDto';
|
|
|
22
22
|
* your own costs nothing and reads better at a delegation site.
|
|
23
23
|
*/
|
|
24
24
|
export declare class WebpiecesDefaultErrorTranslator implements ErrorTranslator {
|
|
25
|
+
/** Reason phrase for a client deadline; see {@link ApiCallTimeoutError}. Never 'Gateway Timeout'. */
|
|
26
|
+
static readonly CALL_TIMEOUT_REASON = "Network Timeout";
|
|
25
27
|
private readonly boundary;
|
|
26
28
|
private readonly genericMessages;
|
|
27
29
|
/**
|
|
@@ -43,6 +45,13 @@ export declare class WebpiecesDefaultErrorTranslator implements ErrorTranslator
|
|
|
43
45
|
* responsible for repeating its router's mode, and repeating it wrongly was silent.
|
|
44
46
|
*/
|
|
45
47
|
toWire(error: Error): HttpResponseDto;
|
|
48
|
+
/**
|
|
49
|
+
* The reason phrase for `error`. An {@link ApiCallTimeoutError} is THIS process's own client
|
|
50
|
+
* deadline firing on an outbound call: no gateway was involved, so it is never titled
|
|
51
|
+
* 'Gateway Timeout' (issue #1170). It keeps its 504 status and `dependency-timeout` payload —
|
|
52
|
+
* retry semantics are unchanged — and only the human framing differs.
|
|
53
|
+
*/
|
|
54
|
+
private reasonPhrase;
|
|
46
55
|
/**
|
|
47
56
|
* CLIENT: the response in, a THROW out — or a silent return for an ordinary success.
|
|
48
57
|
*
|
|
@@ -5,6 +5,7 @@ const ApiErrorBoundary_1 = require("../errors/ApiErrorBoundary");
|
|
|
5
5
|
const ApiErrorCodec_1 = require("../errors/ApiErrorCodec");
|
|
6
6
|
const ApiError_1 = require("../errors/ApiError");
|
|
7
7
|
const ReceivedApiErrorRule_1 = require("../errors/ReceivedApiErrorRule");
|
|
8
|
+
const ApiCallTimeoutError_1 = require("./ApiCallTimeoutError");
|
|
8
9
|
const ApiErrorHttpStatus_1 = require("./ApiErrorHttpStatus");
|
|
9
10
|
const HttpResponseDto_1 = require("./HttpResponseDto");
|
|
10
11
|
/**
|
|
@@ -29,6 +30,8 @@ const HttpResponseDto_1 = require("./HttpResponseDto");
|
|
|
29
30
|
* your own costs nothing and reads better at a delegation site.
|
|
30
31
|
*/
|
|
31
32
|
class WebpiecesDefaultErrorTranslator {
|
|
33
|
+
/** Reason phrase for a client deadline; see {@link ApiCallTimeoutError}. Never 'Gateway Timeout'. */
|
|
34
|
+
static CALL_TIMEOUT_REASON = 'Network Timeout';
|
|
32
35
|
boundary = new ApiErrorBoundary_1.ApiErrorBoundary();
|
|
33
36
|
genericMessages = new Map([
|
|
34
37
|
[266, 'End User Error'],
|
|
@@ -75,7 +78,19 @@ class WebpiecesDefaultErrorTranslator {
|
|
|
75
78
|
const headers = payload.retryAfterSeconds !== undefined
|
|
76
79
|
? [new HttpResponseDto_1.HttpHeader('retry-after', String(payload.retryAfterSeconds))]
|
|
77
80
|
: [];
|
|
78
|
-
return new HttpResponseDto_1.HttpResponseDto(new HttpResponseDto_1.HttpResponseStatus(status, this.
|
|
81
|
+
return new HttpResponseDto_1.HttpResponseDto(new HttpResponseDto_1.HttpResponseStatus(status, this.reasonPhrase(error, status)), headers, payload);
|
|
82
|
+
}
|
|
83
|
+
/**
|
|
84
|
+
* The reason phrase for `error`. An {@link ApiCallTimeoutError} is THIS process's own client
|
|
85
|
+
* deadline firing on an outbound call: no gateway was involved, so it is never titled
|
|
86
|
+
* 'Gateway Timeout' (issue #1170). It keeps its 504 status and `dependency-timeout` payload —
|
|
87
|
+
* retry semantics are unchanged — and only the human framing differs.
|
|
88
|
+
*/
|
|
89
|
+
reasonPhrase(error, status) {
|
|
90
|
+
if (error instanceof ApiCallTimeoutError_1.ApiCallTimeoutError) {
|
|
91
|
+
return WebpiecesDefaultErrorTranslator.CALL_TIMEOUT_REASON;
|
|
92
|
+
}
|
|
93
|
+
return this.genericMessage(status);
|
|
79
94
|
}
|
|
80
95
|
/**
|
|
81
96
|
* CLIENT: the response in, a THROW out — or a silent return for an ordinary success.
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"WebpiecesDefaultErrorTranslator.js","sourceRoot":"","sources":["../../../../../../packages/core/core-util/src/http/WebpiecesDefaultErrorTranslator.ts"],"names":[],"mappings":"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","sourcesContent":["import { ApiErrorBoundary } from '../errors/ApiErrorBoundary';\nimport { ApiErrorCodec } from '../errors/ApiErrorCodec';\nimport { ApiCodedError, ApiError } from '../errors/ApiError';\nimport { ReceivedApiErrorRule } from '../errors/ReceivedApiErrorRule';\nimport { ApiErrorHttpStatus, PublishedKind } from './ApiErrorHttpStatus';\nimport { ErrorTranslator } from './ErrorTranslator';\nimport { HttpHeader, HttpResponseDto, HttpResponseStatus } from './HttpResponseDto';\n\n/**\n * THE webpieces default {@link ErrorTranslator} — ONE class, BOTH halves, used by the node server, by\n * every node client and by the browser bundle alike.\n *\n * It is what {@link ClientRegistry.getErrorTranslator} holds until an app installs its own, and it is\n * what an app's own translator calls to DECLINE an error:\n *\n * ```ts\n * private readonly fallback = new WebpiecesDefaultErrorTranslator();\n * ```\n *\n * so \"no translator registered\" and \"a translator that does not claim this error\" produce\n * byte-identical output. It lives in core-util, beside {@link ClientRegistry}, because core-util is\n * the package every API contract already depends on and the only one both a server and a browser\n * bundle can reach. Both halves needed nothing else: the outbound half wants\n * {@link ApiErrorBoundary} + {@link ApiErrorHttpStatus}, the inbound half wants\n * {@link ApiErrorCodec} + {@link ReceivedApiErrorRule}, and all four were already here.\n *\n * Stateless, so {@link WEBPIECES_DEFAULT_ERROR_TRANSLATOR} is a shared instance — but constructing\n * your own costs nothing and reads better at a delegation site.\n */\nexport class WebpiecesDefaultErrorTranslator implements ErrorTranslator {\n private readonly boundary = new ApiErrorBoundary();\n\n private readonly genericMessages: Map<number, string> = new Map<number, string>([\n [266, 'End User Error'],\n [400, 'Bad Request'],\n [401, 'Unauthorized'],\n [403, 'Forbidden'],\n [404, 'Not Found'],\n [408, 'Request Timeout'],\n [409, 'Conflict'],\n [412, 'Precondition Failed'],\n [415, 'Unsupported Media Type'],\n [422, 'Unprocessable Content'],\n [429, 'Too Many Requests'],\n [500, 'Internal Server Error'],\n [501, 'Not Implemented'],\n [502, 'Bad Gateway'],\n [503, 'Service Unavailable'],\n [504, 'Gateway Timeout'],\n ]);\n\n /**\n * SERVER: an exception in, the exact HTTP response out.\n *\n * The payload IS the answer. {@link ApiErrorBoundary.encode} has already applied both shared\n * rules — a caller-local `ApiConnectionError` (and every non-`ApiError`) publishes as\n * `implementation`, and only an `ApiEndUserError`'s own message is disclosed — and the payload\n * carries everything HTTP needs after that: `kind`, `statusCode`, `edgeHttpStatus` and\n * `retryAfterSeconds`. So there is no `instanceof` here and nothing re-derived from the raw\n * error; unknown throws and caller-local failures come out as a 500 implementation failure\n * because that is what the payload says.\n *\n * An `ApiEndUserError` publishes 266 here, ALWAYS. Whether a particular REQUEST republishes that\n * as a real 4xx is a property of the CALLER, not of the error, so it is decided once per request\n * from `WebpiecesCoreHeaders.SURFACE` by the server boundary (`ExpressWrapper.handleError`) —\n * see {@link SurfaceEndUserStatus}. That is why this class takes no constructor argument any\n * more: the old `new ApiErrorHttpMapper('gui' | 'edge')` made every delegating app translator\n * responsible for repeating its router's mode, and repeating it wrongly was silent.\n */\n toWire(error: Error): HttpResponseDto {\n const payload = this.boundary.encode(error);\n // The boundary never publishes kind 'connection' — that is the whole first rule above — so\n // this is the published subset ApiErrorHttpStatus.codeFor accepts.\n const kind = payload.kind as PublishedKind;\n const status = ApiErrorHttpStatus.codeFor(kind, payload.statusCode);\n const headers =\n payload.retryAfterSeconds !== undefined\n ? [new HttpHeader('retry-after', String(payload.retryAfterSeconds))]\n : [];\n return new HttpResponseDto(\n new HttpResponseStatus(status, this.genericMessage(status)),\n headers,\n payload,\n );\n }\n\n /**\n * CLIENT: the response in, a THROW out — or a silent return for an ordinary success.\n *\n * Called for EVERY response, 2xx included (see {@link ErrorTranslator.fromWire}). A 2xx that is\n * not 266 is exactly the case where webpieces has nothing to say, so it returns and the caller\n * gets its DTO. Everything else throws, which is what lets `ProxyClient` call this and then simply\n * stop — a failure response can never fall through to a typed caller.\n *\n * The rule applied is {@link ReceivedApiErrorRule}, shared verbatim with IPC: ordinary 4xx is MY\n * bug, ordinary 5xx is THEIRS, retry-relevant statuses retain their specific timeout/gateway/\n * backoff/unavailable meaning, and 266 is the end user's own answer.\n */\n fromWire(response: HttpResponseDto): void {\n const code = response.status.code;\n if (code >= 200 && code < 300 && code !== 266) {\n return;\n }\n const decoded = this.decodeBody(response);\n const message =\n decoded?.message ?? this.fallbackMessage(response.body, response.status.reason);\n throw ReceivedApiErrorRule.adapt(\n code,\n message,\n decoded,\n this.retryAfterSeconds(response.headers),\n );\n }\n\n /** The reason phrase webpieces writes beside a status it chose itself. */\n genericMessage(code: number): string {\n return this.genericMessages.get(code) ?? 'Request Failed';\n }\n\n /**\n * A webpieces peer sends an {@link ApiErrorPayload}; a foreign responder sends anything at all.\n * The status is only believed when it AGREES with the kind the body claims — a body saying\n * `not-found` beside a 500 is a mismatched response, not a 404, and falls back to the status.\n */\n private decodeBody(response: HttpResponseDto): ApiError | undefined {\n if (!ApiErrorCodec.isPayload(response.body)) {\n return undefined;\n }\n const decoded = ApiErrorCodec.decode(response.body);\n if (!ApiErrorHttpStatus.hasCode(decoded)) {\n return undefined;\n }\n // `statusCode` is the 'coded' kind's own status, the one thing the shared table cannot know.\n const statusCode = decoded instanceof ApiCodedError ? decoded.statusCode : undefined;\n const declared = ApiErrorHttpStatus.codeFor(decoded.kind, statusCode);\n return declared === response.status.code ? decoded : undefined;\n }\n\n // webpieces-disable no-any-unknown -- HTTP response bodies are app-owned until this boundary safely inspects them\n private fallbackMessage(body: unknown, reason: string): string {\n if (typeof body === 'object' && body !== null) {\n const message = Object.getOwnPropertyDescriptor(body, 'message')?.value;\n if (typeof message === 'string' && message.length > 0) return message.slice(0, 4096);\n }\n return reason || 'Request Failed';\n }\n\n /** Parse either legal Retry-After form and bound untrusted guidance to one day. */\n private retryAfterSeconds(headers: readonly HttpHeader[]): number | undefined {\n const raw = headers.find(\n (header: HttpHeader) => header.name.toLowerCase() === 'retry-after',\n )?.value;\n if (raw === undefined) return undefined;\n const seconds = Number(raw.trim());\n if (Number.isFinite(seconds) && seconds >= 0) return Math.min(Math.ceil(seconds), 86_400);\n const at = Date.parse(raw);\n if (!Number.isFinite(at)) return undefined;\n return Math.min(Math.max(Math.ceil((at - Date.now()) / 1_000), 0), 86_400);\n }\n}\n\n/** Process-wide built-in instance — stateless, so one shared instance is enough. */\nexport const WEBPIECES_DEFAULT_ERROR_TRANSLATOR = new WebpiecesDefaultErrorTranslator();\n"]}
|
|
1
|
+
{"version":3,"file":"WebpiecesDefaultErrorTranslator.js","sourceRoot":"","sources":["../../../../../../packages/core/core-util/src/http/WebpiecesDefaultErrorTranslator.ts"],"names":[],"mappings":"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","sourcesContent":["import { ApiErrorBoundary } from '../errors/ApiErrorBoundary';\nimport { ApiErrorCodec } from '../errors/ApiErrorCodec';\nimport { ApiCodedError, ApiError } from '../errors/ApiError';\nimport { ReceivedApiErrorRule } from '../errors/ReceivedApiErrorRule';\nimport { ApiCallTimeoutError } from './ApiCallTimeoutError';\nimport { ApiErrorHttpStatus, PublishedKind } from './ApiErrorHttpStatus';\nimport { ErrorTranslator } from './ErrorTranslator';\nimport { HttpHeader, HttpResponseDto, HttpResponseStatus } from './HttpResponseDto';\n\n/**\n * THE webpieces default {@link ErrorTranslator} — ONE class, BOTH halves, used by the node server, by\n * every node client and by the browser bundle alike.\n *\n * It is what {@link ClientRegistry.getErrorTranslator} holds until an app installs its own, and it is\n * what an app's own translator calls to DECLINE an error:\n *\n * ```ts\n * private readonly fallback = new WebpiecesDefaultErrorTranslator();\n * ```\n *\n * so \"no translator registered\" and \"a translator that does not claim this error\" produce\n * byte-identical output. It lives in core-util, beside {@link ClientRegistry}, because core-util is\n * the package every API contract already depends on and the only one both a server and a browser\n * bundle can reach. Both halves needed nothing else: the outbound half wants\n * {@link ApiErrorBoundary} + {@link ApiErrorHttpStatus}, the inbound half wants\n * {@link ApiErrorCodec} + {@link ReceivedApiErrorRule}, and all four were already here.\n *\n * Stateless, so {@link WEBPIECES_DEFAULT_ERROR_TRANSLATOR} is a shared instance — but constructing\n * your own costs nothing and reads better at a delegation site.\n */\nexport class WebpiecesDefaultErrorTranslator implements ErrorTranslator {\n /** Reason phrase for a client deadline; see {@link ApiCallTimeoutError}. Never 'Gateway Timeout'. */\n static readonly CALL_TIMEOUT_REASON = 'Network Timeout';\n\n private readonly boundary = new ApiErrorBoundary();\n\n private readonly genericMessages: Map<number, string> = new Map<number, string>([\n [266, 'End User Error'],\n [400, 'Bad Request'],\n [401, 'Unauthorized'],\n [403, 'Forbidden'],\n [404, 'Not Found'],\n [408, 'Request Timeout'],\n [409, 'Conflict'],\n [412, 'Precondition Failed'],\n [415, 'Unsupported Media Type'],\n [422, 'Unprocessable Content'],\n [429, 'Too Many Requests'],\n [500, 'Internal Server Error'],\n [501, 'Not Implemented'],\n [502, 'Bad Gateway'],\n [503, 'Service Unavailable'],\n [504, 'Gateway Timeout'],\n ]);\n\n /**\n * SERVER: an exception in, the exact HTTP response out.\n *\n * The payload IS the answer. {@link ApiErrorBoundary.encode} has already applied both shared\n * rules — a caller-local `ApiConnectionError` (and every non-`ApiError`) publishes as\n * `implementation`, and only an `ApiEndUserError`'s own message is disclosed — and the payload\n * carries everything HTTP needs after that: `kind`, `statusCode`, `edgeHttpStatus` and\n * `retryAfterSeconds`. So there is no `instanceof` here and nothing re-derived from the raw\n * error; unknown throws and caller-local failures come out as a 500 implementation failure\n * because that is what the payload says.\n *\n * An `ApiEndUserError` publishes 266 here, ALWAYS. Whether a particular REQUEST republishes that\n * as a real 4xx is a property of the CALLER, not of the error, so it is decided once per request\n * from `WebpiecesCoreHeaders.SURFACE` by the server boundary (`ExpressWrapper.handleError`) —\n * see {@link SurfaceEndUserStatus}. That is why this class takes no constructor argument any\n * more: the old `new ApiErrorHttpMapper('gui' | 'edge')` made every delegating app translator\n * responsible for repeating its router's mode, and repeating it wrongly was silent.\n */\n toWire(error: Error): HttpResponseDto {\n const payload = this.boundary.encode(error);\n // The boundary never publishes kind 'connection' — that is the whole first rule above — so\n // this is the published subset ApiErrorHttpStatus.codeFor accepts.\n const kind = payload.kind as PublishedKind;\n const status = ApiErrorHttpStatus.codeFor(kind, payload.statusCode);\n const headers =\n payload.retryAfterSeconds !== undefined\n ? [new HttpHeader('retry-after', String(payload.retryAfterSeconds))]\n : [];\n return new HttpResponseDto(\n new HttpResponseStatus(status, this.reasonPhrase(error, status)),\n headers,\n payload,\n );\n }\n\n /**\n * The reason phrase for `error`. An {@link ApiCallTimeoutError} is THIS process's own client\n * deadline firing on an outbound call: no gateway was involved, so it is never titled\n * 'Gateway Timeout' (issue #1170). It keeps its 504 status and `dependency-timeout` payload —\n * retry semantics are unchanged — and only the human framing differs.\n */\n private reasonPhrase(error: Error, status: number): string {\n if (error instanceof ApiCallTimeoutError) {\n return WebpiecesDefaultErrorTranslator.CALL_TIMEOUT_REASON;\n }\n return this.genericMessage(status);\n }\n\n /**\n * CLIENT: the response in, a THROW out — or a silent return for an ordinary success.\n *\n * Called for EVERY response, 2xx included (see {@link ErrorTranslator.fromWire}). A 2xx that is\n * not 266 is exactly the case where webpieces has nothing to say, so it returns and the caller\n * gets its DTO. Everything else throws, which is what lets `ProxyClient` call this and then simply\n * stop — a failure response can never fall through to a typed caller.\n *\n * The rule applied is {@link ReceivedApiErrorRule}, shared verbatim with IPC: ordinary 4xx is MY\n * bug, ordinary 5xx is THEIRS, retry-relevant statuses retain their specific timeout/gateway/\n * backoff/unavailable meaning, and 266 is the end user's own answer.\n */\n fromWire(response: HttpResponseDto): void {\n const code = response.status.code;\n if (code >= 200 && code < 300 && code !== 266) {\n return;\n }\n const decoded = this.decodeBody(response);\n const message =\n decoded?.message ?? this.fallbackMessage(response.body, response.status.reason);\n throw ReceivedApiErrorRule.adapt(\n code,\n message,\n decoded,\n this.retryAfterSeconds(response.headers),\n );\n }\n\n /** The reason phrase webpieces writes beside a status it chose itself. */\n genericMessage(code: number): string {\n return this.genericMessages.get(code) ?? 'Request Failed';\n }\n\n /**\n * A webpieces peer sends an {@link ApiErrorPayload}; a foreign responder sends anything at all.\n * The status is only believed when it AGREES with the kind the body claims — a body saying\n * `not-found` beside a 500 is a mismatched response, not a 404, and falls back to the status.\n */\n private decodeBody(response: HttpResponseDto): ApiError | undefined {\n if (!ApiErrorCodec.isPayload(response.body)) {\n return undefined;\n }\n const decoded = ApiErrorCodec.decode(response.body);\n if (!ApiErrorHttpStatus.hasCode(decoded)) {\n return undefined;\n }\n // `statusCode` is the 'coded' kind's own status, the one thing the shared table cannot know.\n const statusCode = decoded instanceof ApiCodedError ? decoded.statusCode : undefined;\n const declared = ApiErrorHttpStatus.codeFor(decoded.kind, statusCode);\n return declared === response.status.code ? decoded : undefined;\n }\n\n // webpieces-disable no-any-unknown -- HTTP response bodies are app-owned until this boundary safely inspects them\n private fallbackMessage(body: unknown, reason: string): string {\n if (typeof body === 'object' && body !== null) {\n const message = Object.getOwnPropertyDescriptor(body, 'message')?.value;\n if (typeof message === 'string' && message.length > 0) return message.slice(0, 4096);\n }\n return reason || 'Request Failed';\n }\n\n /** Parse either legal Retry-After form and bound untrusted guidance to one day. */\n private retryAfterSeconds(headers: readonly HttpHeader[]): number | undefined {\n const raw = headers.find(\n (header: HttpHeader) => header.name.toLowerCase() === 'retry-after',\n )?.value;\n if (raw === undefined) return undefined;\n const seconds = Number(raw.trim());\n if (Number.isFinite(seconds) && seconds >= 0) return Math.min(Math.ceil(seconds), 86_400);\n const at = Date.parse(raw);\n if (!Number.isFinite(at)) return undefined;\n return Math.min(Math.max(Math.ceil((at - Date.now()) / 1_000), 0), 86_400);\n }\n}\n\n/** Process-wide built-in instance — stateless, so one shared instance is enough. */\nexport const WEBPIECES_DEFAULT_ERROR_TRANSLATOR = new WebpiecesDefaultErrorTranslator();\n"]}
|
package/src/ipc/IpcConnection.js
CHANGED
|
@@ -4,6 +4,7 @@ exports.IpcErrors = exports.IpcConnection = exports.IpcConnectionOptions = expor
|
|
|
4
4
|
const errorUtils_1 = require("../lib/errorUtils");
|
|
5
5
|
const ApiCallTimeoutError_1 = require("../http/ApiCallTimeoutError");
|
|
6
6
|
const CallStrategy_1 = require("../http/CallStrategy");
|
|
7
|
+
const HttpEndpointOptions_1 = require("../http/HttpEndpointOptions");
|
|
7
8
|
const errors_1 = require("../errors");
|
|
8
9
|
const IpcRegistry_1 = require("./IpcRegistry");
|
|
9
10
|
const IpcProtocol_1 = require("./IpcProtocol");
|
|
@@ -103,7 +104,10 @@ class IpcConnection {
|
|
|
103
104
|
const pending = new PendingCall(request.context, resolve, reject);
|
|
104
105
|
this.pending.set(request.context.callId, pending);
|
|
105
106
|
pending.cancelTimer = this.options.scheduler.schedule(() => {
|
|
106
|
-
this.settle(request.context.callId, new ApiCallTimeoutError_1.ApiCallTimeoutError(this.options.timeoutMs,
|
|
107
|
+
this.settle(request.context.callId, new ApiCallTimeoutError_1.ApiCallTimeoutError(this.options.timeoutMs,
|
|
108
|
+
// IPC contracts declare no @Endpoint operation, so the deadline reports the
|
|
109
|
+
// conservative WRITE: the user is told to check before repeating the call.
|
|
110
|
+
new CallStrategy_1.CallContext(request.apiId, request.methodId, HttpEndpointOptions_1.WRITE)), 'expired');
|
|
107
111
|
}, this.options.timeoutMs);
|
|
108
112
|
// This owner observes every async send rejection; a send failure is never a timeout.
|
|
109
113
|
void this.send(request).catch((error) => {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"IpcConnection.js","sourceRoot":"","sources":["../../../../../../packages/core/core-util/src/ipc/IpcConnection.ts"],"names":[],"mappings":"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","sourcesContent":["import { toError } from '../lib/errorUtils';\nimport { ApiCallTimeoutError } from '../http/ApiCallTimeoutError';\nimport { CallContext } from '../http/CallStrategy';\nimport { ApiEndpointNotFoundError, ApiImplementationError } from '../errors';\nimport { IpcRegistry } from './IpcRegistry';\nimport {\n IpcCallContext,\n IpcFailure,\n IpcMessage,\n IpcReply,\n IpcRequest,\n IpcProtocol,\n} from './IpcProtocol';\n\n/** A local connection failed, distinct from a remote implementation's semantic ApiError. */\nexport class IpcTransportError extends Error {\n constructor(message: string, cause?: Error) {\n super(message, { cause });\n this.name = 'IpcTransportError';\n }\n}\n\n/** Implement this at the host transport boundary; no vendor or platform declarations leak out. */\nexport interface IpcTransport {\n send(message: string): Promise<void> | void;\n subscribe(\n receive: (message: string) => void,\n failed: (error: Error) => void,\n closed: (error: Error) => void,\n ): () => void;\n close(): void;\n}\nexport interface IpcScheduler {\n schedule(callback: () => void, delayMs: number): () => void;\n}\nexport interface IpcErrorOwner {\n report(error: Error): void;\n}\nexport class IpcConnectionOptions {\n constructor(\n readonly timeoutMs: number,\n readonly nextId: () => string,\n readonly scheduler: IpcScheduler,\n readonly errors: IpcErrorOwner,\n readonly maxMessageCharacters = 1_000_000,\n readonly settlementHistoryLimit = 100,\n readonly connectionId = 'unspecified',\n ) {\n if (\n !Number.isFinite(timeoutMs) ||\n timeoutMs <= 0 ||\n !Number.isSafeInteger(maxMessageCharacters) ||\n maxMessageCharacters < 1 ||\n !Number.isSafeInteger(settlementHistoryLimit) ||\n settlementHistoryLimit < 1\n ) {\n throw new ApiImplementationError('IPC timeout and message limit must be positive');\n }\n }\n}\n\ntype SettlementReason = 'resolved' | 'expired' | 'rejected';\nclass SettlementRecord {\n constructor(\n readonly context: IpcCallContext,\n readonly reason: SettlementReason,\n readonly settledAt: number,\n ) {}\n}\n\nclass PendingCall {\n cancelTimer: () => void = () => {};\n constructor(\n readonly context: IpcCallContext,\n readonly resolve: (reply: IpcReply) => void,\n readonly reject: (error: Error) => void,\n ) {}\n}\n\n/** One connection owns both directions, one dispatcher, and all pending-call lifetimes. */\nexport class IpcConnection {\n private readonly pending = new Map<string, PendingCall>();\n private readonly settlements = new Map<string, SettlementRecord>();\n private readonly activeRequests = new Set<string>();\n private readonly usedIds = new Set<string>();\n private handler?: (request: IpcRequest) => Promise<IpcReply>;\n private closed?: Error;\n private readonly unsubscribe: () => void;\n\n constructor(\n private readonly transport: IpcTransport,\n private readonly options: IpcConnectionOptions,\n ) {\n this.unsubscribe = transport.subscribe(\n (message) => this.receive(message),\n (error) => this.fail(new IpcTransportError('IPC transport failed', error)),\n (error) => this.fail(new IpcTransportError('IPC connection closed', error)),\n );\n }\n\n /** Install exactly one factory dispatcher BEFORE exposing this connection to the peer. */\n setHandler(handler: (request: IpcRequest) => Promise<IpcReply>): void {\n if (this.handler)\n throw new ApiImplementationError('An IPC connection already has a dispatcher');\n if (this.closed) throw this.closed;\n this.handler = handler;\n }\n\n newContext(parent?: IpcCallContext): IpcCallContext {\n const id = this.options.nextId();\n if (this.usedIds.has(id))\n throw new ApiImplementationError('IPC identity generator reused a call ID');\n this.usedIds.add(id);\n return new IpcCallContext(parent?.txId ?? id, id, parent?.callId);\n }\n\n request(request: IpcRequest): Promise<IpcReply> {\n if (this.closed) return Promise.reject(this.closed);\n if (this.pending.has(request.context.callId))\n return Promise.reject(new ApiImplementationError('Duplicate pending IPC call'));\n return new Promise<IpcReply>((resolve, reject) => {\n const pending = new PendingCall(request.context, resolve, reject);\n this.pending.set(request.context.callId, pending);\n pending.cancelTimer = this.options.scheduler.schedule(() => {\n this.settle(\n request.context.callId,\n new ApiCallTimeoutError(\n this.options.timeoutMs,\n new CallContext(request.apiId, request.methodId),\n ),\n 'expired',\n );\n }, this.options.timeoutMs);\n // This owner observes every async send rejection; a send failure is never a timeout.\n void this.send(request).catch((error) => {\n this.settle(\n request.context.callId,\n new IpcTransportError('IPC send failed', IpcErrors.normalize(error)),\n 'rejected',\n );\n });\n });\n }\n\n dispose(): void {\n this.fail(new IpcTransportError('IPC connection disposed'));\n this.unsubscribe();\n this.transport.close();\n }\n\n private async send(message: IpcMessage): Promise<void> {\n if (this.closed) throw this.closed;\n const json = JSON.stringify(message);\n if (json.length > this.options.maxMessageCharacters)\n throw new ApiImplementationError('IPC message exceeds configured limit');\n await this.transport.send(json);\n }\n\n private receive(json: string): void {\n if (this.closed) return;\n // eslint-disable-next-line @webpieces/no-unmanaged-exceptions -- transport boundary rejects pending calls or encodes a failure that the client throws\n try {\n if (json.length > this.options.maxMessageCharacters)\n throw new ApiImplementationError('IPC message exceeds configured limit');\n const message = IpcProtocol.parse(json);\n if (message.type === 'request') {\n if (this.activeRequests.has(message.context.callId))\n throw new ApiImplementationError('Duplicate active IPC request');\n this.activeRequests.add(message.context.callId);\n void this.dispatch(message).catch((error) => this.fail(IpcErrors.normalize(error)));\n return;\n }\n const pending = this.pending.get(message.context.callId);\n // Late or unsolicited replies are reported, never resolve a different call or replay work.\n if (!pending) {\n this.options.errors.report(this.lateReplyError(message));\n return;\n }\n if (!IpcProtocol.sameContext(pending.context, message.context))\n throw this.correlationError(message, pending.context);\n this.pending.delete(message.context.callId);\n pending.cancelTimer();\n this.remember(pending.context, 'resolved');\n pending.resolve(message);\n } catch (err: unknown) {\n const error = toError(err);\n this.fail(IpcErrors.normalize(error));\n }\n }\n\n private async dispatch(request: IpcRequest): Promise<void> {\n let reply: IpcReply;\n // eslint-disable-next-line @webpieces/no-unmanaged-exceptions -- transport boundary rejects pending calls or encodes a failure that the client throws\n try {\n if (!this.handler)\n throw new ApiEndpointNotFoundError('IPC dispatcher is not installed');\n reply = await this.handler(request);\n if (!IpcProtocol.sameContext(request.context, reply.context))\n throw new ApiImplementationError('IPC handler reply correlation mismatch');\n } catch (err: unknown) {\n const error = toError(err);\n // The ONE app-reachable seam for an outbound IPC failure. The webpieces default applies the\n // same `ApiErrorBoundary` publication rule this used to call inline — notably that an\n // `ApiConnectionError` publishes as `implementation`, because to the PEER this process is\n // what is broken.\n reply = new IpcFailure(request.context, IpcRegistry.getErrorTranslator().toWire(error));\n } finally {\n this.activeRequests.delete(request.context.callId);\n }\n await this.send(reply);\n }\n\n private settle(id: string, error: Error, reason: SettlementReason): void {\n const pending = this.pending.get(id);\n if (!pending) return;\n this.pending.delete(id);\n pending.cancelTimer();\n this.remember(pending.context, reason);\n pending.reject(error);\n }\n\n private remember(context: IpcCallContext, reason: SettlementReason): void {\n this.settlements.delete(context.callId);\n this.settlements.set(context.callId, new SettlementRecord(context, reason, Date.now()));\n while (this.settlements.size > this.options.settlementHistoryLimit) {\n const oldest = this.settlements.keys().next().value;\n if (typeof oldest !== 'string') return;\n this.settlements.delete(oldest);\n }\n }\n\n private lateReplyError(reply: IpcReply): ApiImplementationError {\n const previous = this.settlements.get(reply.context.callId);\n let classification = 'unknown';\n if (previous) {\n classification = IpcProtocol.sameContext(previous.context, reply.context)\n ? previous.reason === 'expired'\n ? 'expired'\n : 'duplicate-settled'\n : 'correlation-mismatched';\n }\n const settlement = previous\n ? ` terminal=${previous.reason} ageMs=${String(Math.max(0, Date.now() - previous.settledAt))}`\n : '';\n return new ApiImplementationError(\n `Late or unknown IPC reply classification=${classification}${settlement} ${this.correlationFields(reply.context)}`,\n );\n }\n\n private correlationError(reply: IpcReply, expected: IpcCallContext): ApiImplementationError {\n return new ApiImplementationError(\n `IPC reply correlation mismatch classification=correlation-mismatched ` +\n `${this.correlationFields(reply.context)} expectedTxId=${JSON.stringify(expected.txId)}`,\n );\n }\n\n private correlationFields(context: IpcCallContext): string {\n return (\n `callId=${JSON.stringify(context.callId)} txId=${JSON.stringify(context.txId)} ` +\n `parentCallId=${JSON.stringify(context.parentCallId ?? null)} ` +\n `connectionId=${JSON.stringify(this.options.connectionId)}`\n );\n }\n\n private fail(error: Error): void {\n if (this.closed) return;\n this.closed = error;\n for (const id of this.pending.keys()) this.settle(id, error, 'rejected');\n this.options.errors.report(error);\n }\n}\n\nexport class IpcErrors {\n // webpieces-disable no-function-outside-class -- portable stateless IPC primitive; no platform DI container exists on this boundary; webpieces-disable no-any-unknown -- untrusted IPC data is schema-validated; generic dispatch cannot assume a DTO type before validation\n static normalize(value: unknown): Error {\n return value instanceof Error\n ? value\n : new ApiImplementationError('IPC failed with a non-Error value');\n }\n}\n"]}
|
|
1
|
+
{"version":3,"file":"IpcConnection.js","sourceRoot":"","sources":["../../../../../../packages/core/core-util/src/ipc/IpcConnection.ts"],"names":[],"mappings":"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","sourcesContent":["import { toError } from '../lib/errorUtils';\nimport { ApiCallTimeoutError } from '../http/ApiCallTimeoutError';\nimport { CallContext } from '../http/CallStrategy';\nimport { WRITE } from '../http/HttpEndpointOptions';\nimport { ApiEndpointNotFoundError, ApiImplementationError } from '../errors';\nimport { IpcRegistry } from './IpcRegistry';\nimport {\n IpcCallContext,\n IpcFailure,\n IpcMessage,\n IpcReply,\n IpcRequest,\n IpcProtocol,\n} from './IpcProtocol';\n\n/** A local connection failed, distinct from a remote implementation's semantic ApiError. */\nexport class IpcTransportError extends Error {\n constructor(message: string, cause?: Error) {\n super(message, { cause });\n this.name = 'IpcTransportError';\n }\n}\n\n/** Implement this at the host transport boundary; no vendor or platform declarations leak out. */\nexport interface IpcTransport {\n send(message: string): Promise<void> | void;\n subscribe(\n receive: (message: string) => void,\n failed: (error: Error) => void,\n closed: (error: Error) => void,\n ): () => void;\n close(): void;\n}\nexport interface IpcScheduler {\n schedule(callback: () => void, delayMs: number): () => void;\n}\nexport interface IpcErrorOwner {\n report(error: Error): void;\n}\nexport class IpcConnectionOptions {\n constructor(\n readonly timeoutMs: number,\n readonly nextId: () => string,\n readonly scheduler: IpcScheduler,\n readonly errors: IpcErrorOwner,\n readonly maxMessageCharacters = 1_000_000,\n readonly settlementHistoryLimit = 100,\n readonly connectionId = 'unspecified',\n ) {\n if (\n !Number.isFinite(timeoutMs) ||\n timeoutMs <= 0 ||\n !Number.isSafeInteger(maxMessageCharacters) ||\n maxMessageCharacters < 1 ||\n !Number.isSafeInteger(settlementHistoryLimit) ||\n settlementHistoryLimit < 1\n ) {\n throw new ApiImplementationError('IPC timeout and message limit must be positive');\n }\n }\n}\n\ntype SettlementReason = 'resolved' | 'expired' | 'rejected';\nclass SettlementRecord {\n constructor(\n readonly context: IpcCallContext,\n readonly reason: SettlementReason,\n readonly settledAt: number,\n ) {}\n}\n\nclass PendingCall {\n cancelTimer: () => void = () => {};\n constructor(\n readonly context: IpcCallContext,\n readonly resolve: (reply: IpcReply) => void,\n readonly reject: (error: Error) => void,\n ) {}\n}\n\n/** One connection owns both directions, one dispatcher, and all pending-call lifetimes. */\nexport class IpcConnection {\n private readonly pending = new Map<string, PendingCall>();\n private readonly settlements = new Map<string, SettlementRecord>();\n private readonly activeRequests = new Set<string>();\n private readonly usedIds = new Set<string>();\n private handler?: (request: IpcRequest) => Promise<IpcReply>;\n private closed?: Error;\n private readonly unsubscribe: () => void;\n\n constructor(\n private readonly transport: IpcTransport,\n private readonly options: IpcConnectionOptions,\n ) {\n this.unsubscribe = transport.subscribe(\n (message) => this.receive(message),\n (error) => this.fail(new IpcTransportError('IPC transport failed', error)),\n (error) => this.fail(new IpcTransportError('IPC connection closed', error)),\n );\n }\n\n /** Install exactly one factory dispatcher BEFORE exposing this connection to the peer. */\n setHandler(handler: (request: IpcRequest) => Promise<IpcReply>): void {\n if (this.handler)\n throw new ApiImplementationError('An IPC connection already has a dispatcher');\n if (this.closed) throw this.closed;\n this.handler = handler;\n }\n\n newContext(parent?: IpcCallContext): IpcCallContext {\n const id = this.options.nextId();\n if (this.usedIds.has(id))\n throw new ApiImplementationError('IPC identity generator reused a call ID');\n this.usedIds.add(id);\n return new IpcCallContext(parent?.txId ?? id, id, parent?.callId);\n }\n\n request(request: IpcRequest): Promise<IpcReply> {\n if (this.closed) return Promise.reject(this.closed);\n if (this.pending.has(request.context.callId))\n return Promise.reject(new ApiImplementationError('Duplicate pending IPC call'));\n return new Promise<IpcReply>((resolve, reject) => {\n const pending = new PendingCall(request.context, resolve, reject);\n this.pending.set(request.context.callId, pending);\n pending.cancelTimer = this.options.scheduler.schedule(() => {\n this.settle(\n request.context.callId,\n new ApiCallTimeoutError(\n this.options.timeoutMs,\n // IPC contracts declare no @Endpoint operation, so the deadline reports the\n // conservative WRITE: the user is told to check before repeating the call.\n new CallContext(request.apiId, request.methodId, WRITE),\n ),\n 'expired',\n );\n }, this.options.timeoutMs);\n // This owner observes every async send rejection; a send failure is never a timeout.\n void this.send(request).catch((error) => {\n this.settle(\n request.context.callId,\n new IpcTransportError('IPC send failed', IpcErrors.normalize(error)),\n 'rejected',\n );\n });\n });\n }\n\n dispose(): void {\n this.fail(new IpcTransportError('IPC connection disposed'));\n this.unsubscribe();\n this.transport.close();\n }\n\n private async send(message: IpcMessage): Promise<void> {\n if (this.closed) throw this.closed;\n const json = JSON.stringify(message);\n if (json.length > this.options.maxMessageCharacters)\n throw new ApiImplementationError('IPC message exceeds configured limit');\n await this.transport.send(json);\n }\n\n private receive(json: string): void {\n if (this.closed) return;\n // eslint-disable-next-line @webpieces/no-unmanaged-exceptions -- transport boundary rejects pending calls or encodes a failure that the client throws\n try {\n if (json.length > this.options.maxMessageCharacters)\n throw new ApiImplementationError('IPC message exceeds configured limit');\n const message = IpcProtocol.parse(json);\n if (message.type === 'request') {\n if (this.activeRequests.has(message.context.callId))\n throw new ApiImplementationError('Duplicate active IPC request');\n this.activeRequests.add(message.context.callId);\n void this.dispatch(message).catch((error) => this.fail(IpcErrors.normalize(error)));\n return;\n }\n const pending = this.pending.get(message.context.callId);\n // Late or unsolicited replies are reported, never resolve a different call or replay work.\n if (!pending) {\n this.options.errors.report(this.lateReplyError(message));\n return;\n }\n if (!IpcProtocol.sameContext(pending.context, message.context))\n throw this.correlationError(message, pending.context);\n this.pending.delete(message.context.callId);\n pending.cancelTimer();\n this.remember(pending.context, 'resolved');\n pending.resolve(message);\n } catch (err: unknown) {\n const error = toError(err);\n this.fail(IpcErrors.normalize(error));\n }\n }\n\n private async dispatch(request: IpcRequest): Promise<void> {\n let reply: IpcReply;\n // eslint-disable-next-line @webpieces/no-unmanaged-exceptions -- transport boundary rejects pending calls or encodes a failure that the client throws\n try {\n if (!this.handler)\n throw new ApiEndpointNotFoundError('IPC dispatcher is not installed');\n reply = await this.handler(request);\n if (!IpcProtocol.sameContext(request.context, reply.context))\n throw new ApiImplementationError('IPC handler reply correlation mismatch');\n } catch (err: unknown) {\n const error = toError(err);\n // The ONE app-reachable seam for an outbound IPC failure. The webpieces default applies the\n // same `ApiErrorBoundary` publication rule this used to call inline — notably that an\n // `ApiConnectionError` publishes as `implementation`, because to the PEER this process is\n // what is broken.\n reply = new IpcFailure(request.context, IpcRegistry.getErrorTranslator().toWire(error));\n } finally {\n this.activeRequests.delete(request.context.callId);\n }\n await this.send(reply);\n }\n\n private settle(id: string, error: Error, reason: SettlementReason): void {\n const pending = this.pending.get(id);\n if (!pending) return;\n this.pending.delete(id);\n pending.cancelTimer();\n this.remember(pending.context, reason);\n pending.reject(error);\n }\n\n private remember(context: IpcCallContext, reason: SettlementReason): void {\n this.settlements.delete(context.callId);\n this.settlements.set(context.callId, new SettlementRecord(context, reason, Date.now()));\n while (this.settlements.size > this.options.settlementHistoryLimit) {\n const oldest = this.settlements.keys().next().value;\n if (typeof oldest !== 'string') return;\n this.settlements.delete(oldest);\n }\n }\n\n private lateReplyError(reply: IpcReply): ApiImplementationError {\n const previous = this.settlements.get(reply.context.callId);\n let classification = 'unknown';\n if (previous) {\n classification = IpcProtocol.sameContext(previous.context, reply.context)\n ? previous.reason === 'expired'\n ? 'expired'\n : 'duplicate-settled'\n : 'correlation-mismatched';\n }\n const settlement = previous\n ? ` terminal=${previous.reason} ageMs=${String(Math.max(0, Date.now() - previous.settledAt))}`\n : '';\n return new ApiImplementationError(\n `Late or unknown IPC reply classification=${classification}${settlement} ${this.correlationFields(reply.context)}`,\n );\n }\n\n private correlationError(reply: IpcReply, expected: IpcCallContext): ApiImplementationError {\n return new ApiImplementationError(\n `IPC reply correlation mismatch classification=correlation-mismatched ` +\n `${this.correlationFields(reply.context)} expectedTxId=${JSON.stringify(expected.txId)}`,\n );\n }\n\n private correlationFields(context: IpcCallContext): string {\n return (\n `callId=${JSON.stringify(context.callId)} txId=${JSON.stringify(context.txId)} ` +\n `parentCallId=${JSON.stringify(context.parentCallId ?? null)} ` +\n `connectionId=${JSON.stringify(this.options.connectionId)}`\n );\n }\n\n private fail(error: Error): void {\n if (this.closed) return;\n this.closed = error;\n for (const id of this.pending.keys()) this.settle(id, error, 'rejected');\n this.options.errors.report(error);\n }\n}\n\nexport class IpcErrors {\n // webpieces-disable no-function-outside-class -- portable stateless IPC primitive; no platform DI container exists on this boundary; webpieces-disable no-any-unknown -- untrusted IPC data is schema-validated; generic dispatch cannot assume a DTO type before validation\n static normalize(value: unknown): Error {\n return value instanceof Error\n ? value\n : new ApiImplementationError('IPC failed with a non-Error value');\n }\n}\n"]}
|