refactor(web): split api.ts into transport seam, domain modules, and stores #119
No reviewers
Labels
No milestone
No project
No assignees
1 participant
Notifications
Due date
No due date set.
Dependencies
No dependencies set.
Reference
Cloonar/deckle!119
Loading…
Add table
Add a link
Reference in a new issue
No description provided.
Delete branch "afk/112"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Closes #112
What changed
web/src/api.ts(2,127 lines — ~60 types, ~40 fetch wrappers, four polling store factories) is gone. In its place:api/client.tsfetchcall site;request<T>(throws),requestText(raw text for byte-compare polling),requestResult(status-driven, never throws),postOp<T>(the house POST contract);ApiError { status, body }; centralized 422 decoding (decodeFieldErrors,decodeFieldMap)api/types.tsPageId,SchemaParam,FieldType,SchemaLoadErrorapi/content.tsDocumentNode/ElementNode/…) + node helpers (nodeAtPath,flattenChildren, …) — a dependency-free leafapi/{status,tree,elements,layouts,pagetypes}.tsapi/{pageedit,pagemanage,pagesettings,elementops,git,publish,media,links}.tsstores/{hugoStatus,pageTree,pagetypes,gitState}.tsNo barrel replaces the monolith — all ~58 importers deep-import exactly the modules they use.
Behavior preservation (the issue's core constraint)
errorpasses through) vs the strict non-empty house contract everywhere else.SaveResult,SettingsSaveResult,ElementOpResult,PublishOutcome,UndoResult,MediaUploadResult,LibraryMediaDeleteResult) keep exact shapes and stay return values; network failures still propagate raw.connectedsemantics).fetchElements,fetchPageElements) resolve after the same microtask count as the monolith viarequest's synchronoustransformhook — the jsdom suite has tests that observe a single extra hop, which is why the hook exists.ApiError extends Errorwith the same.message; #117's typed-error migration builds on this.Verification
@vitest-environment node): success decode, ApiError status/message/body, message modes, 422 decoders, FormData passthrough, tolerant decode, raw network-error propagation. Suite total: 43 files / 898 tests, green.tsc --noEmitclean;vite buildclean;go vet ./...,go test ./...(with freshweb/dist), andgo buildall green.fetch(site (client.ts);res.okappears only in client.ts; no solid-js/DOM anywhere underapi/; stores ride domain functions only.fetchElementsnormalization,res.oknaming onTransportResultlocals) were fixed before this PR.Out of scope (per the brief)
Component migration off
err.messagetext-matching (#117) and page-editor controller extraction (#113), both of which should be sequenced after this lands.🤖 Generated with Claude Code
The 2,127-line web/src/api.ts monolith mixed ~60 type declarations, ~40 fetch wrappers (17 hand-rolled ok-checks, 8 hand-rolled JSON POST bodies), and four Solid store factories with polling timers. It is now: - api/client.ts — the transport seam: the single fetch call site, with request<T> (throw on error), requestText (raw text for the polling stores' byte-compare), requestResult (status-driven, never throws), postOp<T> (the house POST contract), a typed ApiError carrying the HTTP status and decoded body, and the 422 field-error decoding centralized behind decodeFieldErrors/decodeFieldMap. - api/{types,content,status,tree,elements,layouts,pagetypes,pageedit, pagemanage,pagesettings,elementops,git,publish,media,links}.ts — domain endpoint modules that colocate the request/response types they own; api/types.ts holds only the genuinely cross-domain shapes (PageId, SchemaParam, FieldType, SchemaLoadError); api/content.ts is the pure document model + node helpers. All Solid-free and DOM-free. - stores/{hugoStatus,pageTree,pagetypes,gitState}.ts — one polling store per module, behavior byte-identical (intervals, per-field signals, byte-compare caches, catch semantics). No barrel replaces the monolith: all ~58 importers deep-import the module they need. Error message strings, URLs, methods, payloads, tagged-union shapes (SaveResult/SettingsSaveResult/ElementOpResult/PublishOutcome/ UndoResult/MediaUploadResult/LibraryMediaDeleteResult), and polling cadence are preserved byte-for-byte; endpoints on first-render paths keep their exact microtask hop count via request's synchronous transform hook. The transport seam gets 27 jsdom-free unit tests (@vitest-environment node); the existing suite passes with import-path-only edits. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>Landing audit — PASS
Verification signal relied on: the repo's own Forgejo Actions CI,
ci / build-and-test— success in 7m56s. The workflow is unconditional (no path filters, no gated jobs), runningnpm ci && npm run build && npm testplusgo vet ./... && go test ./... && go build. It therefore exercised this diff's full path, including the 27 new transport-seam tests. Not re-run locally.Mergeability: no conflicts;
afk/112sits directly on currentmain(merge-basef8b776a). Single commit, Conventional Commits title,Closes #112present and well-formed.Acceptance criteria (issue #112) — mechanically verified
web/src/api.tsdeleted, noapi/index.ts, zero dangling imports of the old pathfetch+ status-check + JSON decode siteweb/src/api/client.ts:88;res.okappears only inclient.ts(other.okhits areTransportResultlocals)api/POLL_INTERVAL_MS = 1500,TREE_POLL_INTERVAL_MS = 2000Behavior preservation
An independent adversarial pass compared all 128 exports of the old monolith against their new implementations. No divergence in endpoint URL, HTTP method, headers, body serialization, thrown/returned error message strings, tagged-union shapes, or polling cadence. The
/api/…URL-literal multiset is byte-identical (48 entries, same counts); all 128 exports have a home, nothing dropped.The riskiest part — the 422 centralization — was handled correctly. The monolith had two loose extractions (
savePage,savePageSettings: an empty-string servererrorpasses through) against eight strict ones. The split preserves that asymmetry deliberately: strict sites useclient.serverError, the two loose sites keep a dedicatedlooseFormError.Four benign divergences, none blocking:
ApiError(name: "ApiError"), soString(err)changes prefix. Every catch site uses theerr instanceof Error ? err.message : String(err)idiom, which is unaffected.res.json()tores.text()+JSON.parse— differs only for a body of literal JSONnull, where the new code is strictly more robust. Unreachable from this server.asyncwrapper. Thetransformhook exists precisely to preserve await-count and was applied where the jsdom suite observes it; it was applied selectively rather than uniformly.Notes (non-blocking, follow-up material)
looseFormErroris duplicated verbatim inweb/src/api/pageedit.ts:80andweb/src/api/pagesettings.ts:48. Given the PR's headline is centralizing 422 decoding, the loose half staying copy-pasted is the one spot the refactor stops short of its own thesis — it belongs besideserverErrorinclient.tsas a named alternative contract, so the strict-vs-loose distinction lives in one documented place.web/src/test/setup.tsgained atypeof window !== "undefined"guard aroundwindow.scrollTo— the sole non-import test edit. It is forced by the jsdom-free-tests criterion (node-environment tests have nowindow) and weakens no assertion.scaffold/site/AGENTS.mdtracking rule does not apply.Verdict: PASS. Awaiting maintainer confirmation to merge.