Files
atomic-design-poc/libs/shared/src/application/submit.ts
T
ehoandClaude Opus 5 80de261299 refactor(shared): split runResult out of runSubmit (RB-17)
runSubmit did two things at once: fold a call into a Result, and mint
an Idempotency-Key for it. Five call sites are reads and had no
business minting one — brief.adapter.ts:load, org-template.adapter.ts
:list/:load, and stamdata.adapter.ts:list/:load. stamdata.adapter.ts's
own docstring already said "Both endpoints are reads … There is no
write method" while both called runSubmit; that mismatch is the
sharpest evidence, and the reason the baseline's original "~13
mutations" count (derived from the helper's name, not the code) was
wrong by five in one direction.

Split submit.ts in place: runResult is the try/catch + problemDetail
fold with no mint; runSubmit is runResult wrapping
withIdempotencyKey. Zero behaviour change for the 8 real mutations
(brief save/submit/approve/reject/send/reset, org-template
save/publish/rollback) — same fold, same mint, same timing. The five
reads now run the fold with no pendingIdempotencyKey touched.

submit.spec.ts asserts the split behaviourally via
currentIdempotencyKey() (two reads inside the same call agree only
when a key was minted and reused) rather than mocking a relative
import, matching this repo's existing vitest convention. Verified red
without the fix by temporarily reintroducing the mint into runResult.

ApplicationsStore.cancel/AdminCasesStore.delete (RB-20) and
FeatureFlagStore.set are out of scope and untouched — the latter
already calls runSubmit correctly.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-27 16:31:13 +02:00

39 lines
1.6 KiB
TypeScript

import { Result, ok, err } from '@shared/kernel/fp';
import { problemDetail } from '@shared/infrastructure/api-error';
import { withIdempotencyKey } from '@shared/infrastructure/api-client.provider';
/**
* Run an API call and fold it into a `Result` — the one place the try/catch +
* ProblemDetails-mapping lives, so every adapter method is just its own payload
* mapping. The backend re-validates and returns a 422 ProblemDetails on
* rejection, surfaced here as the error string.
*
* This is the **read** half: it mints no Idempotency-Key. Use it for GETs.
* `runSubmit` below wraps it for mutations — never route a read through that
* one, it mints a key for nothing.
*/
export async function runResult<T>(
fn: () => Promise<T>,
fallback: string,
): Promise<Result<string, T>> {
try {
return ok(await fn());
} catch (e) {
return err(problemDetail(e, fallback));
}
}
/**
* `runResult` for a **mutating** call: also mints the Idempotency-Key for this
* logical submit — once per `runSubmit` call, not per HTTP attempt — so a
* retry of this same submit dedupes on the backend (see `withIdempotencyKey`).
* Reads must go through `runResult` instead, which mints nothing.
*/
export function runSubmit<T>(fn: () => Promise<T>, fallback: string): Promise<Result<string, T>> {
return runResult(() => withIdempotencyKey(crypto.randomUUID(), fn), fallback);
}
// Single shared default for a failed submit; the @@id dedupes it at the
// translation layer.
export const SUBMIT_FAILED = $localize`:@@submit.failed:Het indienen is niet gelukt. Probeer het later opnieuw.`;