Files

120 lines
3.3 KiB
TypeScript

export interface Resource {
attributes: Record<string, unknown>;
id: string;
}
interface ListResponse {
data: Resource[];
meta: { pagination: { total: number; total_pages: number } };
}
const PAGE_LIMIT = 500;
const TRAILING_SLASHES = /\/+$/;
const BODY_PREVIEW = 1500;
const preview = (body: unknown) =>
typeof body === "string" ? body.slice(0, BODY_PREVIEW) : "";
export const createApi = (base: string, token: string) => {
const root = `${base.replace(TRAILING_SLASHES, "")}/api/v1`;
const send = async <T>(
method: string,
path: string,
body?: string | Uint8Array<ArrayBuffer>,
contentType = "application/json"
): Promise<T> => {
const response = await fetch(`${root}${path}`, {
body,
headers: {
Accept: "application/vnd.api+json",
Authorization: `Bearer ${token}`,
"Content-Type": contentType,
},
method,
});
const text = await response.text();
if (!response.ok) {
throw new Error(
`${method} ${path} -> ${response.status}\n${text.slice(0, BODY_PREVIEW)}\nrequest: ${preview(body)}`
);
}
return (text ? JSON.parse(text) : undefined) as T;
};
const json = <T>(method: string, path: string, body?: unknown) =>
send<T>(
method,
path,
body === undefined ? undefined : JSON.stringify(body)
);
const create = async (path: string, body: unknown) =>
(await json<{ data: Resource }>("POST", path, body)).data;
const list = async (path: string, page = 1): Promise<Resource[]> => {
const glue = path.includes("?") ? "&" : "?";
const response = await json<ListResponse>(
"GET",
`${path}${glue}limit=${PAGE_LIMIT}&page=${page}`
);
if (page >= response.meta.pagination.total_pages) {
return response.data;
}
return [...response.data, ...(await list(path, page + 1))];
};
const total = async (path: string) => {
const glue = path.includes("?") ? "&" : "?";
const response = await json<ListResponse>("GET", `${path}${glue}limit=1`);
return response.meta.pagination.total;
};
const upload = (path: string, body: Uint8Array<ArrayBuffer>) =>
send<undefined>("POST", path, body, "application/octet-stream");
return { create, json, list, total, upload };
};
export type Api = ReturnType<typeof createApi>;
const PROGRESS_STEP = 25;
const MS_PER_SECOND = 1000;
export const pool = async <T, R>(
label: string,
items: readonly T[],
limit: number,
task: (item: T) => Promise<R>
): Promise<R[]> => {
const started = performance.now();
const results: R[] = new Array(items.length);
let cursor = 0;
let done = 0;
const report = () => {
const step = process.stdout.isTTY ? PROGRESS_STEP : items.length;
if (done % step === 0 || done === items.length) {
process.stdout.write(`\r ${label} ${done}/${items.length}`);
}
};
const worker = async (): Promise<void> => {
if (cursor >= items.length) {
return;
}
const index = cursor;
cursor += 1;
results[index] = await task(items[index] as T);
done += 1;
report();
await worker();
};
await Promise.all(
Array.from({ length: Math.min(limit, items.length) }, worker)
);
if (items.length > 0) {
const seconds = (performance.now() - started) / MS_PER_SECOND;
process.stdout.write(` (${seconds.toFixed(1)}s)\n`);
}
return results;
};