Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions .changeset/typed-pools-return.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
---
"comlink-worker-pool": patch
"comlink-worker-pool-react": patch
---

Correct the public scheduled API types so every pooled method returns a Promise, while reserved `then` and symbol keys are omitted from `getApi()` and the React hook API.
33 changes: 22 additions & 11 deletions packages/comlink-worker-pool-react/src/useWorkerPool.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import {
type PooledApi,
type WorkerFactory,
WorkerPool,
type WorkerPoolOptions,
Expand Down Expand Up @@ -27,6 +28,13 @@ type CallableProxy<TProxy> = {
[K in keyof TProxy]: (...args: any[]) => unknown;
};

type PooledMethod<
TProxy extends CallableProxy<TProxy>,
K extends keyof PooledApi<TProxy>,
> = PooledApi<TProxy>[K] extends (...args: infer TArgs) => infer TResult
? (...args: TArgs) => TResult
: never;

/** Options for configuring useWorkerPool. */
export interface UseWorkerPoolOptions<TProxy extends CallableProxy<TProxy>> {
/** Creates a fresh Worker instance. */
Expand Down Expand Up @@ -81,8 +89,8 @@ export interface UseWorkerPoolOptions<TProxy extends CallableProxy<TProxy>> {

/** State returned from useWorkerPool. */
export interface UseWorkerPoolResult<TProxy extends CallableProxy<TProxy>> {
/** Proxy API for direct calls, or null if initialization failed. */
api: TProxy | null;
/** Scheduled proxy API for direct calls, or null if initialization failed. */
api: PooledApi<TProxy> | null;
/** Lifecycle of the owned pool, separate from the latest task status. */
poolStatus: "initializing" | "ready" | "error" | "closed";
/** State of the latest call started through call(). */
Expand All @@ -92,10 +100,10 @@ export interface UseWorkerPoolResult<TProxy extends CallableProxy<TProxy>> {
/** Error from the latest call or pool initialization. */
error: unknown;
/** Invokes a method and tracks it as the latest call. */
call<K extends keyof TProxy>(
call<K extends keyof PooledApi<TProxy>>(
method: K,
...args: Parameters<TProxy[K]>
): Promise<Awaited<ReturnType<TProxy[K]>>>;
...args: Parameters<PooledMethod<TProxy, K>>
): Promise<Awaited<ReturnType<PooledMethod<TProxy, K>>>>;
/** Immediately closes the owned pool; null means no pool was created. */
close(): Promise<WorkerPoolShutdownReport | null>;
}
Expand All @@ -115,7 +123,7 @@ export function useWorkerPool<TProxy extends CallableProxy<TProxy>>(
>("idle");
const [result, setResult] = useState<unknown>(null);
const [error, setError] = useState<unknown>(null);
const [api, setApi] = useState<TProxy | null>(null);
const [api, setApi] = useState<PooledApi<TProxy> | null>(null);
const [poolStatus, setPoolStatus] = useState<
"initializing" | "ready" | "error" | "closed"
>("initializing");
Expand Down Expand Up @@ -260,10 +268,10 @@ export function useWorkerPool<TProxy extends CallableProxy<TProxy>>(

const callGeneration = generationRef.current;
const call = useCallback(
async <K extends keyof TProxy>(
async <K extends keyof PooledApi<TProxy>>(
method: K,
...args: Parameters<TProxy[K]>
): Promise<Awaited<ReturnType<TProxy[K]>>> => {
...args: Parameters<PooledMethod<TProxy, K>>
): Promise<Awaited<ReturnType<PooledMethod<TProxy, K>>>> => {
const bindingIsCurrent = () =>
activeCallBindingRef.current === callBinding &&
generationRef.current === callGeneration;
Expand Down Expand Up @@ -292,12 +300,15 @@ export function useWorkerPool<TProxy extends CallableProxy<TProxy>>(
}

try {
const value = await api[method](...args);
const task = api[method] as unknown as PooledMethod<TProxy, K>;
const value = (await Reflect.apply(task, api, args)) as Awaited<
ReturnType<PooledMethod<TProxy, K>>
>;
if (isCurrent()) {
setResult(() => value);
setStatus("completed");
}
return value as Awaited<ReturnType<TProxy[K]>>;
return value;
} catch (callError) {
if (isCurrent()) {
setError(() => callError);
Expand Down
69 changes: 68 additions & 1 deletion packages/comlink-worker-pool/src/index.ts
Original file line number Diff line number Diff line change
@@ -1 +1,68 @@
export * from "./WorkerPool";
import {
WorkerPool as WorkerPoolImplementation,
type WorkerPoolOptions,
} from "./WorkerPool";

type CallableProxy<TProxy> = {
// biome-ignore lint/suspicious/noExplicitAny: worker APIs may have arbitrary signatures
[K in keyof TProxy]: (...args: any[]) => unknown;
};

/** Promise-returning API exposed by WorkerPool.getApi(). */
export type PooledApi<TProxy extends CallableProxy<TProxy>> = {
[K in keyof TProxy as K extends string
? K extends "then"
? never
: K
: never]: TProxy[K] extends (...args: infer TArgs) => infer TResult
? (...args: TArgs) => Promise<Awaited<TResult>>
: never;
} & {
/** Reserved so string-indexed APIs cannot make the scheduling proxy thenable. */
readonly then?: never;
};
Comment thread
coderabbitai[bot] marked this conversation as resolved.

/** Public WorkerPool instance with the scheduled API return type. */
export type WorkerPool<
TProxy extends CallableProxy<TProxy>,
TTask extends { method: keyof TProxy; args: unknown[] } = {
method: keyof TProxy;
args: unknown[];
},
TResult = Awaited<ReturnType<TProxy[TTask["method"]]>>,
> = Omit<WorkerPoolImplementation<TProxy, TTask, TResult>, "getApi"> & {
getApi(): PooledApi<TProxy>;
};

interface WorkerPoolConstructor {
new <
TProxy extends CallableProxy<TProxy>,
TTask extends { method: keyof TProxy; args: unknown[] } = {
method: keyof TProxy;
args: unknown[];
},
TResult = Awaited<ReturnType<TProxy[TTask["method"]]>>,
>(
options: WorkerPoolOptions<TProxy>,
): WorkerPool<TProxy, TTask, TResult>;
}

export const WorkerPool =
WorkerPoolImplementation as unknown as WorkerPoolConstructor;

export type {
QueueOverflowPolicy,
Task,
WorkerFactory,
WorkerPoolEvent,
WorkerPoolObserver,
WorkerPoolOptions,
WorkerPoolShutdownReport,
WorkerPoolState,
WorkerPoolStats,
WorkerPoolTaskOutcome,
WorkerPoolWorkerRemovalReason,
WorkerTaskOptions,
WorkerTerminator,
} from "./WorkerPool";
export * from "./errors";
2 changes: 1 addition & 1 deletion scripts/smoke-package-consumer.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ function writeConsumerFiles(directory) {
);
writeFileSync(
join(directory, "consumer.ts"),
'import { WorkerPool, type WorkerPoolShutdownReport } from "comlink-worker-pool";\nimport { useWorkerPool, useWorkerTask } from "comlink-worker-pool-react";\ninterface Api { add(a: number, b: number): Promise<number> }\ndeclare const workerFactory: () => Worker;\ndeclare const proxyFactory: (worker: Worker) => Api;\nconst pool = new WorkerPool<Api>({ size: 1, workerFactory, proxyFactory, maxQueueSize: 2 });\nconst result: Promise<number> = pool.run("add", [1, 2], { priority: 1 });\nconst shutdown: Promise<WorkerPoolShutdownReport> = pool.drain();\nconst hook = useWorkerPool<Api>({ workerFactory, proxyFactory, poolSize: 1 });\nconst task = useWorkerTask(hook.api, "add");\nconst taskResult: number | null = task.result;\nvoid result;\nvoid shutdown;\nvoid taskResult;\n',
'import { WorkerPool, type PooledApi, type WorkerPoolShutdownReport } from "comlink-worker-pool";\nimport { useWorkerPool, useWorkerTask } from "comlink-worker-pool-react";\ninterface Api { add(a: number, b: number): Promise<number> }\ninterface SyncApi { sync(value: number): number; then(): Promise<void>; [Symbol.iterator](): Iterator<number> }\ninterface StringIndexedApi { [method: string]: () => number }\ndeclare const workerFactory: () => Worker;\ndeclare const proxyFactory: (worker: Worker) => Api;\ndeclare const syncProxyFactory: (worker: Worker) => SyncApi;\ndeclare const stringIndexedProxyFactory: (worker: Worker) => StringIndexedApi;\nconst pool = new WorkerPool<Api>({ size: 1, workerFactory, proxyFactory, maxQueueSize: 2 });\nconst result: Promise<number> = pool.run("add", [1, 2], { priority: 1 });\nconst shutdown: Promise<WorkerPoolShutdownReport> = pool.drain();\nconst hook = useWorkerPool<Api>({ workerFactory, proxyFactory, poolSize: 1 });\nconst task = useWorkerTask(hook.api, "add");\nconst taskResult: number | null = task.result;\nconst syncPool = new WorkerPool<SyncApi>({ size: 1, workerFactory, proxyFactory: syncProxyFactory });\nconst pooledApi: PooledApi<SyncApi> = syncPool.getApi();\nconst syncResult: Promise<number> = pooledApi.sync(1);\nconst reservedResult: Promise<void> = syncPool.run("then", []);\n// @ts-expect-error Scheduled calls always return promises.\nconst incorrectSyncResult: number = pooledApi.sync(1);\n// @ts-expect-error The then key is reserved on the scheduled proxy.\npooledApi.then();\n// @ts-expect-error Symbol methods are not exposed by the scheduled proxy.\npooledApi[Symbol.iterator]();\nconst syncHook = useWorkerPool<SyncApi>({ workerFactory, proxyFactory: syncProxyFactory });\nconst hookSyncResult: Promise<number> | undefined = syncHook.api?.sync(1);\nconst trackedSyncResult: Promise<number> = syncHook.call("sync", 1);\n// @ts-expect-error The then key is reserved on the scheduled hook API.\nsyncHook.call("then");\nconst stringIndexedPool = new WorkerPool<StringIndexedApi>({ size: 1, workerFactory, proxyFactory: stringIndexedProxyFactory });\nconst stringIndexedApi: PooledApi<StringIndexedApi> = stringIndexedPool.getApi();\nconst stringIndexedResult: Promise<number> = stringIndexedApi.work();\n// @ts-expect-error The then key stays reserved for string-indexed scheduled APIs.\nstringIndexedApi.then();\nconst stringIndexedHook = useWorkerPool<StringIndexedApi>({ workerFactory, proxyFactory: stringIndexedProxyFactory });\nconst stringIndexedHookResult: Promise<number> | undefined = stringIndexedHook.api?.work();\nconst stringIndexedTrackedResult: Promise<number> = stringIndexedHook.call("work");\n// @ts-expect-error Tracked calls also reserve then for string-indexed APIs.\nstringIndexedHook.call("then");\nvoid result;\nvoid shutdown;\nvoid taskResult;\nvoid syncResult;\nvoid reservedResult;\nvoid incorrectSyncResult;\nvoid hookSyncResult;\nvoid trackedSyncResult;\nvoid stringIndexedResult;\nvoid stringIndexedHookResult;\nvoid stringIndexedTrackedResult;\n',
);
writeFileSync(
join(directory, "tsconfig.json"),
Expand Down
Loading