JUMBO refactor, still work in progress
This commit is contained in:
109
packages/common/src/Client.ts
Normal file
109
packages/common/src/Client.ts
Normal 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),
|
||||
}),
|
||||
});
|
||||
Reference in New Issue
Block a user