1import * as Data from "effect/Data";
2import * as Effect from "effect/Effect";
3import * as Schedule from "effect/Schedule";
4
5/** Typed failure for the fixture-driving HTTP helpers. */
6export class AuthHttpError extends Data.TaggedError("AuthHttpError")<{
7 readonly url: string;
8 readonly status: number;
9 readonly body: string;
10}> {
11 override get message(): string {
12 return `${this.url} -> ${this.status}: ${this.body.slice(0, 300)}`;
13 }
14}
15
16export interface AuthHttpResponse {
17 readonly status: number;
18 readonly body: string;
19 readonly setCookies: ReadonlyArray<string>;
20}
21
22const request = (
23 url: string,
24 init: RequestInit,
25): Effect.Effect<AuthHttpResponse, AuthHttpError> =>
26 Effect.tryPromise({
27 try: async (signal): Promise<AuthHttpResponse> => {
28 const response = await fetch(url, { ...init, signal });
29 return {
30 status: response.status,
31 body: await response.text(),
32 setCookies: response.headers.getSetCookie(),
33 };
34 },
35 catch: (cause) =>
36 new AuthHttpError({ url, status: 0, body: String(cause) }),
37 });
38
39export const postJson = (
40 url: string,
41 body: unknown,
42 headers?: Record<string, string>,
43): Effect.Effect<AuthHttpResponse, AuthHttpError> =>
44 request(url, {
45 method: "POST",
46 headers: { "content-type": "application/json", ...headers },
47 body: JSON.stringify(body),
48 });
49
50export const getJson = <T>(
51 url: string,
52 headers?: Record<string, string>,
53): Effect.Effect<T, AuthHttpError> =>
54 request(url, { method: "GET", headers }).pipe(
55 Effect.filterOrFail(
56 (response) => response.status === 200,
57 (response) => new AuthHttpError({ url, ...response }),
58 ),
59 Effect.map((response) => JSON.parse(response.body) as T),
60 );
61
62/**
63 * Retry for the first requests against a freshly-deployed workers.dev URL
64 * — the subdomain takes a few seconds to start serving.
65 */
66export const edgeRetry = Effect.retry({
67 schedule: Schedule.exponential("1 second", 1.5),
68 times: 8,
69});
70
71/** Reduce set-cookie headers to a `cookie` request header value. */
72export const toCookieHeader = (setCookies: ReadonlyArray<string>): string =>
73 setCookies
74 .map((cookie) => cookie.split(";")[0])
75 .filter((pair): pair is string => pair !== undefined && pair !== "")
76 .join("; ");