@faapi/faapi 6.33.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 CHANGED
@@ -9548,6 +9548,53 @@ async function runTaskInWorker(options) {
9548
9548
  worker.on(
9549
9549
  "message",
9550
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
+ }
9551
9598
  if (phase === "grace") {
9552
9599
  if (msg?.type === "done") {
9553
9600
  finish(
@@ -9669,6 +9716,54 @@ function createTaskQueue(deps) {
9669
9716
  throw new Error(`[faapi] Unknown task "${name}". Registered tasks: ${known}`);
9670
9717
  }
9671
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
+ }
9672
9767
  function snapshotRegistries() {
9673
9768
  return {
9674
9769
  agents: registriesView.agent.listAgents().map((core) => registriesView.agent.getAgentEntry(core.name)).filter((entry) => entry !== void 0),
@@ -9707,11 +9802,13 @@ function createTaskQueue(deps) {
9707
9802
  attempts: 0,
9708
9803
  createdAt: Date.now()
9709
9804
  };
9805
+ if (job.groupId !== void 0) record.groupId = job.groupId;
9710
9806
  record.attempts = job.attempt;
9711
9807
  transitionStatus(record, "running");
9712
9808
  delete record.progress;
9713
9809
  record.error = void 0;
9714
9810
  records.set(job.id, record);
9811
+ const willRetry = job.attempt <= (meta.retries ?? 0);
9715
9812
  try {
9716
9813
  let result;
9717
9814
  if (meta.timeoutMs !== void 0 && meta.timeoutMs > 0) {
@@ -9741,7 +9838,16 @@ function createTaskQueue(deps) {
9741
9838
  onProgress: (value) => {
9742
9839
  if (record.status === "running") record.progress = value;
9743
9840
  },
9744
- 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
+ }
9745
9851
  });
9746
9852
  } else {
9747
9853
  let mod = moduleCache2.get(job.name);
@@ -9754,6 +9860,8 @@ function createTaskQueue(deps) {
9754
9860
  }
9755
9861
  const taskCtx = {
9756
9862
  signal: job.signal,
9863
+ // 活引用:与 ctx.tasks / app.tasks 同一 app 实例队列
9864
+ tasks: queue,
9757
9865
  config: deps.config,
9758
9866
  job: { id: job.id, name: job.name, attempt: job.attempt },
9759
9867
  registries: registriesView,
@@ -9771,19 +9879,22 @@ function createTaskQueue(deps) {
9771
9879
  transitionStatus(record, "done");
9772
9880
  record.result = result;
9773
9881
  evictFinishedRecords();
9882
+ await settleGroup(record, "done", willRetry);
9774
9883
  return result;
9775
9884
  } catch (err) {
9776
9885
  const cancelled = err instanceof TaskCancelledError || job.signal.aborted;
9777
- transitionStatus(record, cancelled ? "cancelled" : "failed");
9886
+ const outcome = cancelled ? "cancelled" : "failed";
9887
+ transitionStatus(record, outcome);
9778
9888
  record.error = err instanceof Error ? err.message : String(err);
9779
9889
  evictFinishedRecords();
9890
+ await settleGroup(record, outcome, willRetry);
9780
9891
  if (deps.onFailed) {
9781
9892
  void Promise.resolve().then(
9782
9893
  () => deps.onFailed({
9783
9894
  task: job.name,
9784
9895
  jobId: job.id,
9785
9896
  attempt: job.attempt,
9786
- willRetry: job.attempt <= (meta.retries ?? 0),
9897
+ willRetry,
9787
9898
  cancelled,
9788
9899
  error: record.error
9789
9900
  })
@@ -9802,35 +9913,86 @@ function createTaskQueue(deps) {
9802
9913
  });
9803
9914
  }
9804
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
+ }
9805
9947
  const queue = {
9806
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) {
9807
9956
  if (stopped) {
9808
9957
  throw new Error("[faapi] Task queue is stopped and no longer accepts jobs");
9809
9958
  }
9959
+ assertGroupSupport();
9810
9960
  assertKnownTask(name);
9811
- const data = await validatePayload(name, payload);
9812
- const meta = registry.get(name);
9813
- const id = await driver.enqueue(name, data, {
9814
- delayMs: opts?.delayMs,
9815
- retries: meta.retries ?? 0,
9816
- dedupId: opts?.dedupId,
9817
- // 执行硬限预算透传驱动(pgboss 映射 expireInSeconds):未声明 timeoutMs 时
9818
- // 两字段为 undefined,驱动用自身兜底
9819
- timeoutMs: meta.timeoutMs,
9820
- graceMs: meta.graceMs
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
9821
9980
  });
9822
- if (!records.has(id)) {
9823
- records.set(id, {
9824
- id,
9825
- name,
9826
- payload: data,
9827
- status: "pending",
9828
- attempts: 0,
9829
- createdAt: Date.now(),
9830
- ...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
9831
9987
  });
9988
+ jobs.push({ id });
9832
9989
  }
9833
- return { id };
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);
9834
9996
  },
9835
9997
  list(name) {
9836
9998
  const snapshot = [];
@@ -9857,6 +10019,7 @@ function createTaskQueue(deps) {
9857
10019
  status: record.status,
9858
10020
  attempts: record.attempts,
9859
10021
  createdAt: record.createdAt,
10022
+ ...record.groupId !== void 0 ? { groupId: record.groupId } : {},
9860
10023
  ...record.result !== void 0 ? { result: record.result } : {},
9861
10024
  ...record.error !== void 0 ? { error: record.error } : {},
9862
10025
  ...record.runAt !== void 0 ? { runAt: record.runAt } : {}
@@ -9876,8 +10039,11 @@ function createTaskQueue(deps) {
9876
10039
  }
9877
10040
  await driver.cancel(name, id);
9878
10041
  const record = records.get(id);
9879
- if (record) transitionStatus(record, "cancelled");
9880
- evictFinishedRecords();
10042
+ if (record) {
10043
+ transitionStatus(record, "cancelled");
10044
+ evictFinishedRecords();
10045
+ await settleGroup(record, "cancelled", false);
10046
+ }
9881
10047
  },
9882
10048
  async retry(name, id) {
9883
10049
  if (!driver.retry) {
@@ -9887,7 +10053,18 @@ function createTaskQueue(deps) {
9887
10053
  }
9888
10054
  await driver.retry(name, id);
9889
10055
  const record = records.get(id);
9890
- if (record) transitionStatus(record, "pending");
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
+ }
9891
10068
  },
9892
10069
  async start() {
9893
10070
  if (stopped) {
@@ -9914,7 +10091,7 @@ function createTaskQueue(deps) {
9914
10091
  };
9915
10092
  return queue;
9916
10093
  }
9917
- var TERMINAL_STATUSES, MAX_FINISHED_RECORDS;
10094
+ var TERMINAL_STATUSES, GROUP_MEMBER_DEDUP, GROUP_COMPLETION_DEDUP, TASK_CLIENT_METHODS, MAX_FINISHED_RECORDS;
9918
10095
  var init_taskQueue = __esm({
9919
10096
  "src/task/taskQueue.ts"() {
9920
10097
  "use strict";
@@ -9923,6 +10100,17 @@ var init_taskQueue = __esm({
9923
10100
  init_registries();
9924
10101
  init_logger();
9925
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
+ ]);
9926
10114
  MAX_FINISHED_RECORDS = 1e3;
9927
10115
  }
9928
10116
  });