Consumers classify rejection codes through one exported registry instead of string-matching reason text. The pinned alpha emits exactly one code over the sync protocol (permission_denied, permanent); unknown codes classify transient so an unrecognized code can never trigger irreversible compensation.
- commit
- 9e0c98d19aa5cfd8afa7fc9b7243e035ceaeb26e
- parent
- 4cdab00eb626f5bb430d808dec32644050c3b894
- author
- Dami <dami.etoile@gmail.com>
| M | mod.ts | 6 | +++ |
| A | src/ | 52 | ++++++++++++++++++++++++++++++ |
| A | tests/ | 28 | ++++++++++++++++ |
@@ -46,3 +46,9 @@ export { decodeAppTicket, encodeAppTicket, looksLikeAppTicket } from "./src/appt
4646 export type { AppTicket } from "./src/appticket.ts";4747 export { CLOSE_TICKET_REVOKED } from "./src/gate.ts";4848 export { MeshUnavailableError } from "./src/errors.ts";49+export {50+ classifyMutationError,51+ isPermanentMutationError,52+ MUTATION_ERROR_CLASSES,53+} from "./src/verdict.ts";54+export type { MutationErrorClass } from "./src/verdict.ts";@@ -0,0 +1,52 @@
1+// Write-verdict taxonomy: classify mutation-error codes so consumers act on2+// the code, never on the human-readable reason string.3+//4+// A Jazz write that reaches the sync server settles into a per-batch fate;5+// when that fate is a rejection it carries a machine-readable `code` plus a6+// free-text `reason`. The reason is diagnostics only — its wording changes7+// between Jazz versions. The code is the contract, and this module is the one8+// place that maps codes onto the two actions a consumer can take:9+//10+// - `permanent`: the store has adjudicated the write and will never accept it11+// (permission denied, and any future validation/schema-enforcement codes).12+// The write's verdict is `rejected`; compensation may run.13+// - `transient`: the outcome is not adjudicated (store unreachable, timeout,14+// ticket hiccup). The write stays pending; keep waiting and retrying.15+//16+// Codes not in the registry classify as `transient` by design: an17+// unrecognized code must never trigger irreversible compensation. Waiting is18+// recoverable; a wrong `rejected` verdict is not.19+20+/** How a consumer must treat a mutation-error code. */21+export type MutationErrorClass = "permanent" | "transient";22+23+/**24+ * The registry of known mutation-error codes.25+ *26+ * The pinned Jazz alpha emits exactly one rejection code over the sync27+ * protocol: `permission_denied` (policy denies the operation on the table for28+ * this session). Writes that violate the CURRENT locally-known policy or29+ * schema never produce a code at all — the client runtime refuses them30+ * synchronously at the call site, before any batch exists.31+ */32+export const MUTATION_ERROR_CLASSES: Readonly<Record<string, MutationErrorClass>> = {33+ permission_denied: "permanent",34+};35+36+/**37+ * Classify a mutation-error code from a rejected batch fate — the `code` on38+ * `MutationErrorEvent`, on `PersistedWriteRejectedError`, or on a39+ * `BatchFate` of kind `rejected`.40+ *41+ * Unknown codes classify as `transient`: the consumer keeps the write42+ * pending rather than compensating on a code it cannot interpret.43+ */44+export function classifyMutationError(code: string): MutationErrorClass {45+ return MUTATION_ERROR_CLASSES[code] ?? "transient";46+}47+48+/** True when {@link classifyMutationError} classifies `code` as permanent —49+ * the write is `rejected` and compensation may run. */50+export function isPermanentMutationError(code: string): boolean {51+ return classifyMutationError(code) === "permanent";52+}@@ -0,0 +1,28 @@
1+// The taxonomy contract: consumers act on the code's classification, never on2+// the reason string, and unknown codes fail safe (keep waiting, never3+// compensate).4+5+import { assert, assertEquals } from "@std/assert";6+import {7+ classifyMutationError,8+ isPermanentMutationError,9+ MUTATION_ERROR_CLASSES,10+} from "../src/verdict.ts";11+12+Deno.test("verdict: permission_denied is permanent", () => {13+ assertEquals(classifyMutationError("permission_denied"), "permanent");14+ assert(isPermanentMutationError("permission_denied"));15+});16+17+Deno.test("verdict: unknown codes classify transient", () => {18+ for (const code of ["", "store_unreachable", "some_future_code", "PERMISSION_DENIED"]) {19+ assertEquals(classifyMutationError(code), "transient", `code ${JSON.stringify(code)}`);20+ assertEquals(isPermanentMutationError(code), false);21+ }22+});23+24+Deno.test("verdict: the registry only holds valid classes", () => {25+ for (const [code, klass] of Object.entries(MUTATION_ERROR_CLASSES)) {26+ assert(klass === "permanent" || klass === "transient", `code ${code}`);27+ }28+});