Skip to content

Commit 9fb4ddd

Browse files
committed
Added support for Svelte & maxThread config option
1 parent 0149b6b commit 9fb4ddd

File tree

7 files changed

+106
-33
lines changed

7 files changed

+106
-33
lines changed

README.md

Lines changed: 34 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,9 +12,9 @@
1212

1313
# multithreading
1414

15-
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.
15+
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.
1616

17-
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/).
17+
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/).
1818

1919
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.
2020

@@ -110,3 +110,35 @@ In this example, the `add` function is used within the multithreaded `addBalance
110110
As with previous examples, the shared state is managed using `$claim` and `$unclaim` to guarantee proper synchronization and prevent data conflicts.
111111

112112
> 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.
113+
114+
### Using imports from external packages
115+
116+
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.
117+
118+
```js
119+
import { threaded } from "multithreading";
120+
121+
const getId = threaded(async function* () {
122+
yield {};
123+
124+
const uuid = await import("uuid"); // Import other package
125+
126+
return uuid.v4();
127+
}
128+
129+
console.log(await getId()); // 1a107623-3052-4f61-aca9-9d9388fb2d81
130+
```
131+
132+
### Usage with Svelte
133+
134+
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.
135+
136+
```js
137+
import { threaded } from "multithreading";
138+
139+
const fn = threaded(function* () {
140+
const { $claim, $unclaim } = yield {};
141+
142+
// ...
143+
}
144+
```

package.json

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "multithreading",
3-
"version": "0.1.14",
3+
"version": "0.1.15",
44
"description": "⚡ Multithreading functions in JavaScript, designed to be as simple and fast as possible.",
55
"author": "Walter van der Giessen <[email protected]>",
66
"homepage": "https://multithreading.io",
@@ -68,4 +68,4 @@
6868
"optionalDependencies": {
6969
"@rollup/rollup-linux-x64-gnu": "^4.9.1"
7070
}
71-
}
71+
}

rollup.config.dev.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@ export default ["cjs"].flatMap((type) => {
2121
],
2222
output: [
2323
{
24-
file: `dist/${type}/index.${ext}`,
24+
file: `dist/index.${ext}`,
2525
format: type,
2626
sourcemap: false,
2727
name: "multithreading",

src/index.ts

Lines changed: 43 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -20,26 +20,60 @@ type CommonGenerator<T, TReturn, TNext> =
2020

2121
type UserFunction<T extends Array<unknown> = [], TReturn = void> = (
2222
...args: T
23-
) => CommonGenerator<any, TReturn, void>;
23+
) => CommonGenerator<
24+
any,
25+
TReturn,
26+
{
27+
$claim: typeof $claim;
28+
$unclaim: typeof $unclaim;
29+
}
30+
>;
31+
32+
interface ThreadedConfig {
33+
debug: boolean;
34+
maxThreads: number;
35+
}
2436

2537
export function threaded<T extends Array<unknown>, TReturn>(
2638
fn: UserFunction<T, TReturn>
39+
): ((...args: T) => Promise<TReturn>) & { dispose: () => void };
40+
41+
export function threaded<T extends Array<unknown>, TReturn>(
42+
config: Partial<ThreadedConfig>,
43+
fn: UserFunction<T, TReturn>
44+
): ((...args: T) => Promise<TReturn>) & { dispose: () => void };
45+
46+
export function threaded<T extends Array<unknown>, TReturn>(
47+
configOrFn: Partial<ThreadedConfig> | UserFunction<T, TReturn>,
48+
maybeFn?: UserFunction<T, TReturn>
2749
): ((...args: T) => Promise<TReturn>) & { dispose: () => void } {
50+
const config: ThreadedConfig = {
51+
debug: false,
52+
maxThreads:
53+
typeof navigator !== "undefined" ? navigator.hardwareConcurrency : 4,
54+
};
55+
let fn: UserFunction<T, TReturn>;
56+
57+
if (typeof configOrFn === "function") {
58+
fn = configOrFn as UserFunction<T, TReturn>;
59+
} else {
60+
Object.assign(config, configOrFn);
61+
fn = maybeFn as UserFunction<T, TReturn>;
62+
}
63+
2864
let context: Record<string, any> = {};
2965
const workerPool: Worker[] = [];
3066
const invocationQueue = new Map<number, PromiseWithResolvers<TReturn>>();
3167

3268
workerPools.set(fn, workerPool);
3369

34-
const workerCount =
35-
typeof navigator !== "undefined" ? navigator.hardwareConcurrency : 4;
3670
let invocationCount = 0;
3771

3872
const init = (async () => {
3973
let fnStr = fn.toString();
40-
const hasDependencies = fnStr.includes("yield");
74+
const hasYield = fnStr.includes("yield");
4175

42-
if (hasDependencies) {
76+
if (hasYield) {
4377
// @ts-ignore - Call function without arguments
4478
const gen = fn();
4579
const result = await gen.next();
@@ -68,7 +102,7 @@ export function threaded<T extends Array<unknown>, TReturn>(
68102
// Polyfill for Node.js
69103
globalThis.Worker ??= (await import("web-worker")).default;
70104

71-
for (let i = 0; i < workerCount; i++) {
105+
for (let i = 0; i < config.maxThreads; i++) {
72106
const worker = new Worker(
73107
"data:text/javascript;charset=utf-8," +
74108
encodeURIComponent(workerCode.join("\n")),
@@ -91,8 +125,9 @@ export function threaded<T extends Array<unknown>, TReturn>(
91125
[$.EventType]: $.Init,
92126
[$.EventValue]: {
93127
[$.ProcessId]: i,
94-
[$.HasYield]: hasDependencies,
128+
[$.HasYield]: hasYield,
95129
[$.Variables]: serializedVariables,
130+
[$.DebugEnabled]: config.debug,
96131
},
97132
} satisfies MainEvent);
98133
}
@@ -101,7 +136,7 @@ export function threaded<T extends Array<unknown>, TReturn>(
101136
const wrapper = async (...args: T) => {
102137
await init;
103138

104-
const worker = workerPool[invocationCount % workerCount];
139+
const worker = workerPool[invocationCount % config.maxThreads];
105140

106141
const pwr = Promise.withResolvers<TReturn>();
107142
invocationQueue.set(invocationCount, pwr);

src/lib/keys.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ export const HasYield = "g";
1919
export const InvocationId = "h";
2020
export const Value = "i";
2121
export const ProcessId = "j";
22+
export const DebugEnabled = "k";
2223

2324
export declare type Function = typeof Function;
2425
export declare type Other = typeof Other;
@@ -41,3 +42,4 @@ export declare type HasYield = typeof HasYield;
4142
export declare type InvocationId = typeof InvocationId;
4243
export declare type Value = typeof Value;
4344
export declare type ProcessId = typeof ProcessId;
45+
export declare type DebugEnabled = typeof DebugEnabled;

src/lib/types.d.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ interface InitEvent {
1414
[$.ProcessId]: number;
1515
[$.HasYield]: boolean;
1616
[$.Variables]: Record<string, any>;
17+
[$.DebugEnabled]: boolean;
1718
};
1819
}
1920

src/lib/worker.worker.ts

Lines changed: 23 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -4,26 +4,21 @@ import { deserialize } from "./serialize.ts";
44
import * as $ from "./keys.ts";
55
import {
66
ClaimAcceptanceEvent,
7-
ClaimEvent,
87
InitEvent,
98
InvocationEvent,
109
MainEvent,
11-
ReturnEvent,
1210
SynchronizationEvent,
1311
ThreadEvent,
14-
UnclaimEvent,
1512
} from "./types";
1613
import { replaceContents } from "./replaceContents.ts";
1714

1815
declare var pid: number;
1916

20-
const cyanStart = "\x1b[36m";
21-
const cyanEnd = "\x1b[39m";
22-
23-
const originalLog = console.log;
24-
console.log = (...args) => {
25-
originalLog(`${cyanStart}[Thread_${pid}]${cyanEnd}`, ...args);
26-
};
17+
declare global {
18+
var pid: number;
19+
function $claim(value: Object): Promise<void>;
20+
function $unclaim(value: Object): void;
21+
}
2722

2823
globalThis.onmessage = async (e: MessageEvent<MainEvent>) => {
2924
switch (e.data[$.EventType]) {
@@ -34,20 +29,25 @@ globalThis.onmessage = async (e: MessageEvent<MainEvent>) => {
3429
Thread.handleInvocation(e.data[$.EventValue]);
3530
break;
3631
case $.ClaimAcceptance:
37-
// console.log("Claimed", e.data[$.EventValue][$.Name]);
3832
Thread.handleClaimAcceptance(e.data[$.EventValue]);
3933
break;
4034
case $.Synchronization:
4135
}
4236
};
4337

44-
globalThis.$claim = async function $claim(value: Object) {
38+
const cyanStart = "\x1b[36m";
39+
const cyanEnd = "\x1b[39m";
40+
41+
const originalLog = console.log;
42+
console.log = (...args) => {
43+
originalLog(`${cyanStart}[Thread_${pid}]${cyanEnd}`, ...args);
44+
};
45+
46+
const $claim = async function $claim(value: Object) {
4547
const valueName = Thread.shareableNameMap.get(value)!;
4648

4749
Thread.valueInUseCount[valueName]++;
4850

49-
// console.log(valueName, "claim");
50-
5151
// First check if the variable is already (being) claimed
5252
if (Thread.valueClaimMap.has(valueName)) {
5353
return Thread.valueClaimMap.get(valueName)!.promise;
@@ -63,13 +63,11 @@ globalThis.$claim = async function $claim(value: Object) {
6363
return Thread.valueClaimMap.get(valueName)!.promise;
6464
};
6565

66-
globalThis.$unclaim = function $unclaim(value: Object) {
66+
const $unclaim = function $unclaim(value: Object) {
6767
const valueName = Thread.shareableNameMap.get(value)!;
6868

6969
if (--Thread.valueInUseCount[valueName] > 0) return;
7070

71-
// console.log("Unclaimed", valueName);
72-
7371
Thread.valueClaimMap.delete(valueName);
7472
globalThis.postMessage({
7573
[$.EventType]: $.Unclaim,
@@ -80,6 +78,10 @@ globalThis.$unclaim = function $unclaim(value: Object) {
8078
} satisfies ThreadEvent);
8179
};
8280

81+
// Make globally available
82+
globalThis.$claim = $claim;
83+
globalThis.$unclaim = $unclaim;
84+
8385
// Separate namespace to avoid polluting the global namespace
8486
// and avoid name collisions with the user defined function
8587
namespace Thread {
@@ -124,9 +126,10 @@ namespace Thread {
124126
const gen = globalThis[GLOBAL_FUNCTION_NAME](...data[$.Args]);
125127

126128
hasYield && gen.next();
127-
const returnValue = await gen.next();
128-
129-
// console.log("Returned", returnValue.value);
129+
const returnValue = await gen.next({
130+
$claim,
131+
$unclaim,
132+
});
130133

131134
globalThis.postMessage({
132135
[$.EventType]: $.Return,

0 commit comments

Comments
 (0)