@faapi/faapi 6.3.0 → 6.4.1

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
@@ -3412,14 +3412,16 @@ async function scanTasks(rootDir, patterns) {
3412
3412
  const cron = CRON_RE.exec(source)?.[1];
3413
3413
  const concurrency = CONCURRENCY_RE.exec(source)?.[1];
3414
3414
  const retries = RETRIES_RE.exec(source)?.[1];
3415
+ const timeoutMs = TIMEOUT_MS_RE.exec(source)?.[1];
3415
3416
  if (cron !== void 0) manifest.cron = cron;
3416
3417
  if (concurrency !== void 0) manifest.concurrency = Number(concurrency);
3417
3418
  if (retries !== void 0) manifest.retries = Number(retries);
3419
+ if (timeoutMs !== void 0) manifest.timeoutMs = Number(timeoutMs);
3418
3420
  tasks.push(manifest);
3419
3421
  }
3420
3422
  return tasks;
3421
3423
  }
3422
- var TASK_PATTERNS, TASK_FILENAME, CRON_RE, CONCURRENCY_RE, RETRIES_RE;
3424
+ var TASK_PATTERNS, TASK_FILENAME, CRON_RE, CONCURRENCY_RE, RETRIES_RE, TIMEOUT_MS_RE;
3423
3425
  var init_scanTasks = __esm({
3424
3426
  "src/task/scanTasks.ts"() {
3425
3427
  "use strict";
@@ -3428,6 +3430,7 @@ var init_scanTasks = __esm({
3428
3430
  CRON_RE = /(?:^|\n)\s*cron:\s*['"`]([^'"`\n]+)['"`]/;
3429
3431
  CONCURRENCY_RE = /(?:^|\n)\s*concurrency:\s*(\d+)/;
3430
3432
  RETRIES_RE = /(?:^|\n)\s*retries:\s*(\d+)/;
3433
+ TIMEOUT_MS_RE = /(?:^|\n)\s*timeoutMs:\s*(\d+)/;
3431
3434
  }
3432
3435
  });
3433
3436
 
@@ -3439,7 +3442,8 @@ function serializeTasks(tasks, dist = "dist") {
3439
3442
  filePath: toProdFilePath(t.filePath, dist),
3440
3443
  ...t.cron !== void 0 ? { cron: t.cron } : {},
3441
3444
  ...t.concurrency !== void 0 ? { concurrency: t.concurrency } : {},
3442
- ...t.retries !== void 0 ? { retries: t.retries } : {}
3445
+ ...t.retries !== void 0 ? { retries: t.retries } : {},
3446
+ ...t.timeoutMs !== void 0 ? { timeoutMs: t.timeoutMs } : {}
3443
3447
  }));
3444
3448
  }
3445
3449
  async function writeTasksModule(manifest, outputPath) {
@@ -3454,7 +3458,8 @@ function hydrateTasks(manifest) {
3454
3458
  filePath: t.filePath,
3455
3459
  cron: t.cron ?? void 0,
3456
3460
  concurrency: t.concurrency ?? void 0,
3457
- retries: t.retries ?? void 0
3461
+ retries: t.retries ?? void 0,
3462
+ timeoutMs: t.timeoutMs ?? void 0
3458
3463
  }));
3459
3464
  }
3460
3465
  function collectTaskSchemaSources(tasks, rootDir) {
@@ -8304,138 +8309,168 @@ var init_startServer = __esm({
8304
8309
  }
8305
8310
  });
8306
8311
 
8307
- // src/task/memoryDriver.ts
8308
- import { randomUUID } from "crypto";
8309
- function createMemoryDriver() {
8310
- const jobs = /* @__PURE__ */ new Map();
8311
- const pendingByTask = /* @__PURE__ */ new Map();
8312
- const runningCount = /* @__PURE__ */ new Map();
8313
- const workers = /* @__PURE__ */ new Map();
8314
- let stopped = false;
8315
- function pump() {
8316
- if (stopped) return;
8317
- for (const [name, worker] of workers) {
8318
- const queue = pendingByTask.get(name);
8319
- if (!queue || queue.length === 0) continue;
8320
- let running = runningCount.get(name) ?? 0;
8321
- while (running < worker.concurrency && queue.length > 0) {
8322
- const head = jobs.get(queue[0]);
8323
- if (!head) {
8324
- queue.shift();
8325
- continue;
8326
- }
8327
- if (head.runAt > Date.now()) break;
8328
- queue.shift();
8329
- running += 1;
8330
- runningCount.set(name, running);
8331
- void dispatch(name, worker, head);
8332
- }
8333
- if (running === 0) runningCount.delete(name);
8334
- else runningCount.set(name, running);
8335
- }
8312
+ // src/task/taskWorker.ts
8313
+ import { Worker } from "worker_threads";
8314
+ import { pathToFileURL as pathToFileURL2 } from "url";
8315
+ function safeConfig(config) {
8316
+ if (config === void 0 || config === null) return config;
8317
+ try {
8318
+ structuredClone(config);
8319
+ return config;
8320
+ } catch {
8336
8321
  }
8337
- async function dispatch(name, worker, job) {
8338
- job.attempts += 1;
8339
- const driverJob = {
8340
- id: job.id,
8341
- name,
8342
- payload: job.payload,
8343
- attempt: job.attempts,
8344
- signal: job.controller.signal
8345
- };
8322
+ try {
8323
+ return JSON.parse(JSON.stringify(config));
8324
+ } catch {
8325
+ return void 0;
8326
+ }
8327
+ }
8328
+ function buildWrapperSource(moduleUrl) {
8329
+ return `
8330
+ import { parentPort } from 'node:worker_threads';
8331
+ import * as mod from '${moduleUrl}';
8332
+
8333
+ const run = mod.run;
8334
+ if (typeof run !== 'function') {
8335
+ parentPort.postMessage({ type: 'error', message: 'Task module has no run export' });
8336
+ } else {
8337
+ let controller = null;
8338
+ parentPort.on('message', async (msg) => {
8339
+ if (msg?.type === 'abort') {
8340
+ controller?.abort(new Error(msg.reason));
8341
+ return;
8342
+ }
8343
+ if (msg?.type !== 'run') return;
8344
+ controller = new AbortController();
8345
+ const { payload, taskCtx } = msg;
8346
8346
  try {
8347
- await worker.process(driverJob);
8347
+ const result = await run(payload, { ...taskCtx, signal: controller.signal });
8348
+ parentPort.postMessage({ type: 'done', result });
8348
8349
  } catch (err) {
8349
- if (job.attempts <= job.retries) {
8350
- const delay = Math.min(RETRY_BASE_DELAY_MS * 2 ** (job.attempts - 1), RETRY_MAX_DELAY_MS);
8351
- job.runAt = Date.now() + delay;
8352
- job.timer = setTimeout(() => {
8353
- job.timer = void 0;
8354
- pendingByTask.get(name)?.push(job.id);
8355
- pump();
8356
- }, delay);
8357
- }
8358
- void err;
8359
- } finally {
8360
- const running = (runningCount.get(name) ?? 1) - 1;
8361
- if (running <= 0) runningCount.delete(name);
8362
- else runningCount.set(name, running);
8363
- pump();
8350
+ parentPort.postMessage({
8351
+ type: 'error',
8352
+ message: err instanceof Error ? err.message : String(err),
8353
+ });
8364
8354
  }
8365
- }
8366
- return {
8367
- async enqueue(name, payload, opts) {
8368
- if (stopped) {
8369
- throw new Error("[faapi] Task queue is stopped and no longer accepts jobs");
8370
- }
8371
- const id = randomUUID();
8372
- const job = {
8373
- id,
8374
- payload,
8375
- retries: opts?.retries ?? 0,
8376
- runAt: opts?.delayMs ? Date.now() + opts.delayMs : Date.now(),
8377
- attempts: 0,
8378
- controller: new AbortController()
8379
- };
8380
- jobs.set(id, job);
8381
- if (job.runAt > Date.now()) {
8382
- job.timer = setTimeout(() => {
8383
- job.timer = void 0;
8384
- pendingByTask.get(name)?.push(id);
8385
- pump();
8386
- }, job.runAt - Date.now());
8355
+ });
8356
+ }
8357
+ `;
8358
+ }
8359
+ async function runTaskInWorker(options) {
8360
+ const { taskModulePath, payload, taskCtx, timeoutMs, externalSignal } = options;
8361
+ const killGraceMs = options.killGraceMs ?? KILL_GRACE_MS;
8362
+ const moduleUrl = pathToFileURL2(taskModulePath).href;
8363
+ const wrapperUrl = new URL(
8364
+ `data:text/javascript,${encodeURIComponent(buildWrapperSource(moduleUrl))}`
8365
+ );
8366
+ return new Promise((resolve3, reject) => {
8367
+ let worker;
8368
+ try {
8369
+ worker = new Worker(wrapperUrl);
8370
+ } catch (err) {
8371
+ reject(err);
8372
+ return;
8373
+ }
8374
+ let phase = "running";
8375
+ let cancelReason = "";
8376
+ let graceTimer;
8377
+ const finish = (settle) => {
8378
+ if (phase === "settled") return;
8379
+ phase = "settled";
8380
+ clearTimeout(timeoutTimer);
8381
+ if (graceTimer) clearTimeout(graceTimer);
8382
+ externalSignal?.removeEventListener("abort", onExternalAbort);
8383
+ void worker.terminate();
8384
+ settle();
8385
+ };
8386
+ const startCancel = (reason) => {
8387
+ if (phase !== "running") return;
8388
+ phase = "grace";
8389
+ cancelReason = reason;
8390
+ worker.postMessage({ type: "abort", reason });
8391
+ graceTimer = setTimeout(() => {
8392
+ finish(
8393
+ () => reject(new TaskCancelledError(`Task "${taskCtx.job.name}" ${reason} and was terminated`))
8394
+ );
8395
+ }, killGraceMs);
8396
+ };
8397
+ const onExternalAbort = () => startCancel("cancelled (driver stop timeout)");
8398
+ if (externalSignal) {
8399
+ if (externalSignal.aborted) {
8400
+ onExternalAbort();
8387
8401
  } else {
8388
- let q = pendingByTask.get(name);
8389
- if (!q) {
8390
- q = [];
8391
- pendingByTask.set(name, q);
8392
- }
8393
- q.push(id);
8394
- pump();
8402
+ externalSignal.addEventListener("abort", onExternalAbort, { once: true });
8395
8403
  }
8396
- return id;
8397
- },
8398
- startWorker(name, opts) {
8399
- workers.set(name, opts);
8400
- pump();
8401
- },
8402
- async stop(timeoutMs = 1e4) {
8403
- stopped = true;
8404
- for (const job of jobs.values()) {
8405
- if (job.timer) {
8406
- clearTimeout(job.timer);
8407
- job.timer = void 0;
8404
+ }
8405
+ const timeoutTimer = setTimeout(
8406
+ () => startCancel(`timed out after ${timeoutMs}ms`),
8407
+ timeoutMs
8408
+ );
8409
+ worker.on("message", (msg) => {
8410
+ if (phase === "grace") {
8411
+ if (msg?.type === "done") {
8412
+ finish(
8413
+ () => reject(
8414
+ new TaskCancelledError(
8415
+ `Task "${taskCtx.job.name}" ${cancelReason} (task completed after the timeout)`
8416
+ )
8417
+ )
8418
+ );
8419
+ } else if (msg?.type === "error") {
8420
+ finish(() => reject(new TaskCancelledError(msg.message ?? cancelReason)));
8408
8421
  }
8422
+ return;
8409
8423
  }
8410
- const start = Date.now();
8411
- while (Date.now() - start < timeoutMs) {
8412
- const total = Array.from(runningCount.values()).reduce((sum, n) => sum + n, 0);
8413
- if (total === 0) return;
8414
- await new Promise((r) => setTimeout(r, 10));
8424
+ if (phase !== "running") return;
8425
+ if (msg?.type === "done") {
8426
+ finish(() => resolve3(msg.result));
8427
+ } else if (msg?.type === "error") {
8428
+ finish(() => reject(new Error(msg.message ?? "task worker error")));
8415
8429
  }
8416
- for (const job of jobs.values()) {
8417
- job.controller.abort();
8430
+ });
8431
+ worker.on("error", (err) => {
8432
+ finish(() => reject(err));
8433
+ });
8434
+ worker.on("exit", (code) => {
8435
+ if (phase === "grace") {
8436
+ finish(() => reject(new TaskCancelledError(`Task "${taskCtx.job.name}" ${cancelReason}`)));
8437
+ } else if (phase === "running") {
8438
+ finish(() => reject(new Error(`Task worker exited unexpectedly (code ${code})`)));
8418
8439
  }
8419
- },
8420
- async stopWorkers() {
8421
- workers.clear();
8440
+ });
8441
+ try {
8442
+ worker.postMessage({
8443
+ type: "run",
8444
+ payload,
8445
+ taskCtx: { config: safeConfig(taskCtx.config), job: taskCtx.job }
8446
+ });
8447
+ } catch (err) {
8448
+ finish(
8449
+ () => reject(
8450
+ new Error(`Task "${taskCtx.job.name}" payload/config is not cloneable: ${String(err)}`)
8451
+ )
8452
+ );
8422
8453
  }
8423
- };
8454
+ });
8424
8455
  }
8425
- var RETRY_BASE_DELAY_MS, RETRY_MAX_DELAY_MS;
8426
- var init_memoryDriver = __esm({
8427
- "src/task/memoryDriver.ts"() {
8456
+ var KILL_GRACE_MS, TaskCancelledError;
8457
+ var init_taskWorker = __esm({
8458
+ "src/task/taskWorker.ts"() {
8428
8459
  "use strict";
8429
- RETRY_BASE_DELAY_MS = 500;
8430
- RETRY_MAX_DELAY_MS = 3e4;
8460
+ KILL_GRACE_MS = 5e3;
8461
+ TaskCancelledError = class extends Error {
8462
+ constructor(message) {
8463
+ super(message);
8464
+ this.name = "TaskCancelledError";
8465
+ }
8466
+ };
8431
8467
  }
8432
8468
  });
8433
8469
 
8434
8470
  // src/task/taskQueue.ts
8435
8471
  import path25 from "path";
8436
8472
  function createTaskQueue(deps) {
8437
- const { registry, rootDir } = deps;
8438
- const driver = deps.driver ?? createMemoryDriver();
8473
+ const { registry, rootDir, driver } = deps;
8439
8474
  const records = /* @__PURE__ */ new Map();
8440
8475
  const moduleCache2 = /* @__PURE__ */ new Map();
8441
8476
  const schemaCache = /* @__PURE__ */ new Map();
@@ -8496,26 +8531,54 @@ function createTaskQueue(deps) {
8496
8531
  record.error = void 0;
8497
8532
  records.set(job.id, record);
8498
8533
  try {
8499
- let mod = moduleCache2.get(job.name);
8500
- if (!mod) {
8501
- mod = await loadTaskModule(path25.resolve(rootDir, meta.filePath));
8502
- moduleCache2.set(job.name, mod);
8503
- }
8504
- if (typeof mod.run !== "function") {
8505
- throw new Error(`Task "${job.name}" module has no run export`);
8506
- }
8507
- const taskCtx = {
8508
- signal: job.signal,
8509
- config: deps.config,
8510
- job: { id: job.id, name: job.name, attempt: job.attempt }
8511
- };
8512
- const result = await mod.run(job.payload, taskCtx);
8534
+ let result;
8535
+ if (meta.timeoutMs !== void 0 && meta.timeoutMs > 0) {
8536
+ result = await (deps.runIsolated ?? runTaskInWorker)({
8537
+ taskModulePath: path25.resolve(rootDir, meta.filePath),
8538
+ payload: job.payload,
8539
+ taskCtx: {
8540
+ config: deps.config,
8541
+ job: { id: job.id, name: job.name, attempt: job.attempt }
8542
+ },
8543
+ timeoutMs: meta.timeoutMs,
8544
+ externalSignal: job.signal
8545
+ });
8546
+ } else {
8547
+ let mod = moduleCache2.get(job.name);
8548
+ if (!mod) {
8549
+ mod = await loadTaskModule(path25.resolve(rootDir, meta.filePath));
8550
+ moduleCache2.set(job.name, mod);
8551
+ }
8552
+ if (typeof mod.run !== "function") {
8553
+ throw new Error(`Task "${job.name}" module has no run export`);
8554
+ }
8555
+ const taskCtx = {
8556
+ signal: job.signal,
8557
+ config: deps.config,
8558
+ job: { id: job.id, name: job.name, attempt: job.attempt }
8559
+ };
8560
+ result = await mod.run(job.payload, taskCtx);
8561
+ }
8513
8562
  record.status = "done";
8514
8563
  record.result = result;
8515
8564
  return result;
8516
8565
  } catch (err) {
8517
- record.status = "failed";
8566
+ const cancelled = err instanceof TaskCancelledError || job.signal.aborted;
8567
+ record.status = cancelled ? "cancelled" : "failed";
8518
8568
  record.error = err instanceof Error ? err.message : String(err);
8569
+ if (deps.onFailed) {
8570
+ void Promise.resolve().then(
8571
+ () => deps.onFailed({
8572
+ task: job.name,
8573
+ jobId: job.id,
8574
+ attempt: job.attempt,
8575
+ willRetry: job.attempt <= (meta.retries ?? 0),
8576
+ cancelled,
8577
+ error: record.error
8578
+ })
8579
+ ).catch(() => {
8580
+ });
8581
+ }
8519
8582
  throw err;
8520
8583
  }
8521
8584
  }
@@ -8537,7 +8600,8 @@ function createTaskQueue(deps) {
8537
8600
  const meta = registry.get(name);
8538
8601
  const id = await driver.enqueue(name, data, {
8539
8602
  delayMs: opts?.delayMs,
8540
- retries: meta.retries ?? 0
8603
+ retries: meta.retries ?? 0,
8604
+ dedupId: opts?.dedupId
8541
8605
  });
8542
8606
  if (!records.has(id)) {
8543
8607
  records.set(id, {
@@ -8560,6 +8624,54 @@ function createTaskQueue(deps) {
8560
8624
  }
8561
8625
  return snapshot;
8562
8626
  },
8627
+ async listQueued(name) {
8628
+ if (!driver.list) {
8629
+ throw new Error(
8630
+ "[faapi] Task driver does not support listing queued tasks (TaskDriver.list is not implemented). Use the queue system's own management tools, or see driverTypes.md for per-driver capability."
8631
+ );
8632
+ }
8633
+ const queued = await driver.list({ name, limit: 50 });
8634
+ const merged = /* @__PURE__ */ new Map();
8635
+ for (const record of queued) {
8636
+ if (name !== void 0 && record.name !== name) continue;
8637
+ merged.set(record.id, {
8638
+ id: record.id,
8639
+ name: record.name,
8640
+ payload: record.payload,
8641
+ status: record.status,
8642
+ attempts: record.attempts,
8643
+ createdAt: record.createdAt,
8644
+ ...record.result !== void 0 ? { result: record.result } : {},
8645
+ ...record.error !== void 0 ? { error: record.error } : {},
8646
+ ...record.runAt !== void 0 ? { runAt: record.runAt } : {}
8647
+ });
8648
+ }
8649
+ for (const local of records.values()) {
8650
+ if (name !== void 0 && local.name !== name) continue;
8651
+ merged.set(local.id, { ...local });
8652
+ }
8653
+ return [...merged.values()];
8654
+ },
8655
+ async cancel(name, id) {
8656
+ if (!driver.cancel) {
8657
+ throw new Error(
8658
+ "[faapi] Task driver does not support cancelling tasks (TaskDriver.cancel is not implemented)."
8659
+ );
8660
+ }
8661
+ await driver.cancel(name, id);
8662
+ const record = records.get(id);
8663
+ if (record) record.status = "cancelled";
8664
+ },
8665
+ async retry(name, id) {
8666
+ if (!driver.retry) {
8667
+ throw new Error(
8668
+ "[faapi] Task driver does not support retrying tasks (TaskDriver.retry is not implemented)."
8669
+ );
8670
+ }
8671
+ await driver.retry(name, id);
8672
+ const record = records.get(id);
8673
+ if (record) record.status = "pending";
8674
+ },
8563
8675
  async start() {
8564
8676
  if (stopped) {
8565
8677
  throw new Error("[faapi] Task queue is stopped and cannot be restarted");
@@ -8589,21 +8701,28 @@ var init_taskQueue = __esm({
8589
8701
  "src/task/taskQueue.ts"() {
8590
8702
  "use strict";
8591
8703
  init_httpErrors();
8592
- init_memoryDriver();
8704
+ init_taskWorker();
8593
8705
  }
8594
8706
  });
8595
8707
 
8596
8708
  // src/task/loadTaskDriver.ts
8597
8709
  async function loadTaskDriver(driver, driverOptions) {
8598
- if (driver === void 0 || driver === "memory") {
8599
- return createMemoryDriver();
8710
+ if (driver === void 0) {
8711
+ throw new Error(
8712
+ "[faapi] config.task.driver is required when tasks are defined. Install @faapi/task-pgboss or @faapi/task-bullmq and set `task.driver` to 'pgboss' or 'bullmq' (or pass a TaskDriver instance)."
8713
+ );
8714
+ }
8715
+ if (driver === "memory") {
8716
+ throw new Error(
8717
+ "[faapi] The built-in memory task driver has been removed. Migrate to a persistent driver: install @faapi/task-pgboss or @faapi/task-bullmq and set `task.driver` to 'pgboss' or 'bullmq'."
8718
+ );
8600
8719
  }
8601
8720
  if (typeof driver === "object") {
8602
8721
  return driver;
8603
8722
  }
8604
8723
  if (driver !== "pgboss" && driver !== "bullmq") {
8605
8724
  throw new Error(
8606
- `[faapi] Unknown task driver "${driver}". Supported: 'memory' (default), 'pgboss', 'bullmq', or a TaskDriver instance.`
8725
+ `[faapi] Unknown task driver "${driver}". Supported: 'pgboss', 'bullmq', or a TaskDriver instance.`
8607
8726
  );
8608
8727
  }
8609
8728
  const specifier = `@faapi/task-${driver}`;
@@ -8630,7 +8749,30 @@ async function loadTaskDriver(driver, driverOptions) {
8630
8749
  var init_loadTaskDriver = __esm({
8631
8750
  "src/task/loadTaskDriver.ts"() {
8632
8751
  "use strict";
8633
- init_memoryDriver();
8752
+ }
8753
+ });
8754
+
8755
+ // src/task/idleTaskDriver.ts
8756
+ function createIdleTaskDriver() {
8757
+ return {
8758
+ enqueue() {
8759
+ return Promise.reject(
8760
+ new Error(
8761
+ "[faapi] New task(s) were registered without a queue driver. Set config.task.driver to 'pgboss' or 'bullmq' (install the matching @faapi/task-* package) and restart the server."
8762
+ )
8763
+ );
8764
+ },
8765
+ startWorker() {
8766
+ },
8767
+ async stop() {
8768
+ },
8769
+ async stopWorkers() {
8770
+ }
8771
+ };
8772
+ }
8773
+ var init_idleTaskDriver = __esm({
8774
+ "src/task/idleTaskDriver.ts"() {
8775
+ "use strict";
8634
8776
  }
8635
8777
  });
8636
8778
 
@@ -8643,13 +8785,17 @@ function createCronScheduler(registry, enqueue) {
8643
8785
  this.stop();
8644
8786
  for (const task of registry.list()) {
8645
8787
  if (!task.cron) continue;
8646
- schedules.push(
8647
- new Cron(task.cron, () => {
8648
- void Promise.resolve().then(() => enqueue(task.name)).catch((err) => {
8649
- console.error(`[faapi] Cron enqueue failed for task "${task.name}":`, err);
8650
- });
8651
- })
8652
- );
8788
+ const schedule = new Cron(task.cron, () => {
8789
+ const runAt = schedule.currentRun();
8790
+ void Promise.resolve().then(
8791
+ () => enqueue(task.name, {
8792
+ dedupId: runAt ? `cron:${task.name}:${runAt.toISOString()}` : void 0
8793
+ })
8794
+ ).catch((err) => {
8795
+ console.error(`[faapi] Cron enqueue failed for task "${task.name}":`, err);
8796
+ });
8797
+ });
8798
+ schedules.push(schedule);
8653
8799
  }
8654
8800
  },
8655
8801
  stop() {
@@ -8669,7 +8815,7 @@ var init_cronScheduler = __esm({
8669
8815
  // src/cli/loadPlugins.ts
8670
8816
  import path26 from "path";
8671
8817
  import fs18 from "fs";
8672
- import { pathToFileURL as pathToFileURL2 } from "url";
8818
+ import { pathToFileURL as pathToFileURL3 } from "url";
8673
8819
  async function loadPlugins(declarations, ctx, rootDir, dist) {
8674
8820
  const handlerWrappers = [];
8675
8821
  const upgradeWrappers = [];
@@ -8762,7 +8908,7 @@ async function importPluginModule(specifier, baseDir, dist) {
8762
8908
  const sourcePath = resolveLocalPluginSource(specifier, baseDir);
8763
8909
  const productPath = sourcePath && distDir ? mirrorProductPath(sourcePath, baseDir, distDir) : null;
8764
8910
  if (productPath !== null && fs18.existsSync(productPath) && (sourcePath === null || !isSourceNewer(sourcePath, productPath))) {
8765
- return import(pathToFileURL2(productPath).href);
8911
+ return import(pathToFileURL3(productPath).href);
8766
8912
  }
8767
8913
  if (sourcePath && TS_EXTS.test(sourcePath)) {
8768
8914
  if (!isDevOnDemandEnabled() || !distDir) {
@@ -8777,11 +8923,11 @@ async function importPluginModule(specifier, baseDir, dist) {
8777
8923
  await compileProjectModules([sourcePath], baseDir, dist);
8778
8924
  }
8779
8925
  if (productPath && fs18.existsSync(productPath)) {
8780
- return import(pathToFileURL2(productPath).href);
8926
+ return import(pathToFileURL3(productPath).href);
8781
8927
  }
8782
8928
  }
8783
8929
  if (sourcePath && JS_EXTS.test(sourcePath)) {
8784
- return import(pathToFileURL2(sourcePath).href);
8930
+ return import(pathToFileURL3(sourcePath).href);
8785
8931
  }
8786
8932
  const candidates = [
8787
8933
  `${specifier}.ts`,
@@ -8848,10 +8994,8 @@ async function loadAndHydrateAgents(rootDir, dist, registries = defaultRegistrie
8848
8994
  }
8849
8995
  function getTaskDriverOptions(config) {
8850
8996
  if (!config?.task) return void 0;
8851
- const taskConfig = config.task;
8852
- const driver = taskConfig.driver;
8853
- if (driver === "pgboss") return taskConfig.pgboss;
8854
- if (driver === "bullmq") return taskConfig.bullmq;
8997
+ if (config.task.driver === "pgboss") return config.task.pgboss;
8998
+ if (config.task.driver === "bullmq") return config.task.bullmq;
8855
8999
  return void 0;
8856
9000
  }
8857
9001
  async function loadAndHydrateTasks(rootDir, dist, registries = defaultRegistries) {
@@ -8920,16 +9064,17 @@ async function createAppBase(options) {
8920
9064
  const tools = await loadAndHydrateTools(rootDir, dist, registries);
8921
9065
  const agents = await loadAndHydrateAgents(rootDir, dist, registries);
8922
9066
  const taskMetas = await loadAndHydrateTasks(rootDir, dist, registries);
8923
- const taskDriver = await loadTaskDriver(config?.task?.driver, getTaskDriverOptions(config));
9067
+ const taskDriver = taskMetas.length ? await loadTaskDriver(config?.task?.driver, getTaskDriverOptions(config)) : createIdleTaskDriver();
8924
9068
  const taskQueue = createTaskQueue({
8925
9069
  registry: registries.task,
8926
9070
  rootDir,
8927
9071
  config,
8928
- driver: taskDriver
9072
+ driver: taskDriver,
9073
+ onFailed: config?.task?.onFailed
8929
9074
  });
8930
9075
  const cronScheduler = createCronScheduler(
8931
9076
  registries.task,
8932
- (name) => taskQueue.enqueue(name)
9077
+ (name, opts) => taskQueue.enqueue(name, void 0, opts)
8933
9078
  );
8934
9079
  const taskEnabled = process.env.FAAPI_TASKS_DISABLED === "1" ? false : config?.task?.enabled ?? true;
8935
9080
  const stopTaskRuntime = async () => {
@@ -8937,7 +9082,7 @@ async function createAppBase(options) {
8937
9082
  await taskQueue.stop(config?.task?.shutdownTimeoutMs ?? 1e4);
8938
9083
  };
8939
9084
  if (taskEnabled) {
8940
- taskQueue.start();
9085
+ await taskQueue.start();
8941
9086
  cronScheduler.start();
8942
9087
  }
8943
9088
  registries.taskHandle.register(() => taskQueue);
@@ -9235,6 +9380,7 @@ var init_createAppCore = __esm({
9235
9380
  init_generateTaskArtifacts();
9236
9381
  init_taskQueue();
9237
9382
  init_loadTaskDriver();
9383
+ init_idleTaskDriver();
9238
9384
  init_cronScheduler();
9239
9385
  init_loadPlugins();
9240
9386
  init_importWithCacheBust();