refactor(msg-bus): prefer explicit enum on AsyncAPI error

This commit is contained in:
Edouard Vanbelle
2026-09-11 00:33:45 +02:00
parent 7918fff47b
commit ad9eab6f92
4 changed files with 93 additions and 30 deletions
@@ -1,14 +0,0 @@
enum RtErrorCode {
MINUS_32001 = -32001,
MINUS_32002 = -32002,
MINUS_32003 = -32003,
MINUS_32004 = -32004,
MINUS_32005 = -32005,
MINUS_32006 = -32006,
MINUS_32007 = -32007,
MINUS_32603 = -32603,
MINUS_32600 = -32600,
MINUS_32601 = -32601,
MINUS_32602 = -32602
}
export type { RtErrorCode as default };
@@ -1,9 +1,8 @@
import type RtErrorCode from './RtErrorCode';
import type RtErrorMessage from './RtErrorMessage';
// AUTO-GENERATED — do not edit by hand.
// Regenerate with `just asyncapi-ts`.
interface RtErrorObject {
code: RtErrorCode;
code: number;
data?: unknown;
message: RtErrorMessage;
}
@@ -0,0 +1,55 @@
// Named constants for JSON-RPC 2.0 error codes emitted on the message-bus
// WebSocket wire. Mirrors `application/ports/message_bus_ports.rs::error_code`
// on the server — the Rust module is the source of truth.
//
// Hand-written, deliberately not generated: Modelina projects a JSON-Schema
// `enum` of numeric values into a TS enum with mangled member names
// (`MINUS_32001 = -32001`), which reads worse than no enum at all. The wire
// type is just `number`; readable name-to-code lookup lives here.
//
// Values are frozen across releases — a new denial cause gets a new value,
// never repurposes an existing one. Adding a code: bump the Rust module and
// this file in the same commit; the wire spec's description text is a
// derivative of the Rust constants (see `generate-asyncapi.rs`).
/**
* Application-defined codes live in the JSON-RPC 2.0 server-defined range
* `-32099..-32000`; standard envelope codes live in `-32700..-32600`.
*/
export const RtErrorCode = {
// ── Application-defined (subscribe / edit path denials) ─────────────
/** Resource-scoped topic, caller lacks Read on the resource (or the
* resource does not exist — the two outcomes are indistinguishable to
* the caller by design, to preserve anti-enumeration). */
NO_READ: -32001,
/** Resource-scoped topic requires Share, caller has Read but not Share.
* Applies to `file:{id}:shares` (Phase B). */
NO_SHARE: -32002,
/** Resource-scoped topic requires Comment (`file:{id}:comments`, Phase B). */
NO_COMMENT: -32003,
/** Identity-scoped mismatch, OR unknown/malformed topic. Same wire code
* regardless of whether the target exists — anti-enum. */
TOPIC_FORBIDDEN: -32004,
/** Per-connection subscription cap hit. */
SUB_LIMIT: -32005,
/** Subscribe-frame token bucket exhausted. */
RATE_LIMITED: -32006,
/** CRDT edit frame from a caller without Edit on the doc. Emitted as an
* `rt.write_denied` notification (not tied to a request id). */
NO_EDIT: -32007,
// ── JSON-RPC 2.0 standard envelope codes ────────────────────────────
/** Server-side failure the client should retry. */
INTERNAL_ERROR: -32603,
/** Malformed JSON-RPC envelope (missing `method`, wrong `jsonrpc` version). */
INVALID_REQUEST: -32600,
/** Method outside the `rt.*` allow-list. */
METHOD_NOT_FOUND: -32601,
/** Method known but `params` shape wrong (missing `topic`, unparseable). */
INVALID_PARAMS: -32602
} as const satisfies Record<string, number>;
/** Union of every named code's numeric value. Narrows a bare `number` on
* `RtErrorObject.code` to the eleven known literals for exhaustive
* `switch` blocks. */
export type RtErrorCodeValue = (typeof RtErrorCode)[keyof typeof RtErrorCode];
+28 -5
View File
@@ -473,10 +473,30 @@ fn rpc_error_object_schema() -> Value {
}
fn rpc_error_code_schema() -> Value {
json!({
"type": "integer",
"description": "Stable integer error code. Values are frozen across releases — a new denial cause gets a new value, never repurposes an existing one.",
"enum": [
// Kept as plain `integer` — Modelina projects a JSON-Schema `enum` of
// numeric values into a TS enum with mangled member names
// (`MINUS_32001 = -32001`), which is worse than no enum at all. The
// Rust `error_code` module is the source of truth for named
// constants; the FE mirrors it in `frontend/src/lib/message-bus/
// error-codes.ts` (hand-written, 11 lines, sits alongside the
// generated DTOs). Description enumerates the full set inline so the
// AsyncAPI spec is still self-documenting.
let full_description = format!(
"Stable integer error code. Values are frozen across releases — a \
new denial cause gets a new value, never repurposes an existing \
one. Application-defined codes ({}..={}):\n\
· {} NO_READ — resource-scoped topic, caller lacks Read (or \
resource doesn't exist — indistinguishable by design)\n\
· {} NO_SHARE — resource requires Share, caller has Read but not Share\n\
· {} NO_COMMENT — resource requires Comment\n\
· {} TOPIC_FORBIDDEN — identity-scoped mismatch or unknown/malformed topic\n\
· {} SUB_LIMIT — per-connection subscription cap hit\n\
· {} RATE_LIMITED — subscribe-frame token bucket exhausted\n\
· {} NO_EDIT — CRDT edit frame from a caller without Edit\n\
Standard JSON-RPC 2.0 codes:\n\
· {} INTERNAL_ERROR · {} INVALID_REQUEST · {} METHOD_NOT_FOUND · {} INVALID_PARAMS",
-32099,
-32000,
error_code::NO_READ,
error_code::NO_SHARE,
error_code::NO_COMMENT,
@@ -488,7 +508,10 @@ fn rpc_error_code_schema() -> Value {
error_code::INVALID_REQUEST,
error_code::METHOD_NOT_FOUND,
error_code::INVALID_PARAMS,
],
);
json!({
"type": "integer",
"description": full_description,
})
}