@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/index.js CHANGED
@@ -1941,131 +1941,161 @@ var PayloadTooLargeError = class extends FaapiError {
1941
1941
  }
1942
1942
  };
1943
1943
 
1944
- // src/task/memoryDriver.ts
1945
- import { randomUUID } from "crypto";
1946
- var RETRY_BASE_DELAY_MS = 500;
1947
- var RETRY_MAX_DELAY_MS = 3e4;
1948
- function createMemoryDriver() {
1949
- const jobs = /* @__PURE__ */ new Map();
1950
- const pendingByTask = /* @__PURE__ */ new Map();
1951
- const runningCount = /* @__PURE__ */ new Map();
1952
- const workers = /* @__PURE__ */ new Map();
1953
- let stopped = false;
1954
- function pump() {
1955
- if (stopped) return;
1956
- for (const [name, worker] of workers) {
1957
- const queue = pendingByTask.get(name);
1958
- if (!queue || queue.length === 0) continue;
1959
- let running = runningCount.get(name) ?? 0;
1960
- while (running < worker.concurrency && queue.length > 0) {
1961
- const head = jobs.get(queue[0]);
1962
- if (!head) {
1963
- queue.shift();
1964
- continue;
1965
- }
1966
- if (head.runAt > Date.now()) break;
1967
- queue.shift();
1968
- running += 1;
1969
- runningCount.set(name, running);
1970
- void dispatch(name, worker, head);
1971
- }
1972
- if (running === 0) runningCount.delete(name);
1973
- else runningCount.set(name, running);
1974
- }
1944
+ // src/task/taskWorker.ts
1945
+ import { Worker } from "worker_threads";
1946
+ import { pathToFileURL } from "url";
1947
+ var KILL_GRACE_MS = 5e3;
1948
+ var TaskCancelledError = class extends Error {
1949
+ constructor(message) {
1950
+ super(message);
1951
+ this.name = "TaskCancelledError";
1975
1952
  }
1976
- async function dispatch(name, worker, job) {
1977
- job.attempts += 1;
1978
- const driverJob = {
1979
- id: job.id,
1980
- name,
1981
- payload: job.payload,
1982
- attempt: job.attempts,
1983
- signal: job.controller.signal
1984
- };
1953
+ };
1954
+ function safeConfig(config) {
1955
+ if (config === void 0 || config === null) return config;
1956
+ try {
1957
+ structuredClone(config);
1958
+ return config;
1959
+ } catch {
1960
+ }
1961
+ try {
1962
+ return JSON.parse(JSON.stringify(config));
1963
+ } catch {
1964
+ return void 0;
1965
+ }
1966
+ }
1967
+ function buildWrapperSource(moduleUrl) {
1968
+ return `
1969
+ import { parentPort } from 'node:worker_threads';
1970
+ import * as mod from '${moduleUrl}';
1971
+
1972
+ const run = mod.run;
1973
+ if (typeof run !== 'function') {
1974
+ parentPort.postMessage({ type: 'error', message: 'Task module has no run export' });
1975
+ } else {
1976
+ let controller = null;
1977
+ parentPort.on('message', async (msg) => {
1978
+ if (msg?.type === 'abort') {
1979
+ controller?.abort(new Error(msg.reason));
1980
+ return;
1981
+ }
1982
+ if (msg?.type !== 'run') return;
1983
+ controller = new AbortController();
1984
+ const { payload, taskCtx } = msg;
1985
1985
  try {
1986
- await worker.process(driverJob);
1986
+ const result = await run(payload, { ...taskCtx, signal: controller.signal });
1987
+ parentPort.postMessage({ type: 'done', result });
1987
1988
  } catch (err) {
1988
- if (job.attempts <= job.retries) {
1989
- const delay = Math.min(RETRY_BASE_DELAY_MS * 2 ** (job.attempts - 1), RETRY_MAX_DELAY_MS);
1990
- job.runAt = Date.now() + delay;
1991
- job.timer = setTimeout(() => {
1992
- job.timer = void 0;
1993
- pendingByTask.get(name)?.push(job.id);
1994
- pump();
1995
- }, delay);
1996
- }
1997
- void err;
1998
- } finally {
1999
- const running = (runningCount.get(name) ?? 1) - 1;
2000
- if (running <= 0) runningCount.delete(name);
2001
- else runningCount.set(name, running);
2002
- pump();
1989
+ parentPort.postMessage({
1990
+ type: 'error',
1991
+ message: err instanceof Error ? err.message : String(err),
1992
+ });
2003
1993
  }
2004
- }
2005
- return {
2006
- async enqueue(name, payload, opts) {
2007
- if (stopped) {
2008
- throw new Error("[faapi] Task queue is stopped and no longer accepts jobs");
2009
- }
2010
- const id = randomUUID();
2011
- const job = {
2012
- id,
2013
- payload,
2014
- retries: opts?.retries ?? 0,
2015
- runAt: opts?.delayMs ? Date.now() + opts.delayMs : Date.now(),
2016
- attempts: 0,
2017
- controller: new AbortController()
2018
- };
2019
- jobs.set(id, job);
2020
- if (job.runAt > Date.now()) {
2021
- job.timer = setTimeout(() => {
2022
- job.timer = void 0;
2023
- pendingByTask.get(name)?.push(id);
2024
- pump();
2025
- }, job.runAt - Date.now());
1994
+ });
1995
+ }
1996
+ `;
1997
+ }
1998
+ async function runTaskInWorker(options) {
1999
+ const { taskModulePath, payload, taskCtx, timeoutMs, externalSignal } = options;
2000
+ const killGraceMs = options.killGraceMs ?? KILL_GRACE_MS;
2001
+ const moduleUrl = pathToFileURL(taskModulePath).href;
2002
+ const wrapperUrl = new URL(
2003
+ `data:text/javascript,${encodeURIComponent(buildWrapperSource(moduleUrl))}`
2004
+ );
2005
+ return new Promise((resolve, reject) => {
2006
+ let worker;
2007
+ try {
2008
+ worker = new Worker(wrapperUrl);
2009
+ } catch (err) {
2010
+ reject(err);
2011
+ return;
2012
+ }
2013
+ let phase = "running";
2014
+ let cancelReason = "";
2015
+ let graceTimer;
2016
+ const finish = (settle) => {
2017
+ if (phase === "settled") return;
2018
+ phase = "settled";
2019
+ clearTimeout(timeoutTimer);
2020
+ if (graceTimer) clearTimeout(graceTimer);
2021
+ externalSignal?.removeEventListener("abort", onExternalAbort);
2022
+ void worker.terminate();
2023
+ settle();
2024
+ };
2025
+ const startCancel = (reason) => {
2026
+ if (phase !== "running") return;
2027
+ phase = "grace";
2028
+ cancelReason = reason;
2029
+ worker.postMessage({ type: "abort", reason });
2030
+ graceTimer = setTimeout(() => {
2031
+ finish(
2032
+ () => reject(new TaskCancelledError(`Task "${taskCtx.job.name}" ${reason} and was terminated`))
2033
+ );
2034
+ }, killGraceMs);
2035
+ };
2036
+ const onExternalAbort = () => startCancel("cancelled (driver stop timeout)");
2037
+ if (externalSignal) {
2038
+ if (externalSignal.aborted) {
2039
+ onExternalAbort();
2026
2040
  } else {
2027
- let q = pendingByTask.get(name);
2028
- if (!q) {
2029
- q = [];
2030
- pendingByTask.set(name, q);
2031
- }
2032
- q.push(id);
2033
- pump();
2041
+ externalSignal.addEventListener("abort", onExternalAbort, { once: true });
2034
2042
  }
2035
- return id;
2036
- },
2037
- startWorker(name, opts) {
2038
- workers.set(name, opts);
2039
- pump();
2040
- },
2041
- async stop(timeoutMs = 1e4) {
2042
- stopped = true;
2043
- for (const job of jobs.values()) {
2044
- if (job.timer) {
2045
- clearTimeout(job.timer);
2046
- job.timer = void 0;
2043
+ }
2044
+ const timeoutTimer = setTimeout(
2045
+ () => startCancel(`timed out after ${timeoutMs}ms`),
2046
+ timeoutMs
2047
+ );
2048
+ worker.on("message", (msg) => {
2049
+ if (phase === "grace") {
2050
+ if (msg?.type === "done") {
2051
+ finish(
2052
+ () => reject(
2053
+ new TaskCancelledError(
2054
+ `Task "${taskCtx.job.name}" ${cancelReason} (task completed after the timeout)`
2055
+ )
2056
+ )
2057
+ );
2058
+ } else if (msg?.type === "error") {
2059
+ finish(() => reject(new TaskCancelledError(msg.message ?? cancelReason)));
2047
2060
  }
2061
+ return;
2048
2062
  }
2049
- const start = Date.now();
2050
- while (Date.now() - start < timeoutMs) {
2051
- const total = Array.from(runningCount.values()).reduce((sum, n) => sum + n, 0);
2052
- if (total === 0) return;
2053
- await new Promise((r) => setTimeout(r, 10));
2063
+ if (phase !== "running") return;
2064
+ if (msg?.type === "done") {
2065
+ finish(() => resolve(msg.result));
2066
+ } else if (msg?.type === "error") {
2067
+ finish(() => reject(new Error(msg.message ?? "task worker error")));
2054
2068
  }
2055
- for (const job of jobs.values()) {
2056
- job.controller.abort();
2069
+ });
2070
+ worker.on("error", (err) => {
2071
+ finish(() => reject(err));
2072
+ });
2073
+ worker.on("exit", (code) => {
2074
+ if (phase === "grace") {
2075
+ finish(() => reject(new TaskCancelledError(`Task "${taskCtx.job.name}" ${cancelReason}`)));
2076
+ } else if (phase === "running") {
2077
+ finish(() => reject(new Error(`Task worker exited unexpectedly (code ${code})`)));
2057
2078
  }
2058
- },
2059
- async stopWorkers() {
2060
- workers.clear();
2079
+ });
2080
+ try {
2081
+ worker.postMessage({
2082
+ type: "run",
2083
+ payload,
2084
+ taskCtx: { config: safeConfig(taskCtx.config), job: taskCtx.job }
2085
+ });
2086
+ } catch (err) {
2087
+ finish(
2088
+ () => reject(
2089
+ new Error(`Task "${taskCtx.job.name}" payload/config is not cloneable: ${String(err)}`)
2090
+ )
2091
+ );
2061
2092
  }
2062
- };
2093
+ });
2063
2094
  }
2064
2095
 
2065
2096
  // src/task/taskQueue.ts
2066
2097
  function createTaskQueue(deps) {
2067
- const { registry, rootDir } = deps;
2068
- const driver = deps.driver ?? createMemoryDriver();
2098
+ const { registry, rootDir, driver } = deps;
2069
2099
  const records = /* @__PURE__ */ new Map();
2070
2100
  const moduleCache2 = /* @__PURE__ */ new Map();
2071
2101
  const schemaCache = /* @__PURE__ */ new Map();
@@ -2126,26 +2156,54 @@ function createTaskQueue(deps) {
2126
2156
  record.error = void 0;
2127
2157
  records.set(job.id, record);
2128
2158
  try {
2129
- let mod = moduleCache2.get(job.name);
2130
- if (!mod) {
2131
- mod = await loadTaskModule(path3.resolve(rootDir, meta.filePath));
2132
- moduleCache2.set(job.name, mod);
2133
- }
2134
- if (typeof mod.run !== "function") {
2135
- throw new Error(`Task "${job.name}" module has no run export`);
2159
+ let result;
2160
+ if (meta.timeoutMs !== void 0 && meta.timeoutMs > 0) {
2161
+ result = await (deps.runIsolated ?? runTaskInWorker)({
2162
+ taskModulePath: path3.resolve(rootDir, meta.filePath),
2163
+ payload: job.payload,
2164
+ taskCtx: {
2165
+ config: deps.config,
2166
+ job: { id: job.id, name: job.name, attempt: job.attempt }
2167
+ },
2168
+ timeoutMs: meta.timeoutMs,
2169
+ externalSignal: job.signal
2170
+ });
2171
+ } else {
2172
+ let mod = moduleCache2.get(job.name);
2173
+ if (!mod) {
2174
+ mod = await loadTaskModule(path3.resolve(rootDir, meta.filePath));
2175
+ moduleCache2.set(job.name, mod);
2176
+ }
2177
+ if (typeof mod.run !== "function") {
2178
+ throw new Error(`Task "${job.name}" module has no run export`);
2179
+ }
2180
+ const taskCtx = {
2181
+ signal: job.signal,
2182
+ config: deps.config,
2183
+ job: { id: job.id, name: job.name, attempt: job.attempt }
2184
+ };
2185
+ result = await mod.run(job.payload, taskCtx);
2136
2186
  }
2137
- const taskCtx = {
2138
- signal: job.signal,
2139
- config: deps.config,
2140
- job: { id: job.id, name: job.name, attempt: job.attempt }
2141
- };
2142
- const result = await mod.run(job.payload, taskCtx);
2143
2187
  record.status = "done";
2144
2188
  record.result = result;
2145
2189
  return result;
2146
2190
  } catch (err) {
2147
- record.status = "failed";
2191
+ const cancelled = err instanceof TaskCancelledError || job.signal.aborted;
2192
+ record.status = cancelled ? "cancelled" : "failed";
2148
2193
  record.error = err instanceof Error ? err.message : String(err);
2194
+ if (deps.onFailed) {
2195
+ void Promise.resolve().then(
2196
+ () => deps.onFailed({
2197
+ task: job.name,
2198
+ jobId: job.id,
2199
+ attempt: job.attempt,
2200
+ willRetry: job.attempt <= (meta.retries ?? 0),
2201
+ cancelled,
2202
+ error: record.error
2203
+ })
2204
+ ).catch(() => {
2205
+ });
2206
+ }
2149
2207
  throw err;
2150
2208
  }
2151
2209
  }
@@ -2167,7 +2225,8 @@ function createTaskQueue(deps) {
2167
2225
  const meta = registry.get(name);
2168
2226
  const id = await driver.enqueue(name, data, {
2169
2227
  delayMs: opts?.delayMs,
2170
- retries: meta.retries ?? 0
2228
+ retries: meta.retries ?? 0,
2229
+ dedupId: opts?.dedupId
2171
2230
  });
2172
2231
  if (!records.has(id)) {
2173
2232
  records.set(id, {
@@ -2190,6 +2249,54 @@ function createTaskQueue(deps) {
2190
2249
  }
2191
2250
  return snapshot;
2192
2251
  },
2252
+ async listQueued(name) {
2253
+ if (!driver.list) {
2254
+ throw new Error(
2255
+ "[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."
2256
+ );
2257
+ }
2258
+ const queued = await driver.list({ name, limit: 50 });
2259
+ const merged = /* @__PURE__ */ new Map();
2260
+ for (const record of queued) {
2261
+ if (name !== void 0 && record.name !== name) continue;
2262
+ merged.set(record.id, {
2263
+ id: record.id,
2264
+ name: record.name,
2265
+ payload: record.payload,
2266
+ status: record.status,
2267
+ attempts: record.attempts,
2268
+ createdAt: record.createdAt,
2269
+ ...record.result !== void 0 ? { result: record.result } : {},
2270
+ ...record.error !== void 0 ? { error: record.error } : {},
2271
+ ...record.runAt !== void 0 ? { runAt: record.runAt } : {}
2272
+ });
2273
+ }
2274
+ for (const local of records.values()) {
2275
+ if (name !== void 0 && local.name !== name) continue;
2276
+ merged.set(local.id, { ...local });
2277
+ }
2278
+ return [...merged.values()];
2279
+ },
2280
+ async cancel(name, id) {
2281
+ if (!driver.cancel) {
2282
+ throw new Error(
2283
+ "[faapi] Task driver does not support cancelling tasks (TaskDriver.cancel is not implemented)."
2284
+ );
2285
+ }
2286
+ await driver.cancel(name, id);
2287
+ const record = records.get(id);
2288
+ if (record) record.status = "cancelled";
2289
+ },
2290
+ async retry(name, id) {
2291
+ if (!driver.retry) {
2292
+ throw new Error(
2293
+ "[faapi] Task driver does not support retrying tasks (TaskDriver.retry is not implemented)."
2294
+ );
2295
+ }
2296
+ await driver.retry(name, id);
2297
+ const record = records.get(id);
2298
+ if (record) record.status = "pending";
2299
+ },
2193
2300
  async start() {
2194
2301
  if (stopped) {
2195
2302
  throw new Error("[faapi] Task queue is stopped and cannot be restarted");
@@ -2218,15 +2325,22 @@ function createTaskQueue(deps) {
2218
2325
 
2219
2326
  // src/task/loadTaskDriver.ts
2220
2327
  async function loadTaskDriver(driver, driverOptions) {
2221
- if (driver === void 0 || driver === "memory") {
2222
- return createMemoryDriver();
2328
+ if (driver === void 0) {
2329
+ throw new Error(
2330
+ "[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)."
2331
+ );
2332
+ }
2333
+ if (driver === "memory") {
2334
+ throw new Error(
2335
+ "[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'."
2336
+ );
2223
2337
  }
2224
2338
  if (typeof driver === "object") {
2225
2339
  return driver;
2226
2340
  }
2227
2341
  if (driver !== "pgboss" && driver !== "bullmq") {
2228
2342
  throw new Error(
2229
- `[faapi] Unknown task driver "${driver}". Supported: 'memory' (default), 'pgboss', 'bullmq', or a TaskDriver instance.`
2343
+ `[faapi] Unknown task driver "${driver}". Supported: 'pgboss', 'bullmq', or a TaskDriver instance.`
2230
2344
  );
2231
2345
  }
2232
2346
  const specifier = `@faapi/task-${driver}`;
@@ -2260,13 +2374,17 @@ function createCronScheduler(registry, enqueue) {
2260
2374
  this.stop();
2261
2375
  for (const task of registry.list()) {
2262
2376
  if (!task.cron) continue;
2263
- schedules.push(
2264
- new Cron(task.cron, () => {
2265
- void Promise.resolve().then(() => enqueue(task.name)).catch((err) => {
2266
- console.error(`[faapi] Cron enqueue failed for task "${task.name}":`, err);
2267
- });
2268
- })
2269
- );
2377
+ const schedule = new Cron(task.cron, () => {
2378
+ const runAt = schedule.currentRun();
2379
+ void Promise.resolve().then(
2380
+ () => enqueue(task.name, {
2381
+ dedupId: runAt ? `cron:${task.name}:${runAt.toISOString()}` : void 0
2382
+ })
2383
+ ).catch((err) => {
2384
+ console.error(`[faapi] Cron enqueue failed for task "${task.name}":`, err);
2385
+ });
2386
+ });
2387
+ schedules.push(schedule);
2270
2388
  }
2271
2389
  },
2272
2390
  stop() {
@@ -2287,6 +2405,7 @@ var TASK_FILENAME = "task.ts";
2287
2405
  var CRON_RE = /(?:^|\n)\s*cron:\s*['"`]([^'"`\n]+)['"`]/;
2288
2406
  var CONCURRENCY_RE = /(?:^|\n)\s*concurrency:\s*(\d+)/;
2289
2407
  var RETRIES_RE = /(?:^|\n)\s*retries:\s*(\d+)/;
2408
+ var TIMEOUT_MS_RE = /(?:^|\n)\s*timeoutMs:\s*(\d+)/;
2290
2409
  function filePathToTaskName(filePath) {
2291
2410
  const normalized = filePath.replace(/\\/g, "/");
2292
2411
  const match = normalized.match(/(?:^|\/)tasks\/(.+)\/task\.ts$/);
@@ -2323,9 +2442,11 @@ async function scanTasks(rootDir, patterns) {
2323
2442
  const cron = CRON_RE.exec(source)?.[1];
2324
2443
  const concurrency = CONCURRENCY_RE.exec(source)?.[1];
2325
2444
  const retries = RETRIES_RE.exec(source)?.[1];
2445
+ const timeoutMs = TIMEOUT_MS_RE.exec(source)?.[1];
2326
2446
  if (cron !== void 0) manifest.cron = cron;
2327
2447
  if (concurrency !== void 0) manifest.concurrency = Number(concurrency);
2328
2448
  if (retries !== void 0) manifest.retries = Number(retries);
2449
+ if (timeoutMs !== void 0) manifest.timeoutMs = Number(timeoutMs);
2329
2450
  tasks.push(manifest);
2330
2451
  }
2331
2452
  return tasks;
@@ -2389,7 +2510,7 @@ function resolveExport(module, exportName) {
2389
2510
  }
2390
2511
 
2391
2512
  // src/utils/importWithCacheBust.ts
2392
- import { pathToFileURL } from "url";
2513
+ import { pathToFileURL as pathToFileURL2 } from "url";
2393
2514
  var loadTs;
2394
2515
  function setLoadTimestamp(ts11) {
2395
2516
  loadTs = ts11;
@@ -2403,13 +2524,13 @@ async function importWithCacheBust(filePath, bustViteCache = false) {
2403
2524
  const importActual = getVitestImportActual();
2404
2525
  if (importActual) {
2405
2526
  if (bustViteCache) {
2406
- let url2 = pathToFileURL(filePath).href;
2527
+ let url2 = pathToFileURL2(filePath).href;
2407
2528
  url2 += `?t=${Date.now()}`;
2408
2529
  return await import(url2);
2409
2530
  }
2410
2531
  return await importActual(filePath);
2411
2532
  }
2412
- let url = pathToFileURL(filePath).href;
2533
+ let url = pathToFileURL2(filePath).href;
2413
2534
  if (loadTs !== void 0) {
2414
2535
  url += `?t=${loadTs}`;
2415
2536
  }
@@ -5891,7 +6012,8 @@ function serializeTasks(tasks, dist = "dist") {
5891
6012
  filePath: toProdFilePath(t.filePath, dist),
5892
6013
  ...t.cron !== void 0 ? { cron: t.cron } : {},
5893
6014
  ...t.concurrency !== void 0 ? { concurrency: t.concurrency } : {},
5894
- ...t.retries !== void 0 ? { retries: t.retries } : {}
6015
+ ...t.retries !== void 0 ? { retries: t.retries } : {},
6016
+ ...t.timeoutMs !== void 0 ? { timeoutMs: t.timeoutMs } : {}
5895
6017
  }));
5896
6018
  }
5897
6019
  async function writeTasksModule(manifest, outputPath) {
@@ -5906,7 +6028,8 @@ function hydrateTasks(manifest) {
5906
6028
  filePath: t.filePath,
5907
6029
  cron: t.cron ?? void 0,
5908
6030
  concurrency: t.concurrency ?? void 0,
5909
- retries: t.retries ?? void 0
6031
+ retries: t.retries ?? void 0,
6032
+ timeoutMs: t.timeoutMs ?? void 0
5910
6033
  }));
5911
6034
  }
5912
6035
  function collectTaskSchemaSources(tasks, rootDir) {
@@ -6034,10 +6157,29 @@ async function generateTaskArtifacts(manifests, rootDir, dist) {
6034
6157
  return hydrateTasks(serialized);
6035
6158
  }
6036
6159
 
6160
+ // src/task/idleTaskDriver.ts
6161
+ function createIdleTaskDriver() {
6162
+ return {
6163
+ enqueue() {
6164
+ return Promise.reject(
6165
+ new Error(
6166
+ "[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."
6167
+ )
6168
+ );
6169
+ },
6170
+ startWorker() {
6171
+ },
6172
+ async stop() {
6173
+ },
6174
+ async stopWorkers() {
6175
+ }
6176
+ };
6177
+ }
6178
+
6037
6179
  // src/cli/loadPlugins.ts
6038
6180
  import path22 from "path";
6039
6181
  import fs17 from "fs";
6040
- import { pathToFileURL as pathToFileURL2 } from "url";
6182
+ import { pathToFileURL as pathToFileURL3 } from "url";
6041
6183
 
6042
6184
  // src/cli/compileConfig.ts
6043
6185
  import path21 from "path";
@@ -6172,7 +6314,7 @@ async function importPluginModule(specifier, baseDir, dist) {
6172
6314
  const sourcePath = resolveLocalPluginSource(specifier, baseDir);
6173
6315
  const productPath = sourcePath && distDir ? mirrorProductPath(sourcePath, baseDir, distDir) : null;
6174
6316
  if (productPath !== null && fs17.existsSync(productPath) && (sourcePath === null || !isSourceNewer(sourcePath, productPath))) {
6175
- return import(pathToFileURL2(productPath).href);
6317
+ return import(pathToFileURL3(productPath).href);
6176
6318
  }
6177
6319
  if (sourcePath && TS_EXTS.test(sourcePath)) {
6178
6320
  if (!isDevOnDemandEnabled() || !distDir) {
@@ -6187,11 +6329,11 @@ async function importPluginModule(specifier, baseDir, dist) {
6187
6329
  await compileProjectModules([sourcePath], baseDir, dist);
6188
6330
  }
6189
6331
  if (productPath && fs17.existsSync(productPath)) {
6190
- return import(pathToFileURL2(productPath).href);
6332
+ return import(pathToFileURL3(productPath).href);
6191
6333
  }
6192
6334
  }
6193
6335
  if (sourcePath && JS_EXTS.test(sourcePath)) {
6194
- return import(pathToFileURL2(sourcePath).href);
6336
+ return import(pathToFileURL3(sourcePath).href);
6195
6337
  }
6196
6338
  const candidates = [
6197
6339
  `${specifier}.ts`,
@@ -6250,10 +6392,8 @@ async function loadAndHydrateAgents(rootDir, dist, registries = defaultRegistrie
6250
6392
  }
6251
6393
  function getTaskDriverOptions(config) {
6252
6394
  if (!config?.task) return void 0;
6253
- const taskConfig = config.task;
6254
- const driver = taskConfig.driver;
6255
- if (driver === "pgboss") return taskConfig.pgboss;
6256
- if (driver === "bullmq") return taskConfig.bullmq;
6395
+ if (config.task.driver === "pgboss") return config.task.pgboss;
6396
+ if (config.task.driver === "bullmq") return config.task.bullmq;
6257
6397
  return void 0;
6258
6398
  }
6259
6399
  async function loadAndHydrateTasks(rootDir, dist, registries = defaultRegistries) {
@@ -6350,16 +6490,17 @@ async function createAppBase(options) {
6350
6490
  const tools = await loadAndHydrateTools(rootDir, dist, registries);
6351
6491
  const agents = await loadAndHydrateAgents(rootDir, dist, registries);
6352
6492
  const taskMetas = await loadAndHydrateTasks(rootDir, dist, registries);
6353
- const taskDriver = await loadTaskDriver(config?.task?.driver, getTaskDriverOptions(config));
6493
+ const taskDriver = taskMetas.length ? await loadTaskDriver(config?.task?.driver, getTaskDriverOptions(config)) : createIdleTaskDriver();
6354
6494
  const taskQueue = createTaskQueue({
6355
6495
  registry: registries.task,
6356
6496
  rootDir,
6357
6497
  config,
6358
- driver: taskDriver
6498
+ driver: taskDriver,
6499
+ onFailed: config?.task?.onFailed
6359
6500
  });
6360
6501
  const cronScheduler = createCronScheduler(
6361
6502
  registries.task,
6362
- (name) => taskQueue.enqueue(name)
6503
+ (name, opts) => taskQueue.enqueue(name, void 0, opts)
6363
6504
  );
6364
6505
  const taskEnabled = process.env.FAAPI_TASKS_DISABLED === "1" ? false : config?.task?.enabled ?? true;
6365
6506
  const stopTaskRuntime = async () => {
@@ -6367,7 +6508,7 @@ async function createAppBase(options) {
6367
6508
  await taskQueue.stop(config?.task?.shutdownTimeoutMs ?? 1e4);
6368
6509
  };
6369
6510
  if (taskEnabled) {
6370
- taskQueue.start();
6511
+ await taskQueue.start();
6371
6512
  cronScheduler.start();
6372
6513
  }
6373
6514
  registries.taskHandle.register(() => taskQueue);
@@ -7034,7 +7175,6 @@ export {
7034
7175
  createAppRegistries,
7035
7176
  createCronScheduler,
7036
7177
  createDevApp,
7037
- createMemoryDriver,
7038
7178
  createProdApp,
7039
7179
  createProgram,
7040
7180
  createPrograms,