diff --git a/apps/examples/src/App.jsx b/apps/examples/src/App.jsx index 8936aec..04aaced 100644 --- a/apps/examples/src/App.jsx +++ b/apps/examples/src/App.jsx @@ -1,26 +1,27 @@ -import React from 'react' -import { Toaster } from 'react-hot-toast' -import { Link, Route, BrowserRouter as Router, Switch } from 'react-router-dom' +import React from "react"; +import { Toaster } from "react-hot-toast"; +import { Link, Route, BrowserRouter as Router, Switch } from "react-router-dom"; -import CsvPage from './pages/Csv/index.jsx' -import ExternalScriptsPage from './pages/ExternalScripts/index.jsx' -import SortingPage from './pages/Sorting/index.jsx' -import TransferablePage from './pages/Transferable' -import logo from './react.png' -import './style.css' +import CsvPage from "./pages/Csv/index.jsx"; +import ExternalScriptsPage from "./pages/ExternalScripts/index.jsx"; +import SortingPage from "./pages/Sorting/index.jsx"; +import TransferablePage from "./pages/Transferable"; +import LocalDepsPage from "./pages/LocalDeps"; +import logo from "./react.png"; +import "./style.css"; -let turn = 0 +let turn = 0; function infiniteLoop() { - const lgoo = document.querySelector('.App-logo') - turn += 8 - lgoo.style.transform = `rotate(${turn % 360}deg)` + const lgoo = document.querySelector(".App-logo"); + turn += 8; + lgoo.style.transform = `rotate(${turn % 360}deg)`; } export default function App() { React.useEffect(() => { - const loopInterval = setInterval(infiniteLoop, 100) - return () => clearInterval(loopInterval) - }, []) + const loopInterval = setInterval(infiniteLoop, 100); + return () => clearInterval(loopInterval); + }, []); return ( <> @@ -31,25 +32,30 @@ export default function App() { logo
@@ -68,10 +74,13 @@ export default function App() { + + + - ) + ); } diff --git a/apps/examples/src/pages/LocalDeps/index.jsx b/apps/examples/src/pages/LocalDeps/index.jsx new file mode 100644 index 0000000..ddb5666 --- /dev/null +++ b/apps/examples/src/pages/LocalDeps/index.jsx @@ -0,0 +1,77 @@ +import { WORKER_STATUS, useWorker } from "@koale/useworker"; +import React, { useState } from "react"; +import toast from "react-hot-toast"; + +const pow = (a) => a * a; + +function App() { + const [inputValue, setInputValue] = useState(2); // State for input value + const [localDepsWorker, { status: localDepsWorkerStatus, kill: killWorker }] = + useWorker((numbers) => pow(numbers), { + autoTerminate: false, + localDependencies: [pow], + }); + + React.useEffect(() => { + console.log("WORKER:", localDepsWorkerStatus); + }, [localDepsWorkerStatus]); + + const onWorkerSortClick = () => { + // Use the inputValue from state + localDepsWorker(Number(inputValue)).then((result) => { + console.log("localdeps useWorker()", result); + toast.success( + "Finished: localdeps using useWorker." + `The result is ${result}` + ); + }); + }; + + // Handle input change + const handleInputChange = (e) => { + setInputValue(e.target.value); + }; + + return ( +
+
+ {/* Input field for dynamic value */} +
+
{"const pow = (a) => a * a;"}
+ + + {localDepsWorkerStatus === WORKER_STATUS.RUNNING ? ( + + ) : null} +
+
+
+ + Open DevTools console to see the results. + +
+
+ ); +} + +export default App; diff --git a/packages/useWorker/src/lib/createWorkerBlobUrl.ts b/packages/useWorker/src/lib/createWorkerBlobUrl.ts index 231ce44..5936958 100644 --- a/packages/useWorker/src/lib/createWorkerBlobUrl.ts +++ b/packages/useWorker/src/lib/createWorkerBlobUrl.ts @@ -1,7 +1,7 @@ // import isoworker from 'isoworker' -import { TRANSFERABLE_TYPE } from '../useWorker' -import jobRunner from './jobRunner' -import remoteDepsParser from './remoteDepsParser' +import { TRANSFERABLE_TYPE } from "../useWorker"; +import jobRunner from "./jobRunner"; +import remoteDepsParser from "./remoteDepsParser"; /** * Converts the "fn" function into the syntax needed to be executed within a web worker @@ -20,19 +20,20 @@ import remoteDepsParser from './remoteDepsParser' const createWorkerBlobUrl = ( fn: Function, deps: string[], - transferable: TRANSFERABLE_TYPE /* localDeps: () => unknown[], */, + localDeps: Function[], + transferable: TRANSFERABLE_TYPE /* localDeps: () => unknown[], */ ) => { // const [context] = isoworker.createContext(localDeps) const blobCode = ` - ${remoteDepsParser(deps)}; + ${remoteDepsParser(deps, localDeps)}; onmessage=(${jobRunner})({ fn: (${fn}), transferable: '${transferable}' }) - ` - const blob = new Blob([blobCode], { type: 'text/javascript' }) - const url = URL.createObjectURL(blob) - return url -} + `; + const blob = new Blob([blobCode], { type: "text/javascript" }); + const url = URL.createObjectURL(blob); + return url; +}; -export default createWorkerBlobUrl +export default createWorkerBlobUrl; diff --git a/packages/useWorker/src/lib/remoteDepsParser.ts b/packages/useWorker/src/lib/remoteDepsParser.ts index f0eefb8..5bfd1e3 100644 --- a/packages/useWorker/src/lib/remoteDepsParser.ts +++ b/packages/useWorker/src/lib/remoteDepsParser.ts @@ -3,18 +3,34 @@ * Concatenates the remote dependencies into a comma separated string. * this string will then be passed as an argument to the "importScripts" function * - * @param {Array.}} deps array of string + * @param {Array.} deps array of string + * @param {Array.} localDeps array of function * @returns {String} a string composed by the concatenation of the array * elements "deps" and "importScripts". * * @example * remoteDepsParser(['http://js.com/1.js', 'http://js.com/2.js']) // importScripts('http://js.com/1.js', 'http://js.com/2.js') */ -const remoteDepsParser = (deps: string[]) => { - if (deps.length === 0) return '' +const remoteDepsParser = (deps: string[], localDeps: Function[]) => { + if (deps.length === 0 && localDeps.length === 0) return ""; - const depsString = deps.map((dep) => `'${dep}'`).toString() - return `importScripts(${depsString})` -} + const depsString = deps.map((dep) => `'${dep}'`).toString(); + const depsFunctionString = localDeps + .filter((dep) => typeof dep === "function") + .map((fn) => { + const str = fn.toString(); + if (str.trim().startsWith("function")) { + return str; + } else { + const name = fn.name; + return `const ${name} = ${str}`; + } + }) + .join(";"); + const importString = `importScripts(${depsString});`; + return `${ + depsString.trim() === "" ? "" : importString + } ${depsFunctionString}`; +}; -export default remoteDepsParser +export default remoteDepsParser; diff --git a/packages/useWorker/src/useWorker.ts b/packages/useWorker/src/useWorker.ts index 1a0bfae..955975a 100644 --- a/packages/useWorker/src/useWorker.ts +++ b/packages/useWorker/src/useWorker.ts @@ -1,34 +1,36 @@ -import React from 'react' -import { useDeepCallback } from './hook/useDeepCallback' -import { AbortError } from './lib/abortError' -import createWorkerBlobUrl from './lib/createWorkerBlobUrl' -import WORKER_STATUS from './lib/status' +import React from "react"; +import { useDeepCallback } from "./hook/useDeepCallback"; +import { AbortError } from "./lib/abortError"; +import createWorkerBlobUrl from "./lib/createWorkerBlobUrl"; +import WORKER_STATUS from "./lib/status"; type WorkerController = { - status: WORKER_STATUS - kill: Function -} + status: WORKER_STATUS; + kill: Function; +}; export enum TRANSFERABLE_TYPE { - AUTO = 'auto', - NONE = 'none', + AUTO = "auto", + NONE = "none", } type Options = { - timeout?: number - remoteDependencies?: string[] - autoTerminate?: boolean - transferable?: TRANSFERABLE_TYPE -} - -const PROMISE_RESOLVE = 'resolve' -const PROMISE_REJECT = 'reject' + timeout?: number; + remoteDependencies?: string[]; + localDependencies?: Function[]; + autoTerminate?: boolean; + transferable?: TRANSFERABLE_TYPE; +}; + +const PROMISE_RESOLVE = "resolve"; +const PROMISE_REJECT = "reject"; const DEFAULT_OPTIONS: Options = { timeout: undefined, remoteDependencies: [], + localDependencies: [], autoTerminate: true, transferable: TRANSFERABLE_TYPE.AUTO, -} +}; /** * @@ -37,33 +39,35 @@ const DEFAULT_OPTIONS: Options = { */ export const useWorker = any>( fn: T, - options: Options = DEFAULT_OPTIONS, + options: Options = DEFAULT_OPTIONS ) => { const [workerStatus, setWorkerStatus] = React.useState( - WORKER_STATUS.PENDING, - ) - const worker = React.useRef() - const isRunning = React.useRef(false) + WORKER_STATUS.PENDING + ); + const worker = React.useRef(); + const isRunning = React.useRef(false); const promise = React.useRef<{ - [PROMISE_REJECT]?: (result: ReturnType | ErrorEvent | AbortError) => void - [PROMISE_RESOLVE]?: (result: ReturnType) => void - }>({}) - const timeoutId = React.useRef() + [PROMISE_REJECT]?: ( + result: ReturnType | ErrorEvent | AbortError + ) => void; + [PROMISE_RESOLVE]?: (result: ReturnType) => void; + }>({}); + const timeoutId = React.useRef(); const killWorker = React.useCallback(() => { if (worker.current?._url) { if (promise.current) { - promise.current[PROMISE_REJECT]?.(new AbortError()) + promise.current[PROMISE_REJECT]?.(new AbortError()); } - worker.current.terminate() - URL.revokeObjectURL(worker.current._url) - promise.current = {} - worker.current = undefined - isRunning.current = false - window.clearTimeout(timeoutId.current) + worker.current.terminate(); + URL.revokeObjectURL(worker.current._url); + promise.current = {}; + worker.current = undefined; + isRunning.current = false; + window.clearTimeout(timeoutId.current); } - }, []) + }, []); // biome-ignore lint/correctness/useExhaustiveDependencies: const onWorkerEnd = React.useCallback( @@ -71,128 +75,137 @@ export const useWorker = any>( const terminate = options.autoTerminate != null ? options.autoTerminate - : DEFAULT_OPTIONS.autoTerminate + : DEFAULT_OPTIONS.autoTerminate; if (terminate) { - killWorker() + killWorker(); } - setWorkerStatus(status) + setWorkerStatus(status); }, - [options.autoTerminate, killWorker, setWorkerStatus], - ) + [options.autoTerminate, killWorker, setWorkerStatus] + ); const generateWorker = useDeepCallback(() => { const { remoteDependencies = DEFAULT_OPTIONS.remoteDependencies, + localDependencies = DEFAULT_OPTIONS.localDependencies, timeout = DEFAULT_OPTIONS.timeout, transferable = DEFAULT_OPTIONS.transferable, - } = options + } = options; - const blobUrl = createWorkerBlobUrl(fn, remoteDependencies!, transferable!) - const newWorker: Worker & { _url?: string } = new Worker(blobUrl) - newWorker._url = blobUrl + const blobUrl = createWorkerBlobUrl( + fn, + remoteDependencies!, + localDependencies!, + transferable! + ); + const newWorker: Worker & { _url?: string } = new Worker(blobUrl); + newWorker._url = blobUrl; newWorker.onmessage = (e: MessageEvent) => { - const [status, result] = e.data as [WORKER_STATUS, ReturnType] + const [status, result] = e.data as [WORKER_STATUS, ReturnType]; switch (status) { case WORKER_STATUS.SUCCESS: - promise.current[PROMISE_RESOLVE]?.(result) - onWorkerEnd(WORKER_STATUS.SUCCESS) - break + promise.current[PROMISE_RESOLVE]?.(result); + onWorkerEnd(WORKER_STATUS.SUCCESS); + break; default: - promise.current[PROMISE_REJECT]?.(result) - onWorkerEnd(WORKER_STATUS.ERROR) - break + promise.current[PROMISE_REJECT]?.(result); + onWorkerEnd(WORKER_STATUS.ERROR); + break; } - } + }; newWorker.onerror = (e: ErrorEvent) => { - promise.current[PROMISE_REJECT]?.(e) - onWorkerEnd(WORKER_STATUS.ERROR) - } + promise.current[PROMISE_REJECT]?.(e); + onWorkerEnd(WORKER_STATUS.ERROR); + }; if (timeout) { timeoutId.current = window.setTimeout(() => { - killWorker() - setWorkerStatus(WORKER_STATUS.TIMEOUT_EXPIRED) - }, timeout) + killWorker(); + setWorkerStatus(WORKER_STATUS.TIMEOUT_EXPIRED); + }, timeout); } - return newWorker - }, [fn, options, killWorker]) + return newWorker; + }, [fn, options, killWorker]); // biome-ignore lint/correctness/useExhaustiveDependencies: const callWorker = React.useCallback( (...workerArgs: Parameters) => { - const { transferable = DEFAULT_OPTIONS.transferable } = options + const { transferable = DEFAULT_OPTIONS.transferable } = options; return new Promise>((resolve, reject) => { promise.current = { [PROMISE_RESOLVE]: resolve, [PROMISE_REJECT]: reject, - } + }; const transferList: any[] = transferable === TRANSFERABLE_TYPE.AUTO ? workerArgs.filter( (val: any) => - ('ArrayBuffer' in window && val instanceof ArrayBuffer) || - ('MessagePort' in window && val instanceof MessagePort) || - ('ImageBitmap' in window && val instanceof ImageBitmap) || - ('OffscreenCanvas' in window && - val instanceof OffscreenCanvas), + ("ArrayBuffer" in window && val instanceof ArrayBuffer) || + ("MessagePort" in window && val instanceof MessagePort) || + ("ImageBitmap" in window && val instanceof ImageBitmap) || + ("OffscreenCanvas" in window && + val instanceof OffscreenCanvas) ) - : [] + : []; - worker.current?.postMessage([[...workerArgs]], transferList) + worker.current?.postMessage([[...workerArgs]], transferList); - setWorkerStatus(WORKER_STATUS.RUNNING) - }) + setWorkerStatus(WORKER_STATUS.RUNNING); + }); }, - [setWorkerStatus], - ) + [setWorkerStatus] + ); const workerHook = React.useCallback( (...fnArgs: Parameters) => { const terminate = options.autoTerminate != null ? options.autoTerminate - : DEFAULT_OPTIONS.autoTerminate + : DEFAULT_OPTIONS.autoTerminate; if (isRunning.current) { console.error( - '[useWorker] You can only run one instance of the worker at a time, if you want to run more than one in parallel, create another instance with the hook useWorker(). Read more: https://github.com/alewin/useWorker', - ) - return Promise.reject() + "[useWorker] You can only run one instance of the worker at a time, if you want to run more than one in parallel, create another instance with the hook useWorker(). Read more: https://github.com/alewin/useWorker" + ); + return Promise.reject(); } if (terminate || !worker.current) { - worker.current = generateWorker() + worker.current = generateWorker(); } - return callWorker(...fnArgs) + return callWorker(...fnArgs); }, - [options.autoTerminate, generateWorker, callWorker], - ) + [options.autoTerminate, generateWorker, callWorker] + ); // biome-ignore lint/correctness/useExhaustiveDependencies: const killWorkerController = React.useCallback(() => { - killWorker() - setWorkerStatus(WORKER_STATUS.KILLED) - }, [killWorker, setWorkerStatus]) + killWorker(); + setWorkerStatus(WORKER_STATUS.KILLED); + }, [killWorker, setWorkerStatus]); const workerController = { status: workerStatus, kill: killWorkerController, - } + }; React.useEffect(() => { - isRunning.current = workerStatus === WORKER_STATUS.RUNNING - }, [workerStatus]) + isRunning.current = workerStatus === WORKER_STATUS.RUNNING; + }, [workerStatus]); React.useEffect( () => () => { - killWorker() + killWorker(); }, - [killWorker], - ) - - return [workerHook, workerController] as [typeof workerHook, WorkerController] -} + [killWorker] + ); + + return [workerHook, workerController] as [ + typeof workerHook, + WorkerController + ]; +};