├── .editorconfig ├── .github └── workflows │ └── main.yml ├── .gitignore ├── .npmrc ├── .prettierignore ├── .prettierrc ├── README.md ├── benchmarks ├── .helix │ └── languages.toml ├── _bm.d.ts ├── _bm.js ├── _util.mjs ├── bench.mjs ├── fasta.mjs ├── nt-file.mjs ├── nt-inline-new.mjs ├── nt-inline.mjs ├── package.json ├── pi.mjs ├── secp256k.mjs ├── threadsjs.mjs ├── tinypool.mjs ├── utils │ ├── index.mjs │ ├── runner.mjs │ ├── setup.mjs │ └── sleep.mjs └── workers │ ├── nanothreads.mjs │ ├── threads-js.js │ └── tinypool.js ├── build.mjs ├── docs ├── .nojekyll ├── assets │ ├── highlight.css │ ├── main.js │ ├── search.js │ └── style.css ├── classes │ ├── InlineThread.html │ ├── PromisePool.html │ ├── Queue.html │ ├── Thread.html │ ├── ThreadImpl.html │ └── ThreadPool.html ├── enums │ └── StatusCode.html ├── functions │ ├── workerInit.html │ └── yieldMicrotask.html ├── index.html ├── interfaces │ ├── IThread.html │ ├── IThreadOptions.html │ └── ThreadError.html ├── types │ ├── AnyThread.html │ ├── GetReturnType.html │ ├── IWorkerImpl.html │ ├── MaybePromise.html │ ├── ThreadArgs.html │ ├── ThreadPoolParams.html │ └── WorkerThreadFn.html └── variables │ └── browser.html ├── index.cjs.js ├── index.mts ├── package.json ├── scripts └── barrel.mjs ├── src ├── global.d.ts ├── index.ts ├── internals │ ├── NodeWorker-cjs.js │ ├── NodeWorker-esm.mjs │ ├── NodeWorker.ts │ ├── index.ts │ └── utils.ts ├── models │ ├── error.ts │ ├── index.ts │ ├── statuses.ts │ └── thread.ts ├── sync │ ├── index.ts │ ├── promisePool.ts │ └── queue.ts ├── threads │ ├── channel-cjs.js │ ├── channel-esm.mjs │ ├── channel.ts │ ├── index.ts │ ├── pool.ts │ ├── thread.ts │ └── transferable.ts └── worker │ ├── index.ts │ └── init.ts ├── tsconfig.json ├── tsconfig.node.json ├── typedoc.json └── worker.mjs /.editorconfig: -------------------------------------------------------------------------------- 1 | # EditorConfig is awesome: https://EditorConfig.org 2 | 3 | # top-most EditorConfig file 4 | root = true 5 | 6 | [*] 7 | end_of_line = lf 8 | insert_final_newline = true 9 | indent_style = tab 10 | indent_size = 2 11 | charset = utf-8 12 | trim_trailing_whitespace = true 13 | -------------------------------------------------------------------------------- /.github/workflows/main.yml: -------------------------------------------------------------------------------- 1 | name: Threadpool Benchmark 2 | on: 3 | workflow_dispatch: 4 | push: 5 | branches: 6 | - master 7 | 8 | permissions: 9 | contents: write 10 | deployments: write 11 | 12 | jobs: 13 | benchmark: 14 | name: Run JavaScript benchmark example 15 | runs-on: ubuntu-latest 16 | steps: 17 | - uses: actions/checkout@v3 18 | - uses: pnpm/action-setup@v2 19 | with: 20 | version: "latest" 21 | - uses: actions/setup-node@v3 22 | with: 23 | node-version: "18" 24 | - name: Run benchmark 25 | run: 26 | pnpm install && pnpm run build && cd benchmarks && pnpm install && pnpm run run:threadpool | tee ./output.txt 27 | 28 | - name: Store benchmark result 29 | uses: benchmark-action/github-action-benchmark@v1 30 | with: 31 | name: Threadpool Benchmark 32 | tool: "benchmarkjs" 33 | output-file-path: benchmarks/output.txt 34 | github-token: ${{ secrets.GITHUB_TOKEN }} 35 | auto-push: true 36 | # Show alert with commit comment on detecting possible performance regression 37 | alert-threshold: "200%" 38 | comment-on-alert: true 39 | fail-on-alert: true 40 | alert-comment-cc-users: "@snuffyDev" 41 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | pnpm-debug.log* 8 | lerna-debug.log* 9 | 10 | node_modules 11 | dist 12 | dist-ssr 13 | *.local 14 | 15 | # Editor directories and files 16 | .vscode/* 17 | !.vscode/extensions.json 18 | .idea 19 | .DS_Store 20 | *.suo 21 | *.ntvs* 22 | *.njsproj 23 | *.sln 24 | *.sw? 25 | 26 | pnpm-lock.yaml 27 | .lapce/* 28 | .future 29 | index.js 30 | .gitignore 31 | NodeWorker-cjs.ts 32 | NodeWorker-esm.mts 33 | BACKUP.old 34 | browser-test/* 35 | -------------------------------------------------------------------------------- /.npmrc: -------------------------------------------------------------------------------- 1 | strict-peer-dependencies=false 2 | legacy-peer-deps=true 3 | -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | pnpm-lock.yaml 2 | node_modules/** 3 | /dist/** 4 | .vscode/** 5 | docs/** 6 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "tabWidth": 2, 3 | "useTabs": true, 4 | "trailingComma": "all", 5 | "singleQuote": false, 6 | "semi": true, 7 | "printWidth": 120, 8 | "proseWrap": "always", 9 | "bracketSpacing": true, 10 | "quoteProps": "as-needed", 11 | "singleAttributePerLine": true, 12 | "arrowParens": "always" 13 | } 14 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # nanothreads 2 | 3 | A super-fast, super-powerful Worker-based multi-threading library for the browser and Node.js. 4 | 5 | Nanothreads is only ~2 KB for browsers, ~2.3 KB for Node.js, making it a _super_ tiny alternative to 6 | [tinypool](https://github.com/tinylibs/tinypool), [threads.js](https://github.com/andywer/threads.js) and others! 7 | 8 | **[\[Benchmarks\]](https://snuffydev.github.io/nanothreads/dev/bench/index.html)** | 9 | **[\[Docs\]](https://snuffydev.github.io/nanothreads/docs/index.html)** 10 | 11 | ## Overview 12 | 13 | - Zero-dependencies :x: 14 | - Tiny bundle size! :see_no_evil: 15 | - 100% Fully Typed :100: 16 | - Super fast, super efficient :fire: 17 | - Check out the [Historical Benchmarks]() or the [Benchmarks](#benchmarks) section of the README for more info 18 | - Works both in the browser, and Node :eyes: 19 | 20 | ### Install 21 | 22 | ``` 23 | npm install nanothreads 24 | 25 | pnpm add nanothreads 26 | 27 | yarn add nanothreads 28 | ``` 29 | 30 | ### Basic Usage 31 | 32 | #### Importing 33 | 34 | ```ts 35 | // Browsers 36 | import { ThreadPool } from "nanothreads/browser"; 37 | 38 | // Node.js 39 | import { ThreadPool } from "nanothreads"; 40 | ``` 41 | 42 | > Note: Browsers must specify the import path as `nanothreads/browser`. 43 | 44 | #### Kitchen Sink 45 | 46 | ```ts 47 | import { InlineThread, Thread, ThreadPool } from "nanothreads"; 48 | 49 | type Quote = { 50 | quote: string; 51 | }; 52 | 53 | // Inline Thread 54 | const inline_thread = new InlineThread<[name: string], string>((name) => { 55 | return "Hello " + name; 56 | }); 57 | 58 | // Thread from a script 59 | const thread = new Thread("./worker.ts"); 60 | 61 | // Thread Pool from an inlined function 62 | const pool = new ThreadPool({ 63 | task: (url) => { 64 | return fetch(url) 65 | .then((res) => res.json()) 66 | .then((json) => json as Quote); 67 | }, 68 | count: 5, // number of threads = 5 69 | }); 70 | 71 | // Using the thread pool 72 | for (let idx = 0; idx < 10; idx++) { 73 | pool.exec("https://api.kanye.rest").then((quote) => { 74 | // output: "{ quote: "Man... whatever happened to my antique fish tank?" };" 75 | console.log(JSON.stringify(quote)); 76 | }); 77 | } 78 | 79 | const greetings = await inline_thread.send("Kanye"); // output: "Hello Kanye" 80 | 81 | const my_number = await thread.send(4); // output: 8 82 | 83 | // Cleanup when done! 84 | await thread.terminate(); 85 | await inline_thread.terminate(); 86 | await pool.terminate(); 87 | ``` 88 | 89 | ### Documentation 90 | 91 | API Documentation can be found here: 92 | [snuffydev.github.io/nanothreads/docs](https://snuffydev.github.io/nanothreads/docs/index.html), or in the `/docs` 93 | directory on GitHub. 94 | 95 | ### Benchmarks 96 | 97 | You can find the historical benchmarks [here](). 98 | 99 | Provided below is the results from my own machine (Intel i7-4700MQ, on Arch Linux): 100 | 101 | ``` 102 | ## Throughput test (don't await - just execute) 103 | nanothreads ([inline] threadpool) x 895,733 ops/sec ±5.75% (68 runs sampled) 104 | nanothreads ([file] threadpool) x 932,900 ops/sec ±5.10% (69 runs sampled) 105 | tinypool x 355,282 ops/sec ±21.83% (50 runs sampled) 106 | threads.js (threadpool) x 1,618 ops/sec ±56.60% (9 runs sampled) 107 | 108 | ## Complete operations (await the result) 109 | 110 | Running "nanothreads inline" suite... 111 | fasta x 17.85 ops/sec ±2.77% (83 runs sampled) 112 | Running "nanothreads file" suite... 113 | "fasta x 18.03 ops/sec ±2.39% (83 runs sampled)" 114 | Running "tinypool" suite... 115 | "fasta x 9.23 ops/sec ±1.91% (47 runs sampled)" 116 | Running "threads.js" suite... 117 | "fasta x 15.98 ops/sec ±1.59% (76 runs sampled)" 118 | 119 | Running "nanothreads inline" suite... 120 | fasta x 18.34 ops/sec ±2.06% (85 runs sampled) 121 | Running "nanothreads file" suite... 122 | "fasta x 18.63 ops/sec ±1.65% (86 runs sampled)" 123 | Running "tinypool" suite... 124 | "fasta x 9.60 ops/sec ±2.02% (49 runs sampled)" 125 | Running "threads.js" suite... 126 | "fasta x 15.29 ops/sec ±2.22% (73 runs sampled)" 127 | 128 | ``` 129 | -------------------------------------------------------------------------------- /benchmarks/.helix/languages.toml: -------------------------------------------------------------------------------- 1 | [[language]] 2 | name = "typescript" 3 | scope = "source.typescript" 4 | file-types = ["ts", "tsx", "js", "mjs", "cjs"] 5 | language-server = { command = "typescript-language-server", args = ["--stdio"]} 6 | 7 | -------------------------------------------------------------------------------- /benchmarks/_bm.d.ts: -------------------------------------------------------------------------------- 1 | declare module "./_bm.js" { 2 | export interface BenchmarkResult { 3 | name: string; 4 | count: number; 5 | cycles: number; 6 | duration: number; 7 | hz: number; 8 | mean: number; 9 | median: number; 10 | variance: number; 11 | standardDeviation: number; 12 | rme: number; 13 | sample: number[]; 14 | error?: Error; 15 | } 16 | function calculateMean(sample: number[]): number; 17 | function calculateMedian(sample: number[]): number; 18 | function calculateVariance(sample: number[], mean: number): number; 19 | function calculateStandardError(sample: number[], mean: number): number; 20 | export function runAsyncBenchmark( 21 | name: string, 22 | asyncFn: () => PromiseLike | Promise, 23 | minCycles?: number, 24 | maxDurationMs?: number, 25 | ): Promise; 26 | } 27 | 28 | export {}; 29 | -------------------------------------------------------------------------------- /benchmarks/_bm.js: -------------------------------------------------------------------------------- 1 | function calculateMean(sample) { 2 | const sum = sample.reduce((accumulator, value) => accumulator + value, 0); 3 | return sum / sample.length; 4 | } 5 | function calculateMedian(sample) { 6 | const sortedSample = [...sample].sort((a, b) => a - b); 7 | const midIndex = Math.floor(sortedSample.length / 2); 8 | return sortedSample.length % 2 === 0 9 | ? (sortedSample[midIndex - 1] + sortedSample[midIndex]) / 2 10 | : sortedSample[midIndex]; 11 | } 12 | function calculateVariance(sample, mean) { 13 | const sumOfSquares = sample.reduce((accumulator, value) => accumulator + (value - mean) ** 2, 0); 14 | return sumOfSquares / (sample.length - 1); 15 | } 16 | function calculateStandardError(sample, mean) { 17 | return Math.sqrt(calculateVariance(sample, mean)) / Math.sqrt(sample.length); 18 | } 19 | export async function runAsyncBenchmark(name, asyncFn, minCycles, maxDurationMs = 5000) { 20 | let cycles = 0; 21 | let sample = []; 22 | 23 | const start = performance.now(); 24 | const end = start + maxDurationMs; 25 | 26 | while (performance.now() < end) { 27 | for (let i = 0; i < minCycles; i++) { 28 | const cycleStart = performance.now(); 29 | try { 30 | await asyncFn() 31 | .then(() => { 32 | const elapsed = performance.now() - cycleStart; 33 | sample.push(elapsed); 34 | }) 35 | .finally(() => {}); 36 | 37 | cycles++; 38 | } catch (error) { 39 | return { 40 | name, 41 | count: minCycles, 42 | cycles, 43 | duration: performance.now() - start, 44 | hz: NaN, 45 | mean: NaN, 46 | median: NaN, 47 | variance: NaN, 48 | standardDeviation: NaN, 49 | rme: NaN, 50 | sample, 51 | error: error instanceof Error ? error : new Error("Unknown error"), 52 | }; 53 | } 54 | } 55 | } 56 | 57 | const totalTime = performance.now() - start; 58 | const mean = calculateMean(sample); 59 | const median = calculateMedian(sample); 60 | const variance = calculateVariance(sample, mean); 61 | const standardDeviation = Math.sqrt(variance); 62 | const rme = (calculateStandardError(sample, mean) / mean) * 100; 63 | const hz = (cycles * 1000) / totalTime; 64 | 65 | return { 66 | name, 67 | count: cycles, 68 | cycles, 69 | duration: totalTime, 70 | hz, 71 | mean, 72 | median, 73 | variance, 74 | standardDeviation, 75 | rme, 76 | sample: sample.length, 77 | }; 78 | } 79 | -------------------------------------------------------------------------------- /benchmarks/_util.mjs: -------------------------------------------------------------------------------- 1 | import fasta from "./fasta.mjs"; 2 | import pi from "./pi.mjs"; 3 | 4 | export const CONSTANTS = Object.freeze({ 5 | thread_count: 4, 6 | input: 25000, 7 | func: fasta, 8 | delay: 0.05, 9 | max_concurrency: 5, 10 | run_count: 20, 11 | }); 12 | 13 | export const queueTasks = async (spawnFn, ...args) => { 14 | const results = []; 15 | for (let idx = 0; idx < CONSTANTS.run_count; idx++) { 16 | results.push(spawnFn(...args)); 17 | } 18 | const data = await Promise.all(results); 19 | return data; 20 | }; 21 | -------------------------------------------------------------------------------- /benchmarks/bench.mjs: -------------------------------------------------------------------------------- 1 | import { Worker } from "worker_threads"; 2 | import v8 from "v8"; 3 | import vm from "vm"; 4 | import { runBenchmark } from "./utils/runner.mjs"; 5 | v8.setFlagsFromString("--expose_gc"); 6 | 7 | const gc = vm.runInNewContext("gc"); 8 | const FILES = ["./threadsjs.mjs", "./tinypool.mjs", "./nt-file.mjs", "./nt-inline.mjs"].reverse().map( 9 | async (v) => async () => 10 | await new Promise((resolve) => { 11 | runBenchmark(v).finally(() => { 12 | setTimeout(resolve, 5000); 13 | }); 14 | }), 15 | ); 16 | class Defer { 17 | resolver = (data = undefined) => { 18 | return; 19 | }; 20 | promise; 21 | constructor() { 22 | let p = {}; 23 | 24 | p.promise = new Promise((r, e) => { 25 | p.resolve = r; 26 | p.reject = e; 27 | }); 28 | 29 | if (p.resolve) { 30 | this.promise = p.promise; 31 | this.resolver = (d) => p.resolve(d); 32 | this.reject = () => p.reject(); 33 | } 34 | } 35 | 36 | resolve(d) { 37 | this.resolver(d); 38 | } 39 | } 40 | 41 | gc(); 42 | for await (const bench of FILES) { 43 | gc(); 44 | await bench(); 45 | } 46 | 47 | gc(); 48 | for await (const bench of FILES) { 49 | gc(); 50 | await bench(); 51 | } 52 | 53 | // for (let idx = 0; idx < 8; idx++) {} 54 | -------------------------------------------------------------------------------- /benchmarks/fasta.mjs: -------------------------------------------------------------------------------- 1 | export default (num = 2500000) => { 2 | var last = 42, 3 | A = 3877, 4 | C = 29573, 5 | M = 139968; 6 | function rand(max) { 7 | last = (last * A + C) % M; 8 | return (max * last) / M; 9 | } 10 | var ALU = 11 | "GGCCGGGCGCGGTGGCTCACGCCTGTAATCCCAGCACTTTGG" + 12 | "GAGGCCGAGGCGGGCGGATCACCTGAGGTCAGGAGTTCGAGA" + 13 | "CCAGCCTGGCCAACATGGTGAAACCCCGTCTCTACTAAAAAT" + 14 | "ACAAAAATTAGCCGGGCGTGGTGGCGCGCGCCTGTAATCCCA" + 15 | "GCTACTCGGGAGGCTGAGGCAGGAGAATCGCTTGAACCCGGG" + 16 | "AGGCGGAGGTTGCAGTGAGCCGAGATCGCGCCACTGCACTCC" + 17 | "AGCCTGGGCGACAGAGCGAGACTCCGTCTCAAAAA"; 18 | var IUB = { 19 | a: 0.27, 20 | c: 0.12, 21 | g: 0.12, 22 | t: 0.27, 23 | B: 0.02, 24 | D: 0.02, 25 | H: 0.02, 26 | K: 0.02, 27 | M: 0.02, 28 | N: 0.02, 29 | R: 0.02, 30 | S: 0.02, 31 | V: 0.02, 32 | W: 0.02, 33 | Y: 0.02, 34 | }; 35 | var HomoSap = { 36 | a: 0.302954942668, 37 | c: 0.1979883004921, 38 | g: 0.1975473066391, 39 | t: 0.3015094502008, 40 | }; 41 | function makeCumulative(table) { 42 | var last = null; 43 | for (var c in table) { 44 | if (last) table[c] += table[last]; 45 | last = c; 46 | } 47 | } 48 | function fastaRepeat(n, seq) { 49 | var seqi = 0, 50 | lenOut = 60; 51 | let out = ""; 52 | while (n > 0) { 53 | if (n < lenOut) lenOut = n; 54 | if (seqi + lenOut < seq.length) { 55 | out += seq.substring(seqi, seqi + lenOut); 56 | seqi += lenOut; 57 | } else { 58 | var s = seq.substring(seqi); 59 | seqi = lenOut - s.length; 60 | out += s + seq.substring(0, seqi); 61 | } 62 | n -= lenOut; 63 | } 64 | return out; 65 | } 66 | function fastaRandom(n, table) { 67 | var line = new Array(60); 68 | makeCumulative(table); 69 | let out = ""; 70 | while (n > 0) { 71 | if (n < line.length) line = new Array(n); 72 | for (var i = 0; i < line.length; i++) { 73 | var r = rand(1); 74 | for (var c in table) { 75 | if (r < table[c]) { 76 | line[i] = c; 77 | break; 78 | } 79 | } 80 | } 81 | out = line.join(""); 82 | n -= line.length; 83 | } 84 | return out; 85 | } 86 | return Promise.resolve([fastaRepeat(num * 2, ALU), fastaRandom(3 * num, IUB), fastaRandom(3 * num, HomoSap)]); 87 | }; 88 | -------------------------------------------------------------------------------- /benchmarks/nt-file.mjs: -------------------------------------------------------------------------------- 1 | import { ThreadPool } from "../dist/index.mjs"; 2 | import { parentPort } from "worker_threads"; 3 | import { fileURLToPath } from "url"; 4 | import { CONSTANTS, queueTasks } from "./_util.mjs"; 5 | import { runAsyncBenchmark } from "./_bm.js"; 6 | import { add, cycle, suite, complete } from "benny"; 7 | import { benchmark } from "./utils/runner.mjs"; 8 | 9 | const nt = new ThreadPool({ 10 | task: fileURLToPath(new URL("./workers/nanothreads.mjs", import.meta.url)), 11 | count: CONSTANTS.thread_count, 12 | maxConcurrency: CONSTANTS.max_concurrency, 13 | }); 14 | 15 | 16 | const num = CONSTANTS.input; 17 | 18 | const result = await benchmark("nanothreads file") 19 | .add("fasta", () => queueTasks(nt.exec, num)) 20 | .run(); 21 | 22 | await nt.terminate().then(() => { 23 | process.send && process.send(JSON.stringify(result)); 24 | process.exit(0); 25 | }); 26 | -------------------------------------------------------------------------------- /benchmarks/nt-inline-new.mjs: -------------------------------------------------------------------------------- 1 | import b from "benchmark"; 2 | import { ThreadPool } from "../dist/index.mjs"; 3 | import { parentPort } from "worker_threads"; 4 | import { fileURLToPath } from "url"; 5 | import { CONSTANTS, queueTasks } from "./_util.mjs"; 6 | import { runAsyncBenchmark } from "./_bm.js"; 7 | 8 | const nt = new ThreadPool({ 9 | task: CONSTANTS.func, 10 | count: CONSTANTS.thread_count, 11 | maxConcurrency: CONSTANTS.max_concurrency, 12 | }); 13 | 14 | const num = CONSTANTS.input; 15 | 16 | nt.exec = nt.exec.bind(nt); 17 | 18 | let count = 0; 19 | 20 | const results = await runAsyncBenchmark("nanothreads inline", () => nt.exec(num), CONSTANTS.run_count); 21 | const { hz, rme, name, sample } = results; 22 | console.log(`${name} x ${hz.toFixed(hz < 100 ? 2 : 0)} ops/sec \xb1${rme.toFixed(2)}% (${sample.length} sampled)`); 23 | 24 | await nt.terminate().then(() => { 25 | process.send && process.send(JSON.stringify(results)); 26 | process.exit(0); 27 | }); 28 | -------------------------------------------------------------------------------- /benchmarks/nt-inline.mjs: -------------------------------------------------------------------------------- 1 | import { cycle, complete, add, suite } from "benny"; 2 | import { ThreadPool } from "../dist/index.mjs"; 3 | import { parentPort } from "worker_threads"; 4 | import { fileURLToPath } from "url"; 5 | import { CONSTANTS, queueTasks } from "./_util.mjs"; 6 | import { benchmark } from "./utils/runner.mjs"; 7 | const nt = new ThreadPool({ 8 | task: CONSTANTS.func, 9 | count: CONSTANTS.thread_count, 10 | maxConcurrency: CONSTANTS.max_concurrency, 11 | }); 12 | 13 | const num = CONSTANTS.input; 14 | 15 | nt.exec = nt.exec.bind(nt); 16 | 17 | let count = 0; 18 | 19 | const result = await benchmark("nanothreads inline") 20 | .add("fasta", () => queueTasks(nt.exec, num)) 21 | .run(); 22 | 23 | await nt.terminate().then(() => { 24 | process.send && process.send(result); 25 | process.exit(0); 26 | }); 27 | -------------------------------------------------------------------------------- /benchmarks/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "benchmarks", 3 | "version": "1.0.0", 4 | "description": "", 5 | "main": "index.js", 6 | "type": "module", 7 | "scripts": { 8 | "run:threadpool": "cd .. && npm run build && cd ./benchmarks/ && node ./bench.mjs" 9 | }, 10 | "keywords": [], 11 | "author": "", 12 | "license": "ISC", 13 | "devDependencies": { 14 | "@types/benchmark": "^2.1.2", 15 | "@types/vscode": "^1.75.0", 16 | "benchmark": "^2.1.4", 17 | "benny": "^3.7.1", 18 | "poolifier": "^2.3.7", 19 | "tinypool": "^0.3.1" 20 | }, 21 | "dependencies": { 22 | "threads": "^1.7.0" 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /benchmarks/pi.mjs: -------------------------------------------------------------------------------- 1 | export default (n) => { 2 | // Int32 3 | n = +n || 10000; 4 | let i = 0, 5 | k = 0, 6 | d = 0, 7 | k2 = 0, 8 | d3 = 0, 9 | d4 = 0; 10 | let output = ""; 11 | // BigInt 12 | let tmp1 = 0n, // mpz_init(tmp1) 13 | tmp2 = 0n, // mpz_init(tmp2) 14 | acc = 0n, // mpz_init_set_ui(acc, 0) 15 | den = 1n, // mpz_init_set_ui(den, 1) 16 | num = 1n; // mpz_init_set_ui(num, 1) 17 | const chr_0 = "0".charCodeAt(0); 18 | // preallocated buffer size 19 | // let bufsize = (10/*value of pi*/ + 2/*\t:*/ + n.toString().length/*index of slice*/ + 1/*\n*/) * (n / 10)/*line count*/; 20 | // croped buffer size 21 | // for (let i = 10, length = 10 ** (Math.log10(n) >>> 0); i < length; i *= 10) { 22 | // bufsize -= i - 1; 23 | // } 24 | // let buf = Buffer.allocUnsafe(bufsize), 25 | // bufoffs = 0; 26 | for (let i = 0; ; ) { 27 | k++; 28 | //#region inline nextTerm(k) 29 | k2 = k * 2 + 1; 30 | acc += num * 2n; // mpz_addmul_ui(acc, num, 2) 31 | acc *= BigInt(k2); // mpz_mul_ui(acc, acc, k2) 32 | den *= BigInt(k2); // mpz_mul_ui(den, den, k2) 33 | num *= BigInt(k); // mpz_mul_ui(num, num, k) 34 | //#endregion inline nextTerm(k) 35 | if (num > acc /* mpz_cmp(num, acc) > 0 */) continue; 36 | //#region inline extractDigit(3); 37 | tmp1 = num * 3n; // mpz_mul_ui(tmp1, num, nth); 38 | tmp2 = tmp1 + acc; // mpz_add(tmp2, tmp1, acc); 39 | tmp1 = tmp2 / den; // mpz_tdiv_q(tmp1, tmp2, den); 40 | d3 = Number(tmp1) >>> 0; // mpz_get_ui(tmp1) 41 | //#region inline extractDigit(3); 42 | d = d3; 43 | //#region inline extractDigit(4); 44 | tmp1 = num * 4n; // mpz_mul_ui(tmp1, num, nth); 45 | tmp2 = tmp1 + acc; // mpz_add(tmp2, tmp1, acc); 46 | tmp1 = tmp2 / den; // mpz_tdiv_q(tmp1, tmp2, den); 47 | d4 = Number(tmp1) >>> 0; // mpz_get_ui(tmp1) 48 | //#region inline extractDigit(4); 49 | if (d !== d4) continue; 50 | output += String.fromCharCode(d + chr_0); 51 | let iMod10 = ++i % 10; 52 | if (iMod10 === 0) { 53 | output += `\t:${i}\n`; 54 | } 55 | if (i >= n) { 56 | if (iMod10 > 0) { 57 | for (let idx = 0; idx < 10 - iMod10; idx++) { 58 | output += " "; 59 | } 60 | output += `\t:${i}\n`; 61 | } 62 | break; 63 | } 64 | //#region inline eliminateDigit(d) 65 | acc -= den * BigInt(d); // mpz_submul_ui(acc, den, d) 66 | acc *= 10n; // mpz_mul_ui(acc, acc, 10) 67 | num *= 10n; // mpz_mul_ui(num, num, 10) 68 | //#endregion inline eliminateDigit(d) 69 | } 70 | return output; 71 | }; 72 | -------------------------------------------------------------------------------- /benchmarks/secp256k.mjs: -------------------------------------------------------------------------------- 1 | export const secp256k1 = function test(num) { 2 | const CURVE = { 3 | a: 0n, 4 | b: 7n, 5 | P: 2n ** 256n - 2n ** 32n - 977n, 6 | n: 2n ** 256n - 432420386565659656852420866394968145599n, 7 | // G x, y values taken from official secp256k1 document 8 | Gx: 55066263022277343669578718895168534326250603453777594175500187360389116729240n, 9 | Gy: 32670510020758816978083085130507043184471273380659243275938904335757337482424n, 10 | beta: BigInt("0x7ae96a2b657c07106e64479eac3434e99cf0497512f58995c1396c28719501ee"), 11 | }; 12 | // Always true for secp256k1. 13 | // We're including it here if you'll want to reuse code to support 14 | // different curve (e.g. secp256r1) - just set it to false then. 15 | // Endomorphism only works for Koblitz curves with a == 0. 16 | // It improves efficiency: 17 | // Uses 2x less RAM, speeds up precomputation by 2x and ECDH / sign key recovery by 20%. 18 | // Should always be used for Jacobian's double-and-add multiplication. 19 | // For affines cached multiplication, it trades off 1/2 init time & 1/3 ram for 20% perf hit. 20 | // https://gist.github.com/paulmillr/eb670806793e84df628a7c434a873066 21 | const USE_ENDOMORPHISM = CURVE.a === 0n; 22 | class JacobianPoint { 23 | constructor(x, y, z) { 24 | this.x = x; 25 | this.y = y; 26 | this.z = z; 27 | } 28 | static fromAffine(p) { 29 | return new JacobianPoint(p.x, p.y, 1n); 30 | } 31 | toAffine(invZ = invert(this.z)) { 32 | const invZ2 = invZ ** 2n; 33 | const x = mod(this.x * invZ2); 34 | const y = mod(this.y * invZ * invZ2); 35 | return new Point(x, y); 36 | } 37 | // Flips point to one corresponding to (x, -y) in Affine coordinates. 38 | negate() { 39 | return new JacobianPoint(this.x, mod(-this.y), this.z); 40 | } 41 | // Fast algo for doubling 2 Jacobian Points when curve's a=0. 42 | // Note: cannot be reused for other curves when a != 0. 43 | // From: http://hyperelliptic.org/EFD/g1p/auto-shortw-jacobian-0.html#doubling-dbl-2009-l 44 | // Cost: 2M + 5S + 6add + 3*2 + 1*3 + 1*8. 45 | double() { 46 | const X1 = this.x; 47 | const Y1 = this.y; 48 | const Z1 = this.z; 49 | const A = mod(X1 ** 2n); 50 | const B = mod(Y1 ** 2n); 51 | const C = mod(B ** 2n); 52 | const D = mod(2n * (mod(mod((X1 + B) ** 2n)) - A - C)); 53 | const E = mod(3n * A); 54 | const F = mod(E ** 2n); 55 | const X3 = mod(F - 2n * D); 56 | const Y3 = mod(E * (D - X3) - 8n * C); 57 | const Z3 = mod(2n * Y1 * Z1); 58 | return new JacobianPoint(X3, Y3, Z3); 59 | } 60 | // Fast algo for adding 2 Jacobian Points when curve's a=0. 61 | // Note: cannot be reused for other curves when a != 0. 62 | // http://hyperelliptic.org/EFD/g1p/auto-shortw-jacobian-0.html#addition-add-1998-cmo-2 63 | // Cost: 12M + 4S + 6add + 1*2. 64 | // Note: 2007 Bernstein-Lange (11M + 5S + 9add + 4*2) is actually *slower*. No idea why. 65 | add(other) { 66 | if (!(other instanceof JacobianPoint)) { 67 | throw new TypeError("JacobianPoint#add: expected JacobianPoint"); 68 | } 69 | const X1 = this.x; 70 | const Y1 = this.y; 71 | const Z1 = this.z; 72 | const X2 = other.x; 73 | const Y2 = other.y; 74 | const Z2 = other.z; 75 | if (X2 === 0n || Y2 === 0n) return this; 76 | if (X1 === 0n || Y1 === 0n) return other; 77 | const Z1Z1 = mod(Z1 ** 2n); 78 | const Z2Z2 = mod(Z2 ** 2n); 79 | const U1 = mod(X1 * Z2Z2); 80 | const U2 = mod(X2 * Z1Z1); 81 | // @ts-ignore 82 | const S1 = mod(Y1 * Z2 * Z2Z2); 83 | const S2 = mod(mod(Y2 * Z1) * Z1Z1); 84 | const H = mod(U2 - U1); 85 | const r = mod(S2 - S1); 86 | // H = 0 meaning it's the same point. 87 | if (H === 0n) { 88 | if (r === 0n) { 89 | return this.double(); 90 | } else { 91 | return JacobianPoint.ZERO; 92 | } 93 | } 94 | const HH = mod(H ** 2n); 95 | const HHH = mod(H * HH); 96 | const V = mod(U1 * HH); 97 | const X3 = mod(r ** 2n - HHH - 2n * V); 98 | const Y3 = mod(r * (V - X3) - S1 * HHH); 99 | // @ts-ignore 100 | const Z3 = mod(Z1 * Z2 * H); 101 | return new JacobianPoint(X3, Y3, Z3); 102 | } 103 | // Non-constant-time multiplication. Uses double-and-add algorithm. 104 | // It's faster, but should only be used when you don't care about 105 | // an exposed private key e.g. sig verification, which works over *public* keys. 106 | multiplyUnsafe(scalar) { 107 | let n = normalizeScalar(scalar); 108 | // The condition is not executed unless you change global var 109 | if (!USE_ENDOMORPHISM) { 110 | let p = JacobianPoint.ZERO; 111 | let d = this; 112 | while (n > 0n) { 113 | if (n & 1n) p = p.add(d); 114 | // @ts-ignore 115 | d = d.double(); 116 | n >>= 1n; 117 | } 118 | return p; 119 | } 120 | let { k1neg, k1, k2neg, k2 } = splitScalarEndo(n); 121 | let k1p = JacobianPoint.ZERO; 122 | let k2p = JacobianPoint.ZERO; 123 | let d = this; 124 | while (k1 > 0n || k2 > 0n) { 125 | if (k1 & 1n) k1p = k1p.add(d); 126 | if (k2 & 1n) k2p = k2p.add(d); 127 | // @ts-ignore 128 | d = d.double(); 129 | k1 >>= 1n; 130 | k2 >>= 1n; 131 | } 132 | if (k1neg) k1p = k1p.negate(); 133 | if (k2neg) k2p = k2p.negate(); 134 | k2p = new JacobianPoint(mod(k2p.x * CURVE.beta), k2p.y, k2p.z); 135 | return k1p.add(k2p); 136 | } 137 | } 138 | JacobianPoint.ZERO = new JacobianPoint(0n, 1n, 0n); 139 | JacobianPoint.BASE = new JacobianPoint(CURVE.Gx, CURVE.Gy, 1n); 140 | class Point { 141 | constructor(x, y) { 142 | this.x = x; 143 | this.y = y; 144 | } 145 | multiply(scalar) { 146 | return JacobianPoint.fromAffine(this).multiplyUnsafe(scalar).toAffine(); 147 | } 148 | } 149 | Point.ZERO = new Point(0n, 0n); // Point at infinity aka identity point aka zero 150 | Point.BASE = new Point(CURVE.Gx, CURVE.Gy); 151 | function mod(a, b = CURVE.P) { 152 | const result = a % b; 153 | return result >= 0 ? result : b + result; 154 | } 155 | // Inverses number over modulo 156 | function invert(number, modulo = CURVE.P) { 157 | if (number === 0n || modulo <= 0n) { 158 | throw new Error(`invert: expected positive integers, got n=${number} mod=${modulo}`); 159 | } 160 | // Eucledian GCD https://brilliant.org/wiki/extended-euclidean-algorithm/ 161 | let a = mod(number, modulo); 162 | let b = modulo; 163 | let [x, y, u, v] = [0n, 1n, 1n, 0n]; 164 | while (a !== 0n) { 165 | const q = b / a; 166 | const r = b % a; 167 | const m = x - u * q; 168 | const n = y - v * q; 169 | [b, a] = [a, r]; 170 | [x, y] = [u, v]; 171 | [u, v] = [m, n]; 172 | } 173 | const gcd = b; 174 | if (gcd !== 1n) throw new Error("invert: does not exist"); 175 | return mod(x, modulo); 176 | } 177 | function isWithinCurveOrder(num) { 178 | return 0 < num && num < CURVE.n; 179 | } 180 | function normalizeScalar(num) { 181 | if (typeof num === "number" && num > 0 && Number.isSafeInteger(num)) return BigInt(num); 182 | if (typeof num === "bigint" && isWithinCurveOrder(num)) return num; 183 | throw new TypeError("Expected valid private scalar: 0 < scalar < curve.n"); 184 | } 185 | const divNearest = (a, b) => (a + b / 2n) / b; 186 | const POW_2_128 = 2n ** BigInt(128); 187 | // Split 256-bit K into 2 128-bit (k1, k2) for which k1 + k2 * lambda = K. 188 | // Used for endomorphism https://gist.github.com/paulmillr/eb670806793e84df628a7c434a873066 189 | function splitScalarEndo(k) { 190 | const { n } = CURVE; 191 | const a1 = BigInt("0x3086d221a7d46bcde86c90e49284eb15"); 192 | const b1 = -1n * BigInt("0xe4437ed6010e88286f547fa90abfe4c3"); 193 | const a2 = BigInt("0x114ca50f7a8e2f3f657c1108d9d44cfd8"); 194 | const b2 = a1; 195 | const c1 = divNearest(b2 * k, n); 196 | const c2 = divNearest(-b1 * k, n); 197 | // @ts-ignore 198 | let k1 = mod(k - c1 * a1 - c2 * a2, n); 199 | // @ts-ignore 200 | let k2 = mod(-c1 * b1 - c2 * b2, n); 201 | const k1neg = k1 > POW_2_128; 202 | const k2neg = k2 > POW_2_128; 203 | if (k1neg) k1 = n - k1; 204 | if (k2neg) k2 = n - k2; 205 | if (k1 > POW_2_128 || k2 > POW_2_128) throw new Error("splitScalarEndo: Endomorphism failed"); 206 | return { k1neg, k1, k2neg, k2 }; 207 | } 208 | const G = new Point(CURVE.Gx, CURVE.Gy); 209 | const PRIVATE_KEY = 0x2dee927079283c3c4fca3ef970ff4d38b64592e3fe0ab0dad9132d70b5bc7693n; 210 | let point = G; 211 | for (let i = 0; i < num; i++) { 212 | point = point.multiply(PRIVATE_KEY); 213 | } 214 | return Promise.resolve(`${point.x.toString(16)},${point.y.toString(16)}`); 215 | }; 216 | -------------------------------------------------------------------------------- /benchmarks/threadsjs.mjs: -------------------------------------------------------------------------------- 1 | import b from "benchmark"; 2 | import { Pool, spawn, Worker } from "threads"; 3 | import { parentPort } from "worker_threads"; 4 | import { CONSTANTS, queueTasks } from "./_util.mjs"; 5 | import { runAsyncBenchmark } from "./_bm.js"; 6 | import { benchmark } from "./utils/runner.mjs"; 7 | 8 | const spawnW = async () => { 9 | return await spawn(new Worker("./workers/threads-js.js", { type: "module" })); 10 | }; 11 | const pool = Pool(spawnW, CONSTANTS.thread_count); 12 | 13 | pool.queue = pool.queue.bind(pool); 14 | const num = CONSTANTS.input; 15 | 16 | let count = 0; 17 | 18 | const results = await benchmark("threads.js") 19 | .add("fasta", () => 20 | queueTasks( 21 | async () => 22 | await pool.queue(async (cb) => { 23 | return await cb(num); 24 | }), 25 | ), 26 | ) 27 | .run(); 28 | 29 | await pool.terminate().then(() => { 30 | process.send && process.send(JSON.stringify(results)); 31 | process.exit(0); 32 | }); 33 | -------------------------------------------------------------------------------- /benchmarks/tinypool.mjs: -------------------------------------------------------------------------------- 1 | import b from "benchmark"; 2 | import { Tinypool } from "tinypool"; 3 | import { parentPort } from "worker_threads"; 4 | import { CONSTANTS, queueTasks } from "./_util.mjs"; 5 | import { runAsyncBenchmark } from "./_bm.js"; 6 | import { benchmark } from "./utils/runner.mjs"; 7 | const tppool = new Tinypool({ 8 | filename: new URL("./workers/tinypool.js", import.meta.url).href, 9 | maxThreads: CONSTANTS.thread_count, 10 | }); 11 | tppool.run = tppool.run.bind(tppool); 12 | const num = CONSTANTS.input; 13 | 14 | const results = await benchmark("tinypool") 15 | .add("fasta", () => queueTasks(tppool.run, num)) 16 | .run(); 17 | 18 | await tppool.destroy().then(() => { 19 | process.send && process.send(JSON.stringify(results)); 20 | process.exit(0); 21 | }); 22 | -------------------------------------------------------------------------------- /benchmarks/utils/index.mjs: -------------------------------------------------------------------------------- 1 | import { spawn } from "child_process"; 2 | import { spawnSync } from "child_process"; 3 | export { sleep } from "./sleep.mjs"; 4 | 5 | export class Runner { 6 | constructor(path = "") { 7 | this.path = path; 8 | } 9 | 10 | run() { 11 | const h = spawn("node", [this.path], { cwd: process.cwd(), shell: true, stdio: "inherit" }); 12 | return new Promise((r) => { 13 | h.on("message", (m) => { 14 | console.log("GOT MESSAGE!!!!"); 15 | console.log(m); 16 | r(m.toString()); 17 | }); 18 | }); 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /benchmarks/utils/runner.mjs: -------------------------------------------------------------------------------- 1 | import { fork } from "child_process"; 2 | import { join } from "path"; 3 | 4 | const ROOT_DIR = process.cwd(); 5 | export async function runBenchmark(benchmarkFile) { 6 | return new Promise((resolve, reject) => { 7 | const child = fork(join(ROOT_DIR, benchmarkFile)); 8 | 9 | child.on("message", (message) => { 10 | console.log(message); 11 | resolve(undefined); 12 | }); 13 | 14 | child.on("error", (error) => { 15 | reject(error); 16 | }); 17 | 18 | child.on("exit", (code) => { 19 | if (code !== 0) { 20 | reject(new Error(`Benchmark exited with code ${code}`)); 21 | } 22 | }); 23 | }); 24 | } 25 | 26 | import { add, cycle, suite } from "benny"; 27 | 28 | export function benchmark(suiteName) { 29 | const testCases = []; 30 | const run = () => { 31 | return new Promise((resolve, reject) => { 32 | suite( 33 | suiteName, 34 | ...testCases, 35 | cycle((result) => { 36 | const { relativeMarginOfError, sampleResults } = result.details; 37 | const { ops, name } = result; 38 | resolve( 39 | `${name} x ${ops.toFixed(ops < 100 ? 2 : 0)} ops/sec \xb1${relativeMarginOfError.toFixed(2)}% (${ 40 | sampleResults.length 41 | } runs sampled)`, 42 | ); 43 | }), 44 | ); 45 | }); 46 | }; 47 | return { 48 | add(name, func) { 49 | testCases.push(add(name, func)); 50 | return this; 51 | }, 52 | run, 53 | }; 54 | } 55 | -------------------------------------------------------------------------------- /benchmarks/utils/setup.mjs: -------------------------------------------------------------------------------- 1 | import Benchmark from "benchmark"; 2 | import { parentPort } from "worker_threads"; 3 | 4 | parentPort?.on("message", async () => { 5 | if (count === 1) { 6 | for (const _ of Array(20)) { 7 | } 8 | await new Promise((r) => 9 | setTimeout(() => { 10 | r(undefined); 11 | }, 500), 12 | ); 13 | parentPort?.postMessage("DRY RUN COMPLETE"); 14 | return; 15 | } 16 | }); 17 | -------------------------------------------------------------------------------- /benchmarks/utils/sleep.mjs: -------------------------------------------------------------------------------- 1 | export const sleep = (ms = 500) => new Promise((resolve) => setTimeout(() => resolve(undefined), ms)); 2 | -------------------------------------------------------------------------------- /benchmarks/workers/nanothreads.mjs: -------------------------------------------------------------------------------- 1 | import { workerInit } from "../../dist/index.mjs"; 2 | import { parentPort } from "worker_threads"; 3 | import fasta from "../fasta.mjs"; 4 | import { secp256k1 } from "../secp256k.mjs"; 5 | import { CONSTANTS } from "../_util.mjs"; 6 | 7 | if (parentPort) { 8 | workerInit( 9 | parentPort, 10 | /** @param {number} num */ 11 | async (num) => await CONSTANTS.func(num), 12 | ); 13 | } 14 | -------------------------------------------------------------------------------- /benchmarks/workers/threads-js.js: -------------------------------------------------------------------------------- 1 | import { expose } from "threads/worker"; 2 | import FASTA from "../fasta.mjs"; 3 | import { secp256k1 } from "../secp256k.mjs"; 4 | import { CONSTANTS } from "../_util.mjs"; 5 | const NUM = CONSTANTS.input; 6 | 7 | expose(async () => await CONSTANTS.func(NUM)); 8 | -------------------------------------------------------------------------------- /benchmarks/workers/tinypool.js: -------------------------------------------------------------------------------- 1 | import { CONSTANTS } from "../_util.mjs"; 2 | const NUM = CONSTANTS.input; 3 | 4 | export default async () => { 5 | return await CONSTANTS.func(NUM); 6 | }; 7 | -------------------------------------------------------------------------------- /build.mjs: -------------------------------------------------------------------------------- 1 | import { buildSync } from "esbuild"; 2 | import { readdirSync, rmSync } from "fs"; 3 | 4 | /** @type {import('esbuild').BuildOptions} */ 5 | const DEFAULT_OPTIONS = { 6 | target: "esnext", 7 | bundle: true, 8 | entryPoints: ["./src/index.ts", "./src/worker/index.ts"], 9 | packages: "external", 10 | keepNames: true, 11 | minifyWhitespace: true, 12 | minifyIdentifiers: true, 13 | treeShaking: true, 14 | drop: ["console", "debugger"], 15 | outdir: "dist", 16 | outbase: "src", 17 | minifySyntax: true, 18 | minify: true, 19 | }; 20 | 21 | try { 22 | rmSync("./dist", { recursive: true }); 23 | } catch {} 24 | function buildWeb() { 25 | /** @type {import('esbuild').BuildOptions} */ 26 | const OPTS = { 27 | treeShaking: true, 28 | splitting: true, 29 | format: "esm", 30 | platform: "browser", 31 | outbase: "src", 32 | external: ["worker_threads"], 33 | outdir: "dist/browser", 34 | }; 35 | 36 | const o = Object.assign({}, DEFAULT_OPTIONS, OPTS); 37 | buildSync(o); 38 | } 39 | 40 | function buildNodeCJS() { 41 | /** @type {import('esbuild').BuildOptions} */ 42 | const OPTS = { 43 | treeShaking: true, 44 | format: "cjs", 45 | outExtension: { ".js": ".cjs" }, 46 | platform: "node", 47 | outbase: "src", 48 | outdir: "dist", 49 | inject: ["./src/internals/NodeWorker-cjs.js", "./src/threads/channel-cjs.js"], 50 | tsconfig: "./tsconfig.node.json", 51 | }; 52 | 53 | const o = Object.assign({}, DEFAULT_OPTIONS, OPTS); 54 | buildSync(o); 55 | } 56 | 57 | function buildNodeESM() { 58 | /** @type {import('esbuild').BuildOptions} */ 59 | const OPTS = { 60 | treeShaking: true, 61 | format: "esm", 62 | outExtension: { ".js": ".mjs" }, 63 | outbase: "src", 64 | outdir: "dist", 65 | splitting: true, 66 | platform: "node", 67 | inject: ["./src/internals/NodeWorker-esm.mjs", "./src/threads/channel-esm.mjs"], 68 | }; 69 | 70 | const o = Object.assign({}, DEFAULT_OPTIONS, OPTS); 71 | buildSync(o); 72 | } 73 | 74 | [buildNodeESM, buildNodeCJS, buildWeb].forEach((f) => f()); 75 | -------------------------------------------------------------------------------- /docs/.nojekyll: -------------------------------------------------------------------------------- 1 | TypeDoc added this file to prevent GitHub Pages from using Jekyll. You can turn off this behavior by setting the `githubPages` option to false. -------------------------------------------------------------------------------- /docs/assets/highlight.css: -------------------------------------------------------------------------------- 1 | :root { 2 | --light-hl-0: #AF00DB; 3 | --dark-hl-0: #C586C0; 4 | --light-hl-1: #000000; 5 | --dark-hl-1: #D4D4D4; 6 | --light-hl-2: #001080; 7 | --dark-hl-2: #9CDCFE; 8 | --light-hl-3: #A31515; 9 | --dark-hl-3: #CE9178; 10 | --light-hl-4: #0000FF; 11 | --dark-hl-4: #569CD6; 12 | --light-hl-5: #0070C1; 13 | --dark-hl-5: #4FC1FF; 14 | --light-hl-6: #795E26; 15 | --dark-hl-6: #DCDCAA; 16 | --light-hl-7: #267F99; 17 | --dark-hl-7: #4EC9B0; 18 | --light-hl-8: #098658; 19 | --dark-hl-8: #B5CEA8; 20 | --light-hl-9: #008000; 21 | --dark-hl-9: #6A9955; 22 | --light-code-background: #FFFFFF; 23 | --dark-code-background: #1E1E1E; 24 | } 25 | 26 | @media (prefers-color-scheme: light) { :root { 27 | --hl-0: var(--light-hl-0); 28 | --hl-1: var(--light-hl-1); 29 | --hl-2: var(--light-hl-2); 30 | --hl-3: var(--light-hl-3); 31 | --hl-4: var(--light-hl-4); 32 | --hl-5: var(--light-hl-5); 33 | --hl-6: var(--light-hl-6); 34 | --hl-7: var(--light-hl-7); 35 | --hl-8: var(--light-hl-8); 36 | --hl-9: var(--light-hl-9); 37 | --code-background: var(--light-code-background); 38 | } } 39 | 40 | @media (prefers-color-scheme: dark) { :root { 41 | --hl-0: var(--dark-hl-0); 42 | --hl-1: var(--dark-hl-1); 43 | --hl-2: var(--dark-hl-2); 44 | --hl-3: var(--dark-hl-3); 45 | --hl-4: var(--dark-hl-4); 46 | --hl-5: var(--dark-hl-5); 47 | --hl-6: var(--dark-hl-6); 48 | --hl-7: var(--dark-hl-7); 49 | --hl-8: var(--dark-hl-8); 50 | --hl-9: var(--dark-hl-9); 51 | --code-background: var(--dark-code-background); 52 | } } 53 | 54 | :root[data-theme='light'] { 55 | --hl-0: var(--light-hl-0); 56 | --hl-1: var(--light-hl-1); 57 | --hl-2: var(--light-hl-2); 58 | --hl-3: var(--light-hl-3); 59 | --hl-4: var(--light-hl-4); 60 | --hl-5: var(--light-hl-5); 61 | --hl-6: var(--light-hl-6); 62 | --hl-7: var(--light-hl-7); 63 | --hl-8: var(--light-hl-8); 64 | --hl-9: var(--light-hl-9); 65 | --code-background: var(--light-code-background); 66 | } 67 | 68 | :root[data-theme='dark'] { 69 | --hl-0: var(--dark-hl-0); 70 | --hl-1: var(--dark-hl-1); 71 | --hl-2: var(--dark-hl-2); 72 | --hl-3: var(--dark-hl-3); 73 | --hl-4: var(--dark-hl-4); 74 | --hl-5: var(--dark-hl-5); 75 | --hl-6: var(--dark-hl-6); 76 | --hl-7: var(--dark-hl-7); 77 | --hl-8: var(--dark-hl-8); 78 | --hl-9: var(--dark-hl-9); 79 | --code-background: var(--dark-code-background); 80 | } 81 | 82 | .hl-0 { color: var(--hl-0); } 83 | .hl-1 { color: var(--hl-1); } 84 | .hl-2 { color: var(--hl-2); } 85 | .hl-3 { color: var(--hl-3); } 86 | .hl-4 { color: var(--hl-4); } 87 | .hl-5 { color: var(--hl-5); } 88 | .hl-6 { color: var(--hl-6); } 89 | .hl-7 { color: var(--hl-7); } 90 | .hl-8 { color: var(--hl-8); } 91 | .hl-9 { color: var(--hl-9); } 92 | pre, code { background: var(--code-background); } 93 | -------------------------------------------------------------------------------- /docs/enums/StatusCode.html: -------------------------------------------------------------------------------- 1 | StatusCode | nanothreads - v0.3.4
2 |
3 | 10 |
11 |
12 |
13 |
14 | 17 |

Enumeration StatusCode

20 |
21 |
22 |
23 | 24 |
25 |
26 |

Enumeration Members

27 |
ERROR 28 | OK 29 | TERMINATED 30 | WAITING 31 |
32 |
33 |

Enumeration Members

34 |
35 | 36 |
ERROR: 400
39 |
40 | 41 |
OK: 200
44 |
45 | 46 |
TERMINATED: 0
49 |
50 | 51 |
WAITING: 300
54 |
82 |
83 |

Generated using TypeDoc

84 |
-------------------------------------------------------------------------------- /docs/functions/yieldMicrotask.html: -------------------------------------------------------------------------------- 1 | yieldMicrotask | nanothreads - v0.3.4
2 |
3 | 10 |
11 |
12 |
13 |
14 | 17 |

Function yieldMicrotask

18 |
19 |
25 |
67 |
68 |

Generated using TypeDoc

69 |
-------------------------------------------------------------------------------- /docs/interfaces/IThread.html: -------------------------------------------------------------------------------- 1 | IThread | nanothreads - v0.3.4
2 |
3 | 10 |
11 |
12 |
13 |
14 | 17 |

Interface IThread<Args, Output>

18 |
19 |

Type Parameters

20 |
    21 |
  • 22 |

    Args extends [args: unknown[]] | unknown

  • 23 |
  • 24 |

    Output

25 |
26 |

Hierarchy

27 |
    28 |
  • IThread
31 |
32 |
33 |
34 | 35 |
36 |
37 |

Methods

38 |
send 39 | terminate 40 |
41 |
42 |

Methods

43 |
44 | 45 |
    46 | 47 |
  • 48 |

    Executes the thread function and returns the result.

    49 | 50 |

    Returns

    51 |
    52 |

    Parameters

    53 |
      54 |
    • 55 |
      Optional Rest ...data: Args extends any[] ? Args : [Args]
      56 |

      optional

      57 |
    58 |

    Returns Promise<Output>

61 |
62 | 63 |
71 |
97 |
98 |

Generated using TypeDoc

99 |
-------------------------------------------------------------------------------- /docs/interfaces/IThreadOptions.html: -------------------------------------------------------------------------------- 1 | IThreadOptions | nanothreads - v0.3.4
2 |
3 | 10 |
11 |
12 |
13 |
14 | 17 |

Interface IThreadOptions

18 |
19 |

Hierarchy

20 |
    21 |
  • IThreadOptions
24 |
25 |
26 |
27 | 28 |
29 |
30 |

Properties

31 |
once? 32 |
33 |
34 |

Properties

35 |
36 | 37 |
once?: boolean
40 |
65 |
66 |

Generated using TypeDoc

67 |
-------------------------------------------------------------------------------- /docs/types/AnyThread.html: -------------------------------------------------------------------------------- 1 | AnyThread | nanothreads - v0.3.4
2 |
3 | 10 |
11 |
12 |
13 |
14 | 17 |

Type alias AnyThread<Arguments, Output>

18 |
AnyThread<Arguments, Output>: InlineThread<Arguments, MaybePromise<Output>> | Thread<Arguments, MaybePromise<Output>>
19 |
20 |

Type Parameters

21 |
    22 |
  • 23 |

    Arguments

  • 24 |
  • 25 |

    Output

28 |
70 |
71 |

Generated using TypeDoc

72 |
-------------------------------------------------------------------------------- /docs/types/GetReturnType.html: -------------------------------------------------------------------------------- 1 | GetReturnType | nanothreads - v0.3.4
2 |
3 | 10 |
11 |
12 |
13 |
14 | 17 |

Type alias GetReturnType<T>

18 |
GetReturnType<T>: T extends ((...args: unknown[]) => Promise<ReturnType<infer Value>>) ? ReturnType<Value> : T
19 |
20 |

Type Parameters

21 |
    22 |
  • 23 |

    T

26 |
68 |
69 |

Generated using TypeDoc

70 |
-------------------------------------------------------------------------------- /docs/types/IWorkerImpl.html: -------------------------------------------------------------------------------- 1 | IWorkerImpl | nanothreads - v0.3.4
2 |
3 | 10 |
11 |
12 |
13 |
14 | 17 |

Type alias IWorkerImpl<T>

18 |
IWorkerImpl<T>: BrowserImpl<T>
19 |
20 |

Type Parameters

21 |
    22 |
  • 23 |

    T

26 |
68 |
69 |

Generated using TypeDoc

70 |
-------------------------------------------------------------------------------- /docs/types/MaybePromise.html: -------------------------------------------------------------------------------- 1 | MaybePromise | nanothreads - v0.3.4
2 |
3 | 10 |
11 |
12 |
13 |
14 | 17 |

Type alias MaybePromise<P>

18 |
MaybePromise<P>: P | Promise<P>
19 |

Utility type for a value that may or may not be a Promise

20 |
21 |
22 |

Type Parameters

23 |
    24 |
  • 25 |

    P

28 |
70 |
71 |

Generated using TypeDoc

72 |
-------------------------------------------------------------------------------- /docs/types/ThreadArgs.html: -------------------------------------------------------------------------------- 1 | ThreadArgs | nanothreads - v0.3.4
2 |
3 | 10 |
11 |
12 |
13 |
14 | 17 |

Type alias ThreadArgs<T>

18 |
ThreadArgs<T>: T extends any[] ? T : [T]
19 |

Utility type for defining Worker Thread arguments

20 |
21 |
22 |

Type Parameters

23 |
    24 |
  • 25 |

    T

28 |
70 |
71 |

Generated using TypeDoc

72 |
-------------------------------------------------------------------------------- /docs/variables/browser.html: -------------------------------------------------------------------------------- 1 | browser | nanothreads - v0.3.4
2 |
3 | 10 |
11 |
12 |
13 |
14 | 17 |

Variable browserConst

18 |
browser: boolean = ...
21 |
63 |
64 |

Generated using TypeDoc

65 |
-------------------------------------------------------------------------------- /index.cjs.js: -------------------------------------------------------------------------------- 1 | const { InlineThread } = require("./dist/index.cjs"); 2 | const { ThreadPool } = require("./dist/index.cjs"); 3 | 4 | const sleep = (ms = 500) => new Promise((res) => setTimeout(res, ms)); 5 | const pool = new ThreadPool({ 6 | task: (data) => { 7 | const FASTA = (num) => { 8 | var last = 42, 9 | A = 3877, 10 | C = 29573, 11 | M = 139968; 12 | function rand(max) { 13 | last = (last * A + C) % M; 14 | return (max * last) / M; 15 | } 16 | var ALU = 17 | "GGCCGGGCGCGGTGGCTCACGCCTGTAATCCCAGCACTTTGG" + 18 | "GAGGCCGAGGCGGGCGGATCACCTGAGGTCAGGAGTTCGAGA" + 19 | "CCAGCCTGGCCAACATGGTGAAACCCCGTCTCTACTAAAAAT" + 20 | "ACAAAAATTAGCCGGGCGTGGTGGCGCGCGCCTGTAATCCCA" + 21 | "GCTACTCGGGAGGCTGAGGCAGGAGAATCGCTTGAACCCGGG" + 22 | "AGGCGGAGGTTGCAGTGAGCCGAGATCGCGCCACTGCACTCC" + 23 | "AGCCTGGGCGACAGAGCGAGACTCCGTCTCAAAAA"; 24 | var IUB = { 25 | a: 0.27, 26 | c: 0.12, 27 | g: 0.12, 28 | t: 0.27, 29 | B: 0.02, 30 | D: 0.02, 31 | H: 0.02, 32 | K: 0.02, 33 | M: 0.02, 34 | N: 0.02, 35 | R: 0.02, 36 | S: 0.02, 37 | V: 0.02, 38 | W: 0.02, 39 | Y: 0.02, 40 | }; 41 | var HomoSap = { 42 | a: 0.302954942668, 43 | c: 0.1979883004921, 44 | g: 0.1975473066391, 45 | t: 0.3015094502008, 46 | }; 47 | function makeCumulative(table) { 48 | var last = null; 49 | for (var c in table) { 50 | if (last) table[c] += table[last]; 51 | last = c; 52 | } 53 | } 54 | function fastaRepeat(n, seq) { 55 | var seqi = 0, 56 | lenOut = 60; 57 | let out = ""; 58 | while (n > 0) { 59 | if (n < lenOut) lenOut = n; 60 | if (seqi + lenOut < seq.length) { 61 | out += seq.substring(seqi, seqi + lenOut); 62 | seqi += lenOut; 63 | } else { 64 | var s = seq.substring(seqi); 65 | seqi = lenOut - s.length; 66 | out += s + seq.substring(0, seqi); 67 | } 68 | n -= lenOut; 69 | } 70 | return out; 71 | } 72 | function fastaRandom(n, table) { 73 | var line = new Array(60); 74 | makeCumulative(table); 75 | let out = ""; 76 | while (n > 0) { 77 | if (n < line.length) line = new Array(n); 78 | for (var i = 0; i < line.length; i++) { 79 | var r = rand(1); 80 | for (var c in table) { 81 | if (r < table[c]) { 82 | line[i] = c; 83 | break; 84 | } 85 | } 86 | } 87 | out = line.join(""); 88 | n -= line.length; 89 | } 90 | return out; 91 | } 92 | return Promise.resolve([fastaRepeat(num * 2, ALU), fastaRandom(3 * num, IUB), fastaRandom(3 * num, HomoSap)]); 93 | }; 94 | return FASTA(data); 95 | }, 96 | count: 8, 97 | }); 98 | new InlineThread( 99 | (name) => { 100 | return "Hello " + name; 101 | }, 102 | { once: true }, 103 | ).send(); 104 | async function rn() { 105 | let runs = 10000; 106 | const tasks = []; 107 | for (let idx = 0; idx < runs; idx++) { 108 | console.log("running thread", idx); 109 | const test = pool.exec(idx); 110 | tasks.push(test); 111 | } 112 | console.log("running thread"); 113 | const results = await Promise.all(tasks); 114 | console.log({ results }); 115 | console.log("running thread EEE"); 116 | pool.terminate(); 117 | } 118 | rn(); 119 | -------------------------------------------------------------------------------- /index.mts: -------------------------------------------------------------------------------- 1 | //@ts-nocheck 2 | import { fileURLToPath } from "node:url"; 3 | import { ThreadPool } from "./dist"; 4 | export const FASTA = (num: number) => { 5 | var last = 42, 6 | A = 3877, 7 | C = 29573, 8 | M = 139968; 9 | if (num % 2) throw new Error("Error!!!!"); 10 | function rand(max: number) { 11 | last = (last * A + C) % M; 12 | return (max * last) / M; 13 | } 14 | 15 | var ALU = 16 | "GGCCGGGCGCGGTGGCTCACGCCTGTAATCCCAGCACTTTGG" + 17 | "GAGGCCGAGGCGGGCGGATCACCTGAGGTCAGGAGTTCGAGA" + 18 | "CCAGCCTGGCCAACATGGTGAAACCCCGTCTCTACTAAAAAT" + 19 | "ACAAAAATTAGCCGGGCGTGGTGGCGCGCGCCTGTAATCCCA" + 20 | "GCTACTCGGGAGGCTGAGGCAGGAGAATCGCTTGAACCCGGG" + 21 | "AGGCGGAGGTTGCAGTGAGCCGAGATCGCGCCACTGCACTCC" + 22 | "AGCCTGGGCGACAGAGCGAGACTCCGTCTCAAAAA"; 23 | 24 | var IUB = { 25 | a: 0.27, 26 | c: 0.12, 27 | g: 0.12, 28 | t: 0.27, 29 | B: 0.02, 30 | D: 0.02, 31 | H: 0.02, 32 | K: 0.02, 33 | M: 0.02, 34 | N: 0.02, 35 | R: 0.02, 36 | S: 0.02, 37 | V: 0.02, 38 | W: 0.02, 39 | Y: 0.02, 40 | }; 41 | 42 | var HomoSap = { 43 | a: 0.302954942668, 44 | c: 0.1979883004921, 45 | g: 0.1975473066391, 46 | t: 0.3015094502008, 47 | }; 48 | 49 | function makeCumulative(table: { [key: string]: number }) { 50 | var last = null; 51 | for (var c in table) { 52 | if (last) table[c] += table[last]; 53 | last = c; 54 | } 55 | } 56 | 57 | function fastaRepeat(n: number, seq: string) { 58 | var seqi = 0, 59 | lenOut = 60; 60 | let out = ""; 61 | while (n > 0) { 62 | if (n < lenOut) lenOut = n; 63 | if (seqi + lenOut < seq.length) { 64 | out += seq.substring(seqi, seqi + lenOut); 65 | seqi += lenOut; 66 | } else { 67 | var s = seq.substring(seqi); 68 | seqi = lenOut - s.length; 69 | out += s + seq.substring(0, seqi); 70 | } 71 | n -= lenOut; 72 | } 73 | return out; 74 | } 75 | 76 | function fastaRandom(n: number, table: { [key: string]: number }) { 77 | var line = new Array(60); 78 | makeCumulative(table); 79 | let out = ""; 80 | while (n > 0) { 81 | if (n < line.length) line = new Array(n); 82 | for (var i = 0; i < line.length; i++) { 83 | var r = rand(1); 84 | for (var c in table) { 85 | if (r < table[c]) { 86 | line[i] = c; 87 | break; 88 | } 89 | } 90 | } 91 | out = line.join(""); 92 | n -= line.length; 93 | } 94 | return out; 95 | } 96 | return Promise.resolve([fastaRepeat(num * 2, ALU), fastaRandom(3 * num, IUB), fastaRandom(3 * num, HomoSap)]); 97 | }; 98 | 99 | const sleep = (ms = 500) => new Promise((res) => setTimeout(res, ms)); 100 | 101 | const pool = new ThreadPool<[number], number>({ 102 | task: "./worker.mjs", 103 | count: 4, 104 | maxConcurrency: 1, 105 | }); 106 | async function rn() { 107 | let runs = 10; 108 | const tasks = []; 109 | for (let idx = 0; idx < runs; idx++) { 110 | // console.log("running thread", idx); 111 | tasks.push(pool.exec(idx)); 112 | } 113 | console.log(await Promise.all(tasks).then((v) => console.log(v))); 114 | 115 | console.log("nice"); 116 | pool.terminate(); 117 | } 118 | rn(); 119 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "nanothreads", 3 | "author": "snuffydev", 4 | "repository": { 5 | "url": "https://github.com/snuffyDev/nanothreads" 6 | }, 7 | "license": "ISC", 8 | "version": "0.3.9", 9 | "source": "./src/index.ts", 10 | "description": "A tiny threading library, made for browsers and Node.", 11 | "keywords": [ 12 | "threads", 13 | "worker", 14 | "concurrency", 15 | "async", 16 | "thread pool", 17 | "worker-pool", 18 | "workers", 19 | "pool", 20 | "threadpool", 21 | "thread", 22 | "multithreading", 23 | "parallel", 24 | "threading", 25 | "browser" 26 | ], 27 | "files": [ 28 | "dist", 29 | "package.json" 30 | ], 31 | "main": "./dist/index.cjs", 32 | "types": "./dist/index.d.ts", 33 | "module": "./dist/index.mjs", 34 | "browser": "./dist/browser/index.js", 35 | "scripts": { 36 | "build": "rm -rf ./dist && node ./build.mjs && pnpm run build:dts", 37 | "build:benchmark": "node ./build.mjs && pnpm run build:dts", 38 | "build:demo": "tsc --moduleResolution node --target ESNext -m esnext ./index.ts", 39 | "build:dts": "tsc --moduleResolution node --target ESNext -m esnext --declaration true --emitDeclarationOnly --rootDir ./src/ --outDir dist", 40 | "export": "node ./scripts/barrel.mjs", 41 | "run:esm:node-demo": "node --loader=ts-node/esm --experimental-specifier-resolution=node ./index.mts", 42 | "run:cjs:node-demo": "node ./index.cjs.js", 43 | "start": "pnpm run build && pnpm run run:esm:node-demo", 44 | "format": "prettier --write .", 45 | "test": "size-limit" 46 | }, 47 | "sideEffects": false, 48 | "exports": { 49 | ".": { 50 | "node": { 51 | "import": "./dist/index.cjs", 52 | "require": "./dist/index.cjs" 53 | }, 54 | "browser": "./dist/browser/index.js", 55 | "import": "./dist/index.mjs" 56 | }, 57 | "./package.json": "./package.json" 58 | }, 59 | "size-limit": [ 60 | { 61 | "name": "Node (ESM)", 62 | "limit": "3 KB", 63 | "import": "*", 64 | "path": "./dist/index.mjs" 65 | }, 66 | { 67 | "name": "Node (CJS)", 68 | "limit": "3.5 KB", 69 | "import": "*", 70 | "path": "./dist/index.cjs" 71 | }, 72 | { 73 | "name": "Web (ESM)", 74 | "limit": "3 KB", 75 | "import": "*", 76 | "path": "./dist/browser/index.js" 77 | } 78 | ], 79 | "devDependencies": { 80 | "@size-limit/esbuild": "^8.2.4", 81 | "@size-limit/esbuild-why": "^8.2.4", 82 | "@size-limit/file": "^8.2.4", 83 | "@size-limit/preset-small-lib": "^8.2.4", 84 | "@types/node": "^18.15.5", 85 | "esbuild": "^0.17.12", 86 | "esbuild-wasm": "^0.17.12", 87 | "glob": "^9.3.1", 88 | "prettier": "^2.8.6", 89 | "size-limit": "^8.2.4", 90 | "size-limit-node-esbuild": "^0.2.0", 91 | "ts-node": "^10.9.1", 92 | "tsup": "^6.7.0", 93 | "typedoc": "^0.23.28", 94 | "typescript": "^5.0.2" 95 | } 96 | } 97 | -------------------------------------------------------------------------------- /scripts/barrel.mjs: -------------------------------------------------------------------------------- 1 | #! 2 | // import { forEach } from './../src/collections/array/forEach'; 3 | // import { forEach } from './../src/collections/array/forEach'; 4 | import * as fs from "fs"; 5 | import _path from "path"; 6 | const BASE_PATH = _path.resolve("."); 7 | 8 | const makePath = (...str) => _path.join(...str); 9 | const TypeExport = /(?<=export (?:type|interface)[\s]?)([a-zA-Z0-9]+)/gim; 10 | const NormalExport = /(?<=export (?:function|const|enum|let|class)[\s]?)([a-zA-Z0-9]+)/gim; 11 | 12 | function matchFile(file) { 13 | const normExports = [...(new Set(file.match(NormalExport)) ?? undefined)] ?? undefined; 14 | const typeExports = [...(new Set(file.match(TypeExport)) ?? undefined)] ?? undefined; 15 | return { 16 | normExports, 17 | typeExports, 18 | }; 19 | } 20 | 21 | function getIntersection(a, b) { 22 | const set1 = new Set(a); 23 | const set2 = new Set(b); 24 | 25 | const intersection = [...set1].filter((element) => set2.has(element)); 26 | 27 | return intersection; 28 | } 29 | function recursiveDirRead(path) { 30 | let skip = false; 31 | let directory = fs.readdirSync(path, { encoding: "utf-8" }); 32 | 33 | const indexPath = makePath(path, "index.ts"); 34 | fs.writeFileSync(indexPath, "", { encoding: "utf-8" }); 35 | const dirs = []; 36 | const type = []; 37 | const normal = []; 38 | directory.forEach((entry) => { 39 | if (fs.statSync(path + _path.sep + entry).isDirectory()) { 40 | dirs.push({ name: entry, exports: recursiveDirRead(makePath(path, entry)) }); 41 | } else { 42 | if (skip) return; 43 | if (!entry.endsWith(".ts")) return; 44 | if (!fs.existsSync(makePath(path, "index.ts"))) 45 | fs.writeFileSync(makePath(path, "index.ts"), "", { encoding: "utf-8" }); 46 | if (entry === "index.ts") return; 47 | const file = fs.readFileSync(makePath(path, entry), { encoding: "utf-8" }); 48 | const { typeExports = [], normExports = [] } = matchFile(file); 49 | if (typeExports.length >= 1) { 50 | type.push(...typeExports); 51 | fs.appendFileSync( 52 | makePath(path, "index.ts"), 53 | `export type { ${typeExports.filter((item) => item !== "").join(", ")} } from './${entry.slice( 54 | 0, 55 | -3, 56 | )}.js';\n`, 57 | { encoding: "utf-8" }, 58 | ); 59 | } 60 | if (normExports.length >= 1) { 61 | normal.push(...normExports); 62 | fs.appendFileSync( 63 | makePath(path, "index.ts"), 64 | `export { ${normExports.filter((item) => item !== "").join(", ")} } from './${entry.slice(0, -3)}.js';\n`, 65 | { encoding: "utf-8" }, 66 | ); 67 | } 68 | } 69 | }); 70 | 71 | dirs.forEach(async (item) => { 72 | if (item.exports.type.length) { 73 | fs.appendFileSync(indexPath, `export type { ${item.exports.type.join(",")} } from './${item.name}';\n`, { 74 | encoding: "utf-8", 75 | }); 76 | } 77 | if (item.exports.normal.length) { 78 | fs.appendFileSync(indexPath, `export { ${item.exports.normal.join(",")} } from './${item.name}';\n`, { 79 | encoding: "utf-8", 80 | }); 81 | } 82 | }); 83 | 84 | return { 85 | normal: normal.flat(1), 86 | type: type.flat(1), 87 | }; 88 | } 89 | recursiveDirRead(makePath(BASE_PATH, "src")); 90 | -------------------------------------------------------------------------------- /src/global.d.ts: -------------------------------------------------------------------------------- 1 | declare interface IWorkerOptions extends WorkerOptions {} 2 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | export type { IWorkerOptions,IWorkerImpl } from './internals'; 2 | export { browser } from './internals'; 3 | export type { ThreadError,GetReturnType,Awaited,WorkerThreadFn,IThreadOptions,IThread,ThreadConstructor } from './models'; 4 | export { StatusCode } from './models'; 5 | export { PromisePool,Queue } from './sync'; 6 | export type { MaybePromise,ThreadArgs,ThreadPoolParams,AnyThread,ITransferable } from './threads'; 7 | export { ThreadPool,yieldMicrotask,ThreadImpl,Thread,InlineThread,isMarkedTransferable,isTransferable,createTransferable } from './threads'; 8 | export { workerInit } from './worker'; 9 | -------------------------------------------------------------------------------- /src/internals/NodeWorker-cjs.js: -------------------------------------------------------------------------------- 1 | const { Worker: _Worker } = require("node:worker_threads"); 2 | /** @internal */ 3 | export class Worker extends _Worker { 4 | /** 5 | * @param {string | import("url").URL} src 6 | */ 7 | constructor(src, opts = {}) { 8 | super(src, opts); 9 | 10 | this.off = this.off.bind(this); 11 | this.on = this.on.bind(this); 12 | this.on("message", this.onmessage); 13 | } 14 | onmessage(e) {} 15 | terminate() { 16 | return super.terminate(); 17 | } 18 | 19 | /** 20 | * @param {any} event 21 | * @param {any} cb 22 | */ 23 | once(event, cb) { 24 | super.once(event, cb); 25 | return this; 26 | } 27 | 28 | /** 29 | * @param {string} event 30 | * @param {(err: Error) => void} cb 31 | * @param {{ once: any; }} opts 32 | */ 33 | addEventListener(event, cb, opts) { 34 | if (!opts?.once) { 35 | this.once(event, cb); 36 | } else { 37 | this.on(event, cb); 38 | } 39 | } 40 | 41 | /** 42 | * @param {string} event 43 | * @param {(err: Error) => void} cb 44 | * @param {any} opts 45 | */ 46 | removeEventListener(event, cb, opts) { 47 | this.off(event, cb); 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /src/internals/NodeWorker-esm.mjs: -------------------------------------------------------------------------------- 1 | import { Worker as _Worker } from "worker_threads"; 2 | 3 | /** @internal */ 4 | export class Worker extends _Worker { 5 | /** 6 | * @param {string | import("url").URL} src 7 | */ 8 | constructor(src, opts = {}) { 9 | super(src, opts); 10 | 11 | this.off = this.off.bind(this); 12 | this.on = this.on.bind(this); 13 | this.on("message", this.onmessage); 14 | } 15 | 16 | onmessage(e) {} 17 | 18 | terminate() { 19 | return super.terminate(); 20 | } 21 | 22 | /** 23 | * @param {any} event 24 | * @param {any} cb 25 | */ 26 | once(event, cb) { 27 | super.once(event, cb); 28 | return this; 29 | } 30 | 31 | /** 32 | * @param {string} event 33 | * @param {(err: Error) => void} cb 34 | * @param {{ once: any; }} opts 35 | */ 36 | addEventListener = (event, cb, opts) => { 37 | if (!opts?.once) { 38 | this.once(event, cb); 39 | } else { 40 | this.on(event, cb); 41 | } 42 | }; 43 | 44 | /** 45 | * @param {string} event 46 | * @param {(err: Error) => void} cb 47 | * @param {any} opts 48 | */ 49 | removeEventListener(event, cb, opts) { 50 | this.off(event, cb); 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /src/internals/NodeWorker.ts: -------------------------------------------------------------------------------- 1 | import { browser } from "./utils.js"; 2 | 3 | /** @internal */ 4 | export interface IWorkerOptions extends WorkerOptions {} 5 | 6 | /** @internal */ 7 | 8 | class BrowserImpl extends Worker implements Pick { 9 | constructor(src: string | URL, opts: (IWorkerOptions & { eval?: boolean | undefined }) | undefined = {}) { 10 | super(src, opts); 11 | } 12 | 13 | postMessage( 14 | value: T, 15 | transferList?: readonly (import("worker_threads").TransferListItem | Transferable)[] | undefined, 16 | ): void; 17 | postMessage(value: T, transferList?: StructuredSerializeOptions): void; 18 | postMessage(value: T, transferList?: undefined): void { 19 | super.postMessage(value, transferList); 20 | } 21 | 22 | addEventListener< 23 | Event extends keyof WorkerEventMap = keyof WorkerEventMap, 24 | Callback extends (...args: any) => void = (event: WorkerEventMap[Event]) => void, 25 | >(event: Event, cb: Callback, opts?: AddEventListenerOptions) { 26 | if (!opts?.once) { 27 | if (browser) { 28 | super.addEventListener(event, cb, Object.assign({}, opts, { once: false })); 29 | } else { 30 | super.addEventListener(event, cb); 31 | } 32 | } else { 33 | if (browser) { 34 | super.addEventListener(event, cb, Object.assign({}, opts, { once: false })); 35 | } else { 36 | //@ts-expect-error 37 | super.once(event, cb); 38 | } 39 | } 40 | } 41 | 42 | removeEventListener< 43 | Event extends keyof WorkerEventMap = keyof WorkerEventMap, 44 | Callback extends (...args: any) => void = (event: WorkerEventMap[Event]) => void, 45 | >(event: Event, cb: Callback, opts?: EventListenerOptions | undefined) { 46 | super.removeEventListener(event, cb); 47 | } 48 | } 49 | const _Worker = BrowserImpl; 50 | export type IWorkerImpl = BrowserImpl; 51 | export { _Worker as Worker }; 52 | -------------------------------------------------------------------------------- /src/internals/index.ts: -------------------------------------------------------------------------------- 1 | export type { IWorkerOptions, IWorkerImpl } from "./NodeWorker.js"; 2 | export { browser } from "./utils.js"; 3 | -------------------------------------------------------------------------------- /src/internals/utils.ts: -------------------------------------------------------------------------------- 1 | export const browser = typeof navigator !== "undefined"; 2 | -------------------------------------------------------------------------------- /src/models/error.ts: -------------------------------------------------------------------------------- 1 | import type { StatusCode } from "./statuses.js"; 2 | 3 | export interface ThreadError extends Error { 4 | status: StatusCode; 5 | } 6 | -------------------------------------------------------------------------------- /src/models/index.ts: -------------------------------------------------------------------------------- 1 | export type { ThreadError } from './error.js'; 2 | export { StatusCode } from './statuses.js'; 3 | export type { GetReturnType, Awaited, WorkerThreadFn, IThreadOptions, IThread, ThreadConstructor } from './thread.js'; 4 | -------------------------------------------------------------------------------- /src/models/statuses.ts: -------------------------------------------------------------------------------- 1 | export enum StatusCode { 2 | OK = 200, 3 | ERROR = 400, 4 | WAITING = 300, 5 | TERMINATED = 0, 6 | } 7 | -------------------------------------------------------------------------------- /src/models/thread.ts: -------------------------------------------------------------------------------- 1 | import type { MaybePromise, ThreadArgs } from "../threads/pool.js"; 2 | import type { StatusCode } from "./statuses.js"; 3 | 4 | export type GetReturnType = T extends (...args: any[]) => ReturnType ? ReturnType : T; 5 | 6 | export type Awaited = T extends PromiseLike ? U : T; 7 | 8 | export type WorkerThreadFn = ( 9 | ...args: Args extends [...args: any[]] ? Args : [Args] 10 | ) => Promise; 11 | 12 | export interface IThreadOptions { 13 | once?: boolean; 14 | } 15 | 16 | export interface IThread, Output> { 17 | /** 18 | * Executes the thread function and returns the result. 19 | * 20 | * @returns {(Promise)} 21 | */ 22 | send(...data: Args extends any[] ? Args : [...args: Args[]]): Promise; 23 | 24 | /** Terminates the thread */ 25 | terminate(): Promise; 26 | } 27 | 28 | export interface ThreadConstructor, Output> { 29 | constructor( 30 | src: string | URL, 31 | options: { type?: "module" | undefined; once?: boolean; id?: number; maxConcurrency?: number }, 32 | ): IThread; 33 | constructor Output>( 34 | src: F, 35 | options: { type?: "module" | undefined; once?: boolean; id?: number; maxConcurrency?: number }, 36 | ): IThread, GetReturnType>; 37 | 38 | constructor( 39 | src: URL | string, 40 | options: { type?: "module" | undefined; once?: boolean; id?: number; maxConcurrency?: number }, 41 | ): IThread; 42 | } 43 | 44 | // 45 | -------------------------------------------------------------------------------- /src/sync/index.ts: -------------------------------------------------------------------------------- 1 | export { PromisePool } from './promisePool.js'; 2 | export { Queue } from './queue.js'; 3 | -------------------------------------------------------------------------------- /src/sync/promisePool.ts: -------------------------------------------------------------------------------- 1 | import { Queue } from "."; 2 | 3 | /** 4 | * PromisePool limits the number of concurrent executions for any given task. 5 | * 6 | * @example 7 | * ```ts 8 | * 9 | * const pool = new PromisePool(4); 10 | * 11 | * for (let idx = 0; idx < 7) { 12 | * // Runs the fetch request 4 times, 13 | * // halting on the 5th call. 14 | * // Waits until at least 1 or more promise resolves 15 | * // before resolving 16 | * await pool.add(() => { 17 | * return fetch(SOME_URL).then((r) => r.json()); 18 | * }); 19 | * } 20 | * ``` 21 | */ 22 | export class PromisePool { 23 | private pendingTasks: Queue<() => Promise> = new Queue(); 24 | private activeTasks: number = 0; 25 | 26 | constructor(private readonly concurrency: number) {} 27 | 28 | private async runTask(task: () => Promise): Promise { 29 | this.activeTasks++; 30 | try { 31 | await task(); 32 | } finally { 33 | this.activeTasks--; 34 | this.runNext(); 35 | } 36 | } 37 | 38 | private runNext(): void { 39 | if (this.activeTasks < this.concurrency && this.pendingTasks.length > 0) { 40 | const nextTask = this.pendingTasks.shift()!; 41 | this.runTask(nextTask); 42 | } 43 | } 44 | 45 | /** 46 | * Adds a task to the pool. 47 | * @param task The task to add to the pool. 48 | */ 49 | add(task: () => Promise): Promise { 50 | return new Promise((resolve, reject) => { 51 | this.pendingTasks.push(() => 52 | task() 53 | .then((result) => resolve(result)) 54 | .catch((error) => reject(error)), 55 | ); 56 | this.runNext(); 57 | }); 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /src/sync/queue.ts: -------------------------------------------------------------------------------- 1 | interface Node { 2 | value: T; 3 | next: Node | null; 4 | prev: Node | null; 5 | } 6 | 7 | class Node { 8 | value: T; 9 | next: Node | null = null; 10 | prev: Node | null = null; 11 | constructor(value: T, next: Node | null, prev: Node | null) { 12 | this.value = value; 13 | this.next = next; 14 | this.prev = prev; 15 | } 16 | } 17 | 18 | export class Queue { 19 | private head: Node | null; 20 | private tail: Node | null; 21 | private _length: number; 22 | 23 | constructor() { 24 | this.head = null; 25 | this.tail = null; 26 | this._length = 0; 27 | } 28 | public get length() { 29 | return this._length; 30 | } 31 | 32 | push(value: T) { 33 | var newNode = new Node(value, null, this.tail); 34 | if (!this.head) { 35 | this.head = newNode; 36 | this.tail = newNode; 37 | newNode.next = newNode; 38 | newNode.prev = newNode; 39 | } else { 40 | newNode.prev = this.tail!; 41 | newNode.next = this.head; 42 | this.tail!.next = newNode!; 43 | this.head.prev = newNode; 44 | this.tail = newNode; 45 | } 46 | this._length++; 47 | } 48 | 49 | unshift(value: T) { 50 | var newNode = new Node(value, this.head, null); 51 | 52 | if (!this.head) { 53 | this.head = newNode; 54 | this.tail = newNode; 55 | newNode.next = newNode; 56 | newNode.prev = newNode; 57 | } else { 58 | newNode.next = this.head; 59 | newNode.prev = this.tail!; 60 | this.head.prev = newNode; 61 | this.tail!.next = newNode; 62 | this.head = newNode; 63 | } 64 | this._length++; 65 | } 66 | 67 | pop() { 68 | const tail = this.tail; 69 | if (!this.head) { 70 | return; 71 | } else if (tail === this.tail) { 72 | this.head = null; 73 | this.tail = null; 74 | } else { 75 | this.tail = this.tail!.prev; 76 | this.tail!.next = this.head; 77 | this.head!.prev = this.tail; 78 | } 79 | this._length--; 80 | return tail!.value; 81 | } 82 | 83 | shift() { 84 | const head = this.head; 85 | if (!this.head) { 86 | return; 87 | } else if (this.head === this.tail) { 88 | this.head = null; 89 | this.tail = null; 90 | } else { 91 | this.head = this.head.next; 92 | this.head!.prev = this.tail!; 93 | this.tail!.next = this.head!; 94 | } 95 | this._length--; 96 | return head!.value; 97 | } 98 | 99 | [Symbol.iterator]() { 100 | let currentNode = this.head; 101 | 102 | return { 103 | next: () => { 104 | if (!currentNode) { 105 | return { done: true, value: undefined }; 106 | } 107 | 108 | const value = currentNode.value; 109 | currentNode = currentNode.next; 110 | 111 | return { done: false, value }; 112 | }, 113 | }; 114 | } 115 | } 116 | -------------------------------------------------------------------------------- /src/threads/channel-cjs.js: -------------------------------------------------------------------------------- 1 | const { MessageChannel: MC } = require("worker_threads"); 2 | 3 | export const MessageChannel = MC; 4 | -------------------------------------------------------------------------------- /src/threads/channel-esm.mjs: -------------------------------------------------------------------------------- 1 | import { MessageChannel as MC } from "worker_threads"; 2 | 3 | export const MessageChannel = MC; 4 | -------------------------------------------------------------------------------- /src/threads/channel.ts: -------------------------------------------------------------------------------- 1 | import { browser } from "./../internals/index.js"; 2 | //@ts-ignore 3 | const _MessageChannel = browser ? globalThis.MessageChannel : MessageChannel; 4 | 5 | export { _MessageChannel as MessageChannel }; 6 | -------------------------------------------------------------------------------- /src/threads/index.ts: -------------------------------------------------------------------------------- 1 | export type { MaybePromise, ThreadArgs, ThreadPoolParams, AnyThread } from "./pool.js"; 2 | export { ThreadPool } from "./pool.js"; 3 | export { yieldMicrotask, ThreadImpl, Thread, InlineThread } from "./thread.js"; 4 | export type { ITransferable } from "./transferable.js"; 5 | export { isMarkedTransferable, isTransferable, createTransferable } from "./transferable.js"; 6 | -------------------------------------------------------------------------------- /src/threads/pool.ts: -------------------------------------------------------------------------------- 1 | // @ts-nocheck 2 | import { InlineThread, Thread, yieldMicrotask } from "./thread.js"; 3 | import type { WorkerThreadFn } from "../models"; 4 | import { Queue } from "../sync/index.js"; 5 | /** Utility type for a value that may or may not be a Promise */ 6 | export type MaybePromise

= P | Promise

; 7 | 8 | /** Utility type for defining Worker Thread arguments */ 9 | export type ThreadArgs = T extends [...args: infer Args] ? [...args: Args] : [...args: T[]]; 10 | 11 | export type ThreadPoolParams, Output> = { 12 | task: WorkerThreadFn> | string | URL; 13 | count: number; 14 | type?: "module" | undefined; 15 | maxConcurrency?: number; 16 | }; 17 | 18 | export type AnyThread = 19 | | InlineThread> 20 | | Thread>; 21 | 22 | export abstract class AbstractThreadPool, Output> { 23 | protected threads: Array> = []; 24 | protected readonly count: number; 25 | constructor( 26 | protected task: WorkerThreadFn> | string | URL, 27 | count: number, 28 | protected type: "module" | undefined = undefined, 29 | ) { 30 | this.count = Math.max(count, 1); 31 | } 32 | 33 | /** Executes the task on a thread */ 34 | public abstract exec(...args: Arguments extends any[] ? Arguments : [Arguments]): Promise; 35 | 36 | /** Kills each thread in the pool, terminating it */ 37 | public abstract terminate(): Promise; 38 | 39 | /** Helper method for getting a worker thread from the pool */ 40 | protected abstract getWorker(): AnyThread | null; 41 | } 42 | 43 | type TaskQueueItem = { 44 | args: Arguments; 45 | resolve: (value: any) => void; 46 | reject: (reason?: any) => void; 47 | }; 48 | 49 | /** 50 | * A static thread pool that will execute a task/function on different threads. 51 | * 52 | * @class ThreadPool 53 | * @example 54 | * import { ThreadPool } from 'nanothreads'; 55 | * 56 | * const pool = new ThreadPool({ 57 | * task: (name) => `Hello ${name}!`, 58 | * count: 4 59 | * }); 60 | * 61 | * await pool.exec("Paul") // output: "Hello Paul!" 62 | */ 63 | export class ThreadPool extends AbstractThreadPool { 64 | protected declare readonly count: number; 65 | private readonly taskQueue: Queue>; 66 | 67 | private idleWorkerQueue: Queue>; 68 | constructor(params: ThreadPoolParams) { 69 | const { task, count, maxConcurrency = 1, type } = params; 70 | super(task, count, type); 71 | 72 | this.count = Math.max(1, count); 73 | this.threads = new Array(count); 74 | this.taskQueue = new Queue(); 75 | this.idleWorkerQueue = new Queue(); 76 | 77 | const TCtor = typeof task === "function" ? InlineThread : Thread; 78 | 79 | for (let idx = 0; idx < count; idx++) { 80 | const worker = new (TCtor as new (...args: any[]) => AnyThread)(task as any, { 81 | once: false, 82 | id: idx, 83 | maxConcurrency, 84 | type, 85 | }); 86 | this.threads[idx] = worker; 87 | this.idleWorkerQueue.push(worker); 88 | } 89 | } 90 | 91 | public exec(...args: Arguments extends any[] ? Arguments : [Arguments]): Promise { 92 | const worker = this.getWorker(); 93 | if (!worker) { 94 | return new Promise((resolve, reject) => { 95 | this.taskQueue.push({ args, resolve, reject }); 96 | }); 97 | } 98 | 99 | return this.executeTask(worker, args); 100 | } 101 | 102 | public getWorker(): AnyThread | null { 103 | const worker = this.idleWorkerQueue.shift(); 104 | if (!worker || worker.isBusy) { 105 | return null; 106 | } 107 | 108 | return worker; 109 | } 110 | 111 | public async execAll(...args: Arguments[]): Promise[]> { 112 | const results: Output[] = []; 113 | 114 | for (const worker of this.threads) { 115 | results.push(worker.send(...args)); 116 | } 117 | 118 | return Promise.all(results); 119 | } 120 | 121 | private async executeTask(worker: AnyThread, args: ThreadArgs): Promise { 122 | const data = await worker.send.call(worker, ...(args as Arguments[])); 123 | 124 | if (this.taskQueue.length > 0) { 125 | const nextTask = this.taskQueue.shift()!; 126 | 127 | this.executeTask(worker, nextTask.args as ThreadArgs) 128 | .then(nextTask.resolve) 129 | .catch(nextTask.reject); 130 | } else { 131 | this.idleWorkerQueue.push(worker); 132 | } 133 | 134 | return data; 135 | } 136 | 137 | public async terminate(): Promise { 138 | await Promise.all(this.threads.map((thread) => thread.terminate())); 139 | } 140 | } 141 | -------------------------------------------------------------------------------- /src/threads/thread.ts: -------------------------------------------------------------------------------- 1 | import { StatusCode } from "../models/index.js"; 2 | import type { IThread as IThread, ThreadConstructor, WorkerThreadFn } from "../models/thread.js"; 3 | import { browser } from "../internals/index.js"; 4 | import { Worker as WorkerImpl } from "../internals/NodeWorker.js"; 5 | import { MessageChannel } from "./channel.js"; 6 | import { Queue } from "../sync/queue.js"; 7 | import type { MessagePort as NodeMessagePort } from "node:worker_threads"; 8 | import { PromisePool } from "../sync/promisePool.js"; 9 | import { isMarkedTransferable, isTransferable } from "./transferable.js"; 10 | import type { ThreadArgs } from "./pool.js"; 11 | 12 | const TEMPLATE_NODE = `const{parentPort,workerData}=require("worker_threads"),func=($1),f=function(...e){return new Promise((r,j)=>{try{r(func.apply(func,e))}catch(e){j(e)}})};parentPort.on("message",e=>{const p=e.port;p.onmessage=async({data})=>{p.postMessage(await f(...data.data))}})`; 13 | 14 | const TEMPLATE_BROWSER = `const H=async(...a)=>($1)(...a),M=p=>({data})=>H(...data.data).then(r=>p.postMessage(r));onmessage=e=>{try{const p=e.ports[0];p.onmessage=M(p)}catch(e){postMessage({data:null,error:e,status:500})}}`; 15 | 16 | function funcToString unknown>(func: Func): string { 17 | let func_str = func.toString(); 18 | return (browser ? TEMPLATE_BROWSER : TEMPLATE_NODE).replace("$1", func_str); 19 | } 20 | 21 | /** @internal creates a new worker thread */ 22 | function createWorker(src: string, options: IWorkerOptions) { 23 | return new WorkerImpl(src, options); 24 | } 25 | 26 | export const yieldMicrotask = () => new Promise((resolve) => queueMicrotask(resolve)); 27 | 28 | const ThreadType = { 29 | Inline: "inline", 30 | InlineBlob: "inline-blob", 31 | File: "file", 32 | } as const; 33 | 34 | export abstract class AbstractThread implements IThread { 35 | protected abstract channel: MessagePort; 36 | protected abstract handle: InstanceType; 37 | protected abstract options: IWorkerOptions & { eval?: boolean }; 38 | protected abstract resolvers: Queue<{ 39 | resolve: (value?: Output | PromiseLike) => void; 40 | reject: (reason?: unknown) => void; 41 | }>; 42 | protected abstract type: (typeof ThreadType)[keyof typeof ThreadType]; 43 | 44 | constructor( 45 | src: URL | string, 46 | config: { maxConcurrency?: number | null; type?: "module" | undefined; once?: boolean; id?: number }, 47 | ); 48 | constructor( 49 | src: string | URL, 50 | options: { 51 | type?: "module" | undefined; 52 | once?: boolean | undefined; 53 | id?: number | undefined; 54 | maxConcurrency?: number | undefined; 55 | }, 56 | ); 57 | constructor( 58 | src: WorkerThreadFn, 59 | config: { maxConcurrency?: number | null; type?: "module" | undefined; once?: boolean; id?: number }, 60 | ); 61 | constructor(protected src: any, protected config: any) { 62 | this.config = config; 63 | } 64 | 65 | public abstract terminate(): Promise; 66 | public abstract send(...data: Args extends any[] ? Args : Args[]): Promise; 67 | 68 | public onMessage(callback: (data: Output) => void): () => void { 69 | const handleMessage = (message: MessageEvent) => callback(message.data); 70 | 71 | this.channel.addEventListener("message", handleMessage); 72 | return () => { 73 | this.channel.removeEventListener("message", handleMessage); 74 | }; 75 | } 76 | } 77 | 78 | /** 79 | * A default implementation of a worker thread, which is used for both the `InlineThread` and the `Thread` classes. 80 | * 81 | * @example 82 | * import { ThreadImpl } from 'nanothreads'; 83 | * 84 | * const thread = new ThreadImpl(...) 85 | * 86 | */ 87 | export class ThreadImpl extends AbstractThread { 88 | private _activeCount = 0; 89 | 90 | protected declare channel: MessagePort; 91 | protected declare handle: InstanceType; 92 | protected options: IWorkerOptions & { eval?: boolean | undefined } = {}; 93 | protected declare resolvers: Queue<{ 94 | resolve: (value?: Output | PromiseLike) => void; 95 | reject: (reason?: unknown) => void; 96 | }>; 97 | protected declare src: string | WorkerThreadFn; 98 | protected declare type: "inline" | "file" | "inline-blob"; 99 | private declare pool: PromisePool; 100 | protected taskFactory: (...data: Args extends any[] ? Args : [...args: Args[]]) => Promise; 101 | 102 | constructor( 103 | src: URL | string, 104 | config: { maxConcurrency?: number | null; type?: "module" | undefined; once?: boolean; id?: number }, 105 | ); 106 | constructor( 107 | src: (...args: any[]) => Output, 108 | config: { maxConcurrency?: number | null; type?: "module" | undefined; once?: boolean; id?: number }, 109 | ); 110 | constructor( 111 | src: WorkerThreadFn, 112 | config: { maxConcurrency?: number | null; type?: "module" | undefined; once?: boolean; id?: number }, 113 | ); 114 | constructor( 115 | src: any, 116 | protected config: { maxConcurrency?: number | null; type?: "module" | undefined; once?: boolean; id?: number }, 117 | ) { 118 | super(src, config); 119 | 120 | this.resolvers = new Queue(); 121 | this.config.maxConcurrency = config.maxConcurrency === null ? null : Math.max(1, this.config.maxConcurrency! ?? 1); 122 | 123 | if (typeof this.config.maxConcurrency === "number") { 124 | this.pool = new PromisePool(this.config.maxConcurrency); 125 | } 126 | 127 | switch (typeof src) { 128 | case "function": 129 | const asStr = funcToString(src); 130 | 131 | if (browser) { 132 | this.src = URL.createObjectURL(new Blob([asStr], { type: "text/javascript" })); 133 | this.type = ThreadType.InlineBlob; 134 | } else { 135 | this.src = asStr; 136 | this.type = ThreadType.Inline; 137 | this.options = { eval: true }; 138 | } 139 | break; 140 | case "string": 141 | this.src = src as string; 142 | this.type = ThreadType.File; 143 | break; 144 | default: { 145 | throw new TypeError("Invalid source. Expected type 'function' or 'string', received " + typeof src); 146 | } 147 | } 148 | 149 | this.taskFactory = this.createTaskFactory(); 150 | 151 | if (this.config.type !== undefined) { 152 | this.options.type = this.config.type; 153 | } 154 | // Setup MessagePorts to communicate with the thread 155 | const { port1, port2 } = new MessageChannel(); 156 | 157 | this.channel = port2; 158 | 159 | this.channel.onmessage = this.onmessage; 160 | 161 | this.handle = createWorker(this.src, this.options); 162 | 163 | // Startup signal for the thread to setup 164 | this.handle.postMessage<{ port: NodeMessagePort } | undefined>( 165 | browser ? undefined : { port: port1 as unknown as NodeMessagePort }, 166 | [port1], 167 | ); 168 | } 169 | 170 | /** Returns how many tasks are waiting to be processed. */ 171 | public get activeCount() { 172 | return this._activeCount; 173 | } 174 | 175 | /** 176 | * The ID for the thread. 177 | * 178 | */ 179 | public get id() { 180 | return this.config.id ?? 0; 181 | } 182 | 183 | /** 184 | * If a concurrency limit is set, `isBusy` will return `true` if `activeCount >= maxConcurrency`. 185 | * Otherwise, it will return false. 186 | * 187 | */ 188 | public get isBusy(): boolean { 189 | if (!this.maxConcurrency) { 190 | return false; 191 | } 192 | return this.activeCount >= this.maxConcurrency; 193 | } 194 | 195 | public get maxConcurrency() { 196 | return this.config.maxConcurrency; 197 | } 198 | 199 | private onmessage = (message: MessageEvent) => { 200 | const promise = this.resolvers.shift()!; 201 | 202 | promise.resolve(message.data); 203 | 204 | if (this.maxConcurrency) { 205 | this._activeCount--; 206 | } 207 | if (this.config?.once) { 208 | this.terminate(); 209 | } 210 | }; 211 | 212 | protected createTaskFactory = () => { 213 | return (...data: Args extends any[] ? Args : [...args: Args[]]) => { 214 | return new Promise((resolve, reject) => { 215 | this.resolvers.push({ resolve, reject }); 216 | const { payload, transferables } = this.preparePayload(data); 217 | this.channel.postMessage({ data: payload }, transferables); 218 | }); 219 | }; 220 | }; 221 | 222 | private preparePayload(data: Args extends any[] ? Args : [...args: Args[]]) { 223 | const payload: any[] = []; 224 | const transferables: any[] = []; 225 | for (const d of data) { 226 | if (isMarkedTransferable(d)) { 227 | transferables.push(d.value); 228 | payload.push(d.value); 229 | } else { 230 | payload.push(d); 231 | } 232 | } 233 | return { payload, transferables }; 234 | } 235 | 236 | public send(...data: Args extends any[] ? Args : [...args: Args[]]): Promise { 237 | this._activeCount++; 238 | return this.pool.add(() => this.taskFactory.apply(this, data)); 239 | } 240 | 241 | public onMessage(callback: (data: Output) => void): () => void { 242 | const handleMessage = (message: MessageEvent) => callback(message.data); 243 | 244 | this.channel.addEventListener("message", handleMessage); 245 | return () => { 246 | this.channel.removeEventListener("message", handleMessage); 247 | }; 248 | } 249 | 250 | public async terminate(): Promise { 251 | if (this.type === "inline-blob") URL.revokeObjectURL(this.src as string); 252 | this.handle.terminate(); 253 | this.channel.close(); 254 | return StatusCode.TERMINATED; 255 | } 256 | } 257 | 258 | /** 259 | * Creates a new thread using a specified script 260 | * @example 261 | * ```ts 262 | * import { Thread } from 'nanothreads'; 263 | * 264 | * const handle = new Thread<[number, number], number>('./worker.js'); 265 | * await handle.send(4, 1); // output: 5 266 | * 267 | * ``` 268 | */ 269 | export class Thread extends ThreadImpl { 270 | constructor( 271 | src: string | URL, 272 | options: { type?: "module" | undefined; once?: boolean; id?: number; maxConcurrency?: number }, 273 | ) { 274 | super(src.toString(), options); 275 | } 276 | } 277 | 278 | /** 279 | * Creates a new thread using an inline function. 280 | * @example 281 | * ```ts 282 | * import { InlineThread } from 'nanothreads'; 283 | * 284 | * const handle = new InlineThread<[number, number], number>((a, b) => a + b); 285 | * await handle.send(4, 1); // output: 5 286 | * ``` 287 | */ 288 | export class InlineThread extends ThreadImpl { 289 | constructor( 290 | src: WorkerThreadFn, 291 | options: { once?: boolean; type?: "module" | undefined; id?: number; maxConcurrency?: number }, 292 | ) { 293 | super(src, options); 294 | } 295 | } 296 | -------------------------------------------------------------------------------- /src/threads/transferable.ts: -------------------------------------------------------------------------------- 1 | /** 2 | Marks a transferable object as being able to be transferred between threads 3 | */ 4 | export interface ITransferable { 5 | /** @internal */ 6 | [transferable]: true; 7 | 8 | value: MarkedTransferable; 9 | } 10 | const transferable = Symbol("__transferable"); 11 | 12 | type MarkedTransferable = Transferable & { __transferable: true }; 13 | 14 | const transferableConstructors: (new (...args: any[]) => any)[] = [ 15 | typeof ArrayBuffer !== "undefined" ? ArrayBuffer : undefined, 16 | typeof MessagePort !== "undefined" ? MessagePort : undefined, 17 | typeof ImageBitmap !== "undefined" ? ImageBitmap : undefined, 18 | typeof OffscreenCanvas !== "undefined" ? OffscreenCanvas : undefined, 19 | typeof TransformStream !== "undefined" ? TransformStream : undefined, 20 | typeof ReadableStream !== "undefined" ? ReadableStream : undefined, 21 | typeof WritableStream !== "undefined" ? WritableStream : undefined, 22 | typeof RTCDataChannel !== "undefined" ? RTCDataChannel : undefined, 23 | typeof VideoFrame !== "undefined" ? VideoFrame : undefined, 24 | ].filter(Boolean) as (new (...args: any[]) => any)[]; 25 | /** 26 | * @param value 27 | * @returns 28 | * @internal 29 | */ 30 | export const isMarkedTransferable = (value: unknown): value is ITransferable => { 31 | if (value && typeof value === "object" && transferable in value) return true; 32 | return false; 33 | }; 34 | 35 | export const isTransferable = (value: unknown): value is Transferable => { 36 | if (value && typeof value === "object") { 37 | if (transferable in value) { 38 | return true; 39 | } 40 | for (const ctor of transferableConstructors) { 41 | if (value instanceof ctor) { 42 | return true; 43 | } 44 | } 45 | } 46 | return false; 47 | }; 48 | 49 | /** 50 | * Marks a value as being transferable 51 | * 52 | * This is used to mark a value as being able to be transferred between threads 53 | * Transferable objects are any of the following: 54 | * - ArrayBuffer 55 | * - MessagePort 56 | * - ImageBitmap 57 | * - OffscreenCanvas 58 | * - TransformStream 59 | * - ReadableStream 60 | * - WritableStream 61 | * - RTCDataChannel 62 | * - VideoFrame 63 | * 64 | * @param {unknown} value 65 | * @returns 66 | * @throws {Error} Value is not transferable 67 | * 68 | * @example 69 | * const transferable = createTransferable(new ArrayBuffer(10)); 70 | * 71 | * // transferable is now marked as transferable 72 | * thread.send(transferable); 73 | * 74 | */ 75 | export const createTransferable = (value: unknown): ITransferable => { 76 | if (isTransferable(value)) { 77 | // @ts-ignore - this is a hack to make sure the value is marked as transferable in the other thread 78 | value[transferable] = true; 79 | return { value: value as MarkedTransferable, [transferable]: true }; 80 | } 81 | throw new Error("Value is not transferable"); 82 | }; 83 | -------------------------------------------------------------------------------- /src/worker/index.ts: -------------------------------------------------------------------------------- 1 | export { workerInit } from './init.js'; 2 | -------------------------------------------------------------------------------- /src/worker/init.ts: -------------------------------------------------------------------------------- 1 | // @ts-nocheck 2 | import { browser } from "../internals/utils.js"; 3 | import type { WorkerThreadFn } from "../models/thread.js"; 4 | 5 | /** 6 | * Initialization function for script-based worker threads 7 | * @example 8 | * ```ts 9 | * // worker.js 10 | * import { workerInit } from 'nanothreads.js.js'; 11 | * 12 | * workerInit(self, (a, b) => a + b); 13 | * ``` 14 | * 15 | * @see {@link Thread} to see how to create the worker thread 16 | * 17 | */ 18 | export const workerInit = ( 19 | target: DedicatedWorkerGlobalScope["self"] | import("node:worker_threads").MessagePort, 20 | func: WorkerThreadFn, 21 | maxConcurrent: number = 1, 22 | ) => { 23 | const f = async (...args: Args extends unknown[] ? Args : [Args]) => func(...args); 24 | 25 | const drain: ( 26 | callback: (value: Output extends Promise ? Promise> : Output) => void | PromiseLike, 27 | ...args: Args extends unknown[] ? Args : [Args] 28 | ) => void = async function (callback, ...args) { 29 | const r = await f(...args); 30 | callback.call(callback, r); 31 | }; 32 | 33 | if (browser) { 34 | ((target as DedicatedWorkerGlobalScope["self"]) ?? globalThis).onmessage = (e) => { 35 | const port = e.ports[0]; 36 | const postMessage = port.postMessage.bind(port); 37 | port.onmessage = ({ data }) => { 38 | drain(postMessage, ...data.data); 39 | }; 40 | }; 41 | } else if ("on" in target) { 42 | target.on("message", function ref(e) { 43 | const port = e.port; 44 | const postMessage = port.postMessage.bind(port); 45 | port.onmessage = ({ data }: { data: { data: Args extends unknown[] ? Args : [Args] } }) => { 46 | drain(postMessage, ...data.data); 47 | }; 48 | target.off("message", ref); 49 | }); 50 | } 51 | }; 52 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ESNext", 4 | "useDefineForClassFields": true, 5 | "module": "ES2022", 6 | "lib": ["ESNext", "WebWorker", "DOM", "ES2022.Array", "es2022"], 7 | "moduleResolution": "bundler", 8 | "strict": true, 9 | "sourceMap": true, 10 | "resolveJsonModule": true, 11 | "rootDir": ".", 12 | "allowJs": true, 13 | "declaration": true, 14 | "skipDefaultLibCheck": true, 15 | "esModuleInterop": true, 16 | "downlevelIteration": true, 17 | "verbatimModuleSyntax": true, 18 | "preserveValueImports": false, 19 | "importHelpers": true, 20 | "noUnusedLocals": false, 21 | "noUnusedParameters": false, 22 | "noImplicitReturns": true, 23 | "strictFunctionTypes": false, 24 | "skipLibCheck": true, 25 | "types": ["node"] 26 | }, 27 | "include": ["src/**/*.ts", "src/**/*.d.ts", "src/internals/NodeWorker.ts"], 28 | "exclude": [ 29 | "node_modules/**/*", 30 | "src/internals/NodeWorker-esm.mjs", 31 | "src/internals/NodeWorker-cjs.js", 32 | "dist/*", 33 | "index.mtss" 34 | ] 35 | } 36 | -------------------------------------------------------------------------------- /tsconfig.node.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ESNext", 4 | "useDefineForClassFields": true, 5 | "module": "CommonJS", 6 | "lib": ["ESNext", "WebWorker", "DOM", "ES2022.Array", "es2022"], 7 | "moduleResolution": "Node", 8 | "strict": true, 9 | "sourceMap": true, 10 | "resolveJsonModule": true, 11 | "baseUrl": ".", 12 | "allowJs": true, 13 | "rootDir": ".", 14 | "declaration": true, 15 | "esModuleInterop": true, 16 | "downlevelIteration": true, 17 | "importsNotUsedAsValues": "error", 18 | "preserveValueImports": false, 19 | "importHelpers": true, 20 | "noUnusedLocals": false, 21 | "noUnusedParameters": false, 22 | "noImplicitReturns": true, 23 | "strictFunctionTypes": false, 24 | "skipLibCheck": true, 25 | "types": ["node"] 26 | }, 27 | "include": ["src/**/*.ts", "src/**/*.d.ts", "src/internals/NodeWorker.ts"], 28 | "exclude": [ 29 | "node_modules/**/*", 30 | "src/internals/NodeWorker-esm.mjs", 31 | "src/internals/NodeWorker-cjs.js", 32 | "dist/*", 33 | "index.ts" 34 | ] 35 | } 36 | -------------------------------------------------------------------------------- /typedoc.json: -------------------------------------------------------------------------------- 1 | { 2 | "entryPoints": ["src/index.ts"], 3 | "excludeInternal": true, 4 | "excludePrivate": true, 5 | "cleanOutputDir": true, 6 | "includeVersion": true, 7 | "readme": "none", 8 | "name": "" 9 | } 10 | -------------------------------------------------------------------------------- /worker.mjs: -------------------------------------------------------------------------------- 1 | import { workerInit } from "./dist/index.mjs"; 2 | import { parentPort } from "worker_threads"; 3 | import fasta from "./benchmarks/fasta.mjs"; 4 | 5 | workerInit(parentPort, async (num) => await fasta(num)); 6 | --------------------------------------------------------------------------------