diff options
Diffstat (limited to 'src/features/layout/adapter/http.ts')
| -rw-r--r-- | src/features/layout/adapter/http.ts | 62 |
1 files changed, 60 insertions, 2 deletions
diff --git a/src/features/layout/adapter/http.ts b/src/features/layout/adapter/http.ts index 2f91e43..ff15a70 100644 --- a/src/features/layout/adapter/http.ts +++ b/src/features/layout/adapter/http.ts @@ -1,11 +1,15 @@ -import type { LayoutTree } from "@dispatch/layout-contract"; +import type { ApplyOpsResult, LayoutOp, LayoutTree } from "@dispatch/layout-contract"; /** * Layout HTTP effects (plan 02 §2.6) — the injected edge that talks to the * ACTIVE layout provider's well-known routes: * - `GET /workspaces/:id/layout` → `{ tree }` (read seeds a fresh workspace) * - `PUT /workspaces/:id/layout` body `{ tree }` → 200 `{ tree }` / 400 `{ error }` - * - `DELETE /workspaces/:id/layout` → the fresh seed (reset; plan 04 adds the UI) + * - `DELETE /workspaces/:id/layout` → the fresh seed (reset; the plugins + * modal's "Reset layout to default" button — plan 04 §4.4) + * - `POST /workspaces/:id/layout/ops` body `{ ops }` → 200 `{ tree, applied }` + * / 400 `{ error, failedOp? }` / 404 no provider (plan 04 §4.2 — the FE + * ABI's `layout.applyOps` door) * * Mirrors the plugins adapter's pattern: `httpBase` + an injected `fetchImpl` * (testable without the network). The 404 is a SIGNAL, not an error: it means @@ -22,6 +26,13 @@ export interface LayoutHttp { getTree(workspaceId: string): Promise<LayoutHttpResult>; putTree(workspaceId: string, tree: LayoutTree): Promise<LayoutHttpResult>; deleteTree(workspaceId: string): Promise<LayoutHttpResult>; + /** + * The plan-04 ops door (§4.2): apply op-vocabulary edits through the + * provider. The result IS the contract's `ApplyOpsResult` — the 404 + * (no provider) maps to the typed `{ ok: false, error: "no layout + * provider active" }` failure the ABI documents. + */ + postOps(workspaceId: string, ops: readonly LayoutOp[]): Promise<ApplyOpsResult>; } /** Read an error message from a non-2xx body (`{ error }`), tolerating junk. */ @@ -113,5 +124,52 @@ export function createLayoutHttp(httpBase: string, fetchImpl: typeof fetch): Lay }; } }, + + async postOps(workspaceId, ops): Promise<ApplyOpsResult> { + try { + const res = await fetchImpl( + `${httpBase}/workspaces/${encodeURIComponent(workspaceId)}/layout/ops`, + { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ ops }), + }, + ); + // The no-provider 404 is the ABI's typed signal (plan 04 §4.5). + if (res.status === 404) { + return { ok: false, error: "no layout provider active" }; + } + if (!res.ok) return await opsError(res); + const body = (await res.json()) as { tree?: unknown; applied?: unknown }; + if (body === null || typeof body !== "object" || body.tree === undefined) + return { ok: false, error: "Malformed layout response" }; + return { + ok: true, + tree: body.tree as LayoutTree, + applied: typeof body.applied === "number" ? body.applied : ops.length, + }; + } catch (err) { + return { + ok: false, + error: err instanceof Error ? err.message : "Layout request failed", + }; + } + }, }; } + +/** Read an ops-failure body (`{ error, failedOp? }`), tolerating junk. */ +async function opsError(res: Response): Promise<ApplyOpsResult> { + try { + const body = (await res.json()) as { error?: unknown; failedOp?: unknown }; + const error = + typeof body.error === "string" && body.error.length > 0 ? body.error : `HTTP ${res.status}`; + return { + ok: false, + error, + ...(typeof body.failedOp === "number" ? { failedOp: body.failedOp } : {}), + }; + } catch { + return { ok: false, error: `HTTP ${res.status}` }; + } +} |
