{"id":22120178,"url":"https://github.com/joshbrew/threadop","last_synced_at":"2025-08-12T00:04:29.178Z","repository":{"id":189687686,"uuid":"681111890","full_name":"joshbrew/threadop","owner":"joshbrew","description":"Thread operations! Create multithreaded pipelines (with esm imports) in a single script file with a clear, minimal workflow.","archived":false,"fork":false,"pushed_at":"2024-07-02T07:25:44.000Z","size":3035,"stargazers_count":2,"open_issues_count":0,"forks_count":0,"subscribers_count":1,"default_branch":"main","last_synced_at":"2025-03-24T06:33:09.668Z","etag":null,"topics":["frontend","javascript","multithreading","thread","threadpool","webworker","workflow"],"latest_commit_sha":null,"homepage":"","language":"JavaScript","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"lgpl-3.0","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/joshbrew.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null}},"created_at":"2023-08-21T09:39:43.000Z","updated_at":"2024-07-02T07:25:47.000Z","dependencies_parsed_at":"2023-08-21T11:00:17.687Z","dependency_job_id":"d9c7fa0a-defd-4642-b1b3-ef2e65ffd3d7","html_url":"https://github.com/joshbrew/threadop","commit_stats":null,"previous_names":["joshbrew/threadop"],"tags_count":0,"template":false,"template_full_name":null,"purl":"pkg:github/joshbrew/threadop","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/joshbrew%2Fthreadop","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/joshbrew%2Fthreadop/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/joshbrew%2Fthreadop/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/joshbrew%2Fthreadop/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/joshbrew","download_url":"https://codeload.github.com/joshbrew/threadop/tar.gz/refs/heads/main","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/joshbrew%2Fthreadop/sbom","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":269976737,"owners_count":24506463,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","status":"online","status_checked_at":"2025-08-11T02:00:10.019Z","response_time":75,"last_error":null,"robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":true,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["frontend","javascript","multithreading","thread","threadpool","webworker","workflow"],"created_at":"2024-12-01T14:21:21.245Z","updated_at":"2025-08-12T00:04:29.100Z","avatar_url":"https://github.com/joshbrew.png","language":"JavaScript","readme":"## threadop\n\n`npm i threadop`\n\n![threadop-status](https://img.shields.io/npm/v/threadop.svg) \n![threadop-downloads](https://img.shields.io/npm/dt/threadop.svg)\n![threadop-l](https://img.shields.io/npm/l/threadop)\n\n### [Example](https://threadop.netlify.app)\n\n## Description\n\nPure (10kb minified) implementation of a Web Worker thread operation helper. For use in browser or with the web worker library in Nodejs\n\nCreate multithreaded pipelines (with esm imports) in a single script file with a clear, minimal workflow.\n\n- Instantiate a thread from a function that simply expects the event.data from the thread postMessage function\n- One-off or repeat use with easy cleanup.\n- Chain multiple workers with message port automation\n- Instantiate threadpools from a single function, chain multiple threadpools.\n- Create threads with lists of functions you can call by name. \n- Specify imports (local or remote) from strings or objects to use the full range of esm import abilities.\n- loops, animations, with propagation.\n- Dramatically increase program performance with easy parallelism! The time to instantiate a basic worker is ~0.1ms. \n\nInstantiate threads from generic functions (with imports!) or from URLS i.e. worker file locations or Blobs.\n\n## List of examples:\n\n#### [Example WONNX video processing app](https://github.com/joshbrew/cameraId-wonnx-wasm)\n\n- [Example 1: One-off usage](#example-1-one-off-usage)\n- [Example 2: Repeat operations](#example-2-repeat-operations)\n- [Example 3: Chaining workers](#example-3-chaining-workers)\n- [Example 4: local import](#example-4-local-import)\n- [Example 5: Web import with a specified library function](#example-5-web-import-with-a-specified-library-function)\n- [Example 6: Thread pool one-off](#example-6-thread-pool-one-off)\n- [Example 7: Thread pool chaining](#example-7-thread-pool-chaining)\n- [Example 8: Looping](#example-8-looping)\n- [Example 9: Canvas Animation](#example-9-canvas-animation)\n- [Example 10: Using dedicated worker files and dynamic imports for a parallelized FFT](#example-10-using-dedicated-worker-files-and-dynamic-imports-for-a-parallelized-fft)\n- [Example 11: WGSL shader for the DFT with re-use (way faster!), transfer buffers to/from](#example-11-wgsl-shader-for-the-dft-with-re-use-way-faster)\n\n\n## Usage\n\nImport `threadop` as a default import \n\n`import threadop from 'threadop'` \n\nOr as a method \n\n`import {threadop} from 'threadop'` \n\nOr access it as a global variable \n\n```html\n\u003chtml\u003e\n    \u003chead\u003e\n        \u003cscript src=\"https://cdn.jsdelivr.net/npm/threadop@latest\"\u003e\u003c/script\u003e\n    \u003c/head\u003e\n    \u003cbody\u003e\n        Hello World\n        \u003cscript\u003e\n            console.log(\"Hello world!\");\n            threadop(data =\u003e {return data*2}).then((thread) =\u003e {\n                thread.run(5).then(console.log);\n            }); //globalThis.threadop\n        \u003c/script\u003e  \n    \u003c/body\u003e\n\u003c/html\u003e\n```\n\nYou can also define the first input as the options object instead of the operation function. This is useful when you want to define a list of functions to call instead of a single operation, for even more complex communications.\n\n## Input Options\n\n```ts\n\n//all possible options\ntype ThreadOptions = {\n    operation?:string|Blob|((data)=\u003e(any|Promise\u003cany\u003e)), \n    imports?:ImportsInput, //ImportsInput\n    functions?:{[key:string]:Function},\n    message?:any, \n    transfer?:Transferable[], \n    port?:Worker|Worker[], \n    blocking?:boolean,\n    pool?:number,\n    loop?:number, //loop the function on a millisecond interval\n    animate?:boolean, //loop the function on an animation frame,\n    callback?:(data) =\u003e void\n};\n\ntype ModuleImport = {\n    [modulePath: string]: \n    | string   // Default import e.g. 'React'\n    | boolean  // Only import module without named imports\n    | {       // Named imports\n        [importName: string]: string | boolean;  // e.g. { useState: true, useEffect: 'useEff' }\n    };\n};\n\ntype ImportsInput = \n    | string             // Single import e.g. './module.js'\n    | string[]           // Multiple imports e.g. ['./mod1.js', './mod2.js']\n    | ModuleImport;      // Object describing imports e.g. { './mod.js': { useState: true } }\n\ntype WorkerHelper = {\n    run: (message: any, transfer?: Transferable[], overridePort?:boolean|number|string|'both') =\u003e Promise\u003cany\u003e;\n    set: (fn:string|Function, fnName?:string) =\u003e Promise\u003cstring\u003e;\n    call: (fnName:string, message: any, transfer?: Transferable[], overridePort?:boolean|number|string|'both') =\u003e Promise\u003cany\u003e;\n    terminate: () =\u003e void;\n    addPort: (port: Worker) =\u003e void;\n    addCallback: (callback?: (data: any) =\u003e void, oneOff?: boolean) =\u003e number;\n    removeCallback: (cb: number) =\u003e void;\n    setLoop: (interval, message, transfer) =\u003e void, //can provide arguments to send results on loop\n    setAnimation: (message, transfer) =\u003e void,      //run an animation function, e.g. transfer a canvas with parameters\n    stop: () =\u003e void, \n    worker: Worker;\n    id:number,\n    callbacks: {[key: number]: (data: any, cb?: number) =\u003e void};\n}\n\ntype WorkerPoolHelper = {\n    run: (message: any|any[], transfer?: (Transferable[])|((Transferable[])[]), overridePort?:boolean|number|string|'both', workerId?:number|string) =\u003e Promise\u003cany\u003e;\n    set: (fn:string|Function, fnName?:string) =\u003e Promise\u003cstring\u003e;\n    call: (fnName:string, message: any, transfer?: Transferable[], overridePort?:boolean|number|string|'both', workerId?:number|string) =\u003e Promise\u003cany\u003e;\n    terminate: (workerId?:number|string) =\u003e void;\n    addPort: (port: Worker, workerId?:number|string) =\u003e boolean|boolean[];\n    addCallback: (callback?: (data: any) =\u003e void, oneOff?: boolean, workerId?:number|string) =\u003e number|number[];\n    removeCallback: (cb: number, workerId?:number|string) =\u003e void;\n    addWorker:() =\u003e number;\n    setLoop: (interval, message, transfer, workerId?:number|string) =\u003e void, //provide arguments and run a function/send results on loop\n    setAnimation: (message, transfer, workerId?:number|string) =\u003e void, //run an animation function, e.g. transfer a canvas with parameters, or transmit results on a framerate-limited loop\n    stop: (workerId?:number|string) =\u003e void, \n    workers: {[key:string]:Worker};\n    helpers: {[key:string]:WorkerHelper};\n    keys: string[],\n    callbacks: {[key: number]: (data: any, cb?: number) =\u003e void};\n}\n\n//overloads\n// When the message is defined, the function returns a Promise\u003cany\u003e.\nfunction threadop(\n    operation?:string|Blob|((data)=\u003e(any|Promise\u003cany\u003e)), \n    options?: {\n        imports?: ImportsInput, \n        functions?:{[key:string]:Function},\n        message: any, \n        transfer?: Transferable[], \n        port?: Worker|Worker[], \n        blocking?: boolean,\n        loop?:number,\n        animate?:boolean,\n        callback?:(data) =\u003e void\n    }\n): Promise\u003cany\u003e;\n\n// When the message is defined and pool is defined, the function returns a Promise\u003cany[]\u003e.\nfunction threadop(\n    operation?:string|Blob|((data)=\u003e(any|Promise\u003cany\u003e)), \n    options?: {\n        imports?: ImportsInput, \n        functions?:{[key:string]:Function},\n        message: any|any[], //array inputs interpreted as per-thread inputs, can be longer than the number of threads\n        transfer?: Transferable[], \n        port?: Worker|Worker[], \n        blocking?: boolean,\n        pool:number,\n        loop?:number,\n        animate?:boolean,\n        callback?:(data) =\u003e void\n    }\n): Promise\u003cany[]\u003e;\n\n// When the message isn't defined, the function returns a Promise\u003cWorkerHelper\u003e.\nfunction threadop(\n    operation?:string|Blob|((data)=\u003e(any|Promise\u003cany\u003e)), \n    options?: {\n        imports?: ImportsInput, \n        functions?:{[key:string]:Function},\n        transfer?: Transferable[], \n        port?: Worker|Worker[], \n        blocking?: boolean,\n        loop?:number,\n        animate?:boolean,\n        callback?:(data) =\u003e void\n    }\n): Promise\u003cWorkerHelper\u003e;\n\n\n\n```\n\n## Examples\n\nThere is an `examples.html` file in the example/ folder in this repo, you can run it with the LiveServer extension in VSCode and look in the console to see it working. Below are all the examples tested.\n\nThere is also a subfolder called `example/npmproject` that you can run following the readme.\n\n\n### Example 1: One-off usage\n```js\n    /*\n        This is the simplest usage of threadop.\n\n        You have a function, workerFunction, that you want to run in a separate thread (i.e., Web Worker).\n        You send a single piece of data (5) to this worker, the worker multiplies this data by 2, and sends the result \n        (10) back to the main thread.\n        The function is executed once and the worker terminates after returning the result.\n                \n    */\n\n    //Define a function to run in the worker\n    const workerFunction = data =\u003e {\n        // Perform some operation on the data\n        console.log('Example 1: input', data);\n        return data * 2;\n    };\n\n    // Run the function with the threadop\n    threadop(workerFunction, {\n        message: 5, // Sending a one-off message\n    }).then(result =\u003e {\n        console.log('Example 1: result', result); // Expected: 10\n    }).catch(error =\u003e {\n        console.error('Example 1: error',error);\n    });\n```\n    \n### Example 2: Repeat operations\n```js\n    /*\n        This is an example of how to run multiple operations sequentially in a worker.\n\n        You initialize the worker with a function (workerFunction2) using threadop.\n        Once the worker is ready, you send it multiple pieces of data sequentially. Each piece of data is processed independently.\n        After all operations, the worker is terminated explicitly using the workerHelper.terminate() method. This is \n        important to ensure that we don't have lingering worker threads.\n    \n    */\n\n    const workerFunction2 = data =\u003e {\n        console.log('Example 2: input', data);\n        return data * 2;\n    };\n\n    threadop(workerFunction2).then(workerHelper =\u003e {\n        workerHelper.run(5).then(r1 =\u003e {\n            console.log('Example 2: r1', r1); // Expected: 10\n        });\n\n        \n        workerHelper.run(10).then(r2 =\u003e {\n            console.log('Example 2: r2', r2); // Expected: 20\n            workerHelper.terminate(); // Terminate the worker after you're done with it.\n        });\n\n    }).catch(error =\u003e {\n        console.error('Example 2: error', error);\n    });\n```   \n \n### Example 3: Chaining workers\n```js\n    /*\n        This example demonstrates how to chain two workers, meaning the output of one worker (workerFunctionA) is used \n        as the input for another worker (workerFunctionB).\n\n        Both workers are initialized independently.\n        A message port is set up between the two workers for chained communication.\n        The first worker (workerFunctionA) processes the data and sends its result to the second worker (workerFunctionB), \n        which processes the result further.\n        Both workers are terminated after processing, ensuring that no worker threads remain active.\n    \n    */\n\n    const workerFunctionA = data =\u003e {\n        console.log('Example 3: A input', data);\n        return data * 2;\n    };\n\n    const workerFunctionB = data =\u003e {\n        console.log('Example 3: B input', data);\n        return data + 3;\n    };\n\n    // Initialize first worker\n    threadop(workerFunctionA, {\n        blocking: true,\n    }).then(workerHelperA =\u003e {\n    \n        // Initialize second worker and set up message port for chained communication\n        threadop(workerFunctionB).then(workerHelperB =\u003e {\n            workerHelperA.addPort(workerHelperB.worker);\n\n            let ctr = 0;\n            workerHelperB.addCallback(result =\u003e {\n                console.log('Chain workerHelperB result', result); // Result from the chained worker operation                \n                ctr++;\n                if(ctr === 2) {\n                    workerHelperA.terminate();\n                    workerHelperB.terminate();\n                }\n                workerHelperA.run(10);\n            });\n\n            workerHelperA.run(5).then(result =\u003e {\n                console.log('Chain workerHelperA result', result); // Result from the chained worker operation\n            });\n\n            workerHelperA.run(5) //Blocked!\n        });\n\n    }).catch(error =\u003e {\n        console.error(error);\n    });\n```\n    \n### Example 4: local import\n\n```js\n    /*\n    \n        This is about loading and using external libraries/modules within the worker.\n\n        You want to perform some operations on data using the numjs library. So, before running the worker, \n        you specify that this library should be imported.\n        The imports option in threadop allows you to specify which external scripts or libraries the worker \n        should load before it begins execution.\n        The data is then sent to the worker, processed using the numjs functions, and the result is returned.\n\n    */\n\n    const computeMean = data =\u003e {\n        const nj = globalThis.nj;  // numjs is attached to globalThis within the worker context\n        let ndarray = nj.array(data);\n        return nj.mean(ndarray);\n    };\n\n    // Sample data\n    const data = [1, 2, 3, 4, 5];\n\n    threadop(computeMean, {\n        imports: './num.min.js', //['./num.min.js'] //or { './num.min.js':true } //use objects to get more fine grained, e.g. for a import url pass an object with specific module methods and alias strings or bools\n        message: data\n    }).then(result =\u003e {\n        console.log('Example 4: Mean:', result);\n    }).catch(error =\u003e {\n        console.error('Example 4: Error:', error);\n    });\n```\n    \n### Example 5: Web import with a specified library function\n\n```js   \n    const lodashop = data =\u003e {\n        return data.map(snakeCase)\n    };\n\n    // Sample data\n    const lodata = ['HelloWorld', 'left pad', 'ECMAScript'];\n\n    threadop(lodashop, {\n        imports: {[`https://cdn.skypack.dev/lodash@4`]:{snakeCase :true}}, //['./num.min.js'] //or { './num.min.js':true } //use objects to get more fine grained, e.g. for a import url pass an object with specific module methods and alias strings or bools\n        message: lodata\n    }).then(result =\u003e {\n        console.log('Example 5: Snake Case result:', result);\n    }).catch(error =\u003e {\n        console.error('Example 5: Error:', error);\n    });\n```\n\n\n### Example 6: Thread pool one-off\n\n```js\n    /**\n        The example demonstrates how to set up a thread pool to parallelly encode each string in an array into a sequence of bytes using the TextEncoder. \n        The results are then logged to the console, and any errors encountered during the process are caught and reported.\n    */\n\n    const threadpoolop = (stringdata) =\u003e {\n        if(!self.encoder) self.encoder = new TextEncoder();\n        return encoder.encode(stringdata);\n    }\n\n    let poolinput = ['Hello','World','My','Old','Friend'];\n\n    threadop(threadpoolop, {\n        pool:poolinput.length,\n        message:poolinput\n    }).then(result =\u003e {\n        console.log('Example 6: Threadpool result:', result, 'input:', poolinput);\n    }).catch(error =\u003e {\n        console.error('Example 5: Error:', error);\n    });\n\n```\n\n\n### Example 7: Thread pool chaining\n\n\n```js\n\n    /*\n        This example showcases how to chain two threadpools, where the output of the first serves as the input to the second. \n        It encodes and then reverses a list of strings. After both operations, the results are re-collected, sorted, and then displayed. \n        In the event of an error in this process, an error message is displayed.\n    */\n\n    const encodeOperation = stringdata =\u003e {\n        // Simulate the encoding operation.\n        //console.log('Example 7 step 1 input', stringdata);\n        let encoded = btoa(stringdata);\n        return {input:stringdata, encoded};\n    }\n\n    const reverseOperation = encodedData =\u003e {\n        // Simulate the reversing operation.\n        //console.log('Example 7 step 2 input', encodedData);\n        let reversed = encodedData.encoded.split(\"\").reverse().join(\"\");\n        return {reversed, input:encodedData.input};\n    }\n    \n    let poolinput = ['Hello','World','My','Old','Friend'];\n\n    // First threadpool to encode the strings. Second to reverse. This is best for async batch processes while you need to implement \n    //something to re-collect results if trying to break up a single problem \n    threadop(reverseOperation, {\n        pool: poolinput.length\n    }).then((pool1) =\u003e {\n\n        return new Promise((res,rej) =\u003e {\n\n            let results = [];\n\n            pool1.addCallback((data) =\u003e {\n                results.push(data);\n                if(results.length == poolinput.length) {//got all our data back\n                    let sortedOutput = [];\n                    poolinput.map((inp,i) =\u003e {\n                        sortedOutput[i] = results.find((v) =\u003e v.input === inp).reversed;\n                    });//we're sorting because pool1 responds asynchronously and we need to check the output order if it's important for the problem\n                    console.log('Example 7: threadpool chain output', sortedOutput, \"\\nRe-decoded:\", sortedOutput.map((v)=\u003e{return atob(v.split(\"\").reverse().join(\"\"));}))// sortedOutput.map(atob));\n                    pool1.terminate();\n                    res(sortedOutput);\n                }\n                \n            });\n\n            console.log(\"Example 7: threadpool chain input\", poolinput);\n            \n            // Using ports, pass the encoded strings from the first pool to the second pool.\n            // Second threadpool to reverse the encoded strings.\n            threadop(encodeOperation, {\n                pool: poolinput.length,\n                message: poolinput,\n                port: Object.values(pool1.workers)\n            });\n        })\n        \n\n    }).catch(error =\u003e {\n        console.error('Example 7: Error:', error);\n    }); //should pass the result to pool1\n\n```\n\n\n### Example 8: Looping\nExecute repeating operations with a set input\n\n```js\nthreadop(\n    (data) =\u003e `Processed ${data}, ${new Date().toLocaleString()}`,\n    {\n        message:\"ABC123\",\n        loop:1000,\n        callback:(data) =\u003e {console.log(data)},\n    }\n).then((helper) =\u003e {\n    setTimeout(() =\u003e {helper.stop()},5000);\n});\n```\n\n\n\n### Example 9: Canvas Animation\nThis isn't really the best way to use this library but we included it for the hell of it.\n\n```js\n\nconst canvas = document.createElement('canvas');\ncanvas.width = 800;\ncanvas.height = 600;\n\ncanvas.style.width = '100%';\ncanvas.style.height = '100%';\ncanvas.style.position = 'absolute';\ndocument.body.appendChild(canvas);\n\nconst offscreen = canvas.transferControlToOffscreen();\n//document.body.appendChild(canvas);\n\nthreadop(\n    (data) =\u003e {\n        if(data.canvas \u0026\u0026 !self.canvas) { //setup\n            const canvas = data.canvas;\n            const ctx = data.canvas.getContext(\"2d\");\n            self.canvas = canvas;\n            self.ctx = ctx;\n\n            let gradientColors = [\n                'red', 'orange', 'yellow', 'green', 'blue', 'indigo', 'violet'\n            ];\n            \n            let offset = 0;\n\n            self.drawWave = () =\u003e {\n                ctx.clearRect(0, 0, canvas.width, canvas.height);\n            \n                // Create gradient\n                let gradient = ctx.createLinearGradient(0, canvas.height / 2,  canvas.width, canvas.height / 2);\n                gradientColors.forEach((color, index) =\u003e {\n                    gradient.addColorStop(index / (gradientColors.length - 1), color);\n                });\n            \n                ctx.fillStyle = gradient;\n                \n                const waveHeight = 100;\n                const waveLength = 0.01;\n                const speed = 0.04;\n            \n                ctx.beginPath();\n            \n                for(let x = 0; x \u003c  canvas.width; x++) {\n                    let y =  canvas.height / 2 + waveHeight * Math.sin(waveLength * x + offset);\n                    ctx.lineTo(x, y);\n                }\n            \n                ctx.lineTo(canvas.width, canvas.height);\n                ctx.lineTo(0, canvas.height);\n                ctx.closePath();\n            \n                ctx.fill();\n\n                offset -= speed;\n            }\n        } else if(data.width \u0026\u0026 self.canvas?.width !== data.width) {\n            console.log(\"resized!\");\n            self.canvas.width = data.width;\n            self.canvas.height = data.height;\n        }\n\n        self.drawWave();\n    },\n    {\n        animate:true,\n        message:{canvas:offscreen},\n        transfer:[offscreen]\n    }\n).then((helper) =\u003e {\n    window.onresize = (ev) =\u003e {\n        helper.setAnimation({width:window.innerWidth, height:window.innerHeight});\n    }\n});\n\n```\n\n\n### Example 10 Using dedicated worker files and dynamic imports for a parallelized FFT\n\nThis isn't exactly a *good* parallel implementation but it's about 30% faster than a single dedicated thread for the divide and conquer FFT\n\n\n```js\n\n\nfunction nextPowerOf2(n) {\n    let count = 0;\n    if (n \u0026\u0026 !(n \u0026 (n - 1))) return n;\n    while(n != 0) {\n        n \u003e\u003e= 1;\n        count += 1;\n    }\n    return 1 \u003c\u003c count;\n}\n\n// Cooley-Tukey radix-2 FFT\nasync function threadfft(input) {\n    const N = input.length;\n\n    if (N \u003c= 1) return input;\n\n    // Decomposition into even and odd components\n    const even = [];\n    const odd = [];\n    for (let i = 0; i \u003c N; i += 2) {\n        even.push(input[i]);\n        if ((i + 1) \u003c N) {\n            odd.push(input[i + 1]);\n        }\n    }\n\n    let E, O;\n    if(even.length \u003e= 7500) { // 25% faster\n        [E,O] = await new Promise((res) =\u003e {\n            import(location.origin+'/lib/threadop.esm.js').then(async (module) =\u003e { \n                res(await Promise.all(\n                    [\n                        module.threadop(threadfft,{message:even}),\n                        module.threadop(threadfft,{message:odd})\n                    ]\n                ));\n            });\n        })\n        \n    } else {\n        [E, O] = [await threadfft(even), await threadfft(odd)];\n    }\n\n    const T = new Array(N);\n    for (let k = 0; k \u003c N / 2; k++) {\n        const angle = -2 * Math.PI * k / N;\n\n        // complex multiplication for e^(-j*angle) and O[k]\n        const tReal = Math.cos(angle) * O[k].real - Math.sin(angle) * O[k].imag;\n        const tImag = Math.cos(angle) * O[k].imag + Math.sin(angle) * O[k].real;\n\n        // complex addition for E[k] and t\n        T[k] = {\n            real: E[k].real + tReal,\n            imag: E[k].imag + tImag\n        };\n\n        // complex subtraction for E[k] and t\n        T[k + N / 2] = {\n            real: E[k].real - tReal,\n            imag: E[k].imag - tImag\n        };\n    }\n    return T//res(T);\n \n}\n\n\nasync function parallelFFT(input, sampleRate = 44100, frequencyResolution = 1, dedicatedThreadFile=true) {\n    const N = input.length;\n\n    // Calculate the number of samples required for the desired resolution\n    const M = Math.max(nextPowerOf2(N), Math.ceil(sampleRate / frequencyResolution));\n    \n    // Pad the input with zeros if needed\n    while (input.length \u003c M) {\n        input.push({real: 0, imag: 0});\n    }\n\n    if (N \u003c= 1) return input;\n    \n    // Wait for the results from both threads\n    //const T = await threadop(threadfft, {message: input});\n    const T = await threadop(dedicatedThreadFile ? './dist/fft.thread.js' : threadfft, {message: input});\n   \n    // Calculate the magnitudes\n    const magnitudes = T.map(bin =\u003e Math.sqrt(bin.real * bin.real + bin.imag * bin.imag)/M); //not scaling perfectly\n\n    // Calculate mid point\n    const midPoint = Math.floor(M/2);\n\n    // Order the magnitudes from -Nyquist to Nyquist\n    let orderedMagnitudes = new Array(M);\n    for(let i = 0; i \u003c M; i++) {\n        if(i \u003c midPoint) {\n            orderedMagnitudes[midPoint + i] = magnitudes[i];\n        } else {\n            orderedMagnitudes[i - midPoint] = magnitudes[i];\n        }\n    }\n\n    // Frequencies from -Nyquist to Nyquist\n    let orderedFreqs = [...Array(M).keys()].map(i =\u003e (i - midPoint) * sampleRate / M);\n\n    return {\n        amplitudes: orderedMagnitudes,\n        freqs: orderedFreqs\n    };\n}\n\n\n\n//Test\n\nimport './lib/plotly-latest.min.js'; //local import\ndocument.body.insertAdjacentHTML('beforeend',`\u003cdiv id=\"plot\"\u003e\u003c/div\u003e`);\ndocument.body.insertAdjacentHTML('beforeend',`\u003cdiv id=\"plot2\"\u003e\u003c/div\u003e`);\n\n// Create a sine wave\nconst sampleRate = 44100;\nconst frequency = 2500; // Frequency of A4 note\nconst duration = 1; // seconds\nconst amplitude = 0.5;\nlet sineWave = [];\nlet sineWave2 = [];\n\nfor (let i = 0; i \u003c sampleRate * duration; i++) {\n    const value = {\n        real: amplitude * Math.sin(2 * Math.PI * frequency * i / sampleRate),\n        imag: 0\n    };\n    sineWave.push(value);\n    sineWave2.push(value.real);\n}\n\n\n\nconsole.time('parallelFFT with file (CPU)')\nparallelFFT(sineWave, sampleRate, 1).then(output =\u003e {\n    console.timeEnd('parallelFFT with file (CPU)');\n    const freqs = output.freqs;\n    const amplitudes = output.amplitudes;\n\n    const trace = {\n        x: freqs,\n        y: amplitudes,\n        type: 'line',\n        name: 'Amplitude Spectrum'\n    };\n\n    const layout = {\n        title: 'Threaded 2-radix FFT Output',\n        xaxis: {\n            title: 'Frequency (Hz)'\n        },\n        yaxis: {\n            title: 'Amplitude'\n        }\n    };\n\n    globalThis.Plotly.newPlot('plot', [trace], layout);\n\n    setTimeout(()=\u003e{\n    console.time('parallelFFT with dynamic importing function (CPU)')\n    parallelFFT(sineWave, sampleRate, 1, false).then(output =\u003e {\n        console.timeEnd('parallelFFT with dynamic importing function (CPU)');\n    });\n    },1000);\n});\n\n\n```\n\nPlus a dedicated worker file to test against the dynamic importing\n\n##### fft.thread.js (built to dist/fft.thread.js)\n```js\n\nimport {threadop, initWorker} from './lib/threadop.esm.js'\n\n// Cooley-Tukey radix-2 FFT\nasync function threadfft(input) {\n    const N = input.length;\n\n    if (N \u003c= 1) return input;\n\n    // Decomposition into even and odd components\n    const even = [];\n    const odd = [];\n    for (let i = 0; i \u003c N; i += 2) {\n        even.push(input[i]);\n        if ((i + 1) \u003c N) {\n            odd.push(input[i + 1]);\n        }\n    }\n\n    let E, O;\n    if(even.length \u003e= 7500) { // 25% faster\n        [E,O] = await Promise.all(\n            [\n                threadop('./fft.thread.js',{message:even}),\n                threadop('./fft.thread.js',{message:odd})\n            ]\n        );\n        \n    } else {\n        [E, O] = [await threadfft(even), await threadfft(odd)];\n    }\n\n    const T = new Array(N);\n    for (let k = 0; k \u003c N / 2; k++) {\n        const angle = -2 * Math.PI * k / N;\n\n        // complex multiplication for e^(-j*angle) and O[k]\n        const tReal = Math.cos(angle) * O[k].real - Math.sin(angle) * O[k].imag;\n        const tImag = Math.cos(angle) * O[k].imag + Math.sin(angle) * O[k].real;\n\n        // complex addition for E[k] and t\n        T[k] = {\n            real: E[k].real + tReal,\n            imag: E[k].imag + tImag\n        };\n\n        // complex subtraction for E[k] and t\n        T[k + N / 2] = {\n            real: E[k].real - tReal,\n            imag: E[k].imag - tImag\n        };\n    }\n    return T//res(T);\n \n}\n\ninitWorker(threadfft);\n\n\n\n```\n\n\n\n### Example 11: WGSL shader for the DFT with re-use (way faster!)\n\n```js\n// Example 11: WGSL shader for the DFT with re-use (way faster!), transfer buffers to/from\n\nasync function WGSLDFT({inputArray, sampleRate, frequencyResolution}) {\n    if(!self.DFT) {\n        class DFTProcessor {\n\n            static async create(device = null) {\n                if (!device) {\n                    const gpu = navigator.gpu;\n                    const adapter = await gpu.requestAdapter();\n                    device = await adapter.requestDevice();\n                }\n                const processor = new DFTProcessor();\n                await processor.init(device);\n                return processor;\n            }\n        \n            async init(device=null) {\n                this.device = device;\n                this.bindGroupLayout = this.device.createBindGroupLayout({\n                    entries: [\n                        {\n                            binding: 0,\n                            visibility: GPUShaderStage.COMPUTE,\n                            buffer: {\n                                type: 'read-only-storage'\n                            }\n                        },\n                        {\n                            binding: 1,\n                            visibility: GPUShaderStage.COMPUTE,\n                            buffer: {\n                                type: 'storage'\n                            }\n                        }\n                    ]\n                });\n        \n                this.pipelineLayout = this.device.createPipelineLayout({\n                    bindGroupLayouts: [this.bindGroupLayout]\n                });\n        \n                this.shaderModule = this.device.createShaderModule({\n                    code: `\n                \n        struct InputData {\n            values : array\u003cf32\u003e\n        }\n        \n        struct OutputData {\n            values: array\u003cf32\u003e\n        }\n        \n        @group(0) @binding(0)\n        var\u003cstorage, read\u003e inputData: InputData;\n        \n        @group(0) @binding(1)\n        var\u003cstorage, read_write\u003e outputData: OutputData;\n        \n        @compute @workgroup_size(256)\n        fn main(\n            @builtin(global_invocation_id) globalId: vec3\u003cu32\u003e\n        ) {\n            let N = arrayLength(\u0026inputData.values);\n            let k = globalId.x;\n            var sum = vec2\u003cf32\u003e(0.0, 0.0);\n        \n            for (var n = 0u; n \u003c N; n = n + 1u) {\n                let phase = 2.0 * 3.14159265359 * f32(k) * f32(n) / f32(N);\n                sum = sum + vec2\u003cf32\u003e(\n                    inputData.values[n] * cos(phase),\n                    -inputData.values[n] * sin(phase)\n                );\n            }\n        \n            let outputIndex = k * 2;\n            if (outputIndex + 1 \u003c arrayLength(\u0026outputData.values)) {\n                outputData.values[outputIndex] = sum.x;\n                outputData.values[outputIndex + 1] = sum.y;\n            }\n        }\n        \n        `\n                });\n        \n                this.computePipeline = this.device.createComputePipeline({\n                    layout: this.pipelineLayout,\n                    compute: {\n                        module: this.shaderModule,\n                        entryPoint: 'main'\n                    }\n                });\n            }\n        \n            process(inputArray, sampleRate, frequencyResolution) {\n        \n                //recycle buffers when input sizes are the same\n                if (!this.inputData || this.inputData.byteLength !== inputArray.byteLength) {\n                    this.inputData = this.device.createBuffer({\n                        size: inputArray.byteLength,\n                        usage: GPUBufferUsage.STORAGE | GPUBufferUsage.COPY_SRC,\n                        mappedAtCreation: true\n                    });\n                    new Float32Array(this.inputData.getMappedRange()).set(inputArray);\n                    this.inputData.unmap();\n\n                    this.outputData = this.device.createBuffer({\n                        size: inputArray.byteLength * 2,\n                        usage: GPUBufferUsage.STORAGE | GPUBufferUsage.COPY_DST | GPUBufferUsage.COPY_SRC\n                    });\n\n                    this.bindGroup = this.device.createBindGroup({\n                        layout: this.bindGroupLayout,\n                        entries: [\n                            {\n                                binding: 0,\n                                resource: {\n                                    buffer: this.inputData\n                                }\n                            },\n                            {\n                                binding: 1,\n                                resource: {\n                                    buffer: this.outputData\n                                }\n                            }\n                        ]\n                    });\n                } else {\n                    const array = new Float32Array(this.inputData.getMappedRange());\n                    array.set(inputArray);\n                    this.inputData.unmap();\n                }\n        \n                const commandEncoder = this.device.createCommandEncoder();\n                const passEncoder = commandEncoder.beginComputePass();\n        \n                passEncoder.setPipeline(this.computePipeline);\n                passEncoder.setBindGroup(0, this.bindGroup);\n                passEncoder.dispatchWorkgroups(Math.ceil(inputArray.length / 64));\n        \n                passEncoder.end();\n        \n                const stagingBuffer = this.device.createBuffer({\n                    size: this.outputData.size,\n                    usage: GPUBufferUsage.MAP_READ | GPUBufferUsage.COPY_DST\n                });\n        \n                commandEncoder.copyBufferToBuffer(\n                    this.outputData, 0,\n                    stagingBuffer, 0,\n                    this.outputData.size\n                );\n        \n        \n                this.device.queue.submit([commandEncoder.finish()]);\n        \n                return new Promise((res) =\u003e {\n                    stagingBuffer.mapAsync(GPUMapMode.READ).then(() =\u003e {\n                        const mappedRange = stagingBuffer.getMappedRange();\n                        const rawResults = new Float32Array(mappedRange); \n                        const copiedResults = new Float32Array(rawResults.length);\n                        \n                        copiedResults.set(rawResults); // Fast copy\n        \n                        stagingBuffer.unmap();\n\n                        res({message:copiedResults, transfer:[copiedResults.buffer]}); //specific output format to trigger transferables \n                    });\n                })\n                \n            }\n        }\n        \n\n        self.DFT = await DFTProcessor.create();\n    }\n\n    return self.DFT.process(inputArray, sampleRate, frequencyResolution);\n}\n\nconst inputArray = new Float32Array(sampleRate); // 1 second of samples\nfor (let i = 0; i \u003c sampleRate; i++) {\n    inputArray[i] = amplitude * Math.sin(2 * Math.PI * frequency * i / sampleRate); // 440Hz\n}\nconst inp2 = new Float32Array(inputArray);\n\nconsole.time('WGSL DFT Thread')\nthreadop(WGSLDFT).then((helper) =\u003e {\n    helper.run({inputArray, sampleRate, frequencyResolution:1},[inputArray.buffer]).then((output) =\u003e {\n        console.timeEnd('WGSL DFT Thread')\n        //console.log('WGSLDFT Result', output); //unordered results\n\n        //console.log(rawResults)\n        function rearrangeDFTOutput(output) {\n            const halfLength = output.length / 2;\n            const rearranged = new Float32Array(output.length);\n\n            // Copy the negative frequencies (second half of the output) to the beginning of the rearranged array\n            rearranged.set(output.subarray(halfLength), 0);\n\n            // Copy the positive frequencies (first half of the output) to the end of the rearranged array\n            rearranged.set(output.subarray(0, halfLength), halfLength);\n\n            return rearranged;\n        }\n\n        const rearrangedResults = rearrangeDFTOutput(output);\n        \n        // Compute the magnitude of the results\n        const magnitudes = [];\n        for (let i = 0; i \u003c rearrangedResults.length; i += 2) {\n            const real = rearrangedResults[i];\n            const imag = rearrangedResults[i + 1];\n            const magnitude = 4 * Math.sqrt(real * real + imag * imag) / rearrangedResults.length;\n            magnitudes.push(magnitude);\n        }\n\n        const frequencyBins = [];\n\n        const numSamples = output.length / 2;\n        const deltaF = sampleRate / numSamples; // Frequency resolution\n        for (let i = 0; i \u003c numSamples / 2; i++) {\n            frequencyBins[i] = -sampleRate / 2 + i * deltaF;\n        }\n        for (let i = numSamples / 2; i \u003c numSamples; i++) {\n            frequencyBins[i] = deltaF * (i - numSamples / 2);\n        }\n\n        const trace = {\n            x: frequencyBins,\n            y: magnitudes,\n            type: 'line'\n        };\n\n        document.body.insertAdjacentHTML('beforeend',`\u003cdiv id=\"plot3\"\u003e\u003c/div\u003e`);\n\n        const layout = {\n            title: 'DFT Magnitude Spectrum',\n            xaxis: {\n                title: 'Frequency (Hz)'\n            },\n            yaxis: {\n                title: 'Magnitude'\n            }\n        };\n    \n        Plotly.newPlot('plot3', [trace], layout);\n\n\n        console.time('WGSL DFT Thread Run 2')\n        helper.run({inputArray:inp2, sampleRate, frequencyResolution:1}, [inp2.buffer]).then((output) =\u003e {\n            console.timeEnd('WGSL DFT Thread Run 2')\n            helper.terminate();\n        });\n    });\n});\n\n\n\n```\n","funding_links":[],"categories":[],"sub_categories":[],"project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fjoshbrew%2Fthreadop","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fjoshbrew%2Fthreadop","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fjoshbrew%2Fthreadop/lists"}