summaryrefslogtreecommitdiffhomepage
path: root/src/features/layout/adapter/http.ts
diff options
context:
space:
mode:
Diffstat (limited to 'src/features/layout/adapter/http.ts')
-rw-r--r--src/features/layout/adapter/http.ts62
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}` };
+ }
+}