clone

a self-hosted sync node for lofi apps: jazz sync over iroh, ticket-gated, with no static ip or cloud needed

feat(verdict): mutation-error taxonomy — permanent vs transient, keyed by code

Dami committed 9e0c98d
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>
3 files changed, +86 -0
Mmod.ts6+++
Asrc/verdict.ts52++++++++++++++++++++++++++++++
Atests/verdict_test.ts28++++++++++++++++
M mod.ts +6 -0
A src/verdict.ts +52 -0
@@ -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+}
A tests/verdict_test.ts +28 -0