@faapi/faapi 6.32.0 → 6.34.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cli/index.js +307 -114
- package/dist/cli/index.js.map +1 -1
- package/dist/{contextTypes-WKsf3cCE.d.ts → contextTypes-Bsc1ljvn.d.ts} +249 -38
- package/dist/index.d.ts +2 -2
- package/dist/index.js +415 -223
- package/dist/index.js.map +1 -1
- package/dist/testing.d.ts +1 -1
- package/dist/testing.js +27 -36
- package/dist/testing.js.map +1 -1
- package/dist/workerEntry.d.ts +29 -8
- package/dist/workerEntry.js +67 -17
- package/dist/workerEntry.js.map +1 -1
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -2126,19 +2126,6 @@ var TaskCancelledError = class extends Error {
|
|
|
2126
2126
|
this.name = "TaskCancelledError";
|
|
2127
2127
|
}
|
|
2128
2128
|
};
|
|
2129
|
-
function safeConfig(config) {
|
|
2130
|
-
if (config === void 0 || config === null) return config;
|
|
2131
|
-
try {
|
|
2132
|
-
structuredClone(config);
|
|
2133
|
-
return config;
|
|
2134
|
-
} catch {
|
|
2135
|
-
}
|
|
2136
|
-
try {
|
|
2137
|
-
return JSON.parse(JSON.stringify(config));
|
|
2138
|
-
} catch {
|
|
2139
|
-
return void 0;
|
|
2140
|
-
}
|
|
2141
|
-
}
|
|
2142
2129
|
function reviveError(payload) {
|
|
2143
2130
|
const err = new Error(payload?.message ?? "task worker error");
|
|
2144
2131
|
if (payload?.name !== void 0) err.name = payload.name;
|
|
@@ -2207,6 +2194,53 @@ async function runTaskInWorker(options) {
|
|
|
2207
2194
|
worker.on(
|
|
2208
2195
|
"message",
|
|
2209
2196
|
(msg) => {
|
|
2197
|
+
if (msg?.type === "tasks-call") {
|
|
2198
|
+
const { seq, method, args } = msg;
|
|
2199
|
+
const reply = (body) => {
|
|
2200
|
+
try {
|
|
2201
|
+
worker.postMessage({ type: "tasks-result", seq, ...body });
|
|
2202
|
+
} catch (err) {
|
|
2203
|
+
try {
|
|
2204
|
+
worker.postMessage({
|
|
2205
|
+
type: "tasks-result",
|
|
2206
|
+
seq,
|
|
2207
|
+
ok: false,
|
|
2208
|
+
error: {
|
|
2209
|
+
name: "Error",
|
|
2210
|
+
message: `taskCtx.tasks.${method} result is not cloneable: ${String(err)}`,
|
|
2211
|
+
stack: void 0,
|
|
2212
|
+
props: {}
|
|
2213
|
+
}
|
|
2214
|
+
});
|
|
2215
|
+
} catch {
|
|
2216
|
+
}
|
|
2217
|
+
}
|
|
2218
|
+
};
|
|
2219
|
+
if (!options.onTasksCall) {
|
|
2220
|
+
reply({
|
|
2221
|
+
ok: false,
|
|
2222
|
+
error: {
|
|
2223
|
+
name: "Error",
|
|
2224
|
+
message: `taskCtx.tasks.${method} is unavailable: no host handler was provided for this execution`,
|
|
2225
|
+
stack: void 0,
|
|
2226
|
+
props: {}
|
|
2227
|
+
}
|
|
2228
|
+
});
|
|
2229
|
+
return;
|
|
2230
|
+
}
|
|
2231
|
+
options.onTasksCall(method, args ?? []).then((value) => reply({ ok: true, value })).catch((err) => {
|
|
2232
|
+
reply({
|
|
2233
|
+
ok: false,
|
|
2234
|
+
error: {
|
|
2235
|
+
name: err instanceof Error ? err.name : "Error",
|
|
2236
|
+
message: err instanceof Error ? err.message : String(err),
|
|
2237
|
+
stack: err instanceof Error ? err.stack : void 0,
|
|
2238
|
+
props: {}
|
|
2239
|
+
}
|
|
2240
|
+
});
|
|
2241
|
+
});
|
|
2242
|
+
return;
|
|
2243
|
+
}
|
|
2210
2244
|
if (phase === "grace") {
|
|
2211
2245
|
if (msg?.type === "done") {
|
|
2212
2246
|
finish(
|
|
@@ -2253,7 +2287,6 @@ async function runTaskInWorker(options) {
|
|
|
2253
2287
|
type: "run",
|
|
2254
2288
|
payload,
|
|
2255
2289
|
taskCtx: {
|
|
2256
|
-
config: safeConfig(taskCtx.config),
|
|
2257
2290
|
job: taskCtx.job
|
|
2258
2291
|
},
|
|
2259
2292
|
registries: options.registries,
|
|
@@ -2262,14 +2295,122 @@ async function runTaskInWorker(options) {
|
|
|
2262
2295
|
});
|
|
2263
2296
|
} catch (err) {
|
|
2264
2297
|
finish(
|
|
2265
|
-
() => reject(
|
|
2266
|
-
new Error(`Task "${taskCtx.job.name}" payload/config is not cloneable: ${String(err)}`)
|
|
2267
|
-
)
|
|
2298
|
+
() => reject(new Error(`Task "${taskCtx.job.name}" payload is not cloneable: ${String(err)}`))
|
|
2268
2299
|
);
|
|
2269
2300
|
}
|
|
2270
2301
|
});
|
|
2271
2302
|
}
|
|
2272
2303
|
|
|
2304
|
+
// src/utils/isPlainObject.ts
|
|
2305
|
+
function isPlainObject(value) {
|
|
2306
|
+
if (value === null || typeof value !== "object") {
|
|
2307
|
+
return false;
|
|
2308
|
+
}
|
|
2309
|
+
if (Array.isArray(value)) {
|
|
2310
|
+
return false;
|
|
2311
|
+
}
|
|
2312
|
+
const proto = Object.getPrototypeOf(value);
|
|
2313
|
+
return proto === null || proto === Object.prototype;
|
|
2314
|
+
}
|
|
2315
|
+
|
|
2316
|
+
// src/utils/stringifyJson.ts
|
|
2317
|
+
function stringifyJson(value) {
|
|
2318
|
+
if (!needsConversion(value, /* @__PURE__ */ new Set())) {
|
|
2319
|
+
return JSON.stringify(value);
|
|
2320
|
+
}
|
|
2321
|
+
return JSON.stringify(convertValue(value, /* @__PURE__ */ new Set()));
|
|
2322
|
+
}
|
|
2323
|
+
function needsConversion(value, ancestors) {
|
|
2324
|
+
if (value === null) return false;
|
|
2325
|
+
const type = typeof value;
|
|
2326
|
+
if (type === "bigint") return true;
|
|
2327
|
+
if (type === "number") return !Number.isFinite(value);
|
|
2328
|
+
if (type !== "object") return false;
|
|
2329
|
+
const obj = value;
|
|
2330
|
+
if (ancestors.has(obj)) {
|
|
2331
|
+
throw new TypeError("[faapi] Converting circular structure to JSON");
|
|
2332
|
+
}
|
|
2333
|
+
if (obj instanceof Date || obj instanceof Map || obj instanceof Set || obj instanceof RegExp) {
|
|
2334
|
+
return true;
|
|
2335
|
+
}
|
|
2336
|
+
if (typeof obj.toJSON === "function") return true;
|
|
2337
|
+
if (Array.isArray(obj)) {
|
|
2338
|
+
ancestors.add(obj);
|
|
2339
|
+
for (let i = 0; i < obj.length; i++) {
|
|
2340
|
+
if (needsConversion(obj[i], ancestors)) {
|
|
2341
|
+
ancestors.delete(obj);
|
|
2342
|
+
return true;
|
|
2343
|
+
}
|
|
2344
|
+
}
|
|
2345
|
+
ancestors.delete(obj);
|
|
2346
|
+
return false;
|
|
2347
|
+
}
|
|
2348
|
+
if (isPlainObject(obj)) {
|
|
2349
|
+
ancestors.add(obj);
|
|
2350
|
+
for (const key in obj) {
|
|
2351
|
+
if (!Object.prototype.hasOwnProperty.call(obj, key)) continue;
|
|
2352
|
+
if (needsConversion(obj[key], ancestors)) {
|
|
2353
|
+
ancestors.delete(obj);
|
|
2354
|
+
return true;
|
|
2355
|
+
}
|
|
2356
|
+
}
|
|
2357
|
+
ancestors.delete(obj);
|
|
2358
|
+
return false;
|
|
2359
|
+
}
|
|
2360
|
+
return false;
|
|
2361
|
+
}
|
|
2362
|
+
function convertValue(value, ancestors) {
|
|
2363
|
+
if (value === null) return value;
|
|
2364
|
+
const type = typeof value;
|
|
2365
|
+
if (type === "bigint") return value.toString();
|
|
2366
|
+
if (type === "number") return Number.isFinite(value) ? value : String(value);
|
|
2367
|
+
if (type !== "object") return value;
|
|
2368
|
+
const obj = value;
|
|
2369
|
+
if (ancestors.has(obj)) {
|
|
2370
|
+
throw new TypeError("[faapi] Converting circular structure to JSON");
|
|
2371
|
+
}
|
|
2372
|
+
if (obj instanceof Date) return obj.getTime();
|
|
2373
|
+
if (obj instanceof Map) {
|
|
2374
|
+
ancestors.add(obj);
|
|
2375
|
+
const entries = Array.from(obj, ([k, v]) => [
|
|
2376
|
+
convertValue(k, ancestors),
|
|
2377
|
+
convertValue(v, ancestors)
|
|
2378
|
+
]);
|
|
2379
|
+
ancestors.delete(obj);
|
|
2380
|
+
return entries;
|
|
2381
|
+
}
|
|
2382
|
+
if (obj instanceof Set) {
|
|
2383
|
+
ancestors.add(obj);
|
|
2384
|
+
const values = Array.from(obj, (v) => convertValue(v, ancestors));
|
|
2385
|
+
ancestors.delete(obj);
|
|
2386
|
+
return values;
|
|
2387
|
+
}
|
|
2388
|
+
if (obj instanceof RegExp) return obj.toString();
|
|
2389
|
+
const toJSON = obj.toJSON;
|
|
2390
|
+
if (typeof toJSON === "function") {
|
|
2391
|
+
ancestors.add(obj);
|
|
2392
|
+
const result = convertValue(toJSON.call(obj), ancestors);
|
|
2393
|
+
ancestors.delete(obj);
|
|
2394
|
+
return result;
|
|
2395
|
+
}
|
|
2396
|
+
if (Array.isArray(obj)) {
|
|
2397
|
+
ancestors.add(obj);
|
|
2398
|
+
const arr = obj.map((item) => convertValue(item, ancestors));
|
|
2399
|
+
ancestors.delete(obj);
|
|
2400
|
+
return arr;
|
|
2401
|
+
}
|
|
2402
|
+
if (isPlainObject(obj)) {
|
|
2403
|
+
ancestors.add(obj);
|
|
2404
|
+
const out = {};
|
|
2405
|
+
for (const [key, val] of Object.entries(obj)) {
|
|
2406
|
+
out[key] = convertValue(val, ancestors);
|
|
2407
|
+
}
|
|
2408
|
+
ancestors.delete(obj);
|
|
2409
|
+
return out;
|
|
2410
|
+
}
|
|
2411
|
+
return obj;
|
|
2412
|
+
}
|
|
2413
|
+
|
|
2273
2414
|
// src/logger/formatEntry.ts
|
|
2274
2415
|
function serializeFieldValue(value) {
|
|
2275
2416
|
if (value instanceof Error) {
|
|
@@ -2285,11 +2426,7 @@ function formatEntry(entry) {
|
|
|
2285
2426
|
const plain = Object.fromEntries(
|
|
2286
2427
|
Object.entries(entry.fields).map(([k, v]) => [k, serializeFieldValue(v)])
|
|
2287
2428
|
);
|
|
2288
|
-
|
|
2289
|
-
line += ` ${JSON.stringify(plain)}`;
|
|
2290
|
-
} catch (err) {
|
|
2291
|
-
line += ` [unserializable fields: ${err instanceof Error ? err.message : String(err)}]`;
|
|
2292
|
-
}
|
|
2429
|
+
line += ` ${stringifyJson(plain)}`;
|
|
2293
2430
|
}
|
|
2294
2431
|
return line;
|
|
2295
2432
|
}
|
|
@@ -2307,7 +2444,14 @@ function createFileLogSink(options) {
|
|
|
2307
2444
|
};
|
|
2308
2445
|
const openStream = (name) => {
|
|
2309
2446
|
const stream = fs2.createWriteStream(path3.join(options.dir, `${name}.log`), { flags: "a" });
|
|
2310
|
-
|
|
2447
|
+
let streamErrorLogged = false;
|
|
2448
|
+
stream.on("error", (err) => {
|
|
2449
|
+
if (streamErrorLogged) return;
|
|
2450
|
+
streamErrorLogged = true;
|
|
2451
|
+
console.error(
|
|
2452
|
+
`[faapi] log file stream "${name}.log" failed \u2014 file logging on this stream stopped (subsequent errors suppressed). Check disk space / permissions for ${options.dir}:`,
|
|
2453
|
+
err
|
|
2454
|
+
);
|
|
2311
2455
|
});
|
|
2312
2456
|
streams.set(name, stream);
|
|
2313
2457
|
};
|
|
@@ -2508,6 +2652,17 @@ function createLogger(scope, options) {
|
|
|
2508
2652
|
|
|
2509
2653
|
// src/task/taskQueue.ts
|
|
2510
2654
|
var TERMINAL_STATUSES = /* @__PURE__ */ new Set(["done", "failed", "cancelled"]);
|
|
2655
|
+
var GROUP_MEMBER_DEDUP = (groupId, index) => `faapi-group:${groupId}:${index}`;
|
|
2656
|
+
var GROUP_COMPLETION_DEDUP = (groupId) => `faapi-group:${groupId}:complete`;
|
|
2657
|
+
var TASK_CLIENT_METHODS = /* @__PURE__ */ new Set([
|
|
2658
|
+
"enqueue",
|
|
2659
|
+
"enqueueGroup",
|
|
2660
|
+
"list",
|
|
2661
|
+
"listQueued",
|
|
2662
|
+
"cancel",
|
|
2663
|
+
"retry",
|
|
2664
|
+
"getGroup"
|
|
2665
|
+
]);
|
|
2511
2666
|
var MAX_FINISHED_RECORDS = 1e3;
|
|
2512
2667
|
function createTaskQueue(deps) {
|
|
2513
2668
|
const { registry, rootDir, driver } = deps;
|
|
@@ -2539,13 +2694,14 @@ function createTaskQueue(deps) {
|
|
|
2539
2694
|
const loadTaskModule = deps.loadTaskModule ?? (async (filePath) => await import(filePath));
|
|
2540
2695
|
const loadPayloadSchema = deps.loadPayloadSchema ?? (async (filePath) => {
|
|
2541
2696
|
const zodPath = path4.join(path4.dirname(filePath), "zod.js");
|
|
2542
|
-
|
|
2543
|
-
|
|
2544
|
-
|
|
2545
|
-
|
|
2546
|
-
|
|
2547
|
-
|
|
2697
|
+
const mod = await import(zodPath);
|
|
2698
|
+
const schemaKey = Object.keys(mod).find((k) => k.endsWith("Schema"));
|
|
2699
|
+
if (!schemaKey) {
|
|
2700
|
+
throw new Error(
|
|
2701
|
+
`[faapi] Task payload schema artifact has no *Schema export: ${zodPath} \u2014 re-run "faapi dev" / "faapi build" to regenerate task artifacts.`
|
|
2702
|
+
);
|
|
2548
2703
|
}
|
|
2704
|
+
return mod[schemaKey];
|
|
2549
2705
|
});
|
|
2550
2706
|
function assertKnownTask(name) {
|
|
2551
2707
|
const meta = registry.get(name);
|
|
@@ -2554,6 +2710,54 @@ function createTaskQueue(deps) {
|
|
|
2554
2710
|
throw new Error(`[faapi] Unknown task "${name}". Registered tasks: ${known}`);
|
|
2555
2711
|
}
|
|
2556
2712
|
}
|
|
2713
|
+
function assertGroupSupport() {
|
|
2714
|
+
if (!driver.groups) {
|
|
2715
|
+
throw new Error(
|
|
2716
|
+
"[faapi] Task driver does not support group accounting (TaskDriver.groups is not implemented). Group enqueue/settle requires a driver that implements it \u2014 see driverTypes.md / taskGroups.md."
|
|
2717
|
+
);
|
|
2718
|
+
}
|
|
2719
|
+
}
|
|
2720
|
+
async function maybeFireCompletion(snap) {
|
|
2721
|
+
if (!driver.groups) return;
|
|
2722
|
+
if (!snap.onComplete || snap.completionEnqueued || snap.settled < snap.total) return;
|
|
2723
|
+
const summary = {
|
|
2724
|
+
groupId: snap.groupId,
|
|
2725
|
+
task: snap.task,
|
|
2726
|
+
total: snap.total,
|
|
2727
|
+
done: snap.done,
|
|
2728
|
+
failed: snap.failed,
|
|
2729
|
+
cancelled: snap.cancelled,
|
|
2730
|
+
settled: snap.settled
|
|
2731
|
+
};
|
|
2732
|
+
try {
|
|
2733
|
+
await enqueueCore(snap.onComplete, summary, {
|
|
2734
|
+
dedupId: GROUP_COMPLETION_DEDUP(snap.groupId)
|
|
2735
|
+
});
|
|
2736
|
+
await driver.groups.markCompletionEnqueued(snap.groupId);
|
|
2737
|
+
} catch (err) {
|
|
2738
|
+
console.error(
|
|
2739
|
+
`[faapi] task group "${snap.groupId}" completion enqueue failed (group is settled; re-invoke enqueueGroup with the same groupId to re-arm the callback):`,
|
|
2740
|
+
err
|
|
2741
|
+
);
|
|
2742
|
+
}
|
|
2743
|
+
}
|
|
2744
|
+
async function settleGroup(record, outcome, willRetry) {
|
|
2745
|
+
const groupId = record.groupId;
|
|
2746
|
+
if (!groupId || !driver.groups) return;
|
|
2747
|
+
if (outcome !== "done" && willRetry) return;
|
|
2748
|
+
try {
|
|
2749
|
+
const snap = await driver.groups.settle(groupId, record.id, outcome);
|
|
2750
|
+
if (outcome === "failed" && snap.onFailure === "fail-fast") {
|
|
2751
|
+
await maybeFireCompletion(await driver.groups.cancelRemaining(groupId));
|
|
2752
|
+
}
|
|
2753
|
+
await maybeFireCompletion(snap);
|
|
2754
|
+
} catch (err) {
|
|
2755
|
+
console.error(
|
|
2756
|
+
`[faapi] task group accounting failed for "${groupId}" (job ${record.id}, outcome ${outcome}):`,
|
|
2757
|
+
err
|
|
2758
|
+
);
|
|
2759
|
+
}
|
|
2760
|
+
}
|
|
2557
2761
|
function snapshotRegistries() {
|
|
2558
2762
|
return {
|
|
2559
2763
|
agents: registriesView.agent.listAgents().map((core) => registriesView.agent.getAgentEntry(core.name)).filter((entry) => entry !== void 0),
|
|
@@ -2564,14 +2768,13 @@ function createTaskQueue(deps) {
|
|
|
2564
2768
|
async function validatePayload(name, payload) {
|
|
2565
2769
|
if (!schemaCache.has(name)) {
|
|
2566
2770
|
const meta = registry.get(name);
|
|
2567
|
-
schemaCache.set(
|
|
2568
|
-
name,
|
|
2569
|
-
await loadPayloadSchema(path4.resolve(rootDir, meta.filePath)).catch(() => void 0)
|
|
2570
|
-
);
|
|
2771
|
+
schemaCache.set(name, await loadPayloadSchema(path4.resolve(rootDir, meta.filePath)));
|
|
2571
2772
|
}
|
|
2572
2773
|
const schema = schemaCache.get(name);
|
|
2573
2774
|
if (!schema || typeof schema.safeParse !== "function") {
|
|
2574
|
-
|
|
2775
|
+
throw new Error(
|
|
2776
|
+
`[faapi] Task "${name}" has no valid payload schema (zod.js missing or no *Schema export). Payload declarations are mandatory: re-run "faapi dev" / "faapi build" to regenerate task artifacts.`
|
|
2777
|
+
);
|
|
2575
2778
|
}
|
|
2576
2779
|
const result = schema.safeParse(payload);
|
|
2577
2780
|
if (!result.success) {
|
|
@@ -2593,11 +2796,13 @@ function createTaskQueue(deps) {
|
|
|
2593
2796
|
attempts: 0,
|
|
2594
2797
|
createdAt: Date.now()
|
|
2595
2798
|
};
|
|
2799
|
+
if (job.groupId !== void 0) record.groupId = job.groupId;
|
|
2596
2800
|
record.attempts = job.attempt;
|
|
2597
2801
|
transitionStatus(record, "running");
|
|
2598
2802
|
delete record.progress;
|
|
2599
2803
|
record.error = void 0;
|
|
2600
2804
|
records.set(job.id, record);
|
|
2805
|
+
const willRetry = job.attempt <= (meta.retries ?? 0);
|
|
2601
2806
|
try {
|
|
2602
2807
|
let result;
|
|
2603
2808
|
if (meta.timeoutMs !== void 0 && meta.timeoutMs > 0) {
|
|
@@ -2605,7 +2810,8 @@ function createTaskQueue(deps) {
|
|
|
2605
2810
|
taskModulePath: path4.resolve(rootDir, meta.filePath),
|
|
2606
2811
|
payload: job.payload,
|
|
2607
2812
|
taskCtx: {
|
|
2608
|
-
config
|
|
2813
|
+
// 隔离任务不传 config:worker 线程不接收进程配置(config 含函数字段
|
|
2814
|
+
// 不可结构化克隆,框架不做降级传递)——任务数据经 payload 显式传入
|
|
2609
2815
|
resourcesDir: deps.resourcesDir,
|
|
2610
2816
|
job: { id: job.id, name: job.name, attempt: job.attempt }
|
|
2611
2817
|
},
|
|
@@ -2626,7 +2832,16 @@ function createTaskQueue(deps) {
|
|
|
2626
2832
|
onProgress: (value) => {
|
|
2627
2833
|
if (record.status === "running") record.progress = value;
|
|
2628
2834
|
},
|
|
2629
|
-
onLog: writeLogEntry
|
|
2835
|
+
onLog: writeLogEntry,
|
|
2836
|
+
// 隔离路径的 taskCtx.tasks 为 postMessage RPC 代理——宿主侧按方法名
|
|
2837
|
+
// 分派到队列本体(与 ctx.tasks / app.tasks 同一实例),结果/错误按 seq 回传
|
|
2838
|
+
onTasksCall: async (method, args) => {
|
|
2839
|
+
if (!TASK_CLIENT_METHODS.has(method)) {
|
|
2840
|
+
throw new Error(`[faapi] Unknown task client method "${method}"`);
|
|
2841
|
+
}
|
|
2842
|
+
const fn = queue[method];
|
|
2843
|
+
return fn.apply(queue, args);
|
|
2844
|
+
}
|
|
2630
2845
|
});
|
|
2631
2846
|
} else {
|
|
2632
2847
|
let mod = moduleCache2.get(job.name);
|
|
@@ -2639,6 +2854,8 @@ function createTaskQueue(deps) {
|
|
|
2639
2854
|
}
|
|
2640
2855
|
const taskCtx = {
|
|
2641
2856
|
signal: job.signal,
|
|
2857
|
+
// 活引用:与 ctx.tasks / app.tasks 同一 app 实例队列
|
|
2858
|
+
tasks: queue,
|
|
2642
2859
|
config: deps.config,
|
|
2643
2860
|
job: { id: job.id, name: job.name, attempt: job.attempt },
|
|
2644
2861
|
registries: registriesView,
|
|
@@ -2656,23 +2873,27 @@ function createTaskQueue(deps) {
|
|
|
2656
2873
|
transitionStatus(record, "done");
|
|
2657
2874
|
record.result = result;
|
|
2658
2875
|
evictFinishedRecords();
|
|
2876
|
+
await settleGroup(record, "done", willRetry);
|
|
2659
2877
|
return result;
|
|
2660
2878
|
} catch (err) {
|
|
2661
2879
|
const cancelled = err instanceof TaskCancelledError || job.signal.aborted;
|
|
2662
|
-
|
|
2880
|
+
const outcome = cancelled ? "cancelled" : "failed";
|
|
2881
|
+
transitionStatus(record, outcome);
|
|
2663
2882
|
record.error = err instanceof Error ? err.message : String(err);
|
|
2664
2883
|
evictFinishedRecords();
|
|
2884
|
+
await settleGroup(record, outcome, willRetry);
|
|
2665
2885
|
if (deps.onFailed) {
|
|
2666
2886
|
void Promise.resolve().then(
|
|
2667
2887
|
() => deps.onFailed({
|
|
2668
2888
|
task: job.name,
|
|
2669
2889
|
jobId: job.id,
|
|
2670
2890
|
attempt: job.attempt,
|
|
2671
|
-
willRetry
|
|
2891
|
+
willRetry,
|
|
2672
2892
|
cancelled,
|
|
2673
2893
|
error: record.error
|
|
2674
2894
|
})
|
|
2675
|
-
).catch(() => {
|
|
2895
|
+
).catch((hookErr) => {
|
|
2896
|
+
console.error(`[faapi] task onFailed hook threw for "${job.name}":`, hookErr);
|
|
2676
2897
|
});
|
|
2677
2898
|
}
|
|
2678
2899
|
throw err;
|
|
@@ -2686,35 +2907,86 @@ function createTaskQueue(deps) {
|
|
|
2686
2907
|
});
|
|
2687
2908
|
}
|
|
2688
2909
|
}
|
|
2910
|
+
async function enqueueCore(name, payload, opts) {
|
|
2911
|
+
if (stopped) {
|
|
2912
|
+
throw new Error("[faapi] Task queue is stopped and no longer accepts jobs");
|
|
2913
|
+
}
|
|
2914
|
+
assertKnownTask(name);
|
|
2915
|
+
const data = await validatePayload(name, payload);
|
|
2916
|
+
const meta = registry.get(name);
|
|
2917
|
+
const id = await driver.enqueue(name, data, {
|
|
2918
|
+
delayMs: opts?.delayMs,
|
|
2919
|
+
retries: meta.retries ?? 0,
|
|
2920
|
+
dedupId: opts?.dedupId,
|
|
2921
|
+
// 执行硬限预算透传驱动(pgboss 映射 expireInSeconds):未声明 timeoutMs 时
|
|
2922
|
+
// 两字段为 undefined,驱动用自身兜底
|
|
2923
|
+
timeoutMs: meta.timeoutMs,
|
|
2924
|
+
graceMs: meta.graceMs,
|
|
2925
|
+
...opts?.groupId !== void 0 ? { groupId: opts.groupId } : {}
|
|
2926
|
+
});
|
|
2927
|
+
if (!records.has(id)) {
|
|
2928
|
+
records.set(id, {
|
|
2929
|
+
id,
|
|
2930
|
+
name,
|
|
2931
|
+
payload: data,
|
|
2932
|
+
status: "pending",
|
|
2933
|
+
attempts: 0,
|
|
2934
|
+
...opts?.groupId !== void 0 ? { groupId: opts.groupId } : {},
|
|
2935
|
+
createdAt: Date.now(),
|
|
2936
|
+
...opts?.delayMs ? { runAt: Date.now() + opts.delayMs } : {}
|
|
2937
|
+
});
|
|
2938
|
+
}
|
|
2939
|
+
return id;
|
|
2940
|
+
}
|
|
2689
2941
|
const queue = {
|
|
2690
2942
|
async enqueue(name, payload = {}, opts) {
|
|
2943
|
+
const id = await enqueueCore(name, payload, {
|
|
2944
|
+
delayMs: opts?.delayMs,
|
|
2945
|
+
dedupId: opts?.dedupId
|
|
2946
|
+
});
|
|
2947
|
+
return { id };
|
|
2948
|
+
},
|
|
2949
|
+
async enqueueGroup(name, payloads, opts) {
|
|
2691
2950
|
if (stopped) {
|
|
2692
2951
|
throw new Error("[faapi] Task queue is stopped and no longer accepts jobs");
|
|
2693
2952
|
}
|
|
2953
|
+
assertGroupSupport();
|
|
2694
2954
|
assertKnownTask(name);
|
|
2695
|
-
|
|
2696
|
-
|
|
2697
|
-
|
|
2698
|
-
|
|
2699
|
-
|
|
2700
|
-
|
|
2701
|
-
|
|
2702
|
-
|
|
2703
|
-
|
|
2704
|
-
|
|
2955
|
+
if (!Array.isArray(payloads) || payloads.length === 0) {
|
|
2956
|
+
throw new Error(
|
|
2957
|
+
"[faapi] enqueueGroup requires a non-empty payloads array \u2014 a group without members can never settle."
|
|
2958
|
+
);
|
|
2959
|
+
}
|
|
2960
|
+
const onComplete = opts?.onComplete;
|
|
2961
|
+
if (onComplete !== void 0) assertKnownTask(onComplete);
|
|
2962
|
+
const onFailure = opts?.onFailure ?? "run-to-completion";
|
|
2963
|
+
const validated = [];
|
|
2964
|
+
for (const p of payloads) {
|
|
2965
|
+
validated.push(await validatePayload(name, p));
|
|
2966
|
+
}
|
|
2967
|
+
const groupId = opts?.groupId ?? crypto.randomUUID();
|
|
2968
|
+
await driver.groups.create({
|
|
2969
|
+
id: groupId,
|
|
2970
|
+
task: name,
|
|
2971
|
+
total: validated.length,
|
|
2972
|
+
...onComplete !== void 0 ? { onComplete } : {},
|
|
2973
|
+
onFailure
|
|
2705
2974
|
});
|
|
2706
|
-
|
|
2707
|
-
|
|
2708
|
-
|
|
2709
|
-
|
|
2710
|
-
|
|
2711
|
-
|
|
2712
|
-
attempts: 0,
|
|
2713
|
-
createdAt: Date.now(),
|
|
2714
|
-
...opts?.delayMs ? { runAt: Date.now() + opts.delayMs } : {}
|
|
2975
|
+
const jobs = [];
|
|
2976
|
+
for (let i = 0; i < validated.length; i++) {
|
|
2977
|
+
const id = await enqueueCore(name, validated[i], {
|
|
2978
|
+
delayMs: opts?.delayMs,
|
|
2979
|
+
dedupId: GROUP_MEMBER_DEDUP(groupId, i + 1),
|
|
2980
|
+
groupId
|
|
2715
2981
|
});
|
|
2982
|
+
jobs.push({ id });
|
|
2716
2983
|
}
|
|
2717
|
-
|
|
2984
|
+
await maybeFireCompletion(await driver.groups.get(groupId));
|
|
2985
|
+
return { groupId, jobs };
|
|
2986
|
+
},
|
|
2987
|
+
async getGroup(groupId) {
|
|
2988
|
+
assertGroupSupport();
|
|
2989
|
+
return driver.groups.get(groupId);
|
|
2718
2990
|
},
|
|
2719
2991
|
list(name) {
|
|
2720
2992
|
const snapshot = [];
|
|
@@ -2741,6 +3013,7 @@ function createTaskQueue(deps) {
|
|
|
2741
3013
|
status: record.status,
|
|
2742
3014
|
attempts: record.attempts,
|
|
2743
3015
|
createdAt: record.createdAt,
|
|
3016
|
+
...record.groupId !== void 0 ? { groupId: record.groupId } : {},
|
|
2744
3017
|
...record.result !== void 0 ? { result: record.result } : {},
|
|
2745
3018
|
...record.error !== void 0 ? { error: record.error } : {},
|
|
2746
3019
|
...record.runAt !== void 0 ? { runAt: record.runAt } : {}
|
|
@@ -2760,8 +3033,11 @@ function createTaskQueue(deps) {
|
|
|
2760
3033
|
}
|
|
2761
3034
|
await driver.cancel(name, id);
|
|
2762
3035
|
const record = records.get(id);
|
|
2763
|
-
if (record)
|
|
2764
|
-
|
|
3036
|
+
if (record) {
|
|
3037
|
+
transitionStatus(record, "cancelled");
|
|
3038
|
+
evictFinishedRecords();
|
|
3039
|
+
await settleGroup(record, "cancelled", false);
|
|
3040
|
+
}
|
|
2765
3041
|
},
|
|
2766
3042
|
async retry(name, id) {
|
|
2767
3043
|
if (!driver.retry) {
|
|
@@ -2771,7 +3047,18 @@ function createTaskQueue(deps) {
|
|
|
2771
3047
|
}
|
|
2772
3048
|
await driver.retry(name, id);
|
|
2773
3049
|
const record = records.get(id);
|
|
2774
|
-
if (record)
|
|
3050
|
+
if (record) {
|
|
3051
|
+
const wasTerminal = TERMINAL_STATUSES.has(record.status);
|
|
3052
|
+
transitionStatus(record, "pending");
|
|
3053
|
+
if (wasTerminal && record.groupId && driver.groups) {
|
|
3054
|
+
await driver.groups.unsettle(record.groupId, record.id).catch((err) => {
|
|
3055
|
+
console.error(
|
|
3056
|
+
`[faapi] task group unsettle failed for "${record.groupId}" (job ${record.id}):`,
|
|
3057
|
+
err
|
|
3058
|
+
);
|
|
3059
|
+
});
|
|
3060
|
+
}
|
|
3061
|
+
}
|
|
2775
3062
|
},
|
|
2776
3063
|
async start() {
|
|
2777
3064
|
if (stopped) {
|
|
@@ -2895,25 +3182,27 @@ function detectIgnoredMetaFields(source, strictHit) {
|
|
|
2895
3182
|
for (const line of source.split("\n")) {
|
|
2896
3183
|
const trimmed = line.trim();
|
|
2897
3184
|
if (trimmed.startsWith("//")) continue;
|
|
3185
|
+
if (trimmed.startsWith("*") || trimmed.startsWith("/*") || trimmed.endsWith("*/")) continue;
|
|
2898
3186
|
for (const field of ALL_META_FIELDS) {
|
|
2899
3187
|
if (new RegExp(`\\b${field}\\s*:`).test(trimmed)) declared.add(field);
|
|
2900
3188
|
}
|
|
2901
3189
|
}
|
|
2902
3190
|
return ALL_META_FIELDS.filter((field) => declared.has(field) && !strictHit[field]);
|
|
2903
3191
|
}
|
|
2904
|
-
function
|
|
2905
|
-
|
|
3192
|
+
function throwIgnoredMetaFields(name, fields) {
|
|
3193
|
+
if (fields.length === 0) return;
|
|
3194
|
+
const details = fields.map((field) => {
|
|
2906
3195
|
if (STRING_META_FIELDS.includes(field)) {
|
|
2907
|
-
|
|
2908
|
-
`[faapi] Task "${name}" declares "${field}" but its value is not a string literal \u2014 the declaration was ignored and the task will not be cron-scheduled. Meta values are extracted from source code without importing it, so only literal values are recognized: use a quoted literal (cron: '0 3 * * *'), not an expression or a dynamic value.`
|
|
2909
|
-
);
|
|
2910
|
-
} else {
|
|
2911
|
-
const consequence = field === "timeoutMs" ? " and the task will run in-process with no timeout" : "";
|
|
2912
|
-
console.warn(
|
|
2913
|
-
`[faapi] Task "${name}" declares "${field}" but its value is not a numeric literal \u2014 the declaration was ignored${consequence}. Meta values are extracted from source code without importing it, so only literal values are recognized (underscores allowed, e.g. 1_800_000); expressions like 30 * 60_000 or dynamic values are not supported.`
|
|
2914
|
-
);
|
|
3196
|
+
return `"${field}": value is not a string literal \u2014 the declaration would be ignored and the task would not be cron-scheduled. Use a quoted literal (cron: '0 3 * * *'), not an expression or a dynamic value.`;
|
|
2915
3197
|
}
|
|
2916
|
-
|
|
3198
|
+
const consequence = field === "timeoutMs" ? " \u2014 the declaration would be ignored and the task would silently run in-process with no timeout" : " \u2014 the declaration would be ignored";
|
|
3199
|
+
return `"${field}": value is not a numeric literal${consequence}. Only literal values are recognized (underscores allowed, e.g. 1_800_000); expressions like 30 * 60_000 or dynamic values are not supported.`;
|
|
3200
|
+
});
|
|
3201
|
+
throw new Error(
|
|
3202
|
+
`[faapi] Task "${name}" declares meta field(s) with non-literal values:
|
|
3203
|
+
${details.join("\n ")}
|
|
3204
|
+
Meta values are extracted from source code without importing it, so only literals are recognized. If a matched line is inside a block comment (e.g. a single-line /* ... */ comment containing a meta-like declaration), rewrite the comment so it does not look like a meta declaration.`
|
|
3205
|
+
);
|
|
2917
3206
|
}
|
|
2918
3207
|
function parseNumericLiteral(raw) {
|
|
2919
3208
|
return Number(raw.replace(/_/g, ""));
|
|
@@ -2983,7 +3272,7 @@ async function scanTasks(rootDir, patterns) {
|
|
|
2983
3272
|
timeoutMs: timeoutMs !== void 0,
|
|
2984
3273
|
graceMs: graceMs !== void 0
|
|
2985
3274
|
};
|
|
2986
|
-
|
|
3275
|
+
throwIgnoredMetaFields(name, detectIgnoredMetaFields(source, strictHit));
|
|
2987
3276
|
tasks.push(manifest);
|
|
2988
3277
|
}
|
|
2989
3278
|
return tasks.sort((a, b) => a.name.localeCompare(b.name));
|
|
@@ -3583,11 +3872,7 @@ async function ensureMiddlewaresCompiled(middlewarePaths, rootDir) {
|
|
|
3583
3872
|
if (!dist) return;
|
|
3584
3873
|
for (const mwPath of middlewarePaths) {
|
|
3585
3874
|
const sourcePath = prodPathToSourcePath(mwPath, rootDir, dist);
|
|
3586
|
-
|
|
3587
|
-
await ensureCompiled(sourcePath, rootDir, dist);
|
|
3588
|
-
} catch (err) {
|
|
3589
|
-
console.error(`[faapi] Failed to compile middleware source ${sourcePath}:`, err);
|
|
3590
|
-
}
|
|
3875
|
+
await ensureCompiled(sourcePath, rootDir, dist);
|
|
3591
3876
|
}
|
|
3592
3877
|
}
|
|
3593
3878
|
function prodSourcePathToProductPath(sourceAbsPath, rootDir, dist) {
|
|
@@ -4591,116 +4876,6 @@ function hasBufferedBody(response) {
|
|
|
4591
4876
|
return bufferedBodyResponses.has(response);
|
|
4592
4877
|
}
|
|
4593
4878
|
|
|
4594
|
-
// src/utils/isPlainObject.ts
|
|
4595
|
-
function isPlainObject(value) {
|
|
4596
|
-
if (value === null || typeof value !== "object") {
|
|
4597
|
-
return false;
|
|
4598
|
-
}
|
|
4599
|
-
if (Array.isArray(value)) {
|
|
4600
|
-
return false;
|
|
4601
|
-
}
|
|
4602
|
-
const proto = Object.getPrototypeOf(value);
|
|
4603
|
-
return proto === null || proto === Object.prototype;
|
|
4604
|
-
}
|
|
4605
|
-
|
|
4606
|
-
// src/utils/stringifyJson.ts
|
|
4607
|
-
function stringifyJson(value) {
|
|
4608
|
-
if (!needsConversion(value, /* @__PURE__ */ new Set())) {
|
|
4609
|
-
return JSON.stringify(value);
|
|
4610
|
-
}
|
|
4611
|
-
return JSON.stringify(convertValue(value, /* @__PURE__ */ new Set()));
|
|
4612
|
-
}
|
|
4613
|
-
function needsConversion(value, ancestors) {
|
|
4614
|
-
if (value === null) return false;
|
|
4615
|
-
const type = typeof value;
|
|
4616
|
-
if (type === "bigint") return true;
|
|
4617
|
-
if (type === "number") return !Number.isFinite(value);
|
|
4618
|
-
if (type !== "object") return false;
|
|
4619
|
-
const obj = value;
|
|
4620
|
-
if (ancestors.has(obj)) {
|
|
4621
|
-
throw new TypeError("[faapi] Converting circular structure to JSON");
|
|
4622
|
-
}
|
|
4623
|
-
if (obj instanceof Date || obj instanceof Map || obj instanceof Set || obj instanceof RegExp) {
|
|
4624
|
-
return true;
|
|
4625
|
-
}
|
|
4626
|
-
if (typeof obj.toJSON === "function") return true;
|
|
4627
|
-
if (Array.isArray(obj)) {
|
|
4628
|
-
ancestors.add(obj);
|
|
4629
|
-
for (let i = 0; i < obj.length; i++) {
|
|
4630
|
-
if (needsConversion(obj[i], ancestors)) {
|
|
4631
|
-
ancestors.delete(obj);
|
|
4632
|
-
return true;
|
|
4633
|
-
}
|
|
4634
|
-
}
|
|
4635
|
-
ancestors.delete(obj);
|
|
4636
|
-
return false;
|
|
4637
|
-
}
|
|
4638
|
-
if (isPlainObject(obj)) {
|
|
4639
|
-
ancestors.add(obj);
|
|
4640
|
-
for (const key in obj) {
|
|
4641
|
-
if (!Object.prototype.hasOwnProperty.call(obj, key)) continue;
|
|
4642
|
-
if (needsConversion(obj[key], ancestors)) {
|
|
4643
|
-
ancestors.delete(obj);
|
|
4644
|
-
return true;
|
|
4645
|
-
}
|
|
4646
|
-
}
|
|
4647
|
-
ancestors.delete(obj);
|
|
4648
|
-
return false;
|
|
4649
|
-
}
|
|
4650
|
-
return false;
|
|
4651
|
-
}
|
|
4652
|
-
function convertValue(value, ancestors) {
|
|
4653
|
-
if (value === null) return value;
|
|
4654
|
-
const type = typeof value;
|
|
4655
|
-
if (type === "bigint") return value.toString();
|
|
4656
|
-
if (type === "number") return Number.isFinite(value) ? value : String(value);
|
|
4657
|
-
if (type !== "object") return value;
|
|
4658
|
-
const obj = value;
|
|
4659
|
-
if (ancestors.has(obj)) {
|
|
4660
|
-
throw new TypeError("[faapi] Converting circular structure to JSON");
|
|
4661
|
-
}
|
|
4662
|
-
if (obj instanceof Date) return obj.getTime();
|
|
4663
|
-
if (obj instanceof Map) {
|
|
4664
|
-
ancestors.add(obj);
|
|
4665
|
-
const entries = Array.from(obj, ([k, v]) => [
|
|
4666
|
-
convertValue(k, ancestors),
|
|
4667
|
-
convertValue(v, ancestors)
|
|
4668
|
-
]);
|
|
4669
|
-
ancestors.delete(obj);
|
|
4670
|
-
return entries;
|
|
4671
|
-
}
|
|
4672
|
-
if (obj instanceof Set) {
|
|
4673
|
-
ancestors.add(obj);
|
|
4674
|
-
const values = Array.from(obj, (v) => convertValue(v, ancestors));
|
|
4675
|
-
ancestors.delete(obj);
|
|
4676
|
-
return values;
|
|
4677
|
-
}
|
|
4678
|
-
if (obj instanceof RegExp) return obj.toString();
|
|
4679
|
-
const toJSON = obj.toJSON;
|
|
4680
|
-
if (typeof toJSON === "function") {
|
|
4681
|
-
ancestors.add(obj);
|
|
4682
|
-
const result = convertValue(toJSON.call(obj), ancestors);
|
|
4683
|
-
ancestors.delete(obj);
|
|
4684
|
-
return result;
|
|
4685
|
-
}
|
|
4686
|
-
if (Array.isArray(obj)) {
|
|
4687
|
-
ancestors.add(obj);
|
|
4688
|
-
const arr = obj.map((item) => convertValue(item, ancestors));
|
|
4689
|
-
ancestors.delete(obj);
|
|
4690
|
-
return arr;
|
|
4691
|
-
}
|
|
4692
|
-
if (isPlainObject(obj)) {
|
|
4693
|
-
ancestors.add(obj);
|
|
4694
|
-
const out = {};
|
|
4695
|
-
for (const [key, val] of Object.entries(obj)) {
|
|
4696
|
-
out[key] = convertValue(val, ancestors);
|
|
4697
|
-
}
|
|
4698
|
-
ancestors.delete(obj);
|
|
4699
|
-
return out;
|
|
4700
|
-
}
|
|
4701
|
-
return obj;
|
|
4702
|
-
}
|
|
4703
|
-
|
|
4704
4879
|
// src/runtime/sse.ts
|
|
4705
4880
|
function encodeSseEvent(event) {
|
|
4706
4881
|
let out = "";
|
|
@@ -5898,38 +6073,30 @@ function setCachedMiddlewares(absPath, bundle) {
|
|
|
5898
6073
|
middlewareCache.set(absPath, bundle);
|
|
5899
6074
|
}
|
|
5900
6075
|
async function loadMiddlewaresFile(filePath) {
|
|
5901
|
-
|
|
5902
|
-
try {
|
|
5903
|
-
module = await importWithCacheBust(filePath);
|
|
5904
|
-
} catch (err) {
|
|
5905
|
-
console.error(
|
|
5906
|
-
`[faapi] Failed to load middlewares from ${filePath}:`,
|
|
5907
|
-
err instanceof Error ? err.stack ?? err.message : err
|
|
5908
|
-
);
|
|
5909
|
-
return { middlewares: [], injectors: {} };
|
|
5910
|
-
}
|
|
6076
|
+
const module = await importWithCacheBust(filePath);
|
|
5911
6077
|
const middlewares = module.default ?? module.middlewares ?? [];
|
|
5912
6078
|
if (!Array.isArray(middlewares)) {
|
|
5913
|
-
|
|
5914
|
-
|
|
6079
|
+
throw new TypeError(
|
|
6080
|
+
`[faapi] middlewares.ts must export an array (default or named "middlewares"): ${filePath}`
|
|
6081
|
+
);
|
|
5915
6082
|
}
|
|
5916
|
-
const validMiddlewares =
|
|
6083
|
+
const validMiddlewares = [];
|
|
6084
|
+
for (const m of middlewares) {
|
|
5917
6085
|
if (typeof m !== "function") {
|
|
5918
|
-
|
|
5919
|
-
|
|
6086
|
+
throw new TypeError(
|
|
6087
|
+
`[faapi] every middleware must be a function, got ${typeof m}: ${filePath}`
|
|
6088
|
+
);
|
|
5920
6089
|
}
|
|
5921
|
-
|
|
5922
|
-
}
|
|
6090
|
+
validMiddlewares.push(m);
|
|
6091
|
+
}
|
|
5923
6092
|
const injectors = module.injectors ?? {};
|
|
5924
6093
|
if (typeof injectors !== "object" || injectors === null) {
|
|
5925
|
-
|
|
5926
|
-
return { middlewares: validMiddlewares, injectors: {} };
|
|
6094
|
+
throw new TypeError(`[faapi] "injectors" export must be an object: ${filePath}`);
|
|
5927
6095
|
}
|
|
5928
6096
|
const validInjectors = {};
|
|
5929
6097
|
for (const [name, injector] of Object.entries(injectors)) {
|
|
5930
6098
|
if (typeof injector !== "function") {
|
|
5931
|
-
|
|
5932
|
-
continue;
|
|
6099
|
+
throw new TypeError(`[faapi] injector "${name}" must be a function: ${filePath}`);
|
|
5933
6100
|
}
|
|
5934
6101
|
validInjectors[name] = injector;
|
|
5935
6102
|
}
|
|
@@ -5944,10 +6111,16 @@ async function loadMergedMiddlewares(middlewarePaths) {
|
|
|
5944
6111
|
if (bundle === void 0) {
|
|
5945
6112
|
let loading = inFlight.get(absMwPath);
|
|
5946
6113
|
if (!loading) {
|
|
5947
|
-
loading = loadMiddlewaresFile(absMwPath).then(
|
|
5948
|
-
|
|
5949
|
-
|
|
5950
|
-
|
|
6114
|
+
loading = loadMiddlewaresFile(absMwPath).then(
|
|
6115
|
+
(result) => {
|
|
6116
|
+
setCachedMiddlewares(absMwPath, result);
|
|
6117
|
+
return result;
|
|
6118
|
+
},
|
|
6119
|
+
(err) => {
|
|
6120
|
+
inFlight.delete(absMwPath);
|
|
6121
|
+
throw err;
|
|
6122
|
+
}
|
|
6123
|
+
);
|
|
5951
6124
|
inFlight.set(absMwPath, loading);
|
|
5952
6125
|
}
|
|
5953
6126
|
bundle = await loading;
|
|
@@ -6542,7 +6715,8 @@ async function sendErrorResponse(err, meta, res, onError, ctx) {
|
|
|
6542
6715
|
if (onError && ctx) {
|
|
6543
6716
|
try {
|
|
6544
6717
|
await onError(err, ctx);
|
|
6545
|
-
} catch {
|
|
6718
|
+
} catch (hookErr) {
|
|
6719
|
+
console.error("[faapi] lifecycle onError hook threw:", hookErr);
|
|
6546
6720
|
}
|
|
6547
6721
|
}
|
|
6548
6722
|
}
|
|
@@ -6735,7 +6909,7 @@ async function loadPlugins(declarations, ctx, rootDir, dist) {
|
|
|
6735
6909
|
const upgradeWrappers = [];
|
|
6736
6910
|
const failures = [];
|
|
6737
6911
|
if (!declarations || declarations.length === 0) {
|
|
6738
|
-
return { handlerWrappers, upgradeWrappers
|
|
6912
|
+
return { handlerWrappers, upgradeWrappers };
|
|
6739
6913
|
}
|
|
6740
6914
|
const fullCtx = {
|
|
6741
6915
|
...ctx,
|
|
@@ -6762,7 +6936,7 @@ async function loadPlugins(declarations, ctx, rootDir, dist) {
|
|
|
6762
6936
|
}
|
|
6763
6937
|
if (enable === false) continue;
|
|
6764
6938
|
if (loaded.has(specifier)) {
|
|
6765
|
-
|
|
6939
|
+
failures.push({ specifier, reason: "duplicate plugin declaration" });
|
|
6766
6940
|
continue;
|
|
6767
6941
|
}
|
|
6768
6942
|
loaded.add(specifier);
|
|
@@ -6783,12 +6957,12 @@ async function loadPlugins(declarations, ctx, rootDir, dist) {
|
|
|
6783
6957
|
}
|
|
6784
6958
|
}
|
|
6785
6959
|
if (failures.length > 0) {
|
|
6786
|
-
|
|
6960
|
+
throw new Error(
|
|
6787
6961
|
`[faapi] ${failures.length} plugin(s) failed to load:
|
|
6788
6962
|
` + failures.map((f) => ` - ${f.specifier}: ${f.reason}`).join("\n")
|
|
6789
6963
|
);
|
|
6790
6964
|
}
|
|
6791
|
-
return { handlerWrappers, upgradeWrappers
|
|
6965
|
+
return { handlerWrappers, upgradeWrappers };
|
|
6792
6966
|
}
|
|
6793
6967
|
var TS_EXTS = /\.(ts|tsx|mts|cts)$/;
|
|
6794
6968
|
var JS_EXTS = /\.(js|mjs|cjs|jsx)$/;
|
|
@@ -7389,6 +7563,7 @@ function generateTaskSchemaFileSource(sources, resolveType, helpersImportPath) {
|
|
|
7389
7563
|
}
|
|
7390
7564
|
async function generateTaskArtifacts(manifests, rootDir, dist) {
|
|
7391
7565
|
const metadata = [];
|
|
7566
|
+
const declarationErrors = [];
|
|
7392
7567
|
if (manifests.length > 0) {
|
|
7393
7568
|
const programByFile = createPrograms(manifests.map((m) => path25.resolve(rootDir, m.filePath)));
|
|
7394
7569
|
for (const manifest of manifests) {
|
|
@@ -7397,11 +7572,28 @@ async function generateTaskArtifacts(manifests, rootDir, dist) {
|
|
|
7397
7572
|
name: manifest.name,
|
|
7398
7573
|
filePath: manifest.filePath
|
|
7399
7574
|
});
|
|
7400
|
-
if (result) {
|
|
7401
|
-
|
|
7575
|
+
if (!result) {
|
|
7576
|
+
declarationErrors.push(
|
|
7577
|
+
`"${manifest.name}" (${manifest.filePath}): no exported run function found`
|
|
7578
|
+
);
|
|
7579
|
+
continue;
|
|
7580
|
+
}
|
|
7581
|
+
if (!result.inputTypeName) {
|
|
7582
|
+
declarationErrors.push(
|
|
7583
|
+
`"${manifest.name}" (${manifest.filePath}): run's first parameter has no type name \u2014 declare a Payload type (an empty interface for parameterless tasks), or explicitly opt out of validation with "type Payload = unknown"`
|
|
7584
|
+
);
|
|
7585
|
+
continue;
|
|
7402
7586
|
}
|
|
7587
|
+
metadata.push({ ...result, taskName: manifest.name });
|
|
7403
7588
|
}
|
|
7404
7589
|
}
|
|
7590
|
+
if (declarationErrors.length > 0) {
|
|
7591
|
+
throw new Error(
|
|
7592
|
+
`[faapi] ${declarationErrors.length} task(s) missing a Payload type declaration \u2014 payload validation must not be silently skipped:
|
|
7593
|
+
${declarationErrors.join("\n ")}
|
|
7594
|
+
Every task must declare a payload type on run's first parameter; "type Payload = unknown" generates an always-passing schema (explicit no-validation opt-out).`
|
|
7595
|
+
);
|
|
7596
|
+
}
|
|
7405
7597
|
const serialized = serializeTasks(manifests, dist);
|
|
7406
7598
|
const tasksPath = path25.resolve(rootDir, dist, TASKS_FILE);
|
|
7407
7599
|
await writeTasksModule(serialized, tasksPath);
|
|
@@ -7679,7 +7871,7 @@ async function createAppBase(options) {
|
|
|
7679
7871
|
const taskQueue = createTaskQueue({
|
|
7680
7872
|
registry: registries.task,
|
|
7681
7873
|
rootDir,
|
|
7682
|
-
config,
|
|
7874
|
+
config: config ?? {},
|
|
7683
7875
|
resourcesDir,
|
|
7684
7876
|
registries: createTaskRegistriesView(registries),
|
|
7685
7877
|
// 轻量补全通道:进程内路径经 store 惰性读取(插件注册后的活引用);
|
|
@@ -7710,7 +7902,7 @@ async function createAppBase(options) {
|
|
|
7710
7902
|
dist,
|
|
7711
7903
|
cors: config?.cors ?? true,
|
|
7712
7904
|
onError: config?.lifecycle?.onError,
|
|
7713
|
-
config
|
|
7905
|
+
config,
|
|
7714
7906
|
wsRoutes,
|
|
7715
7907
|
middlewares: config?.middlewares,
|
|
7716
7908
|
injectors: config?.injectors,
|