@aries0d0f/fetch-worker 0.1.1

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.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 aries0d0f
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,158 @@
1
+ # fetch-worker
2
+
3
+ [![CI](https://github.com/aries0d0f/fetch-worker/actions/workflows/ci.yml/badge.svg)](https://github.com/aries0d0f/fetch-worker/actions/workflows/ci.yml)
4
+ [![npm](https://img.shields.io/npm/v/%40aries0d0f%2Ffetch-worker?logo=npm)](https://www.npmjs.com/package/@aries0d0f/fetch-worker)
5
+ [![GitHub Packages](https://img.shields.io/badge/package-GitHub%20Packages-blue?logo=github)](https://github.com/aries0d0f/fetch-worker/pkgs/npm/fetch-worker)
6
+ [![license](https://img.shields.io/github/license/aries0d0f/fetch-worker.svg)](./LICENSE)
7
+
8
+ An axios-style HTTP client built entirely on native `fetch` — with requests run
9
+ on a dedicated Web Worker thread, in parallel with your main thread, via
10
+ [comlink](https://github.com/GoogleChromeLabs/comlink). TypeScript-first from
11
+ the ground up: every method, response, and error shape is fully typed and
12
+ generic over your payloads.
13
+
14
+ - **Axios-style ergonomics, zero axios** — `http.get/post/put/patch/delete`,
15
+ typed generics, structured error rejections, and abortable requests — all on
16
+ top of the platform's own `fetch`, with no polyfills or XHR fallback.
17
+ - **Runs in parallel with your app** — request/response handling, JSON/YAML
18
+ parsing, and error normalization all happen on a separate Web Worker thread,
19
+ so they proceed alongside your main thread instead of contending with
20
+ rendering and user input for the same event loop.
21
+ - **TypeScript-first** — request bodies, response payloads, and error shapes
22
+ are generic and inferred end-to-end; there's no `any` in the public API.
23
+ - **Same API everywhere** — the client detects its environment at import time.
24
+ In a browser it spins up a worker automatically; in Node or during SSR
25
+ (no `Worker` global) it calls `fetch` directly on the current thread instead.
26
+ - **No bundler lock-in** — the worker is loaded via the portable
27
+ `new URL('./worker.js', import.meta.url)` pattern, which Vite, Webpack 5+,
28
+ and native ESM all understand. No `?worker` suffix or plugin required.
29
+ - **Small surface area** — two real dependencies: `comlink` (the worker RPC
30
+ transport) and `js-yaml` (for the `.yaml()` response helper).
31
+
32
+ ## Install
33
+
34
+ This package is published to both [npmjs.org](https://www.npmjs.com/package/@aries0d0f/fetch-worker)
35
+ and [GitHub Packages](https://github.com/aries0d0f/fetch-worker/pkgs/npm/fetch-worker).
36
+
37
+ From npmjs.org, no extra setup is needed:
38
+
39
+ ```sh
40
+ bun add @aries0d0f/fetch-worker
41
+ # or: npm install @aries0d0f/fetch-worker / pnpm add @aries0d0f/fetch-worker
42
+ ```
43
+
44
+ To install from GitHub Packages instead, note that it requires authentication
45
+ even for public packages, so point your package manager at the registry with
46
+ a token that has `read:packages` scope first.
47
+
48
+ Add to `.npmrc` in your project (or `~/.npmrc` globally):
49
+
50
+ ```ini
51
+ @aries0d0f:registry=https://npm.pkg.github.com
52
+ //npm.pkg.github.com/:_authToken=${GITHUB_TOKEN}
53
+ ```
54
+
55
+ Then install as usual with the commands above.
56
+
57
+ ## Usage
58
+
59
+ ```ts
60
+ import http from '@aries0d0f/fetch-worker';
61
+
62
+ const res = await http.get<{ id: number; name: string }>('/api/users/1');
63
+ const user = await res.json();
64
+
65
+ await http.post('/api/users', { name: 'Ada' });
66
+ ```
67
+
68
+ ### Aborting a request
69
+
70
+ Pass `controllable: true` to get back the in-flight request alongside its
71
+ `AbortController`:
72
+
73
+ ```ts
74
+ const { request, controller } = await http.get('/api/slow', { controllable: true });
75
+
76
+ setTimeout(() => controller.abort(), 1000);
77
+
78
+ const res = await request;
79
+ ```
80
+
81
+ ### Response helpers
82
+
83
+ Every response exposes the usual `fetch` `Response` members (`status`, `ok`,
84
+ `text()`, `blob()`, `arrayBuffer()`, ...) plus:
85
+
86
+ - `res.json<T>()` — parse the body as JSON
87
+ - `res.yaml<T>()` — parse the body as YAML
88
+ - `res.headers` — a `Headers`-like object whose accessors (`get`, `has`,
89
+ `entries`, `keys`, `values`) return Promises, since `Headers` instances
90
+ themselves can't cross the worker boundary
91
+
92
+ ### Error handling
93
+
94
+ A non-2xx response rejects with a plain object rather than throwing:
95
+
96
+ ```ts
97
+ try {
98
+ await http.get('/api/missing');
99
+ } catch (err) {
100
+ // err: { ok, status, statusText, url, headers, error: { title, status, detail } }
101
+ }
102
+ ```
103
+
104
+ ### Skipping the worker
105
+
106
+ Import the core client directly to always run on the current thread (useful
107
+ in tests, or when you deliberately don't want a worker):
108
+
109
+ ```ts
110
+ import { http } from '@aries0d0f/fetch-worker/core';
111
+ ```
112
+
113
+ ### Framework-reactive values
114
+
115
+ If you pass a reactive object (e.g. a Vue `ref`/`reactive` value) as a request
116
+ body or options, it's unwrapped to its plain, cloneable form before being sent
117
+ across the worker boundary — no extra step required on your end.
118
+
119
+ ## API
120
+
121
+ | Method | Signature |
122
+ | --------------------------------- | ---------------------------------------------- |
123
+ | `http.get(url, options?)` | `GET` request |
124
+ | `http.head(url, options?)` | `HEAD` request |
125
+ | `http.post(url, body, options?)` | `POST` request |
126
+ | `http.put(url, body, options?)` | `PUT` request |
127
+ | `http.patch(url, body, options?)` | `PATCH` request |
128
+ | `http.delete(url, options?)` | `DELETE` request |
129
+ | `http.request(method, url, ...)` | Low-level entry point used by all of the above |
130
+
131
+ `options` extends `RequestInit` (minus `headers`/`body`/`signal`, which are
132
+ handled specially) with one addition: `controllable?: boolean`.
133
+
134
+ ## Why a worker?
135
+
136
+ Parsing large JSON/YAML payloads, and the general bookkeeping `fetch` does
137
+ around headers/body streams, all run on whichever thread calls it. Moving that
138
+ work onto a dedicated worker thread lets it run in parallel with your main
139
+ thread instead of on it — keeping the main thread free for rendering and user
140
+ input, particularly useful in UI-heavy apps making frequent or large requests.
141
+
142
+ ## Development
143
+
144
+ ```sh
145
+ bun install
146
+ bun run build # bundle with tsup (ESM output + .d.ts)
147
+ bun run test # vitest + msw
148
+ bun run lint # eslint
149
+ bun run typecheck # tsc --noEmit
150
+ ```
151
+
152
+ This repo uses [Changesets](https://github.com/changesets/changesets) for
153
+ versioning. Run `bun run changeset` alongside your PR to describe the change;
154
+ CI opens a "Version Packages" PR and publishes to GitHub Packages on merge.
155
+
156
+ ## License
157
+
158
+ [MIT](./LICENSE)
package/dist/core.d.ts ADDED
@@ -0,0 +1,70 @@
1
+ type _Response = Response;
2
+ type _Error = Error;
3
+ /**
4
+ * A structured-clone-safe stand-in for the `Headers` API.
5
+ *
6
+ * `Headers` instances cannot cross a worker `postMessage` boundary, so responses
7
+ * expose this instead. It mirrors the read side of `Headers` (`get`, `has`,
8
+ * `entries`, `keys`, `values`, `forEach`) but every accessor returns a `Promise`
9
+ * so it also works correctly when proxied across a worker via comlink.
10
+ */
11
+ declare class SerializableHeaders {
12
+ private headers;
13
+ constructor(headers: Headers | Record<string, string>);
14
+ get(name: string): Promise<string | null>;
15
+ has(name: string): Promise<boolean>;
16
+ entries(): Promise<IterableIterator<[string, string]>>;
17
+ keys(): Promise<IterableIterator<string>>;
18
+ values(): Promise<IterableIterator<string>>;
19
+ forEach(callback: (value: string, key: string) => void): void;
20
+ }
21
+ declare namespace HTTP {
22
+ type Method = 'HEAD' | 'GET' | 'POST' | 'PUT' | 'DELETE' | 'PATCH';
23
+ type Response<T> = Omit<_Response, 'json' | 'headers'> & {
24
+ json(): Promise<T>;
25
+ yaml(): Promise<T>;
26
+ headers: SerializableHeaders;
27
+ };
28
+ type Operator = typeof http;
29
+ type Options<Controllable extends boolean> = Omit<RequestInit, 'headers' | 'body' | 'signal'> & {
30
+ controllable?: Controllable;
31
+ headers?: HeadersInit;
32
+ };
33
+ type Payload<Context, Controllable extends boolean> = Controllable extends true ? Promise<{
34
+ request: Promise<HTTP.Response<Context>>;
35
+ controller: HTTP.Controller;
36
+ }> : Promise<HTTP.Response<Context>>;
37
+ type Exception = Pick<_Response, 'ok' | 'redirected' | 'status' | 'statusText' | 'type' | 'url'> & {
38
+ headers: Record<string, string>;
39
+ error: Error;
40
+ };
41
+ type Error = {
42
+ title: string;
43
+ status: number;
44
+ detail: string;
45
+ error?: _Error;
46
+ };
47
+ type Controller = AbortController;
48
+ }
49
+ /**
50
+ * The core HTTP operator: a thin, typed wrapper around native `fetch`.
51
+ *
52
+ * This module has no worker/comlink transport wiring of its own — it is safe to
53
+ * import directly on the main thread, in Node/SSR, or inside a worker. The
54
+ * worker transport built on top of this lives in `fetch-worker/worker` and the
55
+ * environment-aware client lives in `fetch-worker` (the package root).
56
+ */
57
+ declare const http: {
58
+ request: {
59
+ <Context, Controllable extends boolean, Method = "HEAD" | "GET" | "DELETE">(method: Method, url: string, args_0: HTTP.Options<Controllable> | undefined): HTTP.Payload<Context, Controllable>;
60
+ <Context, Payload, Controllable extends boolean, Method = "POST" | "PUT" | "PATCH">(method: Method, url: string, args_0: Payload, args_1: HTTP.Options<Controllable> | undefined): HTTP.Payload<Context, Controllable>;
61
+ };
62
+ head: <Context, Controllable extends boolean = false>(url: string, options?: HTTP.Options<Controllable>) => HTTP.Payload<Context, Controllable>;
63
+ get: <Context, Controllable extends boolean = false>(url: string, options?: HTTP.Options<Controllable>) => HTTP.Payload<Context, Controllable>;
64
+ post: <Context, Payload, Controllable extends boolean = false>(url: string, body: Payload, options?: HTTP.Options<Controllable>) => HTTP.Payload<Context, Controllable>;
65
+ put: <Context, Payload, Controllable extends boolean = false>(url: string, body: Payload, options?: HTTP.Options<Controllable>) => HTTP.Payload<Context, Controllable>;
66
+ delete: <Controllable extends boolean = false>(url: string, options?: HTTP.Options<Controllable>) => HTTP.Payload<void, Controllable>;
67
+ patch: <Context, Payload, Controllable extends boolean = false>(url: string, body: Payload, options?: HTTP.Options<Controllable>) => HTTP.Payload<Context, Controllable>;
68
+ };
69
+
70
+ export { HTTP, http };
package/dist/core.js ADDED
@@ -0,0 +1,125 @@
1
+ // src/core.ts
2
+ import { proxy } from "comlink";
3
+ import { load } from "js-yaml";
4
+ var SerializableHeaders = class {
5
+ headers;
6
+ constructor(headers) {
7
+ if (headers instanceof Headers) {
8
+ this.headers = Object.fromEntries(
9
+ Array.from(headers.entries()).map(([key, value]) => [key.toLowerCase(), value])
10
+ );
11
+ } else {
12
+ this.headers = Object.fromEntries(
13
+ Object.entries(headers).map(([key, value]) => [key.toLowerCase(), value])
14
+ );
15
+ }
16
+ }
17
+ get(name) {
18
+ return Promise.resolve(this.headers[name.toLowerCase()] ?? null);
19
+ }
20
+ has(name) {
21
+ return Promise.resolve(name.toLowerCase() in this.headers);
22
+ }
23
+ entries() {
24
+ return Promise.resolve(Object.entries(this.headers)[Symbol.iterator]());
25
+ }
26
+ keys() {
27
+ return Promise.resolve(Object.keys(this.headers)[Symbol.iterator]());
28
+ }
29
+ values() {
30
+ return Promise.resolve(Object.values(this.headers)[Symbol.iterator]());
31
+ }
32
+ forEach(callback) {
33
+ Object.entries(this.headers).forEach(([key, value]) => callback(value, key));
34
+ }
35
+ };
36
+ var defaultOptions = {
37
+ credentials: "same-origin",
38
+ cache: "default",
39
+ headers: {
40
+ "Content-Type": "application/json"
41
+ }
42
+ };
43
+ var requestHandler = (method, url, ...args) => {
44
+ const body = ["HEAD", "GET", "DELETE"].includes(method) ? null : args[0];
45
+ const options = ["HEAD", "GET", "DELETE"].includes(method) ? args[0] : args[1];
46
+ const controller = new AbortController();
47
+ const { signal } = controller;
48
+ const isFileListBody = body?.__proto__?.[Symbol.toStringTag] === "FileList";
49
+ const request = fetch(url, {
50
+ method,
51
+ ...body ? {
52
+ body: isFileListBody ? [...body].reduce((payload, file) => {
53
+ payload.append("file", file, `${file.webkitRelativePath || file.name}`);
54
+ return payload;
55
+ }, new FormData()) : typeof body === "object" ? JSON.stringify(body) : String(body)
56
+ } : null,
57
+ ...defaultOptions,
58
+ ...options,
59
+ headers: isFileListBody ? new Headers(options?.headers) : new Headers({ ...defaultOptions.headers, ...options?.headers }),
60
+ signal
61
+ }).then(async (res) => {
62
+ if (!res.ok) {
63
+ let error;
64
+ try {
65
+ error = await res.clone().json();
66
+ } catch (err) {
67
+ error = {
68
+ title: `${res.status === 401 ? "" : "Unknown Error: "}${res.statusText}`,
69
+ status: res.status,
70
+ detail: await res.clone().text() ?? String(err),
71
+ error: new Error(await res.clone().text() ?? String(err))
72
+ };
73
+ }
74
+ return Promise.reject({
75
+ ok: res.ok,
76
+ redirected: res.redirected,
77
+ status: res.status,
78
+ statusText: res.statusText,
79
+ type: res.type,
80
+ url: res.url,
81
+ headers: Object.fromEntries(res.headers.entries()),
82
+ error
83
+ });
84
+ }
85
+ return res.clone();
86
+ }).then(async (res) => {
87
+ const rawBody = await res.clone().text();
88
+ const headers = new SerializableHeaders(res.headers);
89
+ const response = {
90
+ body: res.body,
91
+ clone: res.clone,
92
+ bodyUsed: res.bodyUsed,
93
+ arrayBuffer: () => Promise.resolve(res.arrayBuffer()),
94
+ bytes: () => Promise.resolve(res.bytes()),
95
+ blob: () => Promise.resolve(res.blob()),
96
+ formData: () => Promise.resolve(res.formData()),
97
+ text: () => Promise.resolve(rawBody),
98
+ ok: res.ok,
99
+ redirected: res.redirected,
100
+ status: res.status,
101
+ statusText: res.statusText,
102
+ type: res.type,
103
+ url: res.url,
104
+ headers,
105
+ // Convenience methods that simply return the already-parsed data
106
+ json: async () => Promise.resolve(JSON.parse(rawBody)),
107
+ yaml: async () => Promise.resolve(load(rawBody))
108
+ };
109
+ return proxy(response);
110
+ });
111
+ return options?.controllable ? proxy({ request, controller }) : request;
112
+ };
113
+ var http = {
114
+ request: requestHandler,
115
+ head: (url, options) => requestHandler("HEAD", url, options),
116
+ get: (url, options) => requestHandler("GET", url, options),
117
+ post: (url, body, options) => requestHandler("POST", url, body, options),
118
+ put: (url, body, options) => requestHandler("PUT", url, body, options),
119
+ delete: (url, options) => requestHandler("DELETE", url, options),
120
+ patch: (url, body, options) => requestHandler("PATCH", url, body, options)
121
+ };
122
+ export {
123
+ http
124
+ };
125
+ //# sourceMappingURL=core.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/core.ts"],"sourcesContent":["/// <reference lib=\"webworker\" />\n\nimport { proxy } from 'comlink';\nimport { load } from 'js-yaml';\n\ntype _Response = Response;\ntype _Error = Error;\n\n/**\n * A structured-clone-safe stand-in for the `Headers` API.\n *\n * `Headers` instances cannot cross a worker `postMessage` boundary, so responses\n * expose this instead. It mirrors the read side of `Headers` (`get`, `has`,\n * `entries`, `keys`, `values`, `forEach`) but every accessor returns a `Promise`\n * so it also works correctly when proxied across a worker via comlink.\n */\nclass SerializableHeaders {\n private headers: Record<string, string>;\n\n constructor(headers: Headers | Record<string, string>) {\n if (headers instanceof Headers) {\n this.headers = Object.fromEntries(\n Array.from(headers.entries()).map(([key, value]) => [key.toLowerCase(), value])\n );\n } else {\n this.headers = Object.fromEntries(\n Object.entries(headers).map(([key, value]) => [key.toLowerCase(), value])\n );\n }\n }\n\n get(name: string): Promise<string | null> {\n return Promise.resolve(this.headers[name.toLowerCase()] ?? null);\n }\n\n has(name: string): Promise<boolean> {\n return Promise.resolve(name.toLowerCase() in this.headers);\n }\n\n entries(): Promise<IterableIterator<[string, string]>> {\n return Promise.resolve(Object.entries(this.headers)[Symbol.iterator]());\n }\n\n keys(): Promise<IterableIterator<string>> {\n return Promise.resolve(Object.keys(this.headers)[Symbol.iterator]());\n }\n\n values(): Promise<IterableIterator<string>> {\n return Promise.resolve(Object.values(this.headers)[Symbol.iterator]());\n }\n\n forEach(callback: (value: string, key: string) => void): void {\n Object.entries(this.headers).forEach(([key, value]) => callback(value, key));\n }\n}\n\nexport namespace HTTP {\n export type Method = 'HEAD' | 'GET' | 'POST' | 'PUT' | 'DELETE' | 'PATCH';\n export type Response<T> = Omit<_Response, 'json' | 'headers'> & {\n json(): Promise<T>;\n yaml(): Promise<T>;\n headers: SerializableHeaders;\n };\n export type Operator = typeof http;\n export type Options<Controllable extends boolean> = Omit<\n RequestInit,\n 'headers' | 'body' | 'signal'\n > & {\n controllable?: Controllable;\n headers?: HeadersInit;\n };\n export type Payload<Context, Controllable extends boolean> = Controllable extends true\n ? Promise<{\n request: Promise<HTTP.Response<Context>>;\n controller: HTTP.Controller;\n }>\n : Promise<HTTP.Response<Context>>;\n export type Exception = Pick<\n _Response,\n 'ok' | 'redirected' | 'status' | 'statusText' | 'type' | 'url'\n > & {\n headers: Record<string, string>;\n error: Error;\n };\n export type Error = {\n title: string;\n status: number;\n detail: string;\n error?: _Error;\n };\n export type Controller = AbortController;\n}\n\nconst defaultOptions: HTTP.Options<false> = {\n credentials: 'same-origin',\n cache: 'default',\n headers: {\n 'Content-Type': 'application/json'\n }\n};\n\nconst requestHandler: {\n <Context, Controllable extends boolean, Method = Extract<HTTP.Method, 'HEAD' | 'GET' | 'DELETE'>>(\n method: Method,\n url: string,\n ...args: [HTTP.Options<Controllable> | undefined]\n ): HTTP.Payload<Context, Controllable>;\n <\n Context,\n Payload,\n Controllable extends boolean,\n Method = Exclude<HTTP.Method, 'HEAD' | 'GET' | 'DELETE'>\n >(\n method: Method,\n url: string,\n ...args: [Payload, HTTP.Options<Controllable> | undefined]\n ): HTTP.Payload<Context, Controllable>;\n} = <Context, Payload, Controllable extends boolean, Method = HTTP.Method>(\n method: HTTP.Method,\n url: string,\n ...args: Method extends 'HEAD' | 'GET' | 'DELETE'\n ? [HTTP.Options<Controllable> | undefined]\n : [Payload, HTTP.Options<Controllable> | undefined]\n) => {\n const body = (['HEAD', 'GET', 'DELETE'].includes(method) ? null : args[0]) as Payload;\n const options = (\n ['HEAD', 'GET', 'DELETE'].includes(method) ? args[0] : args[1]\n ) as HTTP.Options<Controllable>;\n\n const controller = new AbortController();\n const { signal } = controller;\n // eslint-disable-next-line @typescript-eslint/ban-ts-comment\n // @ts-ignore: FileList is not defined in the global scope\n // ? NOTE: FileList JSDOM type cannot work with MSW request mocker, use workaround to do instance type check via prototype chain\n // ? This is equal to `body instanceof FileList`\n const isFileListBody = body?.__proto__?.[Symbol.toStringTag] === 'FileList';\n\n const request = fetch(url, {\n method,\n ...(body\n ? {\n body: isFileListBody\n ? [...(body as unknown as FileList)].reduce((payload, file) => {\n payload.append('file', file, `${file.webkitRelativePath || file.name}`);\n return payload;\n }, new FormData())\n : typeof body === 'object'\n ? JSON.stringify(body)\n : String(body)\n }\n : null),\n ...defaultOptions,\n ...options,\n headers: isFileListBody\n ? new Headers(options?.headers)\n : new Headers({ ...defaultOptions.headers, ...options?.headers }),\n signal\n })\n .then(async (res: Omit<HTTP.Response<Context>, 'yaml' | 'headers'> & { headers: Headers }) => {\n if (!res.ok) {\n let error: HTTP.Error;\n\n try {\n error = await res.clone().json();\n } catch (err) {\n error = {\n title: `${res.status === 401 ? '' : 'Unknown Error: '}${res.statusText}`,\n status: res.status,\n detail: (await res.clone().text()) ?? String(err),\n error: new Error((await res.clone().text()) ?? String(err))\n };\n }\n\n return Promise.reject({\n ok: res.ok,\n redirected: res.redirected,\n status: res.status,\n statusText: res.statusText,\n type: res.type,\n url: res.url,\n headers: Object.fromEntries(res.headers.entries()),\n error\n } satisfies HTTP.Exception);\n }\n\n return res.clone();\n })\n .then(async (res: _Response) => {\n const rawBody = await res.clone().text();\n // Create a serializable headers object for cross-worker transfer\n const headers = new SerializableHeaders(res.headers);\n\n // Return a plain object with response metadata and parsed data\n // Include convenience methods for backward compatibility\n // Use proxy() to ensure methods are callable across worker boundary\n const response: HTTP.Response<Context> = {\n body: res.body,\n clone: res.clone,\n bodyUsed: res.bodyUsed,\n arrayBuffer: () => Promise.resolve(res.arrayBuffer()),\n bytes: () => Promise.resolve(res.bytes()),\n blob: () => Promise.resolve(res.blob()),\n formData: () => Promise.resolve(res.formData()),\n text: () => Promise.resolve(rawBody),\n ok: res.ok,\n redirected: res.redirected,\n status: res.status,\n statusText: res.statusText,\n type: res.type,\n url: res.url,\n headers,\n // Convenience methods that simply return the already-parsed data\n json: async () => Promise.resolve(JSON.parse(rawBody) as Context),\n yaml: async () => Promise.resolve(load(rawBody) as Context)\n };\n\n return proxy(response);\n });\n\n return options?.controllable\n ? proxy({ request, controller } as unknown as HTTP.Payload<Context, true>) // Comlink will convert proxied to a Promise\n : (request satisfies HTTP.Payload<Context, false>);\n};\n\n/**\n * The core HTTP operator: a thin, typed wrapper around native `fetch`.\n *\n * This module has no worker/comlink transport wiring of its own — it is safe to\n * import directly on the main thread, in Node/SSR, or inside a worker. The\n * worker transport built on top of this lives in `fetch-worker/worker` and the\n * environment-aware client lives in `fetch-worker` (the package root).\n */\nexport const http = {\n request: requestHandler,\n head: <Context, Controllable extends boolean = false>(\n url: string,\n options?: HTTP.Options<Controllable>\n ) => requestHandler<Context, Controllable, 'HEAD'>('HEAD', url, options),\n get: <Context, Controllable extends boolean = false>(\n url: string,\n options?: HTTP.Options<Controllable>\n ) => requestHandler<Context, Controllable, 'GET'>('GET', url, options),\n post: <Context, Payload, Controllable extends boolean = false>(\n url: string,\n body: Payload,\n options?: HTTP.Options<Controllable>\n ) => requestHandler<Context, Payload, Controllable, 'POST'>('POST', url, body, options),\n put: <Context, Payload, Controllable extends boolean = false>(\n url: string,\n body: Payload,\n options?: HTTP.Options<Controllable>\n ) => requestHandler<Context, Payload, Controllable, 'PUT'>('PUT', url, body, options),\n delete: <Controllable extends boolean = false>(\n url: string,\n options?: HTTP.Options<Controllable>\n ) => requestHandler<void, Controllable, 'DELETE'>('DELETE', url, options),\n patch: <Context, Payload, Controllable extends boolean = false>(\n url: string,\n body: Payload,\n options?: HTTP.Options<Controllable>\n ) => requestHandler<Context, Payload, Controllable, 'PATCH'>('PATCH', url, body, options)\n};\n"],"mappings":"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","names":[]}
@@ -0,0 +1,21 @@
1
+ import { HTTP } from './core.js';
2
+ export { http as core } from './core.js';
3
+
4
+ /**
5
+ * Normalizes a value so it is safely transferable via `postMessage`'s
6
+ * structured clone algorithm: strips reactive Proxy wrappers (Vue, and
7
+ * anything else that follows the same `__v_raw` convention) and converts
8
+ * `Headers` instances to plain objects.
9
+ */
10
+ declare const toCloneable: <T>(value: T) => T;
11
+ /**
12
+ * The environment-aware HTTP client.
13
+ *
14
+ * In any environment with a `Worker` global (browsers), requests are
15
+ * transparently offloaded to a Web Worker via comlink. In environments without
16
+ * one (Node, SSR), it falls back to calling `fetch` directly on the current
17
+ * thread — same API either way.
18
+ */
19
+ declare const http: HTTP.Operator;
20
+
21
+ export { HTTP, http as default, toCloneable };
package/dist/index.js ADDED
@@ -0,0 +1,178 @@
1
+ // src/index.ts
2
+ import { wrap } from "comlink";
3
+
4
+ // src/core.ts
5
+ import { proxy } from "comlink";
6
+ import { load } from "js-yaml";
7
+ var SerializableHeaders = class {
8
+ headers;
9
+ constructor(headers) {
10
+ if (headers instanceof Headers) {
11
+ this.headers = Object.fromEntries(
12
+ Array.from(headers.entries()).map(([key, value]) => [key.toLowerCase(), value])
13
+ );
14
+ } else {
15
+ this.headers = Object.fromEntries(
16
+ Object.entries(headers).map(([key, value]) => [key.toLowerCase(), value])
17
+ );
18
+ }
19
+ }
20
+ get(name) {
21
+ return Promise.resolve(this.headers[name.toLowerCase()] ?? null);
22
+ }
23
+ has(name) {
24
+ return Promise.resolve(name.toLowerCase() in this.headers);
25
+ }
26
+ entries() {
27
+ return Promise.resolve(Object.entries(this.headers)[Symbol.iterator]());
28
+ }
29
+ keys() {
30
+ return Promise.resolve(Object.keys(this.headers)[Symbol.iterator]());
31
+ }
32
+ values() {
33
+ return Promise.resolve(Object.values(this.headers)[Symbol.iterator]());
34
+ }
35
+ forEach(callback) {
36
+ Object.entries(this.headers).forEach(([key, value]) => callback(value, key));
37
+ }
38
+ };
39
+ var defaultOptions = {
40
+ credentials: "same-origin",
41
+ cache: "default",
42
+ headers: {
43
+ "Content-Type": "application/json"
44
+ }
45
+ };
46
+ var requestHandler = (method, url, ...args) => {
47
+ const body = ["HEAD", "GET", "DELETE"].includes(method) ? null : args[0];
48
+ const options = ["HEAD", "GET", "DELETE"].includes(method) ? args[0] : args[1];
49
+ const controller = new AbortController();
50
+ const { signal } = controller;
51
+ const isFileListBody = body?.__proto__?.[Symbol.toStringTag] === "FileList";
52
+ const request = fetch(url, {
53
+ method,
54
+ ...body ? {
55
+ body: isFileListBody ? [...body].reduce((payload, file) => {
56
+ payload.append("file", file, `${file.webkitRelativePath || file.name}`);
57
+ return payload;
58
+ }, new FormData()) : typeof body === "object" ? JSON.stringify(body) : String(body)
59
+ } : null,
60
+ ...defaultOptions,
61
+ ...options,
62
+ headers: isFileListBody ? new Headers(options?.headers) : new Headers({ ...defaultOptions.headers, ...options?.headers }),
63
+ signal
64
+ }).then(async (res) => {
65
+ if (!res.ok) {
66
+ let error;
67
+ try {
68
+ error = await res.clone().json();
69
+ } catch (err) {
70
+ error = {
71
+ title: `${res.status === 401 ? "" : "Unknown Error: "}${res.statusText}`,
72
+ status: res.status,
73
+ detail: await res.clone().text() ?? String(err),
74
+ error: new Error(await res.clone().text() ?? String(err))
75
+ };
76
+ }
77
+ return Promise.reject({
78
+ ok: res.ok,
79
+ redirected: res.redirected,
80
+ status: res.status,
81
+ statusText: res.statusText,
82
+ type: res.type,
83
+ url: res.url,
84
+ headers: Object.fromEntries(res.headers.entries()),
85
+ error
86
+ });
87
+ }
88
+ return res.clone();
89
+ }).then(async (res) => {
90
+ const rawBody = await res.clone().text();
91
+ const headers = new SerializableHeaders(res.headers);
92
+ const response = {
93
+ body: res.body,
94
+ clone: res.clone,
95
+ bodyUsed: res.bodyUsed,
96
+ arrayBuffer: () => Promise.resolve(res.arrayBuffer()),
97
+ bytes: () => Promise.resolve(res.bytes()),
98
+ blob: () => Promise.resolve(res.blob()),
99
+ formData: () => Promise.resolve(res.formData()),
100
+ text: () => Promise.resolve(rawBody),
101
+ ok: res.ok,
102
+ redirected: res.redirected,
103
+ status: res.status,
104
+ statusText: res.statusText,
105
+ type: res.type,
106
+ url: res.url,
107
+ headers,
108
+ // Convenience methods that simply return the already-parsed data
109
+ json: async () => Promise.resolve(JSON.parse(rawBody)),
110
+ yaml: async () => Promise.resolve(load(rawBody))
111
+ };
112
+ return proxy(response);
113
+ });
114
+ return options?.controllable ? proxy({ request, controller }) : request;
115
+ };
116
+ var http = {
117
+ request: requestHandler,
118
+ head: (url, options) => requestHandler("HEAD", url, options),
119
+ get: (url, options) => requestHandler("GET", url, options),
120
+ post: (url, body, options) => requestHandler("POST", url, body, options),
121
+ put: (url, body, options) => requestHandler("PUT", url, body, options),
122
+ delete: (url, options) => requestHandler("DELETE", url, options),
123
+ patch: (url, body, options) => requestHandler("PATCH", url, body, options)
124
+ };
125
+
126
+ // src/index.ts
127
+ var unwrapReactive = (value) => {
128
+ const raw = value?.__v_raw;
129
+ return raw !== void 0 ? raw : value;
130
+ };
131
+ var toCloneable = (value) => {
132
+ if (value === null || value === void 0 || typeof value !== "object") return value;
133
+ const raw = unwrapReactive(value);
134
+ if (raw?.__proto__?.[Symbol.toStringTag] === "FileList") return raw;
135
+ if (raw instanceof Headers) {
136
+ return Object.fromEntries(raw.entries());
137
+ }
138
+ if (raw instanceof Date || raw instanceof RegExp || raw instanceof ArrayBuffer || ArrayBuffer.isView(raw)) {
139
+ return raw;
140
+ }
141
+ if (Array.isArray(raw)) {
142
+ return raw.map(toCloneable);
143
+ }
144
+ const result = {};
145
+ for (const key of Object.keys(raw)) {
146
+ result[key] = toCloneable(raw[key]);
147
+ }
148
+ return result;
149
+ };
150
+ var createWorkerClient = () => {
151
+ const worker = wrap(
152
+ new Worker(new URL("./worker.js", import.meta.url), { type: "module" })
153
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
154
+ );
155
+ return {
156
+ // request() has overloaded signatures; method (index 0) and url (index 1)
157
+ // are strings and don't need normalization.
158
+ request: ((...args) => worker.request(
159
+ args[0],
160
+ args[1],
161
+ ...args.slice(2).map(toCloneable)
162
+ )),
163
+ head: (url, options) => worker.head(url, toCloneable(options)),
164
+ get: (url, options) => worker.get(url, toCloneable(options)),
165
+ post: (url, body, options) => worker.post(url, toCloneable(body), toCloneable(options)),
166
+ put: (url, body, options) => worker.put(url, toCloneable(body), toCloneable(options)),
167
+ delete: (url, options) => worker.delete(url, toCloneable(options)),
168
+ patch: (url, body, options) => worker.patch(url, toCloneable(body), toCloneable(options))
169
+ };
170
+ };
171
+ var http2 = typeof Worker === "undefined" ? http : createWorkerClient();
172
+ var src_default = http2;
173
+ export {
174
+ http as core,
175
+ src_default as default,
176
+ toCloneable
177
+ };
178
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/index.ts","../src/core.ts"],"sourcesContent":["import { wrap } from 'comlink';\n\nimport { http as core } from './core';\n\nimport type { HTTP } from './core';\n\n/**\n * Duck-types Vue 3's reactive Proxy marker instead of importing `vue` as a hard\n * dependency. Vue exposes this same check publicly as `toRaw()`; reading the\n * `__v_raw` marker directly gets identical behavior without requiring\n * consumers who aren't using Vue to install it.\n */\nconst unwrapReactive = <T>(value: T): T => {\n const raw = (value as { __v_raw?: T })?.__v_raw;\n return raw !== undefined ? raw : value;\n};\n\n/**\n * Normalizes a value so it is safely transferable via `postMessage`'s\n * structured clone algorithm: strips reactive Proxy wrappers (Vue, and\n * anything else that follows the same `__v_raw` convention) and converts\n * `Headers` instances to plain objects.\n */\nconst toCloneable = <T>(value: T): T => {\n if (value === null || value === undefined || typeof value !== 'object') return value;\n\n const raw = unwrapReactive(value as object) as T;\n\n // FileList: not structured-cloneable; passed through (the worker handles it via FormData)\n // Uses __proto__ pattern to match core.ts's FileList detection for JSDOM compatibility\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n if ((raw as any)?.__proto__?.[Symbol.toStringTag] === 'FileList') return raw;\n\n // Headers: never structured-cloneable; convert to plain Record<string, string>\n if (raw instanceof Headers) {\n return Object.fromEntries((raw as Headers).entries()) as unknown as T;\n }\n\n // Structured-cloneable built-ins: pass through without modification\n if (\n raw instanceof Date ||\n raw instanceof RegExp ||\n raw instanceof ArrayBuffer ||\n ArrayBuffer.isView(raw)\n ) {\n return raw;\n }\n\n // Array: recursively normalize each element\n if (Array.isArray(raw)) {\n return (raw as unknown[]).map(toCloneable) as unknown as T;\n }\n\n // Plain object: copy own enumerable keys, recursively normalizing values\n const result: Record<string, unknown> = {};\n for (const key of Object.keys(raw as object)) {\n result[key] = toCloneable((raw as Record<string, unknown>)[key]);\n }\n return result as unknown as T;\n};\n\nconst createWorkerClient = (): HTTP.Operator => {\n // `new URL('./worker.js', import.meta.url)` is the portable worker-bundling\n // pattern understood by Vite, Webpack 5+, and native ESM — no bundler-specific\n // import syntax (e.g. Vite's `?worker`) required.\n const worker = wrap<HTTP.Operator>(\n new Worker(new URL('./worker.js', import.meta.url), { type: 'module' })\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n ) as any;\n\n return {\n // request() has overloaded signatures; method (index 0) and url (index 1)\n // are strings and don't need normalization.\n request: ((...args: [string, string, ...unknown[]]) =>\n worker.request(\n args[0],\n args[1],\n ...args.slice(2).map(toCloneable)\n )) as unknown as HTTP.Operator['request'],\n head: (url: string, options?: HTTP.Options<boolean>) => worker.head(url, toCloneable(options)),\n get: (url: string, options?: HTTP.Options<boolean>) => worker.get(url, toCloneable(options)),\n post: (url: string, body: unknown, options?: HTTP.Options<boolean>) =>\n worker.post(url, toCloneable(body), toCloneable(options)),\n put: (url: string, body: unknown, options?: HTTP.Options<boolean>) =>\n worker.put(url, toCloneable(body), toCloneable(options)),\n delete: (url: string, options?: HTTP.Options<boolean>) =>\n worker.delete(url, toCloneable(options)),\n patch: (url: string, body: unknown, options?: HTTP.Options<boolean>) =>\n worker.patch(url, toCloneable(body), toCloneable(options))\n } as unknown as HTTP.Operator;\n};\n\n/**\n * The environment-aware HTTP client.\n *\n * In any environment with a `Worker` global (browsers), requests are\n * transparently offloaded to a Web Worker via comlink. In environments without\n * one (Node, SSR), it falls back to calling `fetch` directly on the current\n * thread — same API either way.\n */\nconst http: HTTP.Operator = typeof Worker === 'undefined' ? core : createWorkerClient();\n\nexport default http;\nexport { core, toCloneable };\nexport type { HTTP };\n","/// <reference lib=\"webworker\" />\n\nimport { proxy } from 'comlink';\nimport { load } from 'js-yaml';\n\ntype _Response = Response;\ntype _Error = Error;\n\n/**\n * A structured-clone-safe stand-in for the `Headers` API.\n *\n * `Headers` instances cannot cross a worker `postMessage` boundary, so responses\n * expose this instead. It mirrors the read side of `Headers` (`get`, `has`,\n * `entries`, `keys`, `values`, `forEach`) but every accessor returns a `Promise`\n * so it also works correctly when proxied across a worker via comlink.\n */\nclass SerializableHeaders {\n private headers: Record<string, string>;\n\n constructor(headers: Headers | Record<string, string>) {\n if (headers instanceof Headers) {\n this.headers = Object.fromEntries(\n Array.from(headers.entries()).map(([key, value]) => [key.toLowerCase(), value])\n );\n } else {\n this.headers = Object.fromEntries(\n Object.entries(headers).map(([key, value]) => [key.toLowerCase(), value])\n );\n }\n }\n\n get(name: string): Promise<string | null> {\n return Promise.resolve(this.headers[name.toLowerCase()] ?? null);\n }\n\n has(name: string): Promise<boolean> {\n return Promise.resolve(name.toLowerCase() in this.headers);\n }\n\n entries(): Promise<IterableIterator<[string, string]>> {\n return Promise.resolve(Object.entries(this.headers)[Symbol.iterator]());\n }\n\n keys(): Promise<IterableIterator<string>> {\n return Promise.resolve(Object.keys(this.headers)[Symbol.iterator]());\n }\n\n values(): Promise<IterableIterator<string>> {\n return Promise.resolve(Object.values(this.headers)[Symbol.iterator]());\n }\n\n forEach(callback: (value: string, key: string) => void): void {\n Object.entries(this.headers).forEach(([key, value]) => callback(value, key));\n }\n}\n\nexport namespace HTTP {\n export type Method = 'HEAD' | 'GET' | 'POST' | 'PUT' | 'DELETE' | 'PATCH';\n export type Response<T> = Omit<_Response, 'json' | 'headers'> & {\n json(): Promise<T>;\n yaml(): Promise<T>;\n headers: SerializableHeaders;\n };\n export type Operator = typeof http;\n export type Options<Controllable extends boolean> = Omit<\n RequestInit,\n 'headers' | 'body' | 'signal'\n > & {\n controllable?: Controllable;\n headers?: HeadersInit;\n };\n export type Payload<Context, Controllable extends boolean> = Controllable extends true\n ? Promise<{\n request: Promise<HTTP.Response<Context>>;\n controller: HTTP.Controller;\n }>\n : Promise<HTTP.Response<Context>>;\n export type Exception = Pick<\n _Response,\n 'ok' | 'redirected' | 'status' | 'statusText' | 'type' | 'url'\n > & {\n headers: Record<string, string>;\n error: Error;\n };\n export type Error = {\n title: string;\n status: number;\n detail: string;\n error?: _Error;\n };\n export type Controller = AbortController;\n}\n\nconst defaultOptions: HTTP.Options<false> = {\n credentials: 'same-origin',\n cache: 'default',\n headers: {\n 'Content-Type': 'application/json'\n }\n};\n\nconst requestHandler: {\n <Context, Controllable extends boolean, Method = Extract<HTTP.Method, 'HEAD' | 'GET' | 'DELETE'>>(\n method: Method,\n url: string,\n ...args: [HTTP.Options<Controllable> | undefined]\n ): HTTP.Payload<Context, Controllable>;\n <\n Context,\n Payload,\n Controllable extends boolean,\n Method = Exclude<HTTP.Method, 'HEAD' | 'GET' | 'DELETE'>\n >(\n method: Method,\n url: string,\n ...args: [Payload, HTTP.Options<Controllable> | undefined]\n ): HTTP.Payload<Context, Controllable>;\n} = <Context, Payload, Controllable extends boolean, Method = HTTP.Method>(\n method: HTTP.Method,\n url: string,\n ...args: Method extends 'HEAD' | 'GET' | 'DELETE'\n ? [HTTP.Options<Controllable> | undefined]\n : [Payload, HTTP.Options<Controllable> | undefined]\n) => {\n const body = (['HEAD', 'GET', 'DELETE'].includes(method) ? null : args[0]) as Payload;\n const options = (\n ['HEAD', 'GET', 'DELETE'].includes(method) ? args[0] : args[1]\n ) as HTTP.Options<Controllable>;\n\n const controller = new AbortController();\n const { signal } = controller;\n // eslint-disable-next-line @typescript-eslint/ban-ts-comment\n // @ts-ignore: FileList is not defined in the global scope\n // ? NOTE: FileList JSDOM type cannot work with MSW request mocker, use workaround to do instance type check via prototype chain\n // ? This is equal to `body instanceof FileList`\n const isFileListBody = body?.__proto__?.[Symbol.toStringTag] === 'FileList';\n\n const request = fetch(url, {\n method,\n ...(body\n ? {\n body: isFileListBody\n ? [...(body as unknown as FileList)].reduce((payload, file) => {\n payload.append('file', file, `${file.webkitRelativePath || file.name}`);\n return payload;\n }, new FormData())\n : typeof body === 'object'\n ? JSON.stringify(body)\n : String(body)\n }\n : null),\n ...defaultOptions,\n ...options,\n headers: isFileListBody\n ? new Headers(options?.headers)\n : new Headers({ ...defaultOptions.headers, ...options?.headers }),\n signal\n })\n .then(async (res: Omit<HTTP.Response<Context>, 'yaml' | 'headers'> & { headers: Headers }) => {\n if (!res.ok) {\n let error: HTTP.Error;\n\n try {\n error = await res.clone().json();\n } catch (err) {\n error = {\n title: `${res.status === 401 ? '' : 'Unknown Error: '}${res.statusText}`,\n status: res.status,\n detail: (await res.clone().text()) ?? String(err),\n error: new Error((await res.clone().text()) ?? String(err))\n };\n }\n\n return Promise.reject({\n ok: res.ok,\n redirected: res.redirected,\n status: res.status,\n statusText: res.statusText,\n type: res.type,\n url: res.url,\n headers: Object.fromEntries(res.headers.entries()),\n error\n } satisfies HTTP.Exception);\n }\n\n return res.clone();\n })\n .then(async (res: _Response) => {\n const rawBody = await res.clone().text();\n // Create a serializable headers object for cross-worker transfer\n const headers = new SerializableHeaders(res.headers);\n\n // Return a plain object with response metadata and parsed data\n // Include convenience methods for backward compatibility\n // Use proxy() to ensure methods are callable across worker boundary\n const response: HTTP.Response<Context> = {\n body: res.body,\n clone: res.clone,\n bodyUsed: res.bodyUsed,\n arrayBuffer: () => Promise.resolve(res.arrayBuffer()),\n bytes: () => Promise.resolve(res.bytes()),\n blob: () => Promise.resolve(res.blob()),\n formData: () => Promise.resolve(res.formData()),\n text: () => Promise.resolve(rawBody),\n ok: res.ok,\n redirected: res.redirected,\n status: res.status,\n statusText: res.statusText,\n type: res.type,\n url: res.url,\n headers,\n // Convenience methods that simply return the already-parsed data\n json: async () => Promise.resolve(JSON.parse(rawBody) as Context),\n yaml: async () => Promise.resolve(load(rawBody) as Context)\n };\n\n return proxy(response);\n });\n\n return options?.controllable\n ? proxy({ request, controller } as unknown as HTTP.Payload<Context, true>) // Comlink will convert proxied to a Promise\n : (request satisfies HTTP.Payload<Context, false>);\n};\n\n/**\n * The core HTTP operator: a thin, typed wrapper around native `fetch`.\n *\n * This module has no worker/comlink transport wiring of its own — it is safe to\n * import directly on the main thread, in Node/SSR, or inside a worker. The\n * worker transport built on top of this lives in `fetch-worker/worker` and the\n * environment-aware client lives in `fetch-worker` (the package root).\n */\nexport const http = {\n request: requestHandler,\n head: <Context, Controllable extends boolean = false>(\n url: string,\n options?: HTTP.Options<Controllable>\n ) => requestHandler<Context, Controllable, 'HEAD'>('HEAD', url, options),\n get: <Context, Controllable extends boolean = false>(\n url: string,\n options?: HTTP.Options<Controllable>\n ) => requestHandler<Context, Controllable, 'GET'>('GET', url, options),\n post: <Context, Payload, Controllable extends boolean = false>(\n url: string,\n body: Payload,\n options?: HTTP.Options<Controllable>\n ) => requestHandler<Context, Payload, Controllable, 'POST'>('POST', url, body, options),\n put: <Context, Payload, Controllable extends boolean = false>(\n url: string,\n body: Payload,\n options?: HTTP.Options<Controllable>\n ) => requestHandler<Context, Payload, Controllable, 'PUT'>('PUT', url, body, options),\n delete: <Controllable extends boolean = false>(\n url: string,\n options?: HTTP.Options<Controllable>\n ) => requestHandler<void, Controllable, 'DELETE'>('DELETE', url, options),\n patch: <Context, Payload, Controllable extends boolean = false>(\n url: string,\n body: Payload,\n options?: HTTP.Options<Controllable>\n ) => requestHandler<Context, Payload, Controllable, 'PATCH'>('PATCH', url, body, options)\n};\n"],"mappings":"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","names":["http"]}
@@ -0,0 +1,3 @@
1
+ declare const _default: null;
2
+
3
+ export { _default as default };
package/dist/worker.js ADDED
@@ -0,0 +1,133 @@
1
+ // src/worker.ts
2
+ import { expose } from "comlink";
3
+
4
+ // src/core.ts
5
+ import { proxy } from "comlink";
6
+ import { load } from "js-yaml";
7
+ var SerializableHeaders = class {
8
+ headers;
9
+ constructor(headers) {
10
+ if (headers instanceof Headers) {
11
+ this.headers = Object.fromEntries(
12
+ Array.from(headers.entries()).map(([key, value]) => [key.toLowerCase(), value])
13
+ );
14
+ } else {
15
+ this.headers = Object.fromEntries(
16
+ Object.entries(headers).map(([key, value]) => [key.toLowerCase(), value])
17
+ );
18
+ }
19
+ }
20
+ get(name) {
21
+ return Promise.resolve(this.headers[name.toLowerCase()] ?? null);
22
+ }
23
+ has(name) {
24
+ return Promise.resolve(name.toLowerCase() in this.headers);
25
+ }
26
+ entries() {
27
+ return Promise.resolve(Object.entries(this.headers)[Symbol.iterator]());
28
+ }
29
+ keys() {
30
+ return Promise.resolve(Object.keys(this.headers)[Symbol.iterator]());
31
+ }
32
+ values() {
33
+ return Promise.resolve(Object.values(this.headers)[Symbol.iterator]());
34
+ }
35
+ forEach(callback) {
36
+ Object.entries(this.headers).forEach(([key, value]) => callback(value, key));
37
+ }
38
+ };
39
+ var defaultOptions = {
40
+ credentials: "same-origin",
41
+ cache: "default",
42
+ headers: {
43
+ "Content-Type": "application/json"
44
+ }
45
+ };
46
+ var requestHandler = (method, url, ...args) => {
47
+ const body = ["HEAD", "GET", "DELETE"].includes(method) ? null : args[0];
48
+ const options = ["HEAD", "GET", "DELETE"].includes(method) ? args[0] : args[1];
49
+ const controller = new AbortController();
50
+ const { signal } = controller;
51
+ const isFileListBody = body?.__proto__?.[Symbol.toStringTag] === "FileList";
52
+ const request = fetch(url, {
53
+ method,
54
+ ...body ? {
55
+ body: isFileListBody ? [...body].reduce((payload, file) => {
56
+ payload.append("file", file, `${file.webkitRelativePath || file.name}`);
57
+ return payload;
58
+ }, new FormData()) : typeof body === "object" ? JSON.stringify(body) : String(body)
59
+ } : null,
60
+ ...defaultOptions,
61
+ ...options,
62
+ headers: isFileListBody ? new Headers(options?.headers) : new Headers({ ...defaultOptions.headers, ...options?.headers }),
63
+ signal
64
+ }).then(async (res) => {
65
+ if (!res.ok) {
66
+ let error;
67
+ try {
68
+ error = await res.clone().json();
69
+ } catch (err) {
70
+ error = {
71
+ title: `${res.status === 401 ? "" : "Unknown Error: "}${res.statusText}`,
72
+ status: res.status,
73
+ detail: await res.clone().text() ?? String(err),
74
+ error: new Error(await res.clone().text() ?? String(err))
75
+ };
76
+ }
77
+ return Promise.reject({
78
+ ok: res.ok,
79
+ redirected: res.redirected,
80
+ status: res.status,
81
+ statusText: res.statusText,
82
+ type: res.type,
83
+ url: res.url,
84
+ headers: Object.fromEntries(res.headers.entries()),
85
+ error
86
+ });
87
+ }
88
+ return res.clone();
89
+ }).then(async (res) => {
90
+ const rawBody = await res.clone().text();
91
+ const headers = new SerializableHeaders(res.headers);
92
+ const response = {
93
+ body: res.body,
94
+ clone: res.clone,
95
+ bodyUsed: res.bodyUsed,
96
+ arrayBuffer: () => Promise.resolve(res.arrayBuffer()),
97
+ bytes: () => Promise.resolve(res.bytes()),
98
+ blob: () => Promise.resolve(res.blob()),
99
+ formData: () => Promise.resolve(res.formData()),
100
+ text: () => Promise.resolve(rawBody),
101
+ ok: res.ok,
102
+ redirected: res.redirected,
103
+ status: res.status,
104
+ statusText: res.statusText,
105
+ type: res.type,
106
+ url: res.url,
107
+ headers,
108
+ // Convenience methods that simply return the already-parsed data
109
+ json: async () => Promise.resolve(JSON.parse(rawBody)),
110
+ yaml: async () => Promise.resolve(load(rawBody))
111
+ };
112
+ return proxy(response);
113
+ });
114
+ return options?.controllable ? proxy({ request, controller }) : request;
115
+ };
116
+ var http = {
117
+ request: requestHandler,
118
+ head: (url, options) => requestHandler("HEAD", url, options),
119
+ get: (url, options) => requestHandler("GET", url, options),
120
+ post: (url, body, options) => requestHandler("POST", url, body, options),
121
+ put: (url, body, options) => requestHandler("PUT", url, body, options),
122
+ delete: (url, options) => requestHandler("DELETE", url, options),
123
+ patch: (url, body, options) => requestHandler("PATCH", url, body, options)
124
+ };
125
+
126
+ // src/worker.ts
127
+ var self = globalThis;
128
+ expose(http, self);
129
+ var worker_default = null;
130
+ export {
131
+ worker_default as default
132
+ };
133
+ //# sourceMappingURL=worker.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/worker.ts","../src/core.ts"],"sourcesContent":["/// <reference lib=\"webworker\" />\n\nimport { expose } from 'comlink';\n\nimport { http } from './core';\n\nconst self: WorkerGlobalScope & typeof globalThis = globalThis as any;\n\nexpose(http, self);\n\nexport default null;\n","/// <reference lib=\"webworker\" />\n\nimport { proxy } from 'comlink';\nimport { load } from 'js-yaml';\n\ntype _Response = Response;\ntype _Error = Error;\n\n/**\n * A structured-clone-safe stand-in for the `Headers` API.\n *\n * `Headers` instances cannot cross a worker `postMessage` boundary, so responses\n * expose this instead. It mirrors the read side of `Headers` (`get`, `has`,\n * `entries`, `keys`, `values`, `forEach`) but every accessor returns a `Promise`\n * so it also works correctly when proxied across a worker via comlink.\n */\nclass SerializableHeaders {\n private headers: Record<string, string>;\n\n constructor(headers: Headers | Record<string, string>) {\n if (headers instanceof Headers) {\n this.headers = Object.fromEntries(\n Array.from(headers.entries()).map(([key, value]) => [key.toLowerCase(), value])\n );\n } else {\n this.headers = Object.fromEntries(\n Object.entries(headers).map(([key, value]) => [key.toLowerCase(), value])\n );\n }\n }\n\n get(name: string): Promise<string | null> {\n return Promise.resolve(this.headers[name.toLowerCase()] ?? null);\n }\n\n has(name: string): Promise<boolean> {\n return Promise.resolve(name.toLowerCase() in this.headers);\n }\n\n entries(): Promise<IterableIterator<[string, string]>> {\n return Promise.resolve(Object.entries(this.headers)[Symbol.iterator]());\n }\n\n keys(): Promise<IterableIterator<string>> {\n return Promise.resolve(Object.keys(this.headers)[Symbol.iterator]());\n }\n\n values(): Promise<IterableIterator<string>> {\n return Promise.resolve(Object.values(this.headers)[Symbol.iterator]());\n }\n\n forEach(callback: (value: string, key: string) => void): void {\n Object.entries(this.headers).forEach(([key, value]) => callback(value, key));\n }\n}\n\nexport namespace HTTP {\n export type Method = 'HEAD' | 'GET' | 'POST' | 'PUT' | 'DELETE' | 'PATCH';\n export type Response<T> = Omit<_Response, 'json' | 'headers'> & {\n json(): Promise<T>;\n yaml(): Promise<T>;\n headers: SerializableHeaders;\n };\n export type Operator = typeof http;\n export type Options<Controllable extends boolean> = Omit<\n RequestInit,\n 'headers' | 'body' | 'signal'\n > & {\n controllable?: Controllable;\n headers?: HeadersInit;\n };\n export type Payload<Context, Controllable extends boolean> = Controllable extends true\n ? Promise<{\n request: Promise<HTTP.Response<Context>>;\n controller: HTTP.Controller;\n }>\n : Promise<HTTP.Response<Context>>;\n export type Exception = Pick<\n _Response,\n 'ok' | 'redirected' | 'status' | 'statusText' | 'type' | 'url'\n > & {\n headers: Record<string, string>;\n error: Error;\n };\n export type Error = {\n title: string;\n status: number;\n detail: string;\n error?: _Error;\n };\n export type Controller = AbortController;\n}\n\nconst defaultOptions: HTTP.Options<false> = {\n credentials: 'same-origin',\n cache: 'default',\n headers: {\n 'Content-Type': 'application/json'\n }\n};\n\nconst requestHandler: {\n <Context, Controllable extends boolean, Method = Extract<HTTP.Method, 'HEAD' | 'GET' | 'DELETE'>>(\n method: Method,\n url: string,\n ...args: [HTTP.Options<Controllable> | undefined]\n ): HTTP.Payload<Context, Controllable>;\n <\n Context,\n Payload,\n Controllable extends boolean,\n Method = Exclude<HTTP.Method, 'HEAD' | 'GET' | 'DELETE'>\n >(\n method: Method,\n url: string,\n ...args: [Payload, HTTP.Options<Controllable> | undefined]\n ): HTTP.Payload<Context, Controllable>;\n} = <Context, Payload, Controllable extends boolean, Method = HTTP.Method>(\n method: HTTP.Method,\n url: string,\n ...args: Method extends 'HEAD' | 'GET' | 'DELETE'\n ? [HTTP.Options<Controllable> | undefined]\n : [Payload, HTTP.Options<Controllable> | undefined]\n) => {\n const body = (['HEAD', 'GET', 'DELETE'].includes(method) ? null : args[0]) as Payload;\n const options = (\n ['HEAD', 'GET', 'DELETE'].includes(method) ? args[0] : args[1]\n ) as HTTP.Options<Controllable>;\n\n const controller = new AbortController();\n const { signal } = controller;\n // eslint-disable-next-line @typescript-eslint/ban-ts-comment\n // @ts-ignore: FileList is not defined in the global scope\n // ? NOTE: FileList JSDOM type cannot work with MSW request mocker, use workaround to do instance type check via prototype chain\n // ? This is equal to `body instanceof FileList`\n const isFileListBody = body?.__proto__?.[Symbol.toStringTag] === 'FileList';\n\n const request = fetch(url, {\n method,\n ...(body\n ? {\n body: isFileListBody\n ? [...(body as unknown as FileList)].reduce((payload, file) => {\n payload.append('file', file, `${file.webkitRelativePath || file.name}`);\n return payload;\n }, new FormData())\n : typeof body === 'object'\n ? JSON.stringify(body)\n : String(body)\n }\n : null),\n ...defaultOptions,\n ...options,\n headers: isFileListBody\n ? new Headers(options?.headers)\n : new Headers({ ...defaultOptions.headers, ...options?.headers }),\n signal\n })\n .then(async (res: Omit<HTTP.Response<Context>, 'yaml' | 'headers'> & { headers: Headers }) => {\n if (!res.ok) {\n let error: HTTP.Error;\n\n try {\n error = await res.clone().json();\n } catch (err) {\n error = {\n title: `${res.status === 401 ? '' : 'Unknown Error: '}${res.statusText}`,\n status: res.status,\n detail: (await res.clone().text()) ?? String(err),\n error: new Error((await res.clone().text()) ?? String(err))\n };\n }\n\n return Promise.reject({\n ok: res.ok,\n redirected: res.redirected,\n status: res.status,\n statusText: res.statusText,\n type: res.type,\n url: res.url,\n headers: Object.fromEntries(res.headers.entries()),\n error\n } satisfies HTTP.Exception);\n }\n\n return res.clone();\n })\n .then(async (res: _Response) => {\n const rawBody = await res.clone().text();\n // Create a serializable headers object for cross-worker transfer\n const headers = new SerializableHeaders(res.headers);\n\n // Return a plain object with response metadata and parsed data\n // Include convenience methods for backward compatibility\n // Use proxy() to ensure methods are callable across worker boundary\n const response: HTTP.Response<Context> = {\n body: res.body,\n clone: res.clone,\n bodyUsed: res.bodyUsed,\n arrayBuffer: () => Promise.resolve(res.arrayBuffer()),\n bytes: () => Promise.resolve(res.bytes()),\n blob: () => Promise.resolve(res.blob()),\n formData: () => Promise.resolve(res.formData()),\n text: () => Promise.resolve(rawBody),\n ok: res.ok,\n redirected: res.redirected,\n status: res.status,\n statusText: res.statusText,\n type: res.type,\n url: res.url,\n headers,\n // Convenience methods that simply return the already-parsed data\n json: async () => Promise.resolve(JSON.parse(rawBody) as Context),\n yaml: async () => Promise.resolve(load(rawBody) as Context)\n };\n\n return proxy(response);\n });\n\n return options?.controllable\n ? proxy({ request, controller } as unknown as HTTP.Payload<Context, true>) // Comlink will convert proxied to a Promise\n : (request satisfies HTTP.Payload<Context, false>);\n};\n\n/**\n * The core HTTP operator: a thin, typed wrapper around native `fetch`.\n *\n * This module has no worker/comlink transport wiring of its own — it is safe to\n * import directly on the main thread, in Node/SSR, or inside a worker. The\n * worker transport built on top of this lives in `fetch-worker/worker` and the\n * environment-aware client lives in `fetch-worker` (the package root).\n */\nexport const http = {\n request: requestHandler,\n head: <Context, Controllable extends boolean = false>(\n url: string,\n options?: HTTP.Options<Controllable>\n ) => requestHandler<Context, Controllable, 'HEAD'>('HEAD', url, options),\n get: <Context, Controllable extends boolean = false>(\n url: string,\n options?: HTTP.Options<Controllable>\n ) => requestHandler<Context, Controllable, 'GET'>('GET', url, options),\n post: <Context, Payload, Controllable extends boolean = false>(\n url: string,\n body: Payload,\n options?: HTTP.Options<Controllable>\n ) => requestHandler<Context, Payload, Controllable, 'POST'>('POST', url, body, options),\n put: <Context, Payload, Controllable extends boolean = false>(\n url: string,\n body: Payload,\n options?: HTTP.Options<Controllable>\n ) => requestHandler<Context, Payload, Controllable, 'PUT'>('PUT', url, body, options),\n delete: <Controllable extends boolean = false>(\n url: string,\n options?: HTTP.Options<Controllable>\n ) => requestHandler<void, Controllable, 'DELETE'>('DELETE', url, options),\n patch: <Context, Payload, Controllable extends boolean = false>(\n url: string,\n body: Payload,\n options?: HTTP.Options<Controllable>\n ) => requestHandler<Context, Payload, Controllable, 'PATCH'>('PATCH', url, body, options)\n};\n"],"mappings":"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","names":[]}
package/package.json ADDED
@@ -0,0 +1,85 @@
1
+ {
2
+ "name": "@aries0d0f/fetch-worker",
3
+ "version": "0.1.1",
4
+ "description": "Native fetch, offloaded to a Web Worker — a tiny, dependency-light HTTP client with a comlink-powered worker transport and automatic main-thread fallback.",
5
+ "keywords": [
6
+ "fetch",
7
+ "http",
8
+ "web-worker",
9
+ "worker",
10
+ "comlink",
11
+ "http-client"
12
+ ],
13
+ "license": "MIT",
14
+ "author": "aries0d0f",
15
+ "homepage": "https://github.com/aries0d0f/fetch-worker#readme",
16
+ "repository": {
17
+ "type": "git",
18
+ "url": "git+https://github.com/aries0d0f/fetch-worker.git"
19
+ },
20
+ "bugs": {
21
+ "url": "https://github.com/aries0d0f/fetch-worker/issues"
22
+ },
23
+ "type": "module",
24
+ "sideEffects": false,
25
+ "exports": {
26
+ ".": {
27
+ "types": "./dist/index.d.ts",
28
+ "import": "./dist/index.js"
29
+ },
30
+ "./core": {
31
+ "types": "./dist/core.d.ts",
32
+ "import": "./dist/core.js"
33
+ },
34
+ "./package.json": "./package.json"
35
+ },
36
+ "main": "./dist/index.js",
37
+ "module": "./dist/index.js",
38
+ "types": "./dist/index.d.ts",
39
+ "files": [
40
+ "dist",
41
+ "README.md",
42
+ "LICENSE"
43
+ ],
44
+ "engines": {
45
+ "node": ">=18"
46
+ },
47
+ "publishConfig": {
48
+ "access": "public"
49
+ },
50
+ "packageManager": "bun@1.3.14",
51
+ "scripts": {
52
+ "build": "tsup",
53
+ "dev": "tsup --watch",
54
+ "test": "vitest run",
55
+ "test:watch": "vitest",
56
+ "test:coverage": "vitest run --coverage",
57
+ "lint": "eslint .",
58
+ "lint:fix": "eslint . --fix",
59
+ "format": "prettier --write .",
60
+ "format:check": "prettier --check .",
61
+ "typecheck": "tsc --noEmit",
62
+ "changeset": "changeset",
63
+ "release": "bun run build && changeset publish",
64
+ "prepublishOnly": "bun run build"
65
+ },
66
+ "dependencies": {
67
+ "comlink": "^4.4.2",
68
+ "js-yaml": "^4.1.0"
69
+ },
70
+ "devDependencies": {
71
+ "@changesets/cli": "^2.29.7",
72
+ "@eslint/js": "^9.38.0",
73
+ "@types/js-yaml": "^4.0.9",
74
+ "@types/node": "^24.10.1",
75
+ "@vitest/coverage-v8": "^5.0.0",
76
+ "eslint": "^9.38.0",
77
+ "eslint-config-prettier": "^10.1.8",
78
+ "msw": "^2.11.6",
79
+ "prettier": "^3.6.2",
80
+ "tsup": "^8.5.0",
81
+ "typescript": "^5.9.3",
82
+ "typescript-eslint": "^8.46.4",
83
+ "vitest": "^5.0.0"
84
+ }
85
+ }