@faapi/faapi 6.5.0 → 6.7.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 CHANGED
@@ -3376,6 +3376,9 @@ var init_generateAgentArtifacts = __esm({
3376
3376
  import fg4 from "fast-glob";
3377
3377
  import path16 from "path";
3378
3378
  import fs12 from "fs";
3379
+ function parseNumericLiteral(raw) {
3380
+ return Number(raw.replace(/_/g, ""));
3381
+ }
3379
3382
  function filePathToTaskName(filePath) {
3380
3383
  const normalized = filePath.replace(/\\/g, "/");
3381
3384
  const match = normalized.match(/(?:^|\/)tasks\/(.+)\/task\.ts$/);
@@ -3413,24 +3416,37 @@ async function scanTasks(rootDir, patterns) {
3413
3416
  const concurrency = CONCURRENCY_RE.exec(source)?.[1];
3414
3417
  const retries = RETRIES_RE.exec(source)?.[1];
3415
3418
  const timeoutMs = TIMEOUT_MS_RE.exec(source)?.[1];
3419
+ const graceMs = GRACE_MS_RE.exec(source)?.[1];
3416
3420
  if (cron !== void 0) manifest.cron = cron;
3417
- if (concurrency !== void 0) manifest.concurrency = Number(concurrency);
3418
- if (retries !== void 0) manifest.retries = Number(retries);
3419
- if (timeoutMs !== void 0) manifest.timeoutMs = Number(timeoutMs);
3421
+ if (concurrency !== void 0) manifest.concurrency = parseNumericLiteral(concurrency);
3422
+ if (retries !== void 0) manifest.retries = parseNumericLiteral(retries);
3423
+ if (timeoutMs !== void 0) {
3424
+ const value = parseNumericLiteral(timeoutMs);
3425
+ if (value < MIN_ISOLATED_TIMEOUT_MS) {
3426
+ throw new Error(
3427
+ `[faapi] Task "${name}" timeoutMs ${value} is below the minimum ${MIN_ISOLATED_TIMEOUT_MS}ms (1 minute). Declaring a timeout means this is a long-running task that needs real cancellation \u2014 tasks finishing within a minute do not need one: remove timeoutMs to run in-process (implement your own deadline with Promise.race if needed), or raise it.`
3428
+ );
3429
+ }
3430
+ manifest.timeoutMs = value;
3431
+ }
3432
+ if (graceMs !== void 0) manifest.graceMs = parseNumericLiteral(graceMs);
3420
3433
  tasks.push(manifest);
3421
3434
  }
3422
- return tasks;
3435
+ return tasks.sort((a, b) => a.name.localeCompare(b.name));
3423
3436
  }
3424
- var TASK_PATTERNS, TASK_FILENAME, CRON_RE, CONCURRENCY_RE, RETRIES_RE, TIMEOUT_MS_RE;
3437
+ var TASK_PATTERNS, TASK_FILENAME, CRON_RE, NUM_LITERAL, CONCURRENCY_RE, RETRIES_RE, TIMEOUT_MS_RE, GRACE_MS_RE, MIN_ISOLATED_TIMEOUT_MS;
3425
3438
  var init_scanTasks = __esm({
3426
3439
  "src/task/scanTasks.ts"() {
3427
3440
  "use strict";
3428
3441
  TASK_PATTERNS = ["src/tasks/**/task.ts"];
3429
3442
  TASK_FILENAME = "task.ts";
3430
3443
  CRON_RE = /(?:^|\n)\s*cron:\s*['"`]([^'"`\n]+)['"`]/;
3431
- CONCURRENCY_RE = /(?:^|\n)\s*concurrency:\s*(\d+)/;
3432
- RETRIES_RE = /(?:^|\n)\s*retries:\s*(\d+)/;
3433
- TIMEOUT_MS_RE = /(?:^|\n)\s*timeoutMs:\s*(\d+)/;
3444
+ NUM_LITERAL = "(\\d+(?:_\\d+)*)";
3445
+ CONCURRENCY_RE = new RegExp(`(?:^|\\n)\\s*concurrency:\\s*${NUM_LITERAL}`);
3446
+ RETRIES_RE = new RegExp(`(?:^|\\n)\\s*retries:\\s*${NUM_LITERAL}`);
3447
+ TIMEOUT_MS_RE = new RegExp(`(?:^|\\n)\\s*timeoutMs:\\s*${NUM_LITERAL}`);
3448
+ GRACE_MS_RE = new RegExp(`(?:^|\\n)\\s*graceMs:\\s*${NUM_LITERAL}`);
3449
+ MIN_ISOLATED_TIMEOUT_MS = 6e4;
3434
3450
  }
3435
3451
  });
3436
3452
 
@@ -3443,7 +3459,8 @@ function serializeTasks(tasks, dist = "dist") {
3443
3459
  ...t.cron !== void 0 ? { cron: t.cron } : {},
3444
3460
  ...t.concurrency !== void 0 ? { concurrency: t.concurrency } : {},
3445
3461
  ...t.retries !== void 0 ? { retries: t.retries } : {},
3446
- ...t.timeoutMs !== void 0 ? { timeoutMs: t.timeoutMs } : {}
3462
+ ...t.timeoutMs !== void 0 ? { timeoutMs: t.timeoutMs } : {},
3463
+ ...t.graceMs !== void 0 ? { graceMs: t.graceMs } : {}
3447
3464
  }));
3448
3465
  }
3449
3466
  async function writeTasksModule(manifest, outputPath) {
@@ -3459,7 +3476,8 @@ function hydrateTasks(manifest) {
3459
3476
  cron: t.cron ?? void 0,
3460
3477
  concurrency: t.concurrency ?? void 0,
3461
3478
  retries: t.retries ?? void 0,
3462
- timeoutMs: t.timeoutMs ?? void 0
3479
+ timeoutMs: t.timeoutMs ?? void 0,
3480
+ graceMs: t.graceMs ?? void 0
3463
3481
  }));
3464
3482
  }
3465
3483
  function collectTaskSchemaSources(tasks, rootDir) {
@@ -8366,9 +8384,14 @@ import * as mod from '${moduleUrl}';
8366
8384
 
8367
8385
  ${BUILD_VIEW_SOURCE}
8368
8386
 
8387
+ ${SERIALIZE_ERROR_SOURCE}
8388
+
8369
8389
  const run = mod.run;
8370
8390
  if (typeof run !== 'function') {
8371
- parentPort.postMessage({ type: 'error', message: 'Task module has no run export' });
8391
+ parentPort.postMessage({
8392
+ type: 'error',
8393
+ error: { name: 'Error', message: 'Task module has no run export', stack: undefined, props: {} },
8394
+ });
8372
8395
  } else {
8373
8396
  let controller = null;
8374
8397
  parentPort.on('message', async (msg) => {
@@ -8381,21 +8404,44 @@ if (typeof run !== 'function') {
8381
8404
  const { payload, taskCtx } = msg;
8382
8405
  try {
8383
8406
  const registries = buildRegistriesView(msg.registries);
8384
- const result = await run(payload, { ...taskCtx, signal: controller.signal, registries });
8407
+ const progress = (value) => parentPort.postMessage({ type: 'progress', value });
8408
+ const result = await run(payload, { ...taskCtx, signal: controller.signal, registries, progress });
8385
8409
  parentPort.postMessage({ type: 'done', result });
8386
8410
  } catch (err) {
8387
- parentPort.postMessage({
8388
- type: 'error',
8389
- message: err instanceof Error ? err.message : String(err),
8390
- });
8411
+ const serialized = serializeError(err);
8412
+ try {
8413
+ parentPort.postMessage({ type: 'error', error: serialized });
8414
+ } catch (_) {
8415
+ // props \u542B\u4E0D\u53EF\u514B\u9686\u503C\uFF08\u51FD\u6570\u7B49\uFF09\u65F6\u4E22\u5F03 props\uFF0C\u4FDD\u5E95 name/message/stack
8416
+ parentPort.postMessage({
8417
+ type: 'error',
8418
+ error: { name: serialized.name, message: serialized.message, stack: serialized.stack, props: {} },
8419
+ });
8420
+ }
8391
8421
  }
8392
8422
  });
8393
8423
  }
8394
8424
  `;
8395
8425
  }
8426
+ function reviveError(payload) {
8427
+ const err = new Error(payload?.message ?? "task worker error");
8428
+ if (payload?.name !== void 0) err.name = payload.name;
8429
+ if (payload?.stack !== void 0) err.stack = payload.stack;
8430
+ if (payload?.props) {
8431
+ for (const [key, value] of Object.entries(payload.props)) {
8432
+ err[key] = value;
8433
+ }
8434
+ }
8435
+ return err;
8436
+ }
8396
8437
  async function runTaskInWorker(options) {
8397
8438
  const { taskModulePath, payload, taskCtx, timeoutMs, externalSignal } = options;
8398
- const killGraceMs = options.killGraceMs ?? KILL_GRACE_MS;
8439
+ const killGraceMs = options.graceMs ?? KILL_GRACE_MS;
8440
+ if (externalSignal?.aborted) {
8441
+ throw new TaskCancelledError(
8442
+ `Task "${taskCtx.job.name}" cancelled before start (driver stop signal already aborted)`
8443
+ );
8444
+ }
8399
8445
  const moduleUrl = pathToFileURL2(taskModulePath).href;
8400
8446
  const wrapperUrl = new URL(
8401
8447
  `data:text/javascript,${encodeURIComponent(buildWrapperSource(moduleUrl))}`
@@ -8443,27 +8489,37 @@ async function runTaskInWorker(options) {
8443
8489
  () => startCancel(`timed out after ${timeoutMs}ms`),
8444
8490
  timeoutMs
8445
8491
  );
8446
- worker.on("message", (msg) => {
8447
- if (phase === "grace") {
8448
- if (msg?.type === "done") {
8449
- finish(
8450
- () => reject(
8451
- new TaskCancelledError(
8452
- `Task "${taskCtx.job.name}" ${cancelReason} (task completed after the timeout)`
8492
+ worker.on(
8493
+ "message",
8494
+ (msg) => {
8495
+ if (phase === "grace") {
8496
+ if (msg?.type === "done") {
8497
+ finish(
8498
+ () => reject(
8499
+ new TaskCancelledError(
8500
+ `Task "${taskCtx.job.name}" ${cancelReason} (task completed after the timeout)`
8501
+ )
8453
8502
  )
8454
- )
8455
- );
8503
+ );
8504
+ } else if (msg?.type === "error") {
8505
+ finish(() => reject(new TaskCancelledError(msg.error?.message ?? cancelReason)));
8506
+ }
8507
+ return;
8508
+ }
8509
+ if (phase !== "running") return;
8510
+ if (msg?.type === "done") {
8511
+ finish(() => resolve3(msg.result));
8456
8512
  } else if (msg?.type === "error") {
8457
- finish(() => reject(new TaskCancelledError(msg.message ?? cancelReason)));
8513
+ finish(() => reject(reviveError(msg.error)));
8514
+ } else if (msg?.type === "progress") {
8515
+ options.onProgress?.(msg.value);
8458
8516
  }
8459
- return;
8460
- }
8461
- if (phase !== "running") return;
8462
- if (msg?.type === "done") {
8463
- finish(() => resolve3(msg.result));
8464
- } else if (msg?.type === "error") {
8465
- finish(() => reject(new Error(msg.message ?? "task worker error")));
8466
8517
  }
8518
+ );
8519
+ worker.on("messageerror", () => {
8520
+ finish(
8521
+ () => reject(new Error(`Task "${taskCtx.job.name}" worker message could not be deserialized`))
8522
+ );
8467
8523
  });
8468
8524
  worker.on("error", (err) => {
8469
8525
  finish(() => reject(err));
@@ -8491,7 +8547,7 @@ async function runTaskInWorker(options) {
8491
8547
  }
8492
8548
  });
8493
8549
  }
8494
- var KILL_GRACE_MS, TaskCancelledError, BUILD_VIEW_SOURCE;
8550
+ var KILL_GRACE_MS, TaskCancelledError, BUILD_VIEW_SOURCE, SERIALIZE_ERROR_SOURCE;
8495
8551
  var init_taskWorker = __esm({
8496
8552
  "src/task/taskWorker.ts"() {
8497
8553
  "use strict";
@@ -8556,6 +8612,19 @@ function buildRegistriesView(snapshot) {
8556
8612
  },
8557
8613
  };
8558
8614
  }
8615
+ `;
8616
+ SERIALIZE_ERROR_SOURCE = `
8617
+ function serializeError(err) {
8618
+ if (err instanceof Error) {
8619
+ var props = {};
8620
+ var keys = Object.keys(err);
8621
+ for (var i = 0; i < keys.length; i++) {
8622
+ props[keys[i]] = err[keys[i]];
8623
+ }
8624
+ return { name: err.name, message: err.message, stack: err.stack, props: props };
8625
+ }
8626
+ return { name: 'Error', message: String(err), stack: undefined, props: {} };
8627
+ }
8559
8628
  `;
8560
8629
  }
8561
8630
  });
@@ -8570,6 +8639,25 @@ function createTaskQueue(deps) {
8570
8639
  const schemaCache = /* @__PURE__ */ new Map();
8571
8640
  let started = false;
8572
8641
  let stopped = false;
8642
+ let finishedCount = 0;
8643
+ function transitionStatus(record, status) {
8644
+ const wasTerminal = TERMINAL_STATUSES.has(record.status);
8645
+ const nowTerminal = TERMINAL_STATUSES.has(status);
8646
+ if (!wasTerminal && nowTerminal) finishedCount++;
8647
+ else if (wasTerminal && !nowTerminal) finishedCount--;
8648
+ record.status = status;
8649
+ }
8650
+ function evictFinishedRecords() {
8651
+ let toEvict = finishedCount - MAX_FINISHED_RECORDS;
8652
+ for (const [id, job] of records) {
8653
+ if (toEvict <= 0) break;
8654
+ if (TERMINAL_STATUSES.has(job.status)) {
8655
+ records.delete(id);
8656
+ finishedCount--;
8657
+ toEvict--;
8658
+ }
8659
+ }
8660
+ }
8573
8661
  const loadTaskModule = deps.loadTaskModule ?? (async (filePath) => await import(filePath));
8574
8662
  const loadPayloadSchema = deps.loadPayloadSchema ?? (async (filePath) => {
8575
8663
  const zodPath = path25.join(path25.dirname(filePath), "zod.js");
@@ -8628,7 +8716,8 @@ function createTaskQueue(deps) {
8628
8716
  createdAt: Date.now()
8629
8717
  };
8630
8718
  record.attempts = job.attempt;
8631
- record.status = "running";
8719
+ transitionStatus(record, "running");
8720
+ delete record.progress;
8632
8721
  record.error = void 0;
8633
8722
  records.set(job.id, record);
8634
8723
  try {
@@ -8643,7 +8732,11 @@ function createTaskQueue(deps) {
8643
8732
  },
8644
8733
  registries: snapshotRegistries(),
8645
8734
  timeoutMs: meta.timeoutMs,
8646
- externalSignal: job.signal
8735
+ graceMs: meta.graceMs,
8736
+ externalSignal: job.signal,
8737
+ onProgress: (value) => {
8738
+ if (record.status === "running") record.progress = value;
8739
+ }
8647
8740
  });
8648
8741
  } else {
8649
8742
  let mod = moduleCache2.get(job.name);
@@ -8658,17 +8751,22 @@ function createTaskQueue(deps) {
8658
8751
  signal: job.signal,
8659
8752
  config: deps.config,
8660
8753
  job: { id: job.id, name: job.name, attempt: job.attempt },
8661
- registries: registriesView
8754
+ registries: registriesView,
8755
+ progress: (value) => {
8756
+ if (record.status === "running") record.progress = value;
8757
+ }
8662
8758
  };
8663
8759
  result = await mod.run(job.payload, taskCtx);
8664
8760
  }
8665
- record.status = "done";
8761
+ transitionStatus(record, "done");
8666
8762
  record.result = result;
8763
+ evictFinishedRecords();
8667
8764
  return result;
8668
8765
  } catch (err) {
8669
8766
  const cancelled = err instanceof TaskCancelledError || job.signal.aborted;
8670
- record.status = cancelled ? "cancelled" : "failed";
8767
+ transitionStatus(record, cancelled ? "cancelled" : "failed");
8671
8768
  record.error = err instanceof Error ? err.message : String(err);
8769
+ evictFinishedRecords();
8672
8770
  if (deps.onFailed) {
8673
8771
  void Promise.resolve().then(
8674
8772
  () => deps.onFailed({
@@ -8763,7 +8861,8 @@ function createTaskQueue(deps) {
8763
8861
  }
8764
8862
  await driver.cancel(name, id);
8765
8863
  const record = records.get(id);
8766
- if (record) record.status = "cancelled";
8864
+ if (record) transitionStatus(record, "cancelled");
8865
+ evictFinishedRecords();
8767
8866
  },
8768
8867
  async retry(name, id) {
8769
8868
  if (!driver.retry) {
@@ -8773,7 +8872,7 @@ function createTaskQueue(deps) {
8773
8872
  }
8774
8873
  await driver.retry(name, id);
8775
8874
  const record = records.get(id);
8776
- if (record) record.status = "pending";
8875
+ if (record) transitionStatus(record, "pending");
8777
8876
  },
8778
8877
  async start() {
8779
8878
  if (stopped) {
@@ -8800,12 +8899,15 @@ function createTaskQueue(deps) {
8800
8899
  };
8801
8900
  return queue;
8802
8901
  }
8902
+ var TERMINAL_STATUSES, MAX_FINISHED_RECORDS;
8803
8903
  var init_taskQueue = __esm({
8804
8904
  "src/task/taskQueue.ts"() {
8805
8905
  "use strict";
8806
8906
  init_httpErrors();
8807
8907
  init_taskWorker();
8808
8908
  init_registries();
8909
+ TERMINAL_STATUSES = /* @__PURE__ */ new Set(["done", "failed", "cancelled"]);
8910
+ MAX_FINISHED_RECORDS = 1e3;
8809
8911
  }
8810
8912
  });
8811
8913