diff --git a/README.md b/README.md index fadae60..b1c3352 100755 --- a/README.md +++ b/README.md @@ -12,9 +12,9 @@ # multithreading -Multithreading is a tiny runtime that allows you to execute functions on separate threads. It is designed to be as simple and fast as possible, and to be used in a similar way to regular functions. +Multithreading is a tiny runtime that allows you to execute JavaScript functions on separate threads. It is designed to be as simple and fast as possible, and to be used in a similar way to regular functions. -With a minified size of only 3.8kb, it has first class support for [Node.js](https://nodejs.org/), [Deno](https://deno.com/) and the [browser](https://caniuse.com/?search=webworkers). It can also be used with any framework or library such as [React](https://react.dev/), [Vue](https://vuejs.org/) or [Svelte](https://svelte.dev/). +With a minified size of only 4.5kb, it has first class support for [Node.js](https://nodejs.org/), [Deno](https://deno.com/) and the [browser](https://caniuse.com/?search=webworkers). It can also be used with any framework or library such as [React](https://react.dev/), [Vue](https://vuejs.org/) or [Svelte](https://svelte.dev/). Depending on the environment, it uses [Worker Threads](https://nodejs.org/api/worker_threads.html) or [Web Workers](https://developer.mozilla.org/en-US/docs/Web/API/Worker). In addition to [ES6 generators](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Statements/function*) to make multithreading as simple as possible. @@ -110,3 +110,35 @@ In this example, the `add` function is used within the multithreaded `addBalance As with previous examples, the shared state is managed using `$claim` and `$unclaim` to guarantee proper synchronization and prevent data conflicts. > External functions like `add` cannot have external dependencies themselves. All variables and functions used by an external function must be declared within the function itself. + +### Using imports from external packages + +When using external modules, you can dynamically import them by using the `import()` statement. This is useful when you want to use other packages within a threaded function. + +```js +import { threaded } from "multithreading"; + +const getId = threaded(async function* () { + yield {}; + + const uuid = await import("uuid"); // Import other package + + return uuid.v4(); +} + +console.log(await getId()); // 1a107623-3052-4f61-aca9-9d9388fb2d81 +``` + +### Usage with Svelte + +Svelte disallows imports whose name start with a `$`. To use multithreading with Svelte, you can also retrieve `$claim` and `$unclaim` directly from the `yield` statement. + +```js +import { threaded } from "multithreading"; + +const fn = threaded(function* () { + const { $claim, $unclaim } = yield {}; + + // ... +} +``` \ No newline at end of file diff --git a/package.json b/package.json index 894d4e3..325c478 100755 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "multithreading", - "version": "0.1.14", + "version": "0.1.15", "description": "⚡ Multithreading functions in JavaScript, designed to be as simple and fast as possible.", "author": "Walter van der Giessen ", "homepage": "https://multithreading.io", @@ -68,4 +68,4 @@ "optionalDependencies": { "@rollup/rollup-linux-x64-gnu": "^4.9.1" } -} +} \ No newline at end of file diff --git a/rollup.config.dev.js b/rollup.config.dev.js index cd9d648..9a51ecb 100644 --- a/rollup.config.dev.js +++ b/rollup.config.dev.js @@ -21,7 +21,7 @@ export default ["cjs"].flatMap((type) => { ], output: [ { - file: `dist/${type}/index.${ext}`, + file: `dist/index.${ext}`, format: type, sourcemap: false, name: "multithreading", diff --git a/src/index.ts b/src/index.ts index 17e47c8..74404b7 100755 --- a/src/index.ts +++ b/src/index.ts @@ -20,26 +20,60 @@ type CommonGenerator = type UserFunction = [], TReturn = void> = ( ...args: T -) => CommonGenerator; +) => CommonGenerator< + any, + TReturn, + { + $claim: typeof $claim; + $unclaim: typeof $unclaim; + } +>; + +interface ThreadedConfig { + debug: boolean; + maxThreads: number; +} export function threaded, TReturn>( fn: UserFunction +): ((...args: T) => Promise) & { dispose: () => void }; + +export function threaded, TReturn>( + config: Partial, + fn: UserFunction +): ((...args: T) => Promise) & { dispose: () => void }; + +export function threaded, TReturn>( + configOrFn: Partial | UserFunction, + maybeFn?: UserFunction ): ((...args: T) => Promise) & { dispose: () => void } { + const config: ThreadedConfig = { + debug: false, + maxThreads: + typeof navigator !== "undefined" ? navigator.hardwareConcurrency : 4, + }; + let fn: UserFunction; + + if (typeof configOrFn === "function") { + fn = configOrFn as UserFunction; + } else { + Object.assign(config, configOrFn); + fn = maybeFn as UserFunction; + } + let context: Record = {}; const workerPool: Worker[] = []; const invocationQueue = new Map>(); workerPools.set(fn, workerPool); - const workerCount = - typeof navigator !== "undefined" ? navigator.hardwareConcurrency : 4; let invocationCount = 0; const init = (async () => { let fnStr = fn.toString(); - const hasDependencies = fnStr.includes("yield"); + const hasYield = fnStr.includes("yield"); - if (hasDependencies) { + if (hasYield) { // @ts-ignore - Call function without arguments const gen = fn(); const result = await gen.next(); @@ -68,7 +102,7 @@ export function threaded, TReturn>( // Polyfill for Node.js globalThis.Worker ??= (await import("web-worker")).default; - for (let i = 0; i < workerCount; i++) { + for (let i = 0; i < config.maxThreads; i++) { const worker = new Worker( "data:text/javascript;charset=utf-8," + encodeURIComponent(workerCode.join("\n")), @@ -91,8 +125,9 @@ export function threaded, TReturn>( [$.EventType]: $.Init, [$.EventValue]: { [$.ProcessId]: i, - [$.HasYield]: hasDependencies, + [$.HasYield]: hasYield, [$.Variables]: serializedVariables, + [$.DebugEnabled]: config.debug, }, } satisfies MainEvent); } @@ -101,7 +136,7 @@ export function threaded, TReturn>( const wrapper = async (...args: T) => { await init; - const worker = workerPool[invocationCount % workerCount]; + const worker = workerPool[invocationCount % config.maxThreads]; const pwr = Promise.withResolvers(); invocationQueue.set(invocationCount, pwr); diff --git a/src/lib/keys.ts b/src/lib/keys.ts index b3ff7d2..fbaf352 100644 --- a/src/lib/keys.ts +++ b/src/lib/keys.ts @@ -19,6 +19,7 @@ export const HasYield = "g"; export const InvocationId = "h"; export const Value = "i"; export const ProcessId = "j"; +export const DebugEnabled = "k"; export declare type Function = typeof Function; export declare type Other = typeof Other; @@ -41,3 +42,4 @@ export declare type HasYield = typeof HasYield; export declare type InvocationId = typeof InvocationId; export declare type Value = typeof Value; export declare type ProcessId = typeof ProcessId; +export declare type DebugEnabled = typeof DebugEnabled; diff --git a/src/lib/types.d.ts b/src/lib/types.d.ts index 62541e9..f03f82c 100644 --- a/src/lib/types.d.ts +++ b/src/lib/types.d.ts @@ -14,6 +14,7 @@ interface InitEvent { [$.ProcessId]: number; [$.HasYield]: boolean; [$.Variables]: Record; + [$.DebugEnabled]: boolean; }; } diff --git a/src/lib/worker.worker.ts b/src/lib/worker.worker.ts index 8227c1d..75135e7 100644 --- a/src/lib/worker.worker.ts +++ b/src/lib/worker.worker.ts @@ -4,26 +4,21 @@ import { deserialize } from "./serialize.ts"; import * as $ from "./keys.ts"; import { ClaimAcceptanceEvent, - ClaimEvent, InitEvent, InvocationEvent, MainEvent, - ReturnEvent, SynchronizationEvent, ThreadEvent, - UnclaimEvent, } from "./types"; import { replaceContents } from "./replaceContents.ts"; declare var pid: number; -const cyanStart = "\x1b[36m"; -const cyanEnd = "\x1b[39m"; - -const originalLog = console.log; -console.log = (...args) => { - originalLog(`${cyanStart}[Thread_${pid}]${cyanEnd}`, ...args); -}; +declare global { + var pid: number; + function $claim(value: Object): Promise; + function $unclaim(value: Object): void; +} globalThis.onmessage = async (e: MessageEvent) => { switch (e.data[$.EventType]) { @@ -34,20 +29,25 @@ globalThis.onmessage = async (e: MessageEvent) => { Thread.handleInvocation(e.data[$.EventValue]); break; case $.ClaimAcceptance: - // console.log("Claimed", e.data[$.EventValue][$.Name]); Thread.handleClaimAcceptance(e.data[$.EventValue]); break; case $.Synchronization: } }; -globalThis.$claim = async function $claim(value: Object) { +const cyanStart = "\x1b[36m"; +const cyanEnd = "\x1b[39m"; + +const originalLog = console.log; +console.log = (...args) => { + originalLog(`${cyanStart}[Thread_${pid}]${cyanEnd}`, ...args); +}; + +const $claim = async function $claim(value: Object) { const valueName = Thread.shareableNameMap.get(value)!; Thread.valueInUseCount[valueName]++; - // console.log(valueName, "claim"); - // First check if the variable is already (being) claimed if (Thread.valueClaimMap.has(valueName)) { return Thread.valueClaimMap.get(valueName)!.promise; @@ -63,13 +63,11 @@ globalThis.$claim = async function $claim(value: Object) { return Thread.valueClaimMap.get(valueName)!.promise; }; -globalThis.$unclaim = function $unclaim(value: Object) { +const $unclaim = function $unclaim(value: Object) { const valueName = Thread.shareableNameMap.get(value)!; if (--Thread.valueInUseCount[valueName] > 0) return; - // console.log("Unclaimed", valueName); - Thread.valueClaimMap.delete(valueName); globalThis.postMessage({ [$.EventType]: $.Unclaim, @@ -80,6 +78,10 @@ globalThis.$unclaim = function $unclaim(value: Object) { } satisfies ThreadEvent); }; +// Make globally available +globalThis.$claim = $claim; +globalThis.$unclaim = $unclaim; + // Separate namespace to avoid polluting the global namespace // and avoid name collisions with the user defined function namespace Thread { @@ -124,9 +126,10 @@ namespace Thread { const gen = globalThis[GLOBAL_FUNCTION_NAME](...data[$.Args]); hasYield && gen.next(); - const returnValue = await gen.next(); - - // console.log("Returned", returnValue.value); + const returnValue = await gen.next({ + $claim, + $unclaim, + }); globalThis.postMessage({ [$.EventType]: $.Return,