diff --git a/src/uploaders/ipfs.ts b/src/uploaders/ipfs.ts index e6a33cd..7438205 100644 --- a/src/uploaders/ipfs.ts +++ b/src/uploaders/ipfs.ts @@ -1,5 +1,6 @@ import { Conf } from '@/config.ts'; import { z } from '@/deps.ts'; +import { fetchWorker } from '@/workers/fetch.ts'; import type { Uploader } from './types.ts'; @@ -22,7 +23,7 @@ const ipfsUploader: Uploader = { const formData = new FormData(); formData.append('file', file); - const response = await fetch(url, { + const response = await fetchWorker(url, { method: 'POST', body: formData, }); @@ -41,7 +42,7 @@ const ipfsUploader: Uploader = { url.search = query.toString(); - await fetch(url, { + await fetchWorker(url, { method: 'POST', }); }, diff --git a/src/workers/fetch.test.ts b/src/workers/fetch.test.ts index 21fa1de..c985ee6 100644 --- a/src/workers/fetch.test.ts +++ b/src/workers/fetch.test.ts @@ -4,10 +4,14 @@ import { fetchWorker } from './fetch.ts'; await sleep(2000); -Deno.test('fetchWorker', async () => { - const response = await fetchWorker('https://example.com'); - const text = await response.text(); - assert(text.includes('Example Domain')); +Deno.test({ + name: 'fetchWorker', + async fn() { + const response = await fetchWorker('https://example.com'); + const text = await response.text(); + assert(text.includes('Example Domain')); + }, + sanitizeResources: false, }); Deno.test({ diff --git a/src/workers/fetch.ts b/src/workers/fetch.ts index 87b622a..3246e78 100644 --- a/src/workers/fetch.ts +++ b/src/workers/fetch.ts @@ -11,11 +11,61 @@ const _worker = Comlink.wrap( ), ); -const fetchWorker: typeof fetch = async (input, init) => { - const { signal, ...rest } = init || {}; - const url = input instanceof Request ? input.url : input.toString(); - const args = await _worker.fetch(url, rest, signal); - return new Response(...args); +/** + * Fetch implementation with a Web Worker. + * Calling this performs the fetch in a separate CPU thread so it doesn't block the main thread. + */ +const fetchWorker: typeof fetch = async (...args) => { + const [url, init] = serializeFetchArgs(args); + const { body, signal, ...rest } = init; + const result = await _worker.fetch(url, { ...rest, body: await prepareBodyForWorker(body) }, signal); + return new Response(...result); }; +/** Take arguments to `fetch`, and turn them into something we can send over Comlink. */ +function serializeFetchArgs(args: Parameters): [string, RequestInit] { + const request = normalizeRequest(args); + const init = requestToInit(request); + return [request.url, init]; +} + +/** Get a `Request` object from arguments to `fetch`. */ +function normalizeRequest(args: Parameters): Request { + return new Request(...args); +} + +/** Get the body as a type we can transfer over Web Workers. */ +async function prepareBodyForWorker( + body: BodyInit | undefined | null, +): Promise { + if (!body || typeof body === 'string' || body instanceof ArrayBuffer || body instanceof Blob) { + return body; + } else { + const response = new Response(body); + return await response.arrayBuffer(); + } +} + +/** + * Convert a `Request` object into its serialized `RequestInit` format. + * `RequestInit` is a subset of `Request`, just lacking helper methods like `json()`, + * making it easier to serialize (exceptions: `body` and `signal`). + */ +function requestToInit(request: Request): RequestInit { + return { + method: request.method, + headers: [...request.headers.entries()], + body: request.body, + referrer: request.referrer, + referrerPolicy: request.referrerPolicy, + mode: request.mode, + credentials: request.credentials, + cache: request.cache, + redirect: request.redirect, + integrity: request.integrity, + keepalive: request.keepalive, + signal: request.signal, + }; +} + export { fetchWorker }; diff --git a/src/workers/fetch.worker.ts b/src/workers/fetch.worker.ts index e36be4a..2988a2e 100644 --- a/src/workers/fetch.worker.ts +++ b/src/workers/fetch.worker.ts @@ -10,11 +10,11 @@ export const FetchWorker = { ): Promise<[BodyInit, ResponseInit]> { const response = await fetch(url, { ...init, signal }); return [ - await response.text(), + await response.arrayBuffer(), { status: response.status, statusText: response.statusText, - headers: Array.from(response.headers.entries()), + headers: [...response.headers.entries()], }, ]; },