@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/cli/index.js
CHANGED
|
@@ -767,38 +767,30 @@ function setCachedMiddlewares(absPath, bundle) {
|
|
|
767
767
|
middlewareCache.set(absPath, bundle);
|
|
768
768
|
}
|
|
769
769
|
async function loadMiddlewaresFile(filePath) {
|
|
770
|
-
|
|
771
|
-
try {
|
|
772
|
-
module = await importWithCacheBust(filePath);
|
|
773
|
-
} catch (err) {
|
|
774
|
-
console.error(
|
|
775
|
-
`[faapi] Failed to load middlewares from ${filePath}:`,
|
|
776
|
-
err instanceof Error ? err.stack ?? err.message : err
|
|
777
|
-
);
|
|
778
|
-
return { middlewares: [], injectors: {} };
|
|
779
|
-
}
|
|
770
|
+
const module = await importWithCacheBust(filePath);
|
|
780
771
|
const middlewares = module.default ?? module.middlewares ?? [];
|
|
781
772
|
if (!Array.isArray(middlewares)) {
|
|
782
|
-
|
|
783
|
-
|
|
773
|
+
throw new TypeError(
|
|
774
|
+
`[faapi] middlewares.ts must export an array (default or named "middlewares"): ${filePath}`
|
|
775
|
+
);
|
|
784
776
|
}
|
|
785
|
-
const validMiddlewares =
|
|
777
|
+
const validMiddlewares = [];
|
|
778
|
+
for (const m of middlewares) {
|
|
786
779
|
if (typeof m !== "function") {
|
|
787
|
-
|
|
788
|
-
|
|
780
|
+
throw new TypeError(
|
|
781
|
+
`[faapi] every middleware must be a function, got ${typeof m}: ${filePath}`
|
|
782
|
+
);
|
|
789
783
|
}
|
|
790
|
-
|
|
791
|
-
}
|
|
784
|
+
validMiddlewares.push(m);
|
|
785
|
+
}
|
|
792
786
|
const injectors = module.injectors ?? {};
|
|
793
787
|
if (typeof injectors !== "object" || injectors === null) {
|
|
794
|
-
|
|
795
|
-
return { middlewares: validMiddlewares, injectors: {} };
|
|
788
|
+
throw new TypeError(`[faapi] "injectors" export must be an object: ${filePath}`);
|
|
796
789
|
}
|
|
797
790
|
const validInjectors = {};
|
|
798
791
|
for (const [name, injector] of Object.entries(injectors)) {
|
|
799
792
|
if (typeof injector !== "function") {
|
|
800
|
-
|
|
801
|
-
continue;
|
|
793
|
+
throw new TypeError(`[faapi] injector "${name}" must be a function: ${filePath}`);
|
|
802
794
|
}
|
|
803
795
|
validInjectors[name] = injector;
|
|
804
796
|
}
|
|
@@ -813,10 +805,16 @@ async function loadMergedMiddlewares(middlewarePaths) {
|
|
|
813
805
|
if (bundle === void 0) {
|
|
814
806
|
let loading = inFlight.get(absMwPath);
|
|
815
807
|
if (!loading) {
|
|
816
|
-
loading = loadMiddlewaresFile(absMwPath).then(
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
|
|
808
|
+
loading = loadMiddlewaresFile(absMwPath).then(
|
|
809
|
+
(result) => {
|
|
810
|
+
setCachedMiddlewares(absMwPath, result);
|
|
811
|
+
return result;
|
|
812
|
+
},
|
|
813
|
+
(err) => {
|
|
814
|
+
inFlight.delete(absMwPath);
|
|
815
|
+
throw err;
|
|
816
|
+
}
|
|
817
|
+
);
|
|
820
818
|
inFlight.set(absMwPath, loading);
|
|
821
819
|
}
|
|
822
820
|
bundle = await loading;
|
|
@@ -3629,25 +3627,27 @@ function detectIgnoredMetaFields(source, strictHit) {
|
|
|
3629
3627
|
for (const line of source.split("\n")) {
|
|
3630
3628
|
const trimmed = line.trim();
|
|
3631
3629
|
if (trimmed.startsWith("//")) continue;
|
|
3630
|
+
if (trimmed.startsWith("*") || trimmed.startsWith("/*") || trimmed.endsWith("*/")) continue;
|
|
3632
3631
|
for (const field of ALL_META_FIELDS) {
|
|
3633
3632
|
if (new RegExp(`\\b${field}\\s*:`).test(trimmed)) declared.add(field);
|
|
3634
3633
|
}
|
|
3635
3634
|
}
|
|
3636
3635
|
return ALL_META_FIELDS.filter((field) => declared.has(field) && !strictHit[field]);
|
|
3637
3636
|
}
|
|
3638
|
-
function
|
|
3639
|
-
|
|
3637
|
+
function throwIgnoredMetaFields(name, fields) {
|
|
3638
|
+
if (fields.length === 0) return;
|
|
3639
|
+
const details = fields.map((field) => {
|
|
3640
3640
|
if (STRING_META_FIELDS.includes(field)) {
|
|
3641
|
-
|
|
3642
|
-
`[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.`
|
|
3643
|
-
);
|
|
3644
|
-
} else {
|
|
3645
|
-
const consequence = field === "timeoutMs" ? " and the task will run in-process with no timeout" : "";
|
|
3646
|
-
console.warn(
|
|
3647
|
-
`[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.`
|
|
3648
|
-
);
|
|
3641
|
+
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.`;
|
|
3649
3642
|
}
|
|
3650
|
-
|
|
3643
|
+
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";
|
|
3644
|
+
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.`;
|
|
3645
|
+
});
|
|
3646
|
+
throw new Error(
|
|
3647
|
+
`[faapi] Task "${name}" declares meta field(s) with non-literal values:
|
|
3648
|
+
${details.join("\n ")}
|
|
3649
|
+
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.`
|
|
3650
|
+
);
|
|
3651
3651
|
}
|
|
3652
3652
|
function parseNumericLiteral(raw) {
|
|
3653
3653
|
return Number(raw.replace(/_/g, ""));
|
|
@@ -3715,7 +3715,7 @@ async function scanTasks(rootDir, patterns) {
|
|
|
3715
3715
|
timeoutMs: timeoutMs !== void 0,
|
|
3716
3716
|
graceMs: graceMs !== void 0
|
|
3717
3717
|
};
|
|
3718
|
-
|
|
3718
|
+
throwIgnoredMetaFields(name, detectIgnoredMetaFields(source, strictHit));
|
|
3719
3719
|
tasks.push(manifest);
|
|
3720
3720
|
}
|
|
3721
3721
|
return tasks.sort((a, b) => a.name.localeCompare(b.name));
|
|
@@ -3830,6 +3830,7 @@ function generateTaskSchemaFileSource(sources, resolveType, helpersImportPath) {
|
|
|
3830
3830
|
}
|
|
3831
3831
|
async function generateTaskArtifacts(manifests, rootDir, dist) {
|
|
3832
3832
|
const metadata = [];
|
|
3833
|
+
const declarationErrors = [];
|
|
3833
3834
|
if (manifests.length > 0) {
|
|
3834
3835
|
const programByFile = createPrograms(manifests.map((m) => path17.resolve(rootDir, m.filePath)));
|
|
3835
3836
|
for (const manifest of manifests) {
|
|
@@ -3838,11 +3839,28 @@ async function generateTaskArtifacts(manifests, rootDir, dist) {
|
|
|
3838
3839
|
name: manifest.name,
|
|
3839
3840
|
filePath: manifest.filePath
|
|
3840
3841
|
});
|
|
3841
|
-
if (result) {
|
|
3842
|
-
|
|
3842
|
+
if (!result) {
|
|
3843
|
+
declarationErrors.push(
|
|
3844
|
+
`"${manifest.name}" (${manifest.filePath}): no exported run function found`
|
|
3845
|
+
);
|
|
3846
|
+
continue;
|
|
3843
3847
|
}
|
|
3848
|
+
if (!result.inputTypeName) {
|
|
3849
|
+
declarationErrors.push(
|
|
3850
|
+
`"${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"`
|
|
3851
|
+
);
|
|
3852
|
+
continue;
|
|
3853
|
+
}
|
|
3854
|
+
metadata.push({ ...result, taskName: manifest.name });
|
|
3844
3855
|
}
|
|
3845
3856
|
}
|
|
3857
|
+
if (declarationErrors.length > 0) {
|
|
3858
|
+
throw new Error(
|
|
3859
|
+
`[faapi] ${declarationErrors.length} task(s) missing a Payload type declaration \u2014 payload validation must not be silently skipped:
|
|
3860
|
+
${declarationErrors.join("\n ")}
|
|
3861
|
+
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).`
|
|
3862
|
+
);
|
|
3863
|
+
}
|
|
3846
3864
|
const serialized = serializeTasks(manifests, dist);
|
|
3847
3865
|
const tasksPath = path17.resolve(rootDir, dist, TASKS_FILE);
|
|
3848
3866
|
await writeTasksModule(serialized, tasksPath);
|
|
@@ -4023,11 +4041,7 @@ async function ensureMiddlewaresCompiled(middlewarePaths, rootDir) {
|
|
|
4023
4041
|
if (!dist) return;
|
|
4024
4042
|
for (const mwPath of middlewarePaths) {
|
|
4025
4043
|
const sourcePath = prodPathToSourcePath(mwPath, rootDir, dist);
|
|
4026
|
-
|
|
4027
|
-
await ensureCompiled(sourcePath, rootDir, dist);
|
|
4028
|
-
} catch (err) {
|
|
4029
|
-
console.error(`[faapi] Failed to compile middleware source ${sourcePath}:`, err);
|
|
4030
|
-
}
|
|
4044
|
+
await ensureCompiled(sourcePath, rootDir, dist);
|
|
4031
4045
|
}
|
|
4032
4046
|
}
|
|
4033
4047
|
function prodSourcePathToProductPath(sourceAbsPath, rootDir, dist) {
|
|
@@ -6944,17 +6958,14 @@ function formatEntry(entry) {
|
|
|
6944
6958
|
const plain = Object.fromEntries(
|
|
6945
6959
|
Object.entries(entry.fields).map(([k, v]) => [k, serializeFieldValue(v)])
|
|
6946
6960
|
);
|
|
6947
|
-
|
|
6948
|
-
line += ` ${JSON.stringify(plain)}`;
|
|
6949
|
-
} catch (err) {
|
|
6950
|
-
line += ` [unserializable fields: ${err instanceof Error ? err.message : String(err)}]`;
|
|
6951
|
-
}
|
|
6961
|
+
line += ` ${stringifyJson(plain)}`;
|
|
6952
6962
|
}
|
|
6953
6963
|
return line;
|
|
6954
6964
|
}
|
|
6955
6965
|
var init_formatEntry = __esm({
|
|
6956
6966
|
"src/logger/formatEntry.ts"() {
|
|
6957
6967
|
"use strict";
|
|
6968
|
+
init_stringifyJson();
|
|
6958
6969
|
}
|
|
6959
6970
|
});
|
|
6960
6971
|
|
|
@@ -6970,7 +6981,14 @@ function createFileLogSink(options) {
|
|
|
6970
6981
|
};
|
|
6971
6982
|
const openStream = (name) => {
|
|
6972
6983
|
const stream = fs18.createWriteStream(path24.join(options.dir, `${name}.log`), { flags: "a" });
|
|
6973
|
-
|
|
6984
|
+
let streamErrorLogged = false;
|
|
6985
|
+
stream.on("error", (err) => {
|
|
6986
|
+
if (streamErrorLogged) return;
|
|
6987
|
+
streamErrorLogged = true;
|
|
6988
|
+
console.error(
|
|
6989
|
+
`[faapi] log file stream "${name}.log" failed \u2014 file logging on this stream stopped (subsequent errors suppressed). Check disk space / permissions for ${options.dir}:`,
|
|
6990
|
+
err
|
|
6991
|
+
);
|
|
6974
6992
|
});
|
|
6975
6993
|
streams.set(name, stream);
|
|
6976
6994
|
};
|
|
@@ -9317,7 +9335,8 @@ async function sendErrorResponse(err, meta, res, onError, ctx) {
|
|
|
9317
9335
|
if (onError && ctx) {
|
|
9318
9336
|
try {
|
|
9319
9337
|
await onError(err, ctx);
|
|
9320
|
-
} catch {
|
|
9338
|
+
} catch (hookErr) {
|
|
9339
|
+
console.error("[faapi] lifecycle onError hook threw:", hookErr);
|
|
9321
9340
|
}
|
|
9322
9341
|
}
|
|
9323
9342
|
}
|
|
@@ -9461,19 +9480,6 @@ function resolveWorkerEntryUrl() {
|
|
|
9461
9480
|
workerEntryUrl = existsSync3(fileURLToPath(js)) ? js : new URL("./workerEntry.ts", import.meta.url);
|
|
9462
9481
|
return workerEntryUrl;
|
|
9463
9482
|
}
|
|
9464
|
-
function safeConfig(config) {
|
|
9465
|
-
if (config === void 0 || config === null) return config;
|
|
9466
|
-
try {
|
|
9467
|
-
structuredClone(config);
|
|
9468
|
-
return config;
|
|
9469
|
-
} catch {
|
|
9470
|
-
}
|
|
9471
|
-
try {
|
|
9472
|
-
return JSON.parse(JSON.stringify(config));
|
|
9473
|
-
} catch {
|
|
9474
|
-
return void 0;
|
|
9475
|
-
}
|
|
9476
|
-
}
|
|
9477
9483
|
function reviveError(payload) {
|
|
9478
9484
|
const err = new Error(payload?.message ?? "task worker error");
|
|
9479
9485
|
if (payload?.name !== void 0) err.name = payload.name;
|
|
@@ -9542,6 +9548,53 @@ async function runTaskInWorker(options) {
|
|
|
9542
9548
|
worker.on(
|
|
9543
9549
|
"message",
|
|
9544
9550
|
(msg) => {
|
|
9551
|
+
if (msg?.type === "tasks-call") {
|
|
9552
|
+
const { seq, method, args } = msg;
|
|
9553
|
+
const reply = (body) => {
|
|
9554
|
+
try {
|
|
9555
|
+
worker.postMessage({ type: "tasks-result", seq, ...body });
|
|
9556
|
+
} catch (err) {
|
|
9557
|
+
try {
|
|
9558
|
+
worker.postMessage({
|
|
9559
|
+
type: "tasks-result",
|
|
9560
|
+
seq,
|
|
9561
|
+
ok: false,
|
|
9562
|
+
error: {
|
|
9563
|
+
name: "Error",
|
|
9564
|
+
message: `taskCtx.tasks.${method} result is not cloneable: ${String(err)}`,
|
|
9565
|
+
stack: void 0,
|
|
9566
|
+
props: {}
|
|
9567
|
+
}
|
|
9568
|
+
});
|
|
9569
|
+
} catch {
|
|
9570
|
+
}
|
|
9571
|
+
}
|
|
9572
|
+
};
|
|
9573
|
+
if (!options.onTasksCall) {
|
|
9574
|
+
reply({
|
|
9575
|
+
ok: false,
|
|
9576
|
+
error: {
|
|
9577
|
+
name: "Error",
|
|
9578
|
+
message: `taskCtx.tasks.${method} is unavailable: no host handler was provided for this execution`,
|
|
9579
|
+
stack: void 0,
|
|
9580
|
+
props: {}
|
|
9581
|
+
}
|
|
9582
|
+
});
|
|
9583
|
+
return;
|
|
9584
|
+
}
|
|
9585
|
+
options.onTasksCall(method, args ?? []).then((value) => reply({ ok: true, value })).catch((err) => {
|
|
9586
|
+
reply({
|
|
9587
|
+
ok: false,
|
|
9588
|
+
error: {
|
|
9589
|
+
name: err instanceof Error ? err.name : "Error",
|
|
9590
|
+
message: err instanceof Error ? err.message : String(err),
|
|
9591
|
+
stack: err instanceof Error ? err.stack : void 0,
|
|
9592
|
+
props: {}
|
|
9593
|
+
}
|
|
9594
|
+
});
|
|
9595
|
+
});
|
|
9596
|
+
return;
|
|
9597
|
+
}
|
|
9545
9598
|
if (phase === "grace") {
|
|
9546
9599
|
if (msg?.type === "done") {
|
|
9547
9600
|
finish(
|
|
@@ -9588,7 +9641,6 @@ async function runTaskInWorker(options) {
|
|
|
9588
9641
|
type: "run",
|
|
9589
9642
|
payload,
|
|
9590
9643
|
taskCtx: {
|
|
9591
|
-
config: safeConfig(taskCtx.config),
|
|
9592
9644
|
job: taskCtx.job
|
|
9593
9645
|
},
|
|
9594
9646
|
registries: options.registries,
|
|
@@ -9597,9 +9649,7 @@ async function runTaskInWorker(options) {
|
|
|
9597
9649
|
});
|
|
9598
9650
|
} catch (err) {
|
|
9599
9651
|
finish(
|
|
9600
|
-
() => reject(
|
|
9601
|
-
new Error(`Task "${taskCtx.job.name}" payload/config is not cloneable: ${String(err)}`)
|
|
9602
|
-
)
|
|
9652
|
+
() => reject(new Error(`Task "${taskCtx.job.name}" payload is not cloneable: ${String(err)}`))
|
|
9603
9653
|
);
|
|
9604
9654
|
}
|
|
9605
9655
|
});
|
|
@@ -9650,13 +9700,14 @@ function createTaskQueue(deps) {
|
|
|
9650
9700
|
const loadTaskModule = deps.loadTaskModule ?? (async (filePath) => await import(filePath));
|
|
9651
9701
|
const loadPayloadSchema = deps.loadPayloadSchema ?? (async (filePath) => {
|
|
9652
9702
|
const zodPath = path28.join(path28.dirname(filePath), "zod.js");
|
|
9653
|
-
|
|
9654
|
-
|
|
9655
|
-
|
|
9656
|
-
|
|
9657
|
-
|
|
9658
|
-
|
|
9703
|
+
const mod = await import(zodPath);
|
|
9704
|
+
const schemaKey = Object.keys(mod).find((k) => k.endsWith("Schema"));
|
|
9705
|
+
if (!schemaKey) {
|
|
9706
|
+
throw new Error(
|
|
9707
|
+
`[faapi] Task payload schema artifact has no *Schema export: ${zodPath} \u2014 re-run "faapi dev" / "faapi build" to regenerate task artifacts.`
|
|
9708
|
+
);
|
|
9659
9709
|
}
|
|
9710
|
+
return mod[schemaKey];
|
|
9660
9711
|
});
|
|
9661
9712
|
function assertKnownTask(name) {
|
|
9662
9713
|
const meta = registry.get(name);
|
|
@@ -9665,6 +9716,54 @@ function createTaskQueue(deps) {
|
|
|
9665
9716
|
throw new Error(`[faapi] Unknown task "${name}". Registered tasks: ${known}`);
|
|
9666
9717
|
}
|
|
9667
9718
|
}
|
|
9719
|
+
function assertGroupSupport() {
|
|
9720
|
+
if (!driver.groups) {
|
|
9721
|
+
throw new Error(
|
|
9722
|
+
"[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."
|
|
9723
|
+
);
|
|
9724
|
+
}
|
|
9725
|
+
}
|
|
9726
|
+
async function maybeFireCompletion(snap) {
|
|
9727
|
+
if (!driver.groups) return;
|
|
9728
|
+
if (!snap.onComplete || snap.completionEnqueued || snap.settled < snap.total) return;
|
|
9729
|
+
const summary = {
|
|
9730
|
+
groupId: snap.groupId,
|
|
9731
|
+
task: snap.task,
|
|
9732
|
+
total: snap.total,
|
|
9733
|
+
done: snap.done,
|
|
9734
|
+
failed: snap.failed,
|
|
9735
|
+
cancelled: snap.cancelled,
|
|
9736
|
+
settled: snap.settled
|
|
9737
|
+
};
|
|
9738
|
+
try {
|
|
9739
|
+
await enqueueCore(snap.onComplete, summary, {
|
|
9740
|
+
dedupId: GROUP_COMPLETION_DEDUP(snap.groupId)
|
|
9741
|
+
});
|
|
9742
|
+
await driver.groups.markCompletionEnqueued(snap.groupId);
|
|
9743
|
+
} catch (err) {
|
|
9744
|
+
console.error(
|
|
9745
|
+
`[faapi] task group "${snap.groupId}" completion enqueue failed (group is settled; re-invoke enqueueGroup with the same groupId to re-arm the callback):`,
|
|
9746
|
+
err
|
|
9747
|
+
);
|
|
9748
|
+
}
|
|
9749
|
+
}
|
|
9750
|
+
async function settleGroup(record, outcome, willRetry) {
|
|
9751
|
+
const groupId = record.groupId;
|
|
9752
|
+
if (!groupId || !driver.groups) return;
|
|
9753
|
+
if (outcome !== "done" && willRetry) return;
|
|
9754
|
+
try {
|
|
9755
|
+
const snap = await driver.groups.settle(groupId, record.id, outcome);
|
|
9756
|
+
if (outcome === "failed" && snap.onFailure === "fail-fast") {
|
|
9757
|
+
await maybeFireCompletion(await driver.groups.cancelRemaining(groupId));
|
|
9758
|
+
}
|
|
9759
|
+
await maybeFireCompletion(snap);
|
|
9760
|
+
} catch (err) {
|
|
9761
|
+
console.error(
|
|
9762
|
+
`[faapi] task group accounting failed for "${groupId}" (job ${record.id}, outcome ${outcome}):`,
|
|
9763
|
+
err
|
|
9764
|
+
);
|
|
9765
|
+
}
|
|
9766
|
+
}
|
|
9668
9767
|
function snapshotRegistries() {
|
|
9669
9768
|
return {
|
|
9670
9769
|
agents: registriesView.agent.listAgents().map((core) => registriesView.agent.getAgentEntry(core.name)).filter((entry) => entry !== void 0),
|
|
@@ -9675,14 +9774,13 @@ function createTaskQueue(deps) {
|
|
|
9675
9774
|
async function validatePayload(name, payload) {
|
|
9676
9775
|
if (!schemaCache.has(name)) {
|
|
9677
9776
|
const meta = registry.get(name);
|
|
9678
|
-
schemaCache.set(
|
|
9679
|
-
name,
|
|
9680
|
-
await loadPayloadSchema(path28.resolve(rootDir, meta.filePath)).catch(() => void 0)
|
|
9681
|
-
);
|
|
9777
|
+
schemaCache.set(name, await loadPayloadSchema(path28.resolve(rootDir, meta.filePath)));
|
|
9682
9778
|
}
|
|
9683
9779
|
const schema = schemaCache.get(name);
|
|
9684
9780
|
if (!schema || typeof schema.safeParse !== "function") {
|
|
9685
|
-
|
|
9781
|
+
throw new Error(
|
|
9782
|
+
`[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.`
|
|
9783
|
+
);
|
|
9686
9784
|
}
|
|
9687
9785
|
const result = schema.safeParse(payload);
|
|
9688
9786
|
if (!result.success) {
|
|
@@ -9704,11 +9802,13 @@ function createTaskQueue(deps) {
|
|
|
9704
9802
|
attempts: 0,
|
|
9705
9803
|
createdAt: Date.now()
|
|
9706
9804
|
};
|
|
9805
|
+
if (job.groupId !== void 0) record.groupId = job.groupId;
|
|
9707
9806
|
record.attempts = job.attempt;
|
|
9708
9807
|
transitionStatus(record, "running");
|
|
9709
9808
|
delete record.progress;
|
|
9710
9809
|
record.error = void 0;
|
|
9711
9810
|
records.set(job.id, record);
|
|
9811
|
+
const willRetry = job.attempt <= (meta.retries ?? 0);
|
|
9712
9812
|
try {
|
|
9713
9813
|
let result;
|
|
9714
9814
|
if (meta.timeoutMs !== void 0 && meta.timeoutMs > 0) {
|
|
@@ -9716,7 +9816,8 @@ function createTaskQueue(deps) {
|
|
|
9716
9816
|
taskModulePath: path28.resolve(rootDir, meta.filePath),
|
|
9717
9817
|
payload: job.payload,
|
|
9718
9818
|
taskCtx: {
|
|
9719
|
-
config
|
|
9819
|
+
// 隔离任务不传 config:worker 线程不接收进程配置(config 含函数字段
|
|
9820
|
+
// 不可结构化克隆,框架不做降级传递)——任务数据经 payload 显式传入
|
|
9720
9821
|
resourcesDir: deps.resourcesDir,
|
|
9721
9822
|
job: { id: job.id, name: job.name, attempt: job.attempt }
|
|
9722
9823
|
},
|
|
@@ -9737,7 +9838,16 @@ function createTaskQueue(deps) {
|
|
|
9737
9838
|
onProgress: (value) => {
|
|
9738
9839
|
if (record.status === "running") record.progress = value;
|
|
9739
9840
|
},
|
|
9740
|
-
onLog: writeLogEntry
|
|
9841
|
+
onLog: writeLogEntry,
|
|
9842
|
+
// 隔离路径的 taskCtx.tasks 为 postMessage RPC 代理——宿主侧按方法名
|
|
9843
|
+
// 分派到队列本体(与 ctx.tasks / app.tasks 同一实例),结果/错误按 seq 回传
|
|
9844
|
+
onTasksCall: async (method, args) => {
|
|
9845
|
+
if (!TASK_CLIENT_METHODS.has(method)) {
|
|
9846
|
+
throw new Error(`[faapi] Unknown task client method "${method}"`);
|
|
9847
|
+
}
|
|
9848
|
+
const fn = queue[method];
|
|
9849
|
+
return fn.apply(queue, args);
|
|
9850
|
+
}
|
|
9741
9851
|
});
|
|
9742
9852
|
} else {
|
|
9743
9853
|
let mod = moduleCache2.get(job.name);
|
|
@@ -9750,6 +9860,8 @@ function createTaskQueue(deps) {
|
|
|
9750
9860
|
}
|
|
9751
9861
|
const taskCtx = {
|
|
9752
9862
|
signal: job.signal,
|
|
9863
|
+
// 活引用:与 ctx.tasks / app.tasks 同一 app 实例队列
|
|
9864
|
+
tasks: queue,
|
|
9753
9865
|
config: deps.config,
|
|
9754
9866
|
job: { id: job.id, name: job.name, attempt: job.attempt },
|
|
9755
9867
|
registries: registriesView,
|
|
@@ -9767,23 +9879,27 @@ function createTaskQueue(deps) {
|
|
|
9767
9879
|
transitionStatus(record, "done");
|
|
9768
9880
|
record.result = result;
|
|
9769
9881
|
evictFinishedRecords();
|
|
9882
|
+
await settleGroup(record, "done", willRetry);
|
|
9770
9883
|
return result;
|
|
9771
9884
|
} catch (err) {
|
|
9772
9885
|
const cancelled = err instanceof TaskCancelledError || job.signal.aborted;
|
|
9773
|
-
|
|
9886
|
+
const outcome = cancelled ? "cancelled" : "failed";
|
|
9887
|
+
transitionStatus(record, outcome);
|
|
9774
9888
|
record.error = err instanceof Error ? err.message : String(err);
|
|
9775
9889
|
evictFinishedRecords();
|
|
9890
|
+
await settleGroup(record, outcome, willRetry);
|
|
9776
9891
|
if (deps.onFailed) {
|
|
9777
9892
|
void Promise.resolve().then(
|
|
9778
9893
|
() => deps.onFailed({
|
|
9779
9894
|
task: job.name,
|
|
9780
9895
|
jobId: job.id,
|
|
9781
9896
|
attempt: job.attempt,
|
|
9782
|
-
willRetry
|
|
9897
|
+
willRetry,
|
|
9783
9898
|
cancelled,
|
|
9784
9899
|
error: record.error
|
|
9785
9900
|
})
|
|
9786
|
-
).catch(() => {
|
|
9901
|
+
).catch((hookErr) => {
|
|
9902
|
+
console.error(`[faapi] task onFailed hook threw for "${job.name}":`, hookErr);
|
|
9787
9903
|
});
|
|
9788
9904
|
}
|
|
9789
9905
|
throw err;
|
|
@@ -9797,35 +9913,86 @@ function createTaskQueue(deps) {
|
|
|
9797
9913
|
});
|
|
9798
9914
|
}
|
|
9799
9915
|
}
|
|
9916
|
+
async function enqueueCore(name, payload, opts) {
|
|
9917
|
+
if (stopped) {
|
|
9918
|
+
throw new Error("[faapi] Task queue is stopped and no longer accepts jobs");
|
|
9919
|
+
}
|
|
9920
|
+
assertKnownTask(name);
|
|
9921
|
+
const data = await validatePayload(name, payload);
|
|
9922
|
+
const meta = registry.get(name);
|
|
9923
|
+
const id = await driver.enqueue(name, data, {
|
|
9924
|
+
delayMs: opts?.delayMs,
|
|
9925
|
+
retries: meta.retries ?? 0,
|
|
9926
|
+
dedupId: opts?.dedupId,
|
|
9927
|
+
// 执行硬限预算透传驱动(pgboss 映射 expireInSeconds):未声明 timeoutMs 时
|
|
9928
|
+
// 两字段为 undefined,驱动用自身兜底
|
|
9929
|
+
timeoutMs: meta.timeoutMs,
|
|
9930
|
+
graceMs: meta.graceMs,
|
|
9931
|
+
...opts?.groupId !== void 0 ? { groupId: opts.groupId } : {}
|
|
9932
|
+
});
|
|
9933
|
+
if (!records.has(id)) {
|
|
9934
|
+
records.set(id, {
|
|
9935
|
+
id,
|
|
9936
|
+
name,
|
|
9937
|
+
payload: data,
|
|
9938
|
+
status: "pending",
|
|
9939
|
+
attempts: 0,
|
|
9940
|
+
...opts?.groupId !== void 0 ? { groupId: opts.groupId } : {},
|
|
9941
|
+
createdAt: Date.now(),
|
|
9942
|
+
...opts?.delayMs ? { runAt: Date.now() + opts.delayMs } : {}
|
|
9943
|
+
});
|
|
9944
|
+
}
|
|
9945
|
+
return id;
|
|
9946
|
+
}
|
|
9800
9947
|
const queue = {
|
|
9801
9948
|
async enqueue(name, payload = {}, opts) {
|
|
9949
|
+
const id = await enqueueCore(name, payload, {
|
|
9950
|
+
delayMs: opts?.delayMs,
|
|
9951
|
+
dedupId: opts?.dedupId
|
|
9952
|
+
});
|
|
9953
|
+
return { id };
|
|
9954
|
+
},
|
|
9955
|
+
async enqueueGroup(name, payloads, opts) {
|
|
9802
9956
|
if (stopped) {
|
|
9803
9957
|
throw new Error("[faapi] Task queue is stopped and no longer accepts jobs");
|
|
9804
9958
|
}
|
|
9959
|
+
assertGroupSupport();
|
|
9805
9960
|
assertKnownTask(name);
|
|
9806
|
-
|
|
9807
|
-
|
|
9808
|
-
|
|
9809
|
-
|
|
9810
|
-
|
|
9811
|
-
|
|
9812
|
-
|
|
9813
|
-
|
|
9814
|
-
|
|
9815
|
-
|
|
9961
|
+
if (!Array.isArray(payloads) || payloads.length === 0) {
|
|
9962
|
+
throw new Error(
|
|
9963
|
+
"[faapi] enqueueGroup requires a non-empty payloads array \u2014 a group without members can never settle."
|
|
9964
|
+
);
|
|
9965
|
+
}
|
|
9966
|
+
const onComplete = opts?.onComplete;
|
|
9967
|
+
if (onComplete !== void 0) assertKnownTask(onComplete);
|
|
9968
|
+
const onFailure = opts?.onFailure ?? "run-to-completion";
|
|
9969
|
+
const validated = [];
|
|
9970
|
+
for (const p of payloads) {
|
|
9971
|
+
validated.push(await validatePayload(name, p));
|
|
9972
|
+
}
|
|
9973
|
+
const groupId = opts?.groupId ?? crypto.randomUUID();
|
|
9974
|
+
await driver.groups.create({
|
|
9975
|
+
id: groupId,
|
|
9976
|
+
task: name,
|
|
9977
|
+
total: validated.length,
|
|
9978
|
+
...onComplete !== void 0 ? { onComplete } : {},
|
|
9979
|
+
onFailure
|
|
9816
9980
|
});
|
|
9817
|
-
|
|
9818
|
-
|
|
9819
|
-
|
|
9820
|
-
|
|
9821
|
-
|
|
9822
|
-
|
|
9823
|
-
attempts: 0,
|
|
9824
|
-
createdAt: Date.now(),
|
|
9825
|
-
...opts?.delayMs ? { runAt: Date.now() + opts.delayMs } : {}
|
|
9981
|
+
const jobs = [];
|
|
9982
|
+
for (let i = 0; i < validated.length; i++) {
|
|
9983
|
+
const id = await enqueueCore(name, validated[i], {
|
|
9984
|
+
delayMs: opts?.delayMs,
|
|
9985
|
+
dedupId: GROUP_MEMBER_DEDUP(groupId, i + 1),
|
|
9986
|
+
groupId
|
|
9826
9987
|
});
|
|
9988
|
+
jobs.push({ id });
|
|
9827
9989
|
}
|
|
9828
|
-
|
|
9990
|
+
await maybeFireCompletion(await driver.groups.get(groupId));
|
|
9991
|
+
return { groupId, jobs };
|
|
9992
|
+
},
|
|
9993
|
+
async getGroup(groupId) {
|
|
9994
|
+
assertGroupSupport();
|
|
9995
|
+
return driver.groups.get(groupId);
|
|
9829
9996
|
},
|
|
9830
9997
|
list(name) {
|
|
9831
9998
|
const snapshot = [];
|
|
@@ -9852,6 +10019,7 @@ function createTaskQueue(deps) {
|
|
|
9852
10019
|
status: record.status,
|
|
9853
10020
|
attempts: record.attempts,
|
|
9854
10021
|
createdAt: record.createdAt,
|
|
10022
|
+
...record.groupId !== void 0 ? { groupId: record.groupId } : {},
|
|
9855
10023
|
...record.result !== void 0 ? { result: record.result } : {},
|
|
9856
10024
|
...record.error !== void 0 ? { error: record.error } : {},
|
|
9857
10025
|
...record.runAt !== void 0 ? { runAt: record.runAt } : {}
|
|
@@ -9871,8 +10039,11 @@ function createTaskQueue(deps) {
|
|
|
9871
10039
|
}
|
|
9872
10040
|
await driver.cancel(name, id);
|
|
9873
10041
|
const record = records.get(id);
|
|
9874
|
-
if (record)
|
|
9875
|
-
|
|
10042
|
+
if (record) {
|
|
10043
|
+
transitionStatus(record, "cancelled");
|
|
10044
|
+
evictFinishedRecords();
|
|
10045
|
+
await settleGroup(record, "cancelled", false);
|
|
10046
|
+
}
|
|
9876
10047
|
},
|
|
9877
10048
|
async retry(name, id) {
|
|
9878
10049
|
if (!driver.retry) {
|
|
@@ -9882,7 +10053,18 @@ function createTaskQueue(deps) {
|
|
|
9882
10053
|
}
|
|
9883
10054
|
await driver.retry(name, id);
|
|
9884
10055
|
const record = records.get(id);
|
|
9885
|
-
if (record)
|
|
10056
|
+
if (record) {
|
|
10057
|
+
const wasTerminal = TERMINAL_STATUSES.has(record.status);
|
|
10058
|
+
transitionStatus(record, "pending");
|
|
10059
|
+
if (wasTerminal && record.groupId && driver.groups) {
|
|
10060
|
+
await driver.groups.unsettle(record.groupId, record.id).catch((err) => {
|
|
10061
|
+
console.error(
|
|
10062
|
+
`[faapi] task group unsettle failed for "${record.groupId}" (job ${record.id}):`,
|
|
10063
|
+
err
|
|
10064
|
+
);
|
|
10065
|
+
});
|
|
10066
|
+
}
|
|
10067
|
+
}
|
|
9886
10068
|
},
|
|
9887
10069
|
async start() {
|
|
9888
10070
|
if (stopped) {
|
|
@@ -9909,7 +10091,7 @@ function createTaskQueue(deps) {
|
|
|
9909
10091
|
};
|
|
9910
10092
|
return queue;
|
|
9911
10093
|
}
|
|
9912
|
-
var TERMINAL_STATUSES, MAX_FINISHED_RECORDS;
|
|
10094
|
+
var TERMINAL_STATUSES, GROUP_MEMBER_DEDUP, GROUP_COMPLETION_DEDUP, TASK_CLIENT_METHODS, MAX_FINISHED_RECORDS;
|
|
9913
10095
|
var init_taskQueue = __esm({
|
|
9914
10096
|
"src/task/taskQueue.ts"() {
|
|
9915
10097
|
"use strict";
|
|
@@ -9918,6 +10100,17 @@ var init_taskQueue = __esm({
|
|
|
9918
10100
|
init_registries();
|
|
9919
10101
|
init_logger();
|
|
9920
10102
|
TERMINAL_STATUSES = /* @__PURE__ */ new Set(["done", "failed", "cancelled"]);
|
|
10103
|
+
GROUP_MEMBER_DEDUP = (groupId, index) => `faapi-group:${groupId}:${index}`;
|
|
10104
|
+
GROUP_COMPLETION_DEDUP = (groupId) => `faapi-group:${groupId}:complete`;
|
|
10105
|
+
TASK_CLIENT_METHODS = /* @__PURE__ */ new Set([
|
|
10106
|
+
"enqueue",
|
|
10107
|
+
"enqueueGroup",
|
|
10108
|
+
"list",
|
|
10109
|
+
"listQueued",
|
|
10110
|
+
"cancel",
|
|
10111
|
+
"retry",
|
|
10112
|
+
"getGroup"
|
|
10113
|
+
]);
|
|
9921
10114
|
MAX_FINISHED_RECORDS = 1e3;
|
|
9922
10115
|
}
|
|
9923
10116
|
});
|
|
@@ -10041,7 +10234,7 @@ async function loadPlugins(declarations, ctx, rootDir, dist) {
|
|
|
10041
10234
|
const upgradeWrappers = [];
|
|
10042
10235
|
const failures = [];
|
|
10043
10236
|
if (!declarations || declarations.length === 0) {
|
|
10044
|
-
return { handlerWrappers, upgradeWrappers
|
|
10237
|
+
return { handlerWrappers, upgradeWrappers };
|
|
10045
10238
|
}
|
|
10046
10239
|
const fullCtx = {
|
|
10047
10240
|
...ctx,
|
|
@@ -10068,7 +10261,7 @@ async function loadPlugins(declarations, ctx, rootDir, dist) {
|
|
|
10068
10261
|
}
|
|
10069
10262
|
if (enable === false) continue;
|
|
10070
10263
|
if (loaded.has(specifier)) {
|
|
10071
|
-
|
|
10264
|
+
failures.push({ specifier, reason: "duplicate plugin declaration" });
|
|
10072
10265
|
continue;
|
|
10073
10266
|
}
|
|
10074
10267
|
loaded.add(specifier);
|
|
@@ -10089,12 +10282,12 @@ async function loadPlugins(declarations, ctx, rootDir, dist) {
|
|
|
10089
10282
|
}
|
|
10090
10283
|
}
|
|
10091
10284
|
if (failures.length > 0) {
|
|
10092
|
-
|
|
10285
|
+
throw new Error(
|
|
10093
10286
|
`[faapi] ${failures.length} plugin(s) failed to load:
|
|
10094
10287
|
` + failures.map((f) => ` - ${f.specifier}: ${f.reason}`).join("\n")
|
|
10095
10288
|
);
|
|
10096
10289
|
}
|
|
10097
|
-
return { handlerWrappers, upgradeWrappers
|
|
10290
|
+
return { handlerWrappers, upgradeWrappers };
|
|
10098
10291
|
}
|
|
10099
10292
|
function resolveLocalPluginSource(specifier, baseDir) {
|
|
10100
10293
|
const base = path29.isAbsolute(specifier) ? specifier : path29.resolve(baseDir, specifier);
|
|
@@ -10428,7 +10621,7 @@ async function createAppBase(options) {
|
|
|
10428
10621
|
const taskQueue = createTaskQueue({
|
|
10429
10622
|
registry: registries.task,
|
|
10430
10623
|
rootDir,
|
|
10431
|
-
config,
|
|
10624
|
+
config: config ?? {},
|
|
10432
10625
|
resourcesDir,
|
|
10433
10626
|
registries: createTaskRegistriesView(registries),
|
|
10434
10627
|
// 轻量补全通道:进程内路径经 store 惰性读取(插件注册后的活引用);
|
|
@@ -10459,7 +10652,7 @@ async function createAppBase(options) {
|
|
|
10459
10652
|
dist,
|
|
10460
10653
|
cors: config?.cors ?? true,
|
|
10461
10654
|
onError: config?.lifecycle?.onError,
|
|
10462
|
-
config
|
|
10655
|
+
config,
|
|
10463
10656
|
wsRoutes,
|
|
10464
10657
|
middlewares: config?.middlewares,
|
|
10465
10658
|
injectors: config?.injectors,
|