Split web/src/api.ts: one request<T> seam, typed errors, stores out #112
Labels
No milestone
No project
No assignees
1 participant
Notifications
Due date
No due date set.
Dependencies
No dependencies set.
Reference
Cloonar/deckle#112
Loading…
Add table
Add a link
Reference in a new issue
No description provided.
Delete branch "%!s()"
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?
Found by an architecture review (deepening opportunities — turning shallow modules into deep ones).
Files:
web/src/api.ts(2,127 lines).Problem: one file mixes ~60 type declarations, ~40 bare
fetchfunctions (17 copies ofif (!res.ok) throw new Error(...), 8 hand-rolled JSON POST bodies), and four Solid store factories with polling timers (createHugoStatus,createPageTree,createPagetypes,createGitState). Each fetch function is shallow — interface ≈ implementation — and errors are untyped strings matched by text. Importing one pure fetch drags in the Solid runtime andsetIntervalloops. The 422-with-errorsvs plain-error decoding is re-implemented per endpoint (savePage,savePageSettings,undoPage).Solution: split into
api/types.ts, anapi/client.tswith a singlerequest<T>(path, init)helper (one place for headers, JSON serialization, status handling, and typed decoding of the taggedSaveResult/ElementOpResult/PublishOutcomeunions), and separatestores/*.tsmodules for the polling state.Benefits: Leverage — ~300 lines of repetition collapse behind one seam; error typing happens once. Locality — "how do we talk to the harness" becomes one module. The fetch layer becomes DOM/Solid-free and unit-testable without jsdom, and the
installMockApitest seam gets simpler.Agent Brief
Category: enhancement
Summary: Split the web client's monolithic API module into a small transport seam, domain endpoint modules with colocated types, and per-store polling modules, introducing a typed
ApiError.Design decisions below were settled with the maintainer during triage grilling; they refine the issue body and win where they differ from it.
Current behavior:
The web client has a single API module (
web/src/api.ts, ~2,100 lines at time of triage) mixing ~60 type declarations, ~40fetchwrapper functions (17 duplicatedif (!res.ok) throw new Error(...)blocks, ~8 hand-rolled JSON POST bodies), and four Solid store factories withsetIntervalpolling loops (createHugoStatus,createPageTree,createPagetypes,createGitState). Errors are thrown as plainErrorwith ad-hoc strings; components displayerr.messagedirectly. Tests mock the network by stubbing globalfetch(installMockApiusesvi.stubGlobal("fetch", ...)), so the seam is transport-level, not module-level.Desired behavior:
A pure refactor — zero runtime behavior change (same endpoints, same polling intervals, byte-identical error message strings) — with this structure:
request<T>(path, init?)helper (the only place that callsfetch, sets JSON headers, serializes bodies, checks status, and decodes JSON) andApiError extends Errorcarryingstatus: numberand the decoded response body.ApiError.messagemust reproduce today's strings exactly — components text-match on them, and their migration to typed inspection belongs to #117, not this issue.TreeNode,PageId). Domain modules must be Solid-free and DOM-free.SaveResult,ElementOpResult,PublishOutcomekeep their current shapes and remain returned (not thrown); the 422-with-errorsdecoding that several endpoints re-implement is centralized so it exists once.stores/directory, unchanged in behavior. Their importers at time of triage: the app shell, the tree pane, the modules registry, and one test file.Key interfaces:
request<T>(path: string, init?: RequestInit-like): Promise<T>— sole owner of fetch/status/JSON handlingApiError extends Error { status: number; body?: unknown }— message strings identical to current literalsSaveResult/ElementOpResult/PublishOutcome— unchanged shapes, unchanged return-value semanticsinstallMockApi(test helper) — must keep working; it stubs globalfetch, so it survives the split as long asfetchremains the transportAcceptance criteria:
fetch+ status-check + JSON decode; zeroif (!res.ok)blocks outside the client moduleApiErrorcarries the HTTP status; all thrown error message strings are byte-identical to before the refactorsolid-jsor touches the DOMstores/with unchanged polling intervals and public shapesrequest<T>: success decode, non-2xx →ApiErrorwith correct status/message, and the centralized 422 field-error decodingweb/distmust exist forgo test ./...; toolchains vianix shell)Out of scope:
err.messagetext display to typedApiErrorinspection — that is #117'serrorMessagework, which will consume theApiErrorintroduced hereSequencing: No blockers. #113 and #117 touch the same importers and should be triaged as
Blocked by #112.