1import * as Result from "effect/Result";
2import * as Schema from "effect/Schema";
3import { RuntimeError, SystemError } from "../RuntimeError.shared.ts";
4
5const ErrorEnvelope = Schema.Struct({
6 ok: Schema.Literal(false),
7 error: RuntimeError,
8});
9type ErrorEnvelope = typeof ErrorEnvelope.Type;
10
11const encodeErrorResponse = Schema.encodeSync(ErrorEnvelope);
12const decodeErrorResponse = Schema.decodeUnknownResult(ErrorEnvelope);
13
14export const makeErrorEnvelope = (error: RuntimeError) =>
15 encodeErrorResponse({ ok: false, error });
16
17export const makeErrorResponse = (
18 error: RuntimeError,
19 init?: { status?: number; headers?: Record<string, string> },
20): Response =>
21 Response.json(makeErrorEnvelope(error), {
22 status: init?.status ?? 500,
23 headers: { "content-type": "application/json", ...init?.headers },
24 });
25
26export const decodeResponse = async <T>(response: Response) => {
27 const text = await response.text().catch(() => "");
28 let json: { ok: true; result: T } | ErrorEnvelope;
29 try {
30 json = JSON.parse(text);
31 } catch {
32 throw new SystemError({
33 subtag: "InvalidResponse",
34 message: `Invalid response from server (${response.status} ${response.statusText})`,
35 detail: { status: response.status, body: text },
36 });
37 }
38 if (json.ok) {
39 return json.result;
40 }
41 const decoded = decodeErrorResponse(json);
42 if (Result.isSuccess(decoded)) {
43 throw decoded.success.error;
44 }
45 throw new SystemError({
46 subtag: "InvalidResponse",
47 message: `Invalid response from server (${response.status} ${response.statusText})`,
48 detail: { status: response.status, body: text },
49 });
50};