From 38abc8a148ba32790f3660c71d9c08c17bbf2753 Mon Sep 17 00:00:00 2001 From: Rod Lewis Date: Sun, 17 Jul 2022 20:37:20 +0800 Subject: [PATCH] improve code and add retry --- README.md | 79 +++++++++++-------- .../fetchTool.ts => fetchWorker/index.ts} | 15 ++-- src/lib/fnWorker/index.ts | 7 ++ src/lib/index.ts | 5 +- src/lib/types.d.ts | 3 +- src/lib/utils/createWorkerPromise.ts | 2 +- src/lib/utils/doHardwork.ts | 7 -- src/lib/utils/index.ts | 6 ++ src/lib/worker/fetch_worker.ts | 54 +++++++++---- src/lib/worker/worker.ts | 4 +- 10 files changed, 111 insertions(+), 71 deletions(-) rename src/lib/{utils/fetchTool.ts => fetchWorker/index.ts} (57%) create mode 100644 src/lib/fnWorker/index.ts delete mode 100644 src/lib/utils/doHardwork.ts diff --git a/README.md b/README.md index 4dac0fc..82697db 100644 --- a/README.md +++ b/README.md @@ -21,63 +21,72 @@ or ### import - import { doHardwork, fetchTool, createWorkerPromise } from "worker-mate" + import { fnWorker, fetchWorker } from "worker-mate" -## doHardwork +## fnWorker Perform a long runnning or expensive task in a worker with a simple promise interface. ### example const contrivedFn = (arrayOfNumbers) => arrayOfNumbers.map(n => n ** n).filter(n => n > 9999).sort()[0] const contrivedArray = [420, 10, 225, 50,100,1000] - let largestSquare = await doHardwork(contrivedFn, contrivedArray).then(n => n) + let largestSquare = await fnWorker(contrivedFn, contrivedArray).then(n => n) -> **Tip:** Each **doHardwork()** opens in a new web worker +> **Tip:** Each **fnWorker()** opens in a new web worker -### doHardwork function arguments -doHardwork requires two arguments. The first should be a pure function, that takes the second, your unprocessed data does some expensive computation and returns the result. Both arguments are required. Side effects are not recommended. +### props +fnWorker requires two arguments. The first should be a pure function, that takes the second, your unprocessed data does some expensive computation and returns the result. Both arguments are required. Side effects are not recommended. - fn - required - rawData- required -## fetchTool +## fetchWorker Fetch with middleware in a worker. Offload expensive data transformations onto their own thread. Need to mutate the body of a request? No dramas, we've got you covered. ### example - fetchTool>({ url: 'https://swapi.dev/api/starships/9', responseMiddleware: (d) => ({ - name: d?.name ?? '', - model: d?.model ?? '', - manufacturer: d?.manufacturer ?? '', - })}) - .then((d: { name: string; model: string; manufacturer: string }) => { setStar(d) }) - .catch((err) => console.log(err)); - }, []); -## createWorkerPromise -This is the function we created to create fetchTool and doHardwork. If we aren't covering your use case, create your own. -### example - import { serializeFunction } from '.'; - import { createWorkerPromise } from './createWorkerPromise'; - import FetchWorker from '../worker/fetch_worker.ts?worker&inline'; - - export interface FetchToolProps { + fetchWorker>({ + url: 'https://swapi.dev/api/starships/9', + responseMiddleware: (d) => ({ + name: d?.name ?? '', + model: d?.model ?? '', + manufacturer: d?.manufacturer ?? '' + }), + retry: { + attempts: 2, + delay: 1000 + } + }) + .then((d: { name: string; model: string; manufacturer: string }) => { setStar(d) }) + .catch((err) => console.log(err)); + +### props + - fetchProps: FetchToolProps + + interface FetchToolProps { url: string; - body?: any; options?: RequestInit; requestMiddleware?: Function; responseMiddleware?: Function; + retry?: { + attempts: number; + delay: number; + }; } - - export const fetchTool = ({ url, body, requestMiddleware, responseMiddleware, options }: FetchToolProps) => { - return createWorkerPromise(FetchWorker, { - url, - body, - options, - requestMiddleware: requestMiddleware && serializeFunction(requestMiddleware), - responseMiddleware: responseMiddleware && serializeFunction(responseMiddleware) - }) - } - +#### url and options +url is the same as the first option in the fetch api and options is exactly the same as the second option. Please refer to [fetch API MDN](https://developer.mozilla.org/en-US/docs/Web/API/fetch) for details. url is equivalant to resource and options is options. + +#### requestMiddleware +requestMiddleware is an optional function you can provide to process data before you send a POST or PUT request. Add your unprocessed data to the body of options using JSON.stringify and your function will be executed on the data in the web worker. + +#### responseMiddleware +responseMiddleware is an optional function you can provide to process data from a response. This will run in the web worker. + +#### retry +retry is an object with two entries := attempts is the amount of times you want to attempt a request and delay is the time you want to wait between attempts. + + + ## Why Worker Mate? Worker mate is just Typescript with no dependencies. It makes offloading expensive computations to web workers simple . This allows you to keep the main thread clear and your site responsive. It's a super simple, easy to use function that returns a promise. Each instantiation creates a new web worker thread, which terminates itself once the request is complete. diff --git a/src/lib/utils/fetchTool.ts b/src/lib/fetchWorker/index.ts similarity index 57% rename from src/lib/utils/fetchTool.ts rename to src/lib/fetchWorker/index.ts index 69d3081..a3ca10f 100644 --- a/src/lib/utils/fetchTool.ts +++ b/src/lib/fetchWorker/index.ts @@ -1,21 +1,24 @@ -import { serializeFunction } from '.'; -import { createWorkerPromise } from './createWorkerPromise'; +import { serializeFunction } from '../utils'; +import { createWorkerPromise } from '../utils/createWorkerPromise'; import FetchWorker from '../worker/fetch_worker.ts?worker&inline'; export interface FetchToolProps { url: string; - body?: any; options?: RequestInit; requestMiddleware?: Function; responseMiddleware?: Function; + retry?: { + attempts: number; + delay: number; + }; } -export const fetchTool = ({ url, body, requestMiddleware, responseMiddleware, options }: FetchToolProps) => { +export const fetchWorker = ({ url, requestMiddleware, responseMiddleware, options, retry }: FetchToolProps) => { return createWorkerPromise(FetchWorker, { url, - body, options, requestMiddleware: requestMiddleware && serializeFunction(requestMiddleware), - responseMiddleware: responseMiddleware && serializeFunction(responseMiddleware) + responseMiddleware: responseMiddleware && serializeFunction(responseMiddleware), + retry }) } diff --git a/src/lib/fnWorker/index.ts b/src/lib/fnWorker/index.ts new file mode 100644 index 0000000..dcd2d0e --- /dev/null +++ b/src/lib/fnWorker/index.ts @@ -0,0 +1,7 @@ +import { createWorkerPromise } from '../utils/createWorkerPromise'; +import DoWork from '../worker/worker.ts?worker&inline'; +import { serializeFunction } from '../utils'; + +export const fnWorker = (fn: Function, rawData: any) => { + return createWorkerPromise(DoWork, {fn: serializeFunction(fn), rawData}) +} \ No newline at end of file diff --git a/src/lib/index.ts b/src/lib/index.ts index 8f6a7fc..98dc3de 100644 --- a/src/lib/index.ts +++ b/src/lib/index.ts @@ -1,3 +1,2 @@ -export { doHardwork } from "./utils/doHardwork"; -export { fetchTool } from "./utils/fetchTool"; -export { createWorkerPromise } from "./utils/createWorkerPromise"; \ No newline at end of file +export { fnWorker } from './fnWorker'; +export { fetchWorker } from './fetchWorker'; \ No newline at end of file diff --git a/src/lib/types.d.ts b/src/lib/types.d.ts index febd2b4..4c3c0d2 100644 --- a/src/lib/types.d.ts +++ b/src/lib/types.d.ts @@ -2,6 +2,7 @@ export interface WorkerResponseType { isTrusted: boolean; data: { type: 'error' | 'data'; - data: T; + data?: T; + error?: Error; }; } \ No newline at end of file diff --git a/src/lib/utils/createWorkerPromise.ts b/src/lib/utils/createWorkerPromise.ts index c9fa08a..b3c808c 100644 --- a/src/lib/utils/createWorkerPromise.ts +++ b/src/lib/utils/createWorkerPromise.ts @@ -6,7 +6,7 @@ export const createWorkerPromise = (Worker: new () => Worker, postMessage: Re if (worker) { worker.onmessage = (e: WorkerResponseType) => { if(e.isTrusted) { - e.data?.type === 'error' ? reject(e.data.data) : resolve(e.data.data); + e.data?.type === 'error' ? reject(e.data.error) : resolve(e.data?.data as T); } worker?.terminate(); } diff --git a/src/lib/utils/doHardwork.ts b/src/lib/utils/doHardwork.ts deleted file mode 100644 index 9604506..0000000 --- a/src/lib/utils/doHardwork.ts +++ /dev/null @@ -1,7 +0,0 @@ -import { createWorkerPromise } from './createWorkerPromise'; -import DoWork from '../worker/worker.ts?worker&inline'; -import { serializeFunction } from '.'; - -export const doHardwork = (fn: Function, rawData: any) => { - return createWorkerPromise(DoWork, {fn: serializeFunction(fn), rawData}) -} \ No newline at end of file diff --git a/src/lib/utils/index.ts b/src/lib/utils/index.ts index b2a1df0..3cf6091 100644 --- a/src/lib/utils/index.ts +++ b/src/lib/utils/index.ts @@ -4,3 +4,9 @@ export const deserializeFunction = (s: string) => new Function(`return ${decodeURI(s)}`)(); export const methodType = (options: RequestInit | undefined) => options?.method?.toUpperCase() ?? "GET"; + +export function* generateRetryDelay(retry: { attempts: number; delay: number }) { + for (let i = 0; i < retry.attempts; i++) { + yield new Promise(resolve => i === 0 ? resolve(void 0) : setTimeout(resolve, retry.delay)); + } +} diff --git a/src/lib/worker/fetch_worker.ts b/src/lib/worker/fetch_worker.ts index 7345c65..87fb338 100644 --- a/src/lib/worker/fetch_worker.ts +++ b/src/lib/worker/fetch_worker.ts @@ -1,25 +1,47 @@ -import { deserializeFunction, isFunction, methodType } from '../utils'; +import { deserializeFunction, generateRetryDelay, isFunction, methodType } from '../utils'; self.addEventListener('message', async (event) => { if(!event.isTrusted) return; let { data } = event; - let { url, body, options } = data; + let { url, options, retry } = data; let method = methodType(options); - if((method === 'POST' || method === 'PUT') && body) { - let requestMiddleware = data?.requestMiddleware && deserializeFunction(data.requestMiddleware); - options = { ...options, body: isFunction(requestMiddleware) ? JSON.stringify(requestMiddleware(body)) : JSON.stringify(body) }; + if((method === 'POST' || method === 'PUT') && data?.requestMiddleware) { + let requestMiddleware = deserializeFunction(data.requestMiddleware); + let body = options.body; + if(body && isFunction(requestMiddleware)) { + options = { ...options, body: JSON.stringify(requestMiddleware(JSON.parse(body)))}; + } + } + const fetchData = async () => { + try { + let response = await fetch(url, options); + if(response.ok && response.status !== 404 && response.status !== 403) { + let responseData = await response.json(); + let responseMiddleware = data?.responseMiddleware && deserializeFunction(data.responseMiddleware); + let responseMiddlewareData = isFunction(responseMiddleware) ? responseMiddleware(responseData) : responseData; + return responseMiddlewareData; + } else { + throw new Error(`${response.status ?? response.statusText}`); + } + } catch (error) { + throw error; + } } - try { - let response = await fetch(url, options); - if(response.ok && response.status !== 404 && response.status !== 403) { - let responseData = await response.json(); - let responseMiddleware = data?.responseMiddleware && deserializeFunction(data.responseMiddleware); - let responseMiddlewareData = isFunction(responseMiddleware) ? responseMiddleware(responseData) : responseData; - self.postMessage({ type: 'data', data: responseMiddlewareData }); - } else { - self.postMessage({ type: 'error', data: `Error: ${response.status}` }); + if(!retry) {retry = { attempts: 1, delay: 0 }} + let retryDelay = generateRetryDelay(retry); + let response; + while(!response) { + try { + response = await fetchData(); + self.postMessage({ type: 'data', data: response }); + } catch (err) { + let delay = retryDelay.next(); + if(!delay.done) { + await delay.value; + } else { + response = true; + self.postMessage({ type: 'error', error: err }); + } } - } catch (error) { - self.postMessage({ type: 'error', data: (error as Error)?.message ?? error }); } }); diff --git a/src/lib/worker/worker.ts b/src/lib/worker/worker.ts index 4824ab3..0ea387c 100644 --- a/src/lib/worker/worker.ts +++ b/src/lib/worker/worker.ts @@ -7,10 +7,10 @@ self.addEventListener('message', async (event) => { if (!isFunction(fn)) { self.postMessage({ type: 'error', - data: 'no function provided', + error: 'no function provided', }); } else if (!data?.rawData) { - self.postMessage({ type: 'error', data: 'no data provided' }); + self.postMessage({ type: 'error', error: 'no data provided' }); } else { self.postMessage({ type: 'data', data: fn(data.rawData) }); }