diff --git a/src/masto/base.ts b/src/masto/base.ts index a0bc4ee..79d9755 100644 --- a/src/masto/base.ts +++ b/src/masto/base.ts @@ -1,19 +1,20 @@ import { createCacheBucket } from "~platform/cache"; +import { MastoHttpError, MastoTimeoutError, MastoUnexpectedError } from "masto"; -export const CACHE_BUCKET_NAME = "mastodon" +export const CACHE_BUCKET_NAME = "mastodon"; export const cacheBucket = /* @__PURE__ */ createCacheBucket(CACHE_BUCKET_NAME); -export function toSmallCamelCase(object: T) :T { +export function toSmallCamelCase(object: T): T { if (!object || typeof object !== "object") { return object; } else if (Array.isArray(object)) { - return object.map(toSmallCamelCase) as T + return object.map(toSmallCamelCase) as T; } const result = {} as Record; for (const k in object) { - const value = toSmallCamelCase(object[k]) + const value = toSmallCamelCase(object[k]); const nk = typeof k === "string" ? k.replace(/_(.)/g, (_, match) => match.toUpperCase()) @@ -23,3 +24,61 @@ export function toSmallCamelCase(object: T) :T { return result as T; } + +function contentTypeOf(headers: Headers) { + const raw = headers.get("Content-Type")?.replace(/\s*;.*$/, ""); + if (!raw) { + return; + } + + return raw; +} + +/** + * Wrpas the error reason into masto's errors: + * {@link MastoHttpError} if the reason is a {@link Response}, + * {@link MastoTimeoutError} if the reason is a timeout error. + * + * @throws If the reason is unexpected, {@link MastoUnexpectedError} will be thrown. + */ +export async function wrapsError(reason: Response): Promise; +export async function wrapsError(reason: { + name: "TimeoutError"; +}): Promise; +export async function wrapsError(reason: T): Promise; + +export async function wrapsError(reason: unknown) { + if (reason instanceof Response) { + const contentType = contentTypeOf(reason.headers); + if (!contentType) { + throw new MastoUnexpectedError( + "The server returned data with an unknown encoding. The server may be down", + ); + } + + const data = await reason.json(); + const { + error: message, + errorDescription, + details, + ...additionalProperties + } = data; + + return new MastoHttpError( + { + statusCode: reason.status, + message, + description: errorDescription, + details, + additionalProperties, + }, + { cause: reason }, + ); + } + + if (reason && (reason as { name?: string }).name === "TimeoutError") { + return new MastoTimeoutError("Request timed out", { cause: reason }); + } + + return reason; +} diff --git a/src/masto/statuses.ts b/src/masto/statuses.ts index 91fdc7a..4da437e 100644 --- a/src/masto/statuses.ts +++ b/src/masto/statuses.ts @@ -1,12 +1,7 @@ import { CachedFetch } from "~platform/cache"; -import { cacheBucket, toSmallCamelCase } from "./base"; -import { - isAccountKey, - type RemoteServer, -} from "../accounts/stores"; -import type { mastodon } from "masto"; - - +import { cacheBucket, toSmallCamelCase, wrapsError } from "./base"; +import { isAccountKey, type RemoteServer } from "../accounts/stores"; +import { type mastodon } from "masto"; export const fetchStatus = /* @__PURE__ */ new CachedFetch( cacheBucket, @@ -23,8 +18,15 @@ export const fetchStatus = /* @__PURE__ */ new CachedFetch( }; }, async (response) => { - return toSmallCamelCase( - await response.json(), - ) as unknown as mastodon.v1.Status; + try { + if (!response.ok) { + throw response; + } + return toSmallCamelCase( + await response.json(), + ) as unknown as mastodon.v1.Status; + } catch (reason) { + throw wrapsError(reason); + } }, );