1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
|
import type { TileGeometryMode, TileLayoutData } from "@dispatch/tile-layout-contract";
/**
* Home tile-layout HTTP effects — the injected edge that talks to the
* `home-tile-layout` plugin's well-known routes:
* - `GET /home/tile-layout/:mode` → `{ layout }` (a fresh orientation reads
* as the seeded empty document)
* - `PUT /home/tile-layout/:mode` body `{ layout }` → 200 `{ layout }` /
* 400 `{ error }`
* - `DELETE /home/tile-layout/:mode` → the fresh empty document (reset)
*
* Mirrors the layout adapter's pattern: `httpBase` + an injected `fetchImpl`
* (testable without the network). A 404 is NOT a "no document" signal (the
* backend never 404s a fresh orientation — it seeds): any non-2xx / network
* failure means "the routes aren't served" (an old backend) or "backend
* unreachable" — both degrade the store to its local state with ephemeral
* edits, never an error UI.
*/
/** Outcome of a tile-layout read/write. */
export type TileLayoutHttpResult =
| { readonly ok: true; readonly layout: TileLayoutData }
| { readonly ok: false; readonly notFound: boolean; readonly error: string };
export interface TileLayoutHttp {
getLayout(mode: TileGeometryMode): Promise<TileLayoutHttpResult>;
putLayout(mode: TileGeometryMode, layout: TileLayoutData): Promise<TileLayoutHttpResult>;
deleteLayout(mode: TileGeometryMode): Promise<TileLayoutHttpResult>;
}
/** Read an error message from a non-2xx body (`{ error }`), tolerating junk. */
async function errorOf(res: Response): Promise<string> {
try {
const body = (await res.json()) as { error?: unknown };
if (typeof body.error === "string" && body.error.length > 0) return body.error;
} catch {
// fall through
}
return `HTTP ${res.status}`;
}
/** A malformed 200 (e.g. an SPA-fallback page) degrades like a failure — never poisons the store. */
function malformed(): TileLayoutHttpResult {
return { ok: false, notFound: false, error: "Malformed tile-layout response" };
}
function route(httpBase: string, mode: TileGeometryMode): string {
return `${httpBase}/home/tile-layout/${encodeURIComponent(mode)}`;
}
export function createTileLayoutHttp(httpBase: string, fetchImpl: typeof fetch): TileLayoutHttp {
return {
async getLayout(mode): Promise<TileLayoutHttpResult> {
try {
const res = await fetchImpl(route(httpBase, mode));
if (res.status === 404) {
return { ok: false, notFound: true, error: "Tile-layout routes are not served" };
}
if (!res.ok) return { ok: false, notFound: false, error: await errorOf(res) };
const body = (await res.json()) as { layout?: unknown };
if (body === null || typeof body !== "object" || body.layout === undefined)
return malformed();
return { ok: true, layout: body.layout as TileLayoutData };
} catch (err) {
return {
ok: false,
notFound: false,
error: err instanceof Error ? err.message : "Tile-layout request failed",
};
}
},
async putLayout(mode, layout): Promise<TileLayoutHttpResult> {
try {
const res = await fetchImpl(route(httpBase, mode), {
method: "PUT",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ layout }),
});
if (res.status === 404) {
return { ok: false, notFound: true, error: "Tile-layout routes are not served" };
}
if (!res.ok) return { ok: false, notFound: false, error: await errorOf(res) };
const body = (await res.json()) as { layout?: unknown };
if (body === null || typeof body !== "object" || body.layout === undefined)
return malformed();
return { ok: true, layout: body.layout as TileLayoutData };
} catch (err) {
return {
ok: false,
notFound: false,
error: err instanceof Error ? err.message : "Tile-layout request failed",
};
}
},
async deleteLayout(mode): Promise<TileLayoutHttpResult> {
try {
const res = await fetchImpl(route(httpBase, mode), { method: "DELETE" });
if (res.status === 404) {
return { ok: false, notFound: true, error: "Tile-layout routes are not served" };
}
if (!res.ok) return { ok: false, notFound: false, error: await errorOf(res) };
const body = (await res.json()) as { layout?: unknown };
if (body === null || typeof body !== "object" || body.layout === undefined)
return malformed();
return { ok: true, layout: body.layout as TileLayoutData };
} catch (err) {
return {
ok: false,
notFound: false,
error: err instanceof Error ? err.message : "Tile-layout request failed",
};
}
},
};
}
|