Skip to content

Commit c191918

Browse files
authored
Merge pull request #6 from ivancorrea/main
Refactor: Optimization: Single `fastHash` call per execution
2 parents c136264 + a932987 commit c191918

File tree

7 files changed

+100
-57
lines changed

7 files changed

+100
-57
lines changed

package-lock.json

Lines changed: 2 additions & 2 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "bee-threads",
3-
"version": "4.0.5",
3+
"version": "4.0.9",
44
"description": "Handle threading as promises. Run CPU-intensive code in worker threads with a simple fluent API.",
55
"main": "dist/index.js",
66
"module": "dist/index.js",

src/coalescing.ts

Lines changed: 9 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -171,22 +171,24 @@ export function clearNonDeterministicCache(): void {
171171
* @param fnString - Function source code
172172
* @param args - Function arguments
173173
* @param context - Execution context (closures)
174+
* @param fnHash - Pre-computed function hash (optional, avoids recomputation)
174175
* @returns Unique request key
175176
*/
176177
function createRequestKey(
177178
fnString: string,
178179
args: unknown[],
179-
context: Record<string, unknown> | null | undefined
180+
context: Record<string, unknown> | null | undefined,
181+
fnHash?: string
180182
): string {
181183
// Hash the function (avoids huge keys for large functions)
182-
const fnHash = fastHash(fnString);
184+
const hash = fnHash ?? fastHash(fnString);
183185

184186
// Use createContextKey for args and context (faster than JSON.stringify)
185187
// Note: createContextKey handles empty arrays/objects efficiently
186188
const argsKey = createContextKey(args);
187189
const contextKey = context ? createContextKey(context) : '';
188190

189-
return `${fnHash}:${argsKey}:${contextKey}`;
191+
return `${hash}:${argsKey}:${contextKey}`;
190192
}
191193

192194
// ============================================================================
@@ -209,14 +211,16 @@ function createRequestKey(
209211
* @param context - Execution context
210212
* @param factory - Factory function that creates the actual promise
211213
* @param skipCoalescing - Force skip coalescing for this request
214+
* @param fnHash - Pre-computed function hash (optional, avoids recomputation)
212215
* @returns The (possibly shared) promise
213216
*/
214217
export function coalesce<T>(
215218
fnString: string,
216219
args: unknown[],
217220
context: Record<string, unknown> | null | undefined,
218221
factory: () => Promise<T>,
219-
skipCoalescing: boolean = false
222+
skipCoalescing: boolean = false,
223+
fnHash?: string
220224
): Promise<T> {
221225
// Skip coalescing if:
222226
// 1. Globally disabled
@@ -226,7 +230,7 @@ export function coalesce<T>(
226230
return factory();
227231
}
228232

229-
const key = createRequestKey(fnString, args, context);
233+
const key = createRequestKey(fnString, args, context, fnHash);
230234

231235
// Check if there's already an in-flight promise for this request
232236
const existing = inFlightPromises.get(key);

src/execution.ts

Lines changed: 23 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -56,11 +56,17 @@ import type {
5656

5757
/**
5858
* Executes a function once in a worker thread (no retry).
59+
*
60+
* @param fn - Function to execute
61+
* @param args - Arguments to pass to the function
62+
* @param options - Execution options
63+
* @param precomputedHash - Pre-computed function hash (optional, avoids recomputation)
5964
*/
6065
export async function executeOnce<T = unknown>(
6166
fn: Function | { toString(): string },
6267
args: unknown[],
63-
options: ExecutionOptions = {}
68+
options: ExecutionOptions = {},
69+
precomputedHash?: string
6470
): Promise<T | SafeResult<T>> {
6571
const {
6672
safe = false,
@@ -75,8 +81,8 @@ export async function executeOnce<T = unknown>(
7581
const startTime = Date.now();
7682
const fnString = fn.toString();
7783

78-
// Compute hash for worker affinity
79-
const fnHash = fastHash(fnString);
84+
// Use pre-computed hash or compute now
85+
const fnHash = precomputedHash ?? fastHash(fnString);
8086

8187
// Pre-execution checks
8288
if (signal?.aborted) {
@@ -303,14 +309,23 @@ export async function executeOnce<T = unknown>(
303309

304310
/**
305311
* Executes a function with optional retry logic and exponential backoff.
312+
*
313+
* @param fn - Function to execute
314+
* @param args - Arguments to pass to the function
315+
* @param options - Execution options
316+
* @param precomputedHash - Pre-computed function hash (optional, avoids recomputation)
306317
*/
307318
export async function execute<T = unknown>(
308319
fn: Function | { toString(): string },
309320
args: unknown[],
310-
options: ExecutionOptions & { retry?: RetryConfig } = {}
321+
options: ExecutionOptions & { retry?: RetryConfig } = {},
322+
precomputedHash?: string
311323
): Promise<T | SafeResult<T>> {
312324
const { retry: retryOpts = config.retry, safe = false, context = null, skipCoalescing = false } = options;
313325
const fnString = fn.toString();
326+
327+
// Use pre-computed hash or compute now (fallback for direct execute() calls)
328+
const fnHash = precomputedHash ?? fastHash(fnString);
314329

315330
// Wrap execution in coalescing to deduplicate identical concurrent requests
316331
// Note: Coalescing is skipped for requests with AbortSignal (each needs its own lifecycle)
@@ -319,15 +334,15 @@ export async function execute<T = unknown>(
319334
const executeWithRetry = async (): Promise<T | SafeResult<T>> => {
320335
// No retry enabled - execute once
321336
if (!retryOpts?.enabled) {
322-
return executeOnce<T>(fn, args, options);
337+
return executeOnce<T>(fn, args, options, fnHash);
323338
}
324339

325340
const { maxAttempts, baseDelay, maxDelay, backoffFactor } = retryOpts;
326341
let lastError: Error | undefined;
327342

328343
for (let attempt = 0, len = maxAttempts; attempt < len; attempt++) {
329344
try {
330-
const result = await executeOnce<T>(fn, args, { ...options, safe: false });
345+
const result = await executeOnce<T>(fn, args, { ...options, safe: false }, fnHash);
331346
return safe ? { status: 'fulfilled', value: result as T } : (result as T);
332347
} catch (err) {
333348
lastError = err as Error;
@@ -356,7 +371,8 @@ export async function execute<T = unknown>(
356371
args,
357372
context,
358373
executeWithRetry,
359-
skipCoalescing
374+
skipCoalescing,
375+
fnHash
360376
);
361377
}
362378

src/executor.ts

Lines changed: 17 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,7 @@
4040

4141
import { config } from './config';
4242
import { execute } from './execution';
43+
import { fastHash } from './pool';
4344
import { validateFunction, validateFunctionSize, validateContextSecurity } from './validation';
4445
import type { Priority, ExecutionOptions, RetryOptions } from './types';
4546

@@ -50,6 +51,7 @@ import type { Priority, ExecutionOptions, RetryOptions } from './types';
5051
/** Internal state for an executor instance */
5152
interface ExecutorState {
5253
fnString: string;
54+
fnHash: string;
5355
options: ExecutionOptions;
5456
args: unknown[];
5557
}
@@ -76,7 +78,7 @@ export interface Executor<T = unknown> {
7678
* Creates an immutable, chainable executor.
7779
*/
7880
export function createExecutor<T = unknown>(state: ExecutorState): Executor<T> {
79-
const { fnString, options, args } = state;
81+
const { fnString, fnHash, options, args } = state;
8082

8183
const executor: Executor<T> = {
8284
/**
@@ -85,6 +87,7 @@ export function createExecutor<T = unknown>(state: ExecutorState): Executor<T> {
8587
usingParams(...params: unknown[]): Executor<T> {
8688
return createExecutor<T>({
8789
fnString,
90+
fnHash,
8891
options,
8992
args: args.length > 0 ? args.concat(params) : params
9093
});
@@ -132,6 +135,7 @@ export function createExecutor<T = unknown>(state: ExecutorState): Executor<T> {
132135
}
133136
return createExecutor<T>({
134137
fnString,
138+
fnHash,
135139
options: { ...options, context: serializedContext },
136140
args
137141
});
@@ -143,6 +147,7 @@ export function createExecutor<T = unknown>(state: ExecutorState): Executor<T> {
143147
signal(abortSignal: AbortSignal): Executor<T> {
144148
return createExecutor<T>({
145149
fnString,
150+
fnHash,
146151
options: { ...options, signal: abortSignal },
147152
args
148153
});
@@ -154,6 +159,7 @@ export function createExecutor<T = unknown>(state: ExecutorState): Executor<T> {
154159
transfer(list: ArrayBufferLike[]): Executor<T> {
155160
return createExecutor<T>({
156161
fnString,
162+
fnHash,
157163
options: { ...options, transfer: list },
158164
args
159165
});
@@ -172,6 +178,7 @@ export function createExecutor<T = unknown>(state: ExecutorState): Executor<T> {
172178
};
173179
return createExecutor<T>({
174180
fnString,
181+
fnHash,
175182
options: {
176183
...options,
177184
retry: mergedRetry
@@ -190,6 +197,7 @@ export function createExecutor<T = unknown>(state: ExecutorState): Executor<T> {
190197
}
191198
return createExecutor<T>({
192199
fnString,
200+
fnHash,
193201
options: { ...options, priority: level },
194202
args
195203
});
@@ -208,6 +216,7 @@ export function createExecutor<T = unknown>(state: ExecutorState): Executor<T> {
208216
noCoalesce(): Executor<T> {
209217
return createExecutor<T>({
210218
fnString,
219+
fnHash,
211220
options: { ...options, skipCoalescing: true },
212221
args
213222
});
@@ -231,6 +240,7 @@ export function createExecutor<T = unknown>(state: ExecutorState): Executor<T> {
231240
reconstructBuffers(): Executor<T> {
232241
return createExecutor<T>({
233242
fnString,
243+
fnHash,
234244
options: { ...options, reconstructBuffers: true },
235245
args
236246
});
@@ -243,7 +253,8 @@ export function createExecutor<T = unknown>(state: ExecutorState): Executor<T> {
243253
return execute<T>(
244254
{ toString: () => fnString },
245255
args,
246-
{ ...options, poolType: 'normal' }
256+
{ ...options, poolType: 'normal' },
257+
fnHash
247258
) as Promise<T>;
248259
}
249260
};
@@ -273,8 +284,12 @@ export function createCurriedRunner(
273284
// Security: Validate function size (DoS prevention)
274285
validateFunctionSize(fnString, config.security.maxFunctionSize);
275286

287+
// Compute hash once at executor creation (not at execute time)
288+
const fnHash = fastHash(fnString);
289+
276290
return createExecutor<ReturnType<T>>({
277291
fnString,
292+
fnHash,
278293
options: baseOptions,
279294
args: []
280295
});

src/index.ts

Lines changed: 12 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -37,8 +37,8 @@
3737

3838
import { config, pools, poolCounters, queues, metrics, RUNTIME, IS_BUN } from './config';
3939
import { createCurriedRunner, Executor } from './executor';
40-
import { warmupPool, getQueueLength } from './pool';
41-
import { validateTimeout, validatePoolSize, validateContextSecurity } from './validation';
40+
import { warmupPool, getQueueLength, fastHash } from './pool';
41+
import { validateTimeout, validatePoolSize, validateContextSecurity, validateFunctionSize } from './validation';
4242
import { deepFreeze } from './utils';
4343
import { createFileWorker, terminateFileWorkers } from './file-worker';
4444
import type { FileWorkerExecutor } from './file-worker';
@@ -148,14 +148,12 @@ export function bee<T extends (...args: any[]) => any>(fn: T): CurriedFunction<R
148148
}
149149

150150
const fnString = fn.toString();
151-
151+
152152
// Security: Validate function size (DoS prevention)
153-
const fnSize = Buffer.byteLength(fnString, 'utf8');
154-
if (fnSize > config.security.maxFunctionSize) {
155-
throw new RangeError(
156-
`Function source exceeds maximum size (${fnSize} bytes > ${config.security.maxFunctionSize} bytes limit)`
157-
);
158-
}
153+
validateFunctionSize(fnString, config.security.maxFunctionSize);
154+
155+
// Compute hash once at executor creation (not at execute time)
156+
const fnHash = fastHash(fnString);
159157

160158
type R = ReturnType<T>;
161159

@@ -198,12 +196,12 @@ export function bee<T extends (...args: any[]) => any>(fn: T): CurriedFunction<R
198196
const allArgs = accumulatedArgs.length > 0
199197
? accumulatedArgs.concat(params)
200198
: params;
201-
return execute<R>(fnString, allArgs, { context: serializedClosures }) as unknown as CurriedFunction<R>;
199+
return execute<R>(fnString, allArgs, { context: serializedClosures }, fnHash) as unknown as CurriedFunction<R>;
202200
}
203201

204202
if (callArgs.length === 0) {
205203
// Empty call () - execute with accumulated args (returns Promise, which is PromiseLike)
206-
return execute<R>(fnString, accumulatedArgs, {}) as unknown as CurriedFunction<R>;
204+
return execute<R>(fnString, accumulatedArgs, {}, fnHash) as unknown as CurriedFunction<R>;
207205
}
208206

209207
// Accumulate args and return new curry (thenable for await)
@@ -221,17 +219,17 @@ export function bee<T extends (...args: any[]) => any>(fn: T): CurriedFunction<R
221219
onFulfilled?: ((value: R) => TResult1 | PromiseLike<TResult1>) | null,
222220
onRejected?: ((reason: unknown) => TResult2 | PromiseLike<TResult2>) | null
223221
): Promise<TResult1 | TResult2> => {
224-
return (execute<R>(fnString, accumulatedArgs, {}) as Promise<R>).then(onFulfilled, onRejected);
222+
return (execute<R>(fnString, accumulatedArgs, {}, fnHash) as Promise<R>).then(onFulfilled, onRejected);
225223
};
226224

227225
curry.catch = <TResult = never>(
228226
onRejected?: ((reason: unknown) => TResult | PromiseLike<TResult>) | null
229227
): Promise<R | TResult> => {
230-
return (execute<R>(fnString, accumulatedArgs, {}) as Promise<R>).catch(onRejected);
228+
return (execute<R>(fnString, accumulatedArgs, {}, fnHash) as Promise<R>).catch(onRejected);
231229
};
232230

233231
curry.finally = (onFinally?: (() => void) | null): Promise<R> => {
234-
return (execute<R>(fnString, accumulatedArgs, {}) as Promise<R>).finally(onFinally);
232+
return (execute<R>(fnString, accumulatedArgs, {}, fnHash) as Promise<R>).finally(onFinally);
235233
};
236234

237235
// Symbol.toStringTag for full Promise compatibility (enables Promise.all, etc.)

0 commit comments

Comments
 (0)