@schorts/shared-kernel
Version:
A modular, type-safe foundation for building expressive, maintainable applications. This package provides core abstractions for domain modeling, HTTP integration, authentication, state management, and more — designed to be framework-agnostic and highly ex
152 lines (122 loc) • 3.95 kB
text/typescript
import type { HTTPProvider } from "./http-provider";
import { HTTPException } from "./exceptions";
import type { HTTPInterceptor } from "./http-interceptor";
export class FetchHTTPProvider implements HTTPProvider {
private ongoingRequests = new Map<string, Promise<any>>();
private readonly init: {
credentials?: RequestCredentials;
headers?: HeadersInit;
} | undefined;
private readonly getAuthorization: (() => string) | undefined;
private readonly interceptors: HTTPInterceptor[] = [];
constructor(
getAuthorization?: () => string,
init?: {
credentials?: RequestCredentials;
headers?: HeadersInit;
}
) {
this.getAuthorization = getAuthorization;
this.init = init;
}
useInterceptor(interceptor: HTTPInterceptor) {
this.interceptors.push(interceptor);
}
get<ResponseType>(url: URL): Promise<ResponseType> {
return this.request("GET", url);
}
post<RequestBodySchema, ResponseType>(
url: URL,
body: RequestBodySchema
): Promise<ResponseType> {
return this.request("POST", url, body);
}
put<RequestBodySchema, ResponseType>(
url: URL,
body: RequestBodySchema
): Promise<ResponseType> {
return this.request("PUT", url, body);
}
patch<RequestBodySchema, ResponseType>(
url: URL,
body: RequestBodySchema
): Promise<ResponseType> {
return this.request("PATCH", url, body);
}
delete<ResponseType>(url: URL): Promise<ResponseType> {
return this.request("DELETE", url);
}
private async request<ResponseType>(
method: string,
url: URL,
body?: unknown
): Promise<ResponseType> {
const key = this.generateRequestKey(method, url, body);
if (this.ongoingRequests.has(key)) {
return this.ongoingRequests.get(key) as Promise<ResponseType>;
}
const baseHeaders = this.init?.headers ?? {};
const authHeader = this.getAuthorization ? { Authorization: this.getAuthorization() } : {};
const contentTypeHeader = body !== undefined ? { "Content-Type": "application/json" } : {};
const headers: HeadersInit = {
...baseHeaders,
...contentTypeHeader,
...authHeader,
};
let init: RequestInit = {
method,
body: body !== undefined ? JSON.stringify(body) : null,
headers,
};
if (this.init?.credentials !== undefined) {
init.credentials = this.init.credentials;
}
for (const interceptor of this.interceptors) {
init = interceptor.intercept(init, url);
}
const request = (async () => {
const response = await fetch(url.href, init);
if (!response) {
throw new HTTPException(0, undefined);
}
if (response.status === 204) {
return undefined as ResponseType;
}
const contentType = response.headers.get("Content-Type") ?? "";
let parsed: any;
try {
if (contentType.includes("application/json")) {
parsed = await response.json();
} else if (contentType.includes("text/")) {
parsed = await response.text();
} else {
parsed = await response.blob();
}
} catch {
parsed = undefined;
}
if (!response.ok) {
throw new HTTPException(response.status, parsed);
}
return parsed as ResponseType;
})().finally(() => {
this.ongoingRequests.delete(key);
});
this.ongoingRequests.set(key, request);
return request;
}
private generateRequestKey(method: string, url: URL, body?: unknown): string {
const base = `${method}:${url.href}`;
const bodyHash = body !== undefined ? this.hashString(JSON.stringify(body)) : "";
return `${base}:${bodyHash}`;
}
private hashString(input: string): string {
let hash = 0;
for (let i = 0; i < input.length; i++) {
const chr = input.charCodeAt(i);
hash = (hash << 5) - hash + chr;
hash |= 0;
}
return hash.toString();
}
}