Skip to content

refactor(frontend): centralize fetch wrapper, stale-response guard and LRU cache#795

Open
tdjager wants to merge 2 commits into
mainfrom
refactor-frontend-api-fetch
Open

refactor(frontend): centralize fetch wrapper, stale-response guard and LRU cache#795
tdjager wants to merge 2 commits into
mainfrom
refactor-frontend-api-fetch

Conversation

@tdjager

@tdjager tdjager commented Jun 11, 2026

Copy link
Copy Markdown
Collaborator

Summary

Implements audit advice #8: consolidates three patterns that were copy-pasted across the editor SPA and deletes the dead X-Editor-Session machinery. This is a pure refactor — all user-visible behavior and Dutch error texts are preserved byte-for-byte.

1. lib/apiFetch.js — thin fetch wrapper

  • ok-check + consistent error shaping: a non-ok response throws ApiError carrying status, best-effort body text and contentType; callers branch on .status === 404 or surface the backend's own message.
  • apiFetchJson / apiFetchText for the common parse cases; plain apiFetch returns the raw Response so the recently-added ETag/If-Match optimistic-concurrency plumbing (useLaw, useScenarios, useTrajectDocuments) keeps reading headers unchanged, including the 412 conflict texts.
  • allowStatuses lets 404/412/503/401-branching call sites keep their normal-branch handling; errorMessage(status, body, contentType) keeps every existing user-facing text identical (incl. the text/plain proxy-HTML guard in useLaw.saveLaw / useDraftNotes.saveToRepo).
  • Deliberately no retries, timeouts or global state. The 401 login redirect stays in apiAuthGuard.js, which wraps window.fetch and therefore still sits under apiFetch.
  • 46 of 52 raw fetch sites migrated. The 6 that remain are deliberate multi-status branches where a throwing wrapper would collapse distinct outcomes (useScenarios.fetchScenarios, useTrajects.loadTrajects, useTrajectDocuments open/save/HEAD/delete) — each now carries a comment saying so.

2. lib/useLatest.js — the stale-response "generation counter"

claim() starts a generation and returns an isCurrent() predicate. Replaces 11 hand-rolled counters: useLaw (load/switch), useNotes (×2), useTrajectDocuments.openDocument, useLawGraph.rebuild, LibraryView (×2), EditorView (×2), ScenarioBuilder (×2), SearchPopover.

3. lib/lruMap.js — Map-based LRU with onEvict

Replaces the three copy-pasted LRUs in useLaw (cap 50), useCorpusLaws (cap 5, companion promise map synced via onEvict) and useEngine (cap 50, WASM unload via onEvict). peek() preserves the one non-touching read in useCorpusLaws.

4. Dead X-Editor-Session removal

Verified by grepping packages/: no backend code reads the header (editor-api never extracts it; the editor/session-* branch code in packages/corpus is server-side and header-independent). The header is no longer sent on law/scenario saves; writes are scoped by the traject in the URL path. useEditorSession.js is deleted — its still-used exports lastSavedPr/sanitizeSavedPr move to composables/useSavedPr.js with corrected docs.

Bare catch {} blocks

Kept their swallow decisions (no new error UI in this PR) but each now states why the failure is ignored.

Testing

  • New unit tests for all three helpers (apiFetch.test.js, useLatest.test.js, lruMap.test.js, 23 tests).
  • Full vitest suite: 302 passed (302), 28 files. Three pre-existing strict toHaveBeenCalledWith(url) assertions updated to account for the wrapper's explicit init argument.
  • npm run build passes (no new warnings).
  • No UI changes; no custom CSS added.

Review notes (round 1)

  • Fixed: a 200 response with a non-array body no longer kills the implementors scan permanently (useDependencies) — it stays retryable like other failures.
  • Intentional improvement kept (was: strict equivalence): useEngine's LRU now touches on cache hits too, so actively-used laws are less likely to be evicted (and WASM-unloaded) under cap pressure. The old code only bumped recency on insert.

…d LRU cache

Consolidates three patterns that were copy-pasted across the editor SPA
and deletes the dead X-Editor-Session machinery. Pure refactor: all
user-visible behavior and (Dutch) error texts are preserved exactly.

- lib/apiFetch.js: thin fetch wrapper (ok-check, ApiError with
  status/body/contentType, optional JSON/text parsing). Returns the raw
  Response so the ETag/If-Match concurrency plumbing keeps reading
  headers unchanged. 46 of 52 raw fetch sites migrated; the 6 that
  remain are deliberate multi-status branches (documented inline).
- lib/useLatest.js: the "generation counter" guard against stale async
  writes, previously re-implemented 11 times (useLaw, useNotes x2,
  useTrajectDocuments, useLawGraph, LibraryView x2, EditorView x2,
  ScenarioBuilder x2, SearchPopover).
- lib/lruMap.js: Map-based LRU with onEvict, replacing the three
  hand-rolled copies in useLaw, useCorpusLaws and useEngine.
- Remove dead X-Editor-Session plumbing: no backend code reads the
  header; writes are scoped by the traject in the URL path. The
  still-used lastSavedPr/sanitizeSavedPr move from useEditorSession.js
  (deleted) to useSavedPr.js with corrected docs.
- Bare catch blocks keep their swallow decision but now say why.

All three helpers are unit-tested; the full vitest suite (302 tests)
and the production build pass.
@github-actions

Copy link
Copy Markdown

Preview Deployment — editor — editor

Your changes have been deployed to a preview environment:

URL: https://editor.pr795.rig.prd1.gn2.quattro.rijksapps.nl

This deployment will be automatically cleaned up when the PR is closed.

@tdjager tdjager marked this pull request as ready for review June 11, 2026 15:08
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant