hoa 0.3.3 → 0.3.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,42 @@
1
+ import { statusTextMapping } from "./utils.mjs";
2
+
3
+ //#region src/lib/http-error.js
4
+ /**
5
+ * @typedef {Object} HttpErrorOptions
6
+ * @property {string} [message] - Custom error message
7
+ * @property {Error} [cause] - The underlying cause of this error
8
+ * @property {boolean} [expose] - Whether to expose the error message to clients (defaults based on status)
9
+ * @property {HeadersInit} [headers] - Additional response headers
10
+ */
11
+ var HttpError = class HttpError extends Error {
12
+ /**
13
+ * Create a new HttpError instance.
14
+ *
15
+ * @param {number} status - HTTP status code (400-599, invalid codes become 500)
16
+ * @param {string|HttpErrorOptions} [message] - Error message or options object
17
+ * @param {HttpErrorOptions} [options] - Additional options when second param is string
18
+ * @throws {TypeError}
19
+ */
20
+ constructor(status, message, options) {
21
+ if (!Number.isInteger(status)) throw new TypeError("status code must be an integer");
22
+ if (status < 400 || status >= 600) status = 500;
23
+ let finalOptions = {};
24
+ if (typeof message === "string") {
25
+ finalOptions.message = message;
26
+ if (options && typeof options === "object") finalOptions = {
27
+ ...finalOptions,
28
+ ...options
29
+ };
30
+ } else if (message && typeof message === "object") finalOptions = message;
31
+ message = finalOptions.message ?? statusTextMapping[status] ?? "Unknown error";
32
+ super(message, { cause: finalOptions.cause });
33
+ this.name = "HttpError";
34
+ this.status = this.statusCode = status;
35
+ this.expose = finalOptions.expose ?? status < 500;
36
+ if (finalOptions.headers) this.headers = Object.fromEntries(new Headers(finalOptions.headers).entries());
37
+ if (Error.captureStackTrace) Error.captureStackTrace(this, HttpError);
38
+ }
39
+ };
40
+
41
+ //#endregion
42
+ export { HttpError as default };
@@ -0,0 +1,207 @@
1
+ //#region src/lib/utils.js
2
+ /**
3
+ * Parse URLSearchParams into a query object, handling multiple values for the same key.
4
+ * When a key appears multiple times, values are collected into an array.
5
+ *
6
+ * @param {URLSearchParams} searchParams - The URLSearchParams object to parse
7
+ * @returns {Record<string, string|string[]>} Query object with string values or arrays for multiple values
8
+ * @public
9
+ */
10
+ function parseSearchParamsToQuery(searchParams) {
11
+ const query = {};
12
+ for (const [key, value] of searchParams) if (query[key] !== void 0) query[key] = [].concat(query[key], value);
13
+ else query[key] = value;
14
+ return query;
15
+ }
16
+ /**
17
+ * Convert a query object to a URL query string.
18
+ * Handles arrays by appending multiple parameters with the same key.
19
+ *
20
+ * @param {Record<string, string|string[]|undefined|null>} query - Query object to stringify
21
+ * @returns {string} URL-encoded query string (without leading '?')
22
+ * @public
23
+ */
24
+ function stringifyQueryToString(query) {
25
+ if (!query) return "";
26
+ const params = new URLSearchParams();
27
+ for (const [key, value] of Object.entries(query)) if (Array.isArray(value)) value.forEach((v) => params.append(key, v ?? ""));
28
+ else params.append(key, value ?? "");
29
+ return params.toString();
30
+ }
31
+ /**
32
+ * Mapping of HTTP status codes to their standard reason phrases.
33
+ *
34
+ * @type {Record<number, string>}
35
+ * @public
36
+ */
37
+ const statusTextMapping = {
38
+ 100: "Continue",
39
+ 101: "Switching Protocols",
40
+ 102: "Processing",
41
+ 103: "Early Hints",
42
+ 200: "OK",
43
+ 201: "Created",
44
+ 202: "Accepted",
45
+ 203: "Non-Authoritative Information",
46
+ 204: "No Content",
47
+ 205: "Reset Content",
48
+ 206: "Partial Content",
49
+ 207: "Multi-Status",
50
+ 208: "Already Reported",
51
+ 226: "IM Used",
52
+ 300: "Multiple Choices",
53
+ 301: "Moved Permanently",
54
+ 302: "Found",
55
+ 303: "See Other",
56
+ 304: "Not Modified",
57
+ 305: "Use Proxy",
58
+ 307: "Temporary Redirect",
59
+ 308: "Permanent Redirect",
60
+ 400: "Bad Request",
61
+ 401: "Unauthorized",
62
+ 402: "Payment Required",
63
+ 403: "Forbidden",
64
+ 404: "Not Found",
65
+ 405: "Method Not Allowed",
66
+ 406: "Not Acceptable",
67
+ 407: "Proxy Authentication Required",
68
+ 408: "Request Timeout",
69
+ 409: "Conflict",
70
+ 410: "Gone",
71
+ 411: "Length Required",
72
+ 412: "Precondition Failed",
73
+ 413: "Payload Too Large",
74
+ 414: "URI Too Long",
75
+ 415: "Unsupported Media Type",
76
+ 416: "Range Not Satisfiable",
77
+ 417: "Expectation Failed",
78
+ 418: "I'm a Teapot",
79
+ 421: "Misdirected Request",
80
+ 422: "Unprocessable Entity",
81
+ 423: "Locked",
82
+ 424: "Failed Dependency",
83
+ 425: "Too Early",
84
+ 426: "Upgrade Required",
85
+ 428: "Precondition Required",
86
+ 429: "Too Many Requests",
87
+ 431: "Request Header Fields Too Large",
88
+ 451: "Unavailable For Legal Reasons",
89
+ 500: "Internal Server Error",
90
+ 501: "Not Implemented",
91
+ 502: "Bad Gateway",
92
+ 503: "Service Unavailable",
93
+ 504: "Gateway Timeout",
94
+ 505: "HTTP Version Not Supported",
95
+ 506: "Variant Also Negotiates",
96
+ 507: "Insufficient Storage",
97
+ 508: "Loop Detected",
98
+ 509: "Bandwidth Limit Exceeded",
99
+ 510: "Not Extended",
100
+ 511: "Network Authentication Required"
101
+ };
102
+ /**
103
+ * Mapping of HTTP status codes that indicate redirects.
104
+ * Used to determine if a response should trigger a redirect.
105
+ *
106
+ * @type {Record<number, boolean>}
107
+ * @readonly
108
+ * @public
109
+ */
110
+ const statusRedirectMapping = {
111
+ 300: true,
112
+ 301: true,
113
+ 302: true,
114
+ 303: true,
115
+ 305: true,
116
+ 307: true,
117
+ 308: true
118
+ };
119
+ /**
120
+ * Mapping of HTTP status codes that should have empty response bodies.
121
+ * These status codes by specification should not include a message body.
122
+ *
123
+ * @type {Record<number, boolean>}
124
+ * @readonly
125
+ * @public
126
+ */
127
+ const statusEmptyMapping = {
128
+ 204: true,
129
+ 205: true,
130
+ 304: true
131
+ };
132
+ /**
133
+ * Mapping of common content type aliases to their full MIME types.
134
+ * Provides convenient shortcuts for setting response content types.
135
+ *
136
+ * @type {Record<string, string>}
137
+ * @readonly
138
+ * @public
139
+ */
140
+ const commonTypeMapping = {
141
+ html: "text/html;charset=UTF-8",
142
+ text: "text/plain;charset=UTF-8",
143
+ xml: "text/xml;charset=UTF-8",
144
+ md: "text/markdown;charset=UTF-8",
145
+ json: "application/json",
146
+ form: "application/x-www-form-urlencoded;charset=UTF-8",
147
+ pdf: "application/pdf",
148
+ zip: "application/zip",
149
+ wasm: "application/wasm",
150
+ webmanifest: "application/manifest+json",
151
+ js: "application/javascript;charset=UTF-8",
152
+ ts: "application/typescript;charset=UTF-8",
153
+ png: "image/png",
154
+ jpg: "image/jpeg",
155
+ jpeg: "image/jpeg",
156
+ gif: "image/gif",
157
+ svg: "image/svg+xml",
158
+ webp: "image/webp",
159
+ avif: "image/avif",
160
+ ico: "image/x-icon",
161
+ mp3: "audio/mpeg",
162
+ wav: "audio/wav",
163
+ ogg: "audio/ogg",
164
+ mp4: "video/mp4",
165
+ webm: "video/webm",
166
+ avi: "video/x-msvideo",
167
+ mov: "video/quicktime",
168
+ woff: "font/woff",
169
+ woff2: "font/woff2",
170
+ ttf: "font/ttf",
171
+ otf: "font/otf",
172
+ bin: "application/octet-stream"
173
+ };
174
+ const ENCODE_CHARS_REGEXP = /(?:[^\x21\x23-\x3B\x3D\x3F-\x5F\x61-\x7A\x7C\x7E]|%(?:[^0-9A-Fa-f]|[0-9A-Fa-f][^0-9A-Fa-f]|$))+/g;
175
+ /**
176
+ * RegExp to match unmatched surrogate pair.
177
+ * @private
178
+ */
179
+ const UNMATCHED_SURROGATE_PAIR_REGEXP = /(^|[^\uD800-\uDBFF])[\uDC00-\uDFFF]|[\uD800-\uDBFF]([^\uDC00-\uDFFF]|$)/g;
180
+ /**
181
+ * String to replace unmatched surrogate pair with.
182
+ * @private
183
+ */
184
+ const UNMATCHED_SURROGATE_PAIR_REPLACE = "$1�$2";
185
+ /**
186
+ * Encode a URL to a percent-encoded form, excluding already-encoded sequences.
187
+ *
188
+ * This function will take an already-encoded URL and encode all the non-URL
189
+ * code points. This function will not encode the "%" character unless it is
190
+ * not part of a valid sequence (`%20` will be left as-is, but `%foo` will
191
+ * be encoded as `%25foo`).
192
+ *
193
+ * This encode is meant to be "safe" and does not throw errors. It will try as
194
+ * hard as it can to properly encode the given URL, including replacing any raw,
195
+ * unpaired surrogate pairs with the Unicode replacement character prior to
196
+ * encoding.
197
+ *
198
+ * @param {string} url - URL string to encode
199
+ * @return {string} Encoded URL string
200
+ * @public
201
+ */
202
+ function encodeUrl(url) {
203
+ return String(url).replace(UNMATCHED_SURROGATE_PAIR_REGEXP, UNMATCHED_SURROGATE_PAIR_REPLACE).replace(ENCODE_CHARS_REGEXP, encodeURI);
204
+ }
205
+
206
+ //#endregion
207
+ export { commonTypeMapping, encodeUrl, parseSearchParamsToQuery, statusEmptyMapping, statusRedirectMapping, statusTextMapping, stringifyQueryToString };