Files
UHD_Media_Tool/packages/tasks/src/registry.ts
T
rootandClaude Fable 5.1 1262340856 feat(tasks): OpenWorkflow-backed TaskEngine, task registry, outbox relay and op-ledger fencing (W1-4)
- TaskEngine port + OpenWorkflowEngine ([email protected].1, schema openworkflow):
  enqueue/signal/cancel/get/list/startWorker/stop
- defineTask registry with zod input schema; 9 stub task definitions (§3.1-8)
- TaskContext: step.run/sleep/waitForFact, redacting logger, traceId,
  attemptToken, abortSignal driven by PG cancel flag
- signal persists fact to tasks_fact first; waitForFact checks DB before waiting
- OutboxRelay: FOR UPDATE SKIP LOCKED, idempotencyKey outbox:<id>
- withAttemptToken fencing on tasks_op_ledger (own migration, merged in W2)
- tests: kill -9 crash recovery (child worker), cancel, idempotency,
  concurrent relays, early fact, lease-loss fencing

Co-Authored-By: Claude Fable 5.1 <[email protected]>
2026-09-27 17:51:21 +08:00

78 lines
2.9 KiB
TypeScript

import type { z } from "zod";
import { UnknownTaskError } from "./errors";
import type { RetryPolicy, TaskContext } from "./types";
export interface TaskDefinition<S extends z.ZodType = z.ZodType, O = unknown> {
name: string;
/** 正整数;不兼容改动 +1,并在 worker 上同时注册新旧版本直到旧 run 清空 */
version: number;
inputSchema: S;
/** 步骤默认重试策略(步骤可单独覆盖)。外部网络类建议 5 次 1s→60s,文件类 3 次 */
retryPolicy?: Partial<RetryPolicy>;
handler: (ctx: TaskContext<z.output<S>>) => Promise<O>;
}
export function defineTask<S extends z.ZodType, O>(def: TaskDefinition<S, O>): TaskDefinition<S, O> {
if (!/^[A-Za-z][A-Za-z0-9_.-]*$/.test(def.name)) throw new Error(`invalid task name: ${def.name}`);
if (!Number.isInteger(def.version) || def.version < 1) throw new Error(`invalid task version: ${def.version}`);
return def;
}
export const engineVersion = (version: number) => `v${version}`;
export const parseEngineVersion = (v: string | null): number | null => {
const m = v ? /^v(\d+)$/.exec(v) : null;
return m ? Number(m[1]) : null;
};
export class TaskRegistry {
private readonly defs = new Map<string, Map<number, TaskDefinition>>();
// biome-ignore lint/suspicious/noExplicitAny: 注册表按名存放异构的任务定义
register(...defs: TaskDefinition<any, any>[]): this {
for (const def of defs) {
let byVersion = this.defs.get(def.name);
if (!byVersion) {
byVersion = new Map();
this.defs.set(def.name, byVersion);
}
if (byVersion.has(def.version)) throw new Error(`task ${def.name}@${def.version} already registered`);
byVersion.set(def.version, def as TaskDefinition);
}
return this;
}
get(name: string, version: number): TaskDefinition {
const def = this.defs.get(name)?.get(version);
if (!def) throw new UnknownTaskError(name, version);
return def;
}
/** 新 run 一律用最高版本 */
latest(name: string): TaskDefinition {
const byVersion = this.defs.get(name);
if (!byVersion || byVersion.size === 0) throw new UnknownTaskError(name);
return byVersion.get(Math.max(...byVersion.keys()))!;
}
names(): string[] {
return [...this.defs.keys()].sort();
}
all(): TaskDefinition[] {
return [...this.defs.values()].flatMap((m) => [...m.values()]);
}
/** 按 StartWorkerOptions.versions 选择:"name@2" / "@1" / "name"(该任务全部版本) */
select(selectors?: string[]): TaskDefinition[] {
if (!selectors || selectors.length === 0) return this.all();
const picked = this.all().filter((d) =>
selectors.some((s) => {
const [name, ver] = s.split("@");
return (!name || name === d.name) && (ver === undefined || ver === "" || Number(ver) === d.version);
}),
);
if (picked.length === 0) throw new Error(`no task definitions match ${selectors.join(",")}`);
return picked;
}
}