JUMBO refactor, still work in progress

This commit is contained in:
2025-10-07 00:14:31 +02:00
parent 3694492e1a
commit dc0ec5c635
50 changed files with 4283 additions and 3698 deletions

View File

@@ -0,0 +1,109 @@
import { Data, Effect, Option, pipe, Record, Schema } from "effect";
import { constant } from "effect/Function";
import * as Api from "./Api";
import * as Body from "./Body";
import * as Cbor from "./Cbor";
import { fetch, FetchError } from "./Fetch";
export class ServerDie extends Data.TaggedError("ServerDie")<{ cause: unknown }> { }
export class UnexpectedStatus extends Data.TaggedError("UnexpectedStatusCode")<{ status: number }> { }
export interface ClientOptions {
readonly baseUrl?: string | URL | undefined;
}
export const client = <const Record extends { readonly [_: string]: Api.ApiAny }>(
bundle: Api.ApiBundle<Record>,
{ baseUrl }: ClientOptions = {},
) => {
return Object.freeze(Record.map(bundle.record, (api, key) => {
const url = new URL(`/api/${key}`, baseUrl);
const requestEncoder = encodeBody(api.request);
const responseDecoder = decodeBody(api.response);
const errorDecoder = decodeBody(api.error);
return Effect.fn(`call.${key}`)(function* (request: any) {
const body = yield* pipe(
request,
requestEncoder,
Effect.flatMap(({ body, headers }) => fetch(url, {
method: "POST",
credentials: "include",
body,
headers,
})),
Effect.catchTags({
CborEncodeError: Effect.die,
ParseError: Effect.die,
}),
);
switch (body.status) {
case 200: {
const result = yield* responseDecoder(body).pipe(Effect.orDie);
return result;
}
case 400: {
const result = yield* errorDecoder(body).pipe(Effect.orDie);
return yield* Effect.fail(result);
}
case 500: {
const result = yield* readBody(body).pipe(Effect.orDie);
return yield* Effect.die(new ServerDie({ cause: result }));
}
default: {
return yield* Effect.die(new UnexpectedStatus({ status: body.status }));
}
}
});
})) as { readonly [K in keyof Record]: (request: Record[K]["request"]["Type"]) => Effect.Effect<Record[K]["response"]["Type"], Record[K]["error"]["Type"] | FetchError, never> };
};
export const readBody = (body: Body) => pipe(
body,
Body.bytes,
Effect.map((bytes) => bytes.byteLength > 0 ? Option.some(bytes) : Option.none()),
Effect.flatMap((maybeBytes) => pipe(
maybeBytes,
Effect.transposeMapOption(Cbor.decode),
)),
Effect.map(Option.getOrUndefined),
);
export const decodeBody = <A>(schema: Schema.Schema<A, any>) => {
const decoder = Schema.decodeUnknown(schema);
return (body: Body) => Effect.flatMap(readBody(body), decoder);
};
export interface BodyData {
readonly body: null | Uint8Array;
readonly headers: { readonly [_: string]: string };
}
export const encodeBody = <A>(schema: Schema.Schema<A, any>) => {
const encoder = Cbor.encodeSchema(schema);
return (a: A) => pipe(
a,
Option.fromNullable,
Effect.transposeMapOption(encoder),
Effect.map(Option.match({
onNone: emptyBody,
onSome: cborBody,
}))
);
};
export const emptyBody: () => BodyData = constant(Object.freeze<BodyData>({
body: null,
headers: Object.freeze({}),
}));
export const cborBody: (bytes: Uint8Array) => BodyData = (bytes) => Object.freeze<BodyData>({
body: bytes,
headers: Object.freeze({
"Content-Type": "application/cbor",
"Content-Length": String(bytes.byteLength),
}),
});