nextjs-threadify
Version:
Threading/multitasking utilities for Next.js
566 lines (556 loc) • 15.8 kB
JavaScript
// src/react/Threadium.tsx
import { useCallback, useMemo } from "react";
// src/core/env.ts
var isBrowser = typeof window !== "undefined" && typeof document !== "undefined";
var hasWorker = isBrowser && typeof Worker !== "undefined";
// src/core/transferables.ts
function isTypedArray(v) {
return ArrayBuffer.isView(v) && v.buffer instanceof ArrayBuffer;
}
function collectTransferablesDeep(value, limit = 128) {
const out = [];
const stack = [value];
const seen = /* @__PURE__ */ new Set();
while (stack.length && out.length < limit) {
const v = stack.pop();
if (!v || typeof v !== "object") continue;
if (seen.has(v)) continue;
seen.add(v);
if (v instanceof ArrayBuffer) {
out.push(v);
continue;
}
if (isTypedArray(v)) {
out.push(v.buffer);
continue;
}
if (Array.isArray(v)) {
for (let i = 0; i < v.length; i++) stack.push(v[i]);
continue;
}
for (const k in v) {
try {
stack.push(v[k]);
} catch {
}
}
}
return out;
}
// src/core/worker-blob.ts
function makeWorkerBlobUrl() {
const workerFn = () => {
const serializeError = (err) => {
if (!err) return { message: "Unknown error" };
return {
message: err.message || String(err),
name: err.name || "Error",
stack: err.stack || ""
};
};
function isTypedArray2(v) {
return ArrayBuffer.isView(v) && v.buffer instanceof ArrayBuffer;
}
function collectTransferablesDeep2(value, limit = 128) {
const out = [];
const stack = [value];
const seen = /* @__PURE__ */ new Set();
while (stack.length && out.length < limit) {
const v = stack.pop();
if (!v || typeof v !== "object") continue;
if (seen.has(v)) continue;
seen.add(v);
if (v instanceof ArrayBuffer) {
out.push(v);
continue;
}
if (isTypedArray2(v)) {
out.push(v.buffer);
continue;
}
if (Array.isArray(v)) {
for (let i = 0; i < v.length; i++) stack.push(v[i]);
continue;
}
for (const k in v) {
try {
stack.push(v[k]);
} catch {
}
}
}
return out;
}
const fnCache = /* @__PURE__ */ new Map();
onmessage = async (e) => {
const msg = e.data || {};
const { id, code, args, preferTransferables } = msg;
if (!id) return;
try {
let fn = fnCache.get(code);
if (!fn) {
fn = new Function(
"ARGS",
`"use strict"; const __FN__ = (${code}); return __FN__.apply(null, ARGS);`
);
fnCache.set(code, fn);
}
const result = await fn(args);
if (preferTransferables) {
const transfers = collectTransferablesDeep2(result);
postMessage({ id, ok: true, result }, transfers);
} else {
postMessage({ id, ok: true, result });
}
} catch (err) {
postMessage({ id, ok: false, error: serializeError(err) });
}
};
};
const src = `(${workerFn.toString()})();`;
const blob = new Blob([src], { type: "text/javascript" });
return URL.createObjectURL(blob);
}
// src/core/id.ts
var __taskCounter = 0;
var nextTaskId = () => ++__taskCounter;
// src/core/worker-pool.ts
var MemoryPool = class {
constructor(createFn, resetFn, initialSize = 100) {
this.pool = [];
this.createFn = createFn;
this.resetFn = resetFn;
for (let i = 0; i < initialSize; i++) {
this.pool.push(createFn());
}
}
acquire() {
return this.pool.pop() || this.createFn();
}
release(obj) {
this.resetFn(obj);
this.pool.push(obj);
}
};
var LockFreeQueue = class {
constructor(size) {
this.head = 0;
this.tail = 0;
const actualSize = Math.pow(2, Math.ceil(Math.log2(size)));
this.buffer = new Array(actualSize);
this.mask = actualSize - 1;
}
enqueue(item) {
const nextTail = this.tail + 1 & this.mask;
if (nextTail === this.head) return false;
this.buffer[this.tail] = item;
this.tail = nextTail;
return true;
}
dequeue() {
if (this.head === this.tail) return void 0;
const item = this.buffer[this.head];
this.buffer[this.head] = void 0;
this.head = this.head + 1 & this.mask;
return item;
}
isEmpty() {
return this.head === this.tail;
}
size() {
return this.tail - this.head & this.mask;
}
};
var WorkerPool = class {
constructor(opts = {}) {
this.url = null;
this.workers = [];
this.taskMap = /* @__PURE__ */ new Map();
this.completed = 0;
this.failed = 0;
this.latencies = [];
this.destroyed = false;
const cores = isBrowser && navigator?.hardwareConcurrency || 4;
const defaultPool = Math.max(1, Math.min(cores - 1, 4));
this.opts = {
poolSize: Math.max(1, opts.poolSize ?? defaultPool),
maxQueue: opts.maxQueue ?? 256,
warmup: opts.warmup ?? true,
strategy: opts.strategy ?? "auto",
minWorkTimeMs: opts.minWorkTimeMs ?? 6,
saturation: opts.saturation ?? "enqueue",
preferTransferables: opts.preferTransferables ?? true,
name: opts.name ?? "cthread",
timeoutMs: opts.timeoutMs ?? 1e4
};
this.queue = new LockFreeQueue(this.opts.maxQueue);
this.taskPool = new MemoryPool(
() => ({
id: 0,
code: "",
args: [],
resolve: () => {
},
reject: () => {
},
priority: 0,
timeoutAt: void 0,
signal: null,
preferTransferables: false
}),
(task) => {
task.id = 0;
task.code = "";
task.args = [];
task.resolve = () => {
};
task.reject = () => {
};
task.priority = 0;
task.timeoutAt = void 0;
task.signal = null;
task.preferTransferables = false;
},
this.opts.maxQueue
);
this.messagePool = new MemoryPool(
() => ({ id: 0, code: "", args: [], preferTransferables: false }),
(msg) => {
msg.id = 0;
msg.code = "";
msg.args = [];
msg.preferTransferables = false;
},
this.opts.maxQueue
);
if (hasWorker) {
this.url = makeWorkerBlobUrl();
for (let i = 0; i < this.opts.poolSize; i++) {
const w = new Worker(this.url);
const slot = { id: i, w, busy: false };
w.onmessage = (e) => this.handleWorkerMessage(slot, e);
w.onerror = () => {
};
this.workers.push(slot);
}
if (this.opts.warmup) this.warmup();
}
}
/** Get current pool statistics with ultra-fast calculations */
getStats() {
const avgLatency = this.latencies.length ? Math.round(
this.latencies.reduce((a, b) => a + b, 0) / this.latencies.length
) : 0;
return {
name: this.opts.name,
poolSize: this.opts.poolSize,
busy: this.workers.filter((w) => w.busy).length,
idle: this.workers.filter((w) => !w.busy).length,
inFlight: this.taskMap.size,
queued: this.queue.size(),
// Use lock-free queue size
completed: this.completed,
failed: this.failed,
avgLatencyMs: avgLatency
};
}
/** Ultra-fast task execution with memory pooling */
run(code, args, options = {}) {
if (this.destroyed) {
return Promise.reject(new Error("Pool has been destroyed"));
}
const task = this.taskPool.acquire();
task.id = nextTaskId();
task.code = code;
task.args = args;
task.priority = options.priority ?? 0;
task.timeoutAt = options.timeoutMs ? Date.now() + options.timeoutMs : void 0;
task.signal = options.signal ?? null;
task.preferTransferables = options.preferTransferables ?? this.opts.preferTransferables;
return new Promise((resolve, reject) => {
task.resolve = resolve;
task.reject = reject;
this.taskMap.set(task.id, {
createdAt: Date.now(),
resolve,
reject
});
if (task.timeoutAt) {
const timeout = setTimeout(() => {
this.taskMap.delete(task.id);
reject(new Error(`Task ${task.id} timed out`));
}, options.timeoutMs);
const originalResolve = task.resolve;
task.resolve = (value) => {
clearTimeout(timeout);
originalResolve(value);
};
}
if (task.signal?.aborted) {
this.taskMap.delete(task.id);
reject(new Error(`Task ${task.id} was aborted`));
return;
}
if (task.signal) {
const abortHandler = () => {
this.taskMap.delete(task.id);
reject(new Error(`Task ${task.id} was aborted`));
};
task.signal.addEventListener("abort", abortHandler, { once: true });
}
this.enqueueTask(task);
});
}
/** Ultra-fast task enqueueing with lock-free queue */
enqueueTask(task) {
if (this.opts.strategy === "inline") {
this.runInline(task);
return;
}
if (!this.queue.enqueue(task)) {
if (this.opts.saturation === "reject") {
this.taskPool.release(task);
task.reject(new Error("Queue is full"));
return;
} else if (this.opts.saturation === "inline") {
this.runInline(task);
return;
}
}
this.processQueue();
}
/** Ultra-fast queue processing with lock-free operations */
processQueue() {
while (!this.queue.isEmpty()) {
const worker = this.findAvailableWorker();
if (!worker) break;
const task = this.queue.dequeue();
if (!task) break;
this.assignTaskToWorker(worker, task);
}
}
/** Find an available worker */
findAvailableWorker() {
return this.workers.find((w) => !w.busy) || null;
}
/** Ultra-fast worker assignment with memory pooling */
assignTaskToWorker(worker, task) {
worker.busy = true;
const message = this.messagePool.acquire();
message.id = task.id;
message.code = task.code;
message.args = task.args;
message.preferTransferables = task.preferTransferables;
const transferables = task.preferTransferables ? collectTransferablesDeep(task.args) : [];
if (transferables.length > 0) {
worker.w.postMessage(message, transferables);
} else {
worker.w.postMessage(message);
}
this.messagePool.release(message);
}
/** Run task inline on main thread */
runInline(task) {
try {
const fn = new Function("return " + task.code)();
const result = fn(...task.args);
if (result instanceof Promise) {
result.then(task.resolve).catch(task.reject);
} else {
task.resolve(result);
}
} catch (error) {
task.reject(error);
}
}
/** Ultra-fast worker message handling with memory pool management */
handleWorkerMessage(worker, e) {
const { id, result, error } = e.data;
const taskInfo = this.taskMap.get(id);
if (!taskInfo) return;
this.taskMap.delete(id);
worker.busy = false;
const latency = Date.now() - taskInfo.createdAt;
this.latencies.push(latency);
if (this.latencies.length > 100) {
this.latencies[this.latencies.length % 100] = latency;
}
if (error) {
this.failed++;
taskInfo.reject(new Error(error));
} else {
this.completed++;
taskInfo.resolve(result);
}
this.processQueue();
}
/** Warm up workers */
warmup() {
const warmupCode = "() => 0";
const promises = this.workers.map((worker) => {
return new Promise((resolve) => {
const originalOnMessage = worker.w.onmessage;
worker.w.onmessage = () => {
worker.w.onmessage = originalOnMessage;
resolve();
};
worker.w.postMessage({
id: -1,
code: warmupCode,
args: [],
preferTransferables: false
});
});
});
Promise.all(promises).catch(() => {
});
}
/** Destroy the pool and clean up all resources */
destroy() {
this.destroyed = true;
while (!this.queue.isEmpty()) {
const task = this.queue.dequeue();
if (task) {
this.taskPool.release(task);
}
}
this.taskMap.clear();
for (const worker of this.workers) {
worker.w.terminate();
}
this.workers.length = 0;
if (this.url) {
URL.revokeObjectURL(this.url);
this.url = null;
}
}
};
// src/api/pool.ts
var __pool = null;
var __poolOpts = null;
function configureThreaded(opts = {}) {
__poolOpts = { ...__poolOpts || {}, ...opts };
if (__pool && isBrowser) {
__pool.destroy();
__pool = null;
}
}
function getPool() {
if (!isBrowser || !hasWorker) {
return new WorkerPool({
...__poolOpts || {},
poolSize: 0,
strategy: "inline",
warmup: false
});
}
if (!__pool) {
__pool = new WorkerPool(__poolOpts || {});
}
return __pool;
}
function getThreadedStats() {
const pool = getPool();
return pool.getStats();
}
function destroyThreaded() {
if (__pool) {
__pool.destroy();
__pool = null;
}
}
// src/api/threaded.ts
var codeCache = /* @__PURE__ */ new WeakMap();
function threaded(fn, defaults = {}) {
let code = codeCache.get(fn);
if (!code) {
code = fn.toString();
codeCache.set(fn, code);
}
const optimizedDefaults = {
priority: 10,
timeoutMs: 5e3,
minWorkTimeMs: 0.1,
preferTransferables: true,
...defaults
};
return (...args) => {
const pool = getPool();
return pool.run(code, args, optimizedDefaults);
};
}
function Threaded(defaults = {}) {
return (target, context) => {
if (context && typeof context === "object" && "kind" in context && (context.kind === "method" || context.kind === "getter" || context.kind === "setter")) {
const original = target;
const code = original.toString();
return function(...args) {
const pool = getPool();
return pool.run(code, args, defaults);
};
}
if (typeof target === "function") {
const code = target.toString();
return (...args) => {
const pool = getPool();
return pool.run(code, args, defaults);
};
}
return target;
};
}
// src/react/Threadium.tsx
function useThreaded(fn, deps = []) {
const threadedFn = useMemo(() => {
return threaded(fn, {
priority: 50,
// High priority for fastest execution
timeoutMs: 5e3,
minWorkTimeMs: 0.1,
preferTransferables: true
// Enable zero-copy transfers
});
}, deps);
return useCallback(
(...args) => {
return threadedFn(...args);
},
[threadedFn]
);
}
// src/api/parallel.ts
async function parallelMap(items, mapper, options = {}) {
const pool = getPool();
const code = mapper.toString();
const chunkSize = Math.max(
1,
options.chunkSize ?? Math.ceil(items.length / Math.max(1, pool.getStats().poolSize))
);
const chunks = [];
for (let i = 0; i < items.length; i += chunkSize)
chunks.push({ start: i, end: Math.min(items.length, i + chunkSize) });
const out = new Array(items.length);
await Promise.all(
chunks.map(({ start, end }) => {
const slice = items.slice(start, end).map((v, i) => [v, start + i]);
const chunkRunner = (pairs) => {
const mapperFn = (0, eval)(`(${code})`);
return pairs.map(([v, idx]) => mapperFn(v, idx, []));
};
return pool.run(chunkRunner.toString(), [slice], options).then((results) => {
const typedResults = results;
for (let i = 0; i < typedResults.length; i++) out[start + i] = typedResults[i];
});
})
);
return out;
}
export {
Threaded,
configureThreaded,
destroyThreaded,
getThreadedStats,
parallelMap,
threaded,
useThreaded
};