offline-first, p2p synced, atproto enabled, feed reader
1import {z} from 'zod/mini'
2
3const StatusCodes: Record<number, string> = {
4 400: 'Bad Request',
5 401: 'Unauthorized',
6 403: 'Forbidden',
7 404: 'Not Found',
8 408: 'Request Timeout',
9 409: 'Conflict',
10 429: 'Too Many Requests',
11 499: 'Client Closed Request',
12 500: 'Internal Server Error',
13}
14
15/** base error options interface */
16export interface BaseErrorOpts {
17 /** the cause of the error */
18 cause?: Error
19}
20
21/**
22 * common base class for skypod errors
23 * only difference is that we explicitly type cause to be Error
24 */
25export class BaseError extends Error {
26 /** the cause of the error */
27 declare cause?: Error
28
29 constructor(message: string, options?: BaseErrorOpts) {
30 super(message, options)
31 if (options?.cause) this.cause = normalizeError(options.cause)
32 }
33}
34
35/** common base class for websocket errors */
36export class ProtocolError extends BaseError {
37 /** the HTTP status code representing this error */
38 status: number
39
40 constructor(message: string, status: number, options?: BaseErrorOpts) {
41 const statusText = StatusCodes[status] || 'Unknown'
42 super(`${status} ${statusText}: ${message}`, options)
43
44 this.name = this.constructor.name
45 this.status = status
46 }
47}
48
49/** check if an error is a protocol error with optional status check */
50export function isProtocolError(e: Error, status?: number): e is ProtocolError {
51 return e instanceof ProtocolError && (status === undefined || e.status == status)
52}
53
54/**
55 * normalizes the given error into a protocol error
56 * passes through input that is already protocol errors.
57 */
58export function normalizeProtocolError(cause: unknown, status = 500): ProtocolError {
59 if (cause instanceof ProtocolError) return cause
60 if (cause instanceof z.core.$ZodError) return new ProtocolError(z.prettifyError(cause), 400, {cause})
61
62 if (cause instanceof Error || cause instanceof DOMException) {
63 if (cause.name === 'TimeoutError') return new ProtocolError('operation timed out', 408, {cause})
64 if (cause.name === 'AbortError') return new ProtocolError('operation was aborted', 499, {cause})
65
66 return new ProtocolError(cause.message, status, {cause})
67 }
68
69 // fallback, unknown
70 const options = cause == undefined ? undefined : {cause: normalizeError(cause)}
71 return new ProtocolError(`Error! ${cause}`, status, options)
72}
73
74/** error wrapper for unknown errors (not an Error?) */
75export class NormalizedError extends BaseError {}
76
77/**
78 * wrap the given failure error into an error
79 * passes through input that is already an Error object.
80 */
81export function normalizeError(failure: unknown): Error {
82 if (failure instanceof Error) return failure
83
84 return new NormalizedError(`unnormalized failure ${failure}`)
85}