chore: initialize from frontend template 4dc033c
This commit is contained in:
@@ -0,0 +1,424 @@
|
||||
const DROP_SENSITIVE = Object.freeze([
|
||||
"cause",
|
||||
"body",
|
||||
"headers",
|
||||
"authorization",
|
||||
"url",
|
||||
"query",
|
||||
"stack",
|
||||
"storageValue",
|
||||
] as const);
|
||||
|
||||
export type ErrorAction =
|
||||
| "retry"
|
||||
| "reauth"
|
||||
| "navigate"
|
||||
| "reload-once"
|
||||
| "contact-support"
|
||||
| "none";
|
||||
|
||||
export type ErrorDefinition<Kind extends string> = Readonly<{
|
||||
kind: Kind;
|
||||
defaultRetryable: boolean;
|
||||
severity: string;
|
||||
userMessageKey: string;
|
||||
action: ErrorAction;
|
||||
telemetryEvent: string;
|
||||
redaction: readonly string[];
|
||||
}>;
|
||||
|
||||
const row = <Kind extends string>(
|
||||
kind: Kind,
|
||||
defaultRetryable: boolean,
|
||||
severity: string,
|
||||
action: ErrorAction,
|
||||
telemetryEvent = "api.request.failed",
|
||||
): ErrorDefinition<Kind> =>
|
||||
Object.freeze({
|
||||
kind,
|
||||
defaultRetryable,
|
||||
severity,
|
||||
userMessageKey: `error.${kind.toLowerCase()}`,
|
||||
action,
|
||||
telemetryEvent,
|
||||
redaction: DROP_SENSITIVE,
|
||||
});
|
||||
|
||||
export const ERROR_REGISTRY = Object.freeze({
|
||||
NETWORK_UNREACHABLE: row("NETWORK_UNREACHABLE", true, "warning", "retry"),
|
||||
REQUEST_TIMEOUT: row("REQUEST_TIMEOUT", true, "warning", "retry"),
|
||||
REQUEST_ABORTED: row("REQUEST_ABORTED", false, "info", "none"),
|
||||
CONTENT_TYPE_MISMATCH: row(
|
||||
"CONTENT_TYPE_MISMATCH",
|
||||
false,
|
||||
"error",
|
||||
"contact-support",
|
||||
),
|
||||
MALFORMED_JSON: row("MALFORMED_JSON", false, "error", "contact-support"),
|
||||
RESPONSE_BODY_LIMIT: row(
|
||||
"RESPONSE_BODY_LIMIT",
|
||||
false,
|
||||
"error",
|
||||
"contact-support",
|
||||
),
|
||||
ENVELOPE_MISMATCH: row("ENVELOPE_MISMATCH", false, "error", "contact-support"),
|
||||
SCHEMA_MISMATCH: row("SCHEMA_MISMATCH", false, "error", "contact-support"),
|
||||
MAPPING_CONTRACT_VIOLATION: row(
|
||||
"MAPPING_CONTRACT_VIOLATION",
|
||||
false,
|
||||
"error",
|
||||
"contact-support",
|
||||
),
|
||||
RESULT_LIMIT_EXCEEDED: row(
|
||||
"RESULT_LIMIT_EXCEEDED",
|
||||
false,
|
||||
"error",
|
||||
"contact-support",
|
||||
),
|
||||
SCOPE_GENERATION_CHANGED: row(
|
||||
"SCOPE_GENERATION_CHANGED",
|
||||
false,
|
||||
"info",
|
||||
"none",
|
||||
),
|
||||
IDENTITY_INTERN_LIMIT_EXCEEDED: row(
|
||||
"IDENTITY_INTERN_LIMIT_EXCEEDED",
|
||||
false,
|
||||
"warning",
|
||||
"retry",
|
||||
),
|
||||
DUPLICATE_IN_FLIGHT: row(
|
||||
"DUPLICATE_IN_FLIGHT",
|
||||
false,
|
||||
"info",
|
||||
"none",
|
||||
),
|
||||
PAGINATION_CONTRACT_VIOLATION: row(
|
||||
"PAGINATION_CONTRACT_VIOLATION",
|
||||
false,
|
||||
"error",
|
||||
"contact-support",
|
||||
),
|
||||
AUTH_REQUIRED: row("AUTH_REQUIRED", false, "info", "reauth"),
|
||||
AUTH_INTEGRATION_FAILURE: row(
|
||||
"AUTH_INTEGRATION_FAILURE",
|
||||
false,
|
||||
"error",
|
||||
"contact-support",
|
||||
),
|
||||
FORBIDDEN: row("FORBIDDEN", false, "warning", "navigate"),
|
||||
NOT_FOUND: row("NOT_FOUND", false, "info", "navigate"),
|
||||
CONFLICT: row("CONFLICT", false, "warning", "retry"),
|
||||
VALIDATION_REJECTED: row("VALIDATION_REJECTED", false, "info", "none"),
|
||||
UNKNOWN_CLIENT_FAILURE: row(
|
||||
"UNKNOWN_CLIENT_FAILURE",
|
||||
false,
|
||||
"warning",
|
||||
"contact-support",
|
||||
),
|
||||
RATE_LIMITED: row("RATE_LIMITED", true, "warning", "retry"),
|
||||
SERVER_FAILURE: row("SERVER_FAILURE", true, "error", "retry"),
|
||||
CHUNK_LOAD_FAILURE: row(
|
||||
"CHUNK_LOAD_FAILURE",
|
||||
false,
|
||||
"error",
|
||||
"reload-once",
|
||||
"release.mismatch.detected",
|
||||
),
|
||||
BOOT_CONFIG_FAILURE: row(
|
||||
"BOOT_CONFIG_FAILURE",
|
||||
false,
|
||||
"error",
|
||||
"contact-support",
|
||||
"app.boot.failed",
|
||||
),
|
||||
RELEASE_MANIFEST_FAILURE: row(
|
||||
"RELEASE_MANIFEST_FAILURE",
|
||||
false,
|
||||
"error",
|
||||
"contact-support",
|
||||
"app.boot.failed",
|
||||
),
|
||||
DEPLOY_MISMATCH: row(
|
||||
"DEPLOY_MISMATCH",
|
||||
false,
|
||||
"error",
|
||||
"reload-once",
|
||||
"release.mismatch.detected",
|
||||
),
|
||||
BUILD_MISMATCH: row(
|
||||
"BUILD_MISMATCH",
|
||||
false,
|
||||
"error",
|
||||
"reload-once",
|
||||
"release.mismatch.detected",
|
||||
),
|
||||
CONFIG_MISMATCH: row(
|
||||
"CONFIG_MISMATCH",
|
||||
false,
|
||||
"error",
|
||||
"contact-support",
|
||||
"app.boot.failed",
|
||||
),
|
||||
API_CONTRACT_MISMATCH: row(
|
||||
"API_CONTRACT_MISMATCH",
|
||||
false,
|
||||
"error",
|
||||
"contact-support",
|
||||
"app.boot.failed",
|
||||
),
|
||||
RELEASE_MISMATCH: row(
|
||||
"RELEASE_MISMATCH",
|
||||
false,
|
||||
"error",
|
||||
"reload-once",
|
||||
"release.mismatch.detected",
|
||||
),
|
||||
ASSET_MISMATCH: row(
|
||||
"ASSET_MISMATCH",
|
||||
false,
|
||||
"error",
|
||||
"reload-once",
|
||||
"release.mismatch.detected",
|
||||
),
|
||||
STORAGE_UNAVAILABLE: row(
|
||||
"STORAGE_UNAVAILABLE",
|
||||
false,
|
||||
"warning",
|
||||
"none",
|
||||
"storage.operation.failed",
|
||||
),
|
||||
STORAGE_QUOTA_EXCEEDED: row(
|
||||
"STORAGE_QUOTA_EXCEEDED",
|
||||
false,
|
||||
"warning",
|
||||
"none",
|
||||
"storage.operation.failed",
|
||||
),
|
||||
RENDER_FAILURE: row(
|
||||
"RENDER_FAILURE",
|
||||
false,
|
||||
"error",
|
||||
"reload-once",
|
||||
"ui.render.failed",
|
||||
),
|
||||
TELEMETRY_FAILURE: row(
|
||||
"TELEMETRY_FAILURE",
|
||||
false,
|
||||
"info",
|
||||
"none",
|
||||
"telemetry.delivery.dropped",
|
||||
),
|
||||
QUERY_CACHE_FAILURE: row(
|
||||
"QUERY_CACHE_FAILURE",
|
||||
false,
|
||||
"error",
|
||||
"retry",
|
||||
"query.cache.failed",
|
||||
),
|
||||
UNKNOWN_FAILURE: row("UNKNOWN_FAILURE", false, "error", "contact-support"),
|
||||
});
|
||||
|
||||
/**
|
||||
* Every failure crossing an application input boundary must use one of the
|
||||
* registry-owned kinds. Adapters may accept untrusted backend codes, but must
|
||||
* map those codes to this closed vocabulary before returning.
|
||||
*/
|
||||
export type FailureKind = keyof typeof ERROR_REGISTRY;
|
||||
|
||||
export type ValidationIssue = Readonly<{ path: string; code: string }>;
|
||||
|
||||
export type FailureEffectCertainty =
|
||||
| "NOT_APPLICABLE"
|
||||
| "NOT_STARTED"
|
||||
| "NOT_APPLIED"
|
||||
| "APPLIED_CONFIRMED"
|
||||
| "MAYBE_APPLIED";
|
||||
|
||||
export type AppFailure = Readonly<{
|
||||
kind: FailureKind;
|
||||
code: string;
|
||||
httpStatus?: number;
|
||||
retryable: boolean;
|
||||
operationId: string;
|
||||
attemptCount: number;
|
||||
requestId?: string;
|
||||
traceId?: string;
|
||||
retryAfterMs?: number;
|
||||
effect?: FailureEffectCertainty;
|
||||
validationIssues?: readonly ValidationIssue[];
|
||||
userMessageKey: string;
|
||||
action: ErrorAction;
|
||||
causeClass?: string;
|
||||
}>;
|
||||
|
||||
/**
|
||||
* Backward-compatible transport-facing name. New application and presentation
|
||||
* code should prefer AppFailure.
|
||||
*
|
||||
*/
|
||||
export type ApiFailure = AppFailure;
|
||||
|
||||
export type FailureDetails = Readonly<{
|
||||
code?: string;
|
||||
httpStatus?: number;
|
||||
requestId?: string;
|
||||
traceId?: string;
|
||||
retryAfterMs?: number;
|
||||
effect?: FailureEffectCertainty;
|
||||
validationIssues?: readonly ValidationIssue[];
|
||||
causeClass?: string;
|
||||
}>;
|
||||
|
||||
export function createFailure(
|
||||
kind: FailureKind,
|
||||
operationId: string,
|
||||
attempt: number,
|
||||
details: FailureDetails = {},
|
||||
): AppFailure {
|
||||
const definition: ErrorDefinition<FailureKind> = ERROR_REGISTRY[kind];
|
||||
return Object.freeze({
|
||||
kind: definition.kind,
|
||||
code: typeof details.code === "string" ? details.code : definition.kind,
|
||||
retryable:
|
||||
details.effect === "MAYBE_APPLIED" ||
|
||||
details.effect === "APPLIED_CONFIRMED"
|
||||
? false
|
||||
: definition.defaultRetryable,
|
||||
operationId,
|
||||
attemptCount: Math.max(1, attempt + 1),
|
||||
...(Number.isInteger(details.httpStatus)
|
||||
? { httpStatus: details.httpStatus }
|
||||
: {}),
|
||||
...(typeof details.requestId === "string" ? { requestId: details.requestId } : {}),
|
||||
...(typeof details.traceId === "string" ? { traceId: details.traceId } : {}),
|
||||
...(typeof details.retryAfterMs === "number"
|
||||
? { retryAfterMs: details.retryAfterMs }
|
||||
: {}),
|
||||
...(details.effect === undefined ? {} : { effect: details.effect }),
|
||||
...(Array.isArray(details.validationIssues)
|
||||
? {
|
||||
validationIssues: Object.freeze(
|
||||
details.validationIssues
|
||||
.filter(
|
||||
(issue) =>
|
||||
issue &&
|
||||
typeof issue === "object" &&
|
||||
typeof issue.path === "string" &&
|
||||
typeof issue.code === "string",
|
||||
)
|
||||
.slice(0, 50)
|
||||
.map((issue) =>
|
||||
Object.freeze({ path: issue.path, code: issue.code }),
|
||||
),
|
||||
),
|
||||
}
|
||||
: {}),
|
||||
...(typeof details.causeClass === "string"
|
||||
? { causeClass: details.causeClass }
|
||||
: {}),
|
||||
userMessageKey: definition.userMessageKey,
|
||||
action:
|
||||
details.effect === "MAYBE_APPLIED"
|
||||
? "contact-support"
|
||||
: details.effect === "APPLIED_CONFIRMED"
|
||||
? "none"
|
||||
: definition.action,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds controller-owned mutation effect knowledge without weakening the
|
||||
* fail-safe handling required for an unknown server-side outcome.
|
||||
*/
|
||||
export function withFailureEffect(
|
||||
failure: AppFailure,
|
||||
effect: Exclude<FailureEffectCertainty, "NOT_APPLICABLE">,
|
||||
): AppFailure {
|
||||
return Object.freeze({
|
||||
...failure,
|
||||
effect,
|
||||
...(effect === "MAYBE_APPLIED"
|
||||
? { retryable: false as const, action: "contact-support" as const }
|
||||
: effect === "APPLIED_CONFIRMED"
|
||||
? { retryable: false as const, action: "none" as const }
|
||||
: {}),
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Projects an untrusted 422 details payload into the only validation metadata
|
||||
* allowed to cross the HTTP boundary. Backend copy and additional values are
|
||||
* deliberately discarded.
|
||||
*
|
||||
*/
|
||||
export function safeValidationIssues(
|
||||
value: unknown,
|
||||
): readonly ValidationIssue[] {
|
||||
if (!value || typeof value !== "object") return Object.freeze([]);
|
||||
const candidate = value as Readonly<{
|
||||
issues?: unknown;
|
||||
fieldErrors?: unknown;
|
||||
}>;
|
||||
const issues = Array.isArray(candidate.issues)
|
||||
? candidate.issues
|
||||
: Array.isArray(candidate.fieldErrors)
|
||||
? candidate.fieldErrors
|
||||
: [];
|
||||
return Object.freeze(
|
||||
issues
|
||||
.filter(
|
||||
(issue): issue is ValidationIssue =>
|
||||
issue &&
|
||||
typeof issue === "object" &&
|
||||
"path" in issue &&
|
||||
"code" in issue &&
|
||||
typeof issue.path === "string" &&
|
||||
typeof issue.code === "string" &&
|
||||
issue.path.length <= 120 &&
|
||||
issue.code.length <= 80,
|
||||
)
|
||||
.slice(0, 50)
|
||||
.map((issue) =>
|
||||
Object.freeze({
|
||||
path: issue.path,
|
||||
code: issue.code,
|
||||
}),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
export function kindForStatus(status: number): FailureKind {
|
||||
if (status === 401) return "AUTH_REQUIRED";
|
||||
if (status === 403) return "FORBIDDEN";
|
||||
if (status === 404) return "NOT_FOUND";
|
||||
if (status === 409) return "CONFLICT";
|
||||
if (status === 422) return "VALIDATION_REJECTED";
|
||||
if (status === 429) return "RATE_LIMITED";
|
||||
if (status >= 500) return "SERVER_FAILURE";
|
||||
if (status >= 400) return "UNKNOWN_CLIENT_FAILURE";
|
||||
return "ENVELOPE_MISMATCH";
|
||||
}
|
||||
|
||||
/**
|
||||
* Total catch-all that intentionally discards the thrown value.
|
||||
*
|
||||
*/
|
||||
export function normalizeUnknownFailure(
|
||||
value: unknown,
|
||||
context: Readonly<{ operationId?: string; attempt?: number }> = {},
|
||||
): AppFailure {
|
||||
const causeClass =
|
||||
value instanceof Error
|
||||
? value.name
|
||||
: value === null
|
||||
? "null"
|
||||
: typeof value;
|
||||
|
||||
return createFailure(
|
||||
"UNKNOWN_FAILURE",
|
||||
context.operationId ?? "UNKNOWN_OPERATION",
|
||||
context.attempt ?? 0,
|
||||
{ code: "UNKNOWN_FAILURE", causeClass },
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user