@juspay/neurolink 9.40.0 → 9.41.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.
Files changed (56) hide show
  1. package/CHANGELOG.md +6 -0
  2. package/dist/browser/neurolink.min.js +440 -433
  3. package/dist/cli/commands/task.d.ts +56 -0
  4. package/dist/cli/commands/task.js +835 -0
  5. package/dist/cli/parser.js +4 -1
  6. package/dist/lib/neurolink.d.ts +16 -0
  7. package/dist/lib/neurolink.js +119 -2
  8. package/dist/lib/tasks/backends/bullmqBackend.d.ts +32 -0
  9. package/dist/lib/tasks/backends/bullmqBackend.js +189 -0
  10. package/dist/lib/tasks/backends/nodeTimeoutBackend.d.ts +27 -0
  11. package/dist/lib/tasks/backends/nodeTimeoutBackend.js +141 -0
  12. package/dist/lib/tasks/backends/taskBackendRegistry.d.ts +31 -0
  13. package/dist/lib/tasks/backends/taskBackendRegistry.js +66 -0
  14. package/dist/lib/tasks/errors.d.ts +31 -0
  15. package/dist/lib/tasks/errors.js +18 -0
  16. package/dist/lib/tasks/store/fileTaskStore.d.ts +43 -0
  17. package/dist/lib/tasks/store/fileTaskStore.js +179 -0
  18. package/dist/lib/tasks/store/redisTaskStore.d.ts +42 -0
  19. package/dist/lib/tasks/store/redisTaskStore.js +189 -0
  20. package/dist/lib/tasks/taskExecutor.d.ts +21 -0
  21. package/dist/lib/tasks/taskExecutor.js +166 -0
  22. package/dist/lib/tasks/taskManager.d.ts +60 -0
  23. package/dist/lib/tasks/taskManager.js +393 -0
  24. package/dist/lib/tasks/tools/taskTools.d.ts +135 -0
  25. package/dist/lib/tasks/tools/taskTools.js +274 -0
  26. package/dist/lib/types/configTypes.d.ts +3 -0
  27. package/dist/lib/types/generateTypes.d.ts +13 -0
  28. package/dist/lib/types/index.d.ts +1 -0
  29. package/dist/lib/types/taskTypes.d.ts +275 -0
  30. package/dist/lib/types/taskTypes.js +37 -0
  31. package/dist/neurolink.d.ts +16 -0
  32. package/dist/neurolink.js +119 -2
  33. package/dist/tasks/backends/bullmqBackend.d.ts +32 -0
  34. package/dist/tasks/backends/bullmqBackend.js +188 -0
  35. package/dist/tasks/backends/nodeTimeoutBackend.d.ts +27 -0
  36. package/dist/tasks/backends/nodeTimeoutBackend.js +140 -0
  37. package/dist/tasks/backends/taskBackendRegistry.d.ts +31 -0
  38. package/dist/tasks/backends/taskBackendRegistry.js +65 -0
  39. package/dist/tasks/errors.d.ts +31 -0
  40. package/dist/tasks/errors.js +17 -0
  41. package/dist/tasks/store/fileTaskStore.d.ts +43 -0
  42. package/dist/tasks/store/fileTaskStore.js +178 -0
  43. package/dist/tasks/store/redisTaskStore.d.ts +42 -0
  44. package/dist/tasks/store/redisTaskStore.js +188 -0
  45. package/dist/tasks/taskExecutor.d.ts +21 -0
  46. package/dist/tasks/taskExecutor.js +165 -0
  47. package/dist/tasks/taskManager.d.ts +60 -0
  48. package/dist/tasks/taskManager.js +392 -0
  49. package/dist/tasks/tools/taskTools.d.ts +135 -0
  50. package/dist/tasks/tools/taskTools.js +273 -0
  51. package/dist/types/configTypes.d.ts +3 -0
  52. package/dist/types/generateTypes.d.ts +13 -0
  53. package/dist/types/index.d.ts +1 -0
  54. package/dist/types/taskTypes.d.ts +275 -0
  55. package/dist/types/taskTypes.js +36 -0
  56. package/package.json +3 -1
@@ -15,6 +15,7 @@ import { ObservabilityCommandFactory } from "./commands/observability.js";
15
15
  import { TelemetryCommandFactory } from "./commands/telemetry.js";
16
16
  import { proxyStartCommand, proxyStatusCommand, proxySetupCommand, proxyGuardCommand, proxyInstallCommand, proxyUninstallCommand, } from "./commands/proxy.js";
17
17
  import { EvaluateCommandFactory } from "./commands/evaluate.js";
18
+ import { TaskCommandFactory } from "./commands/task.js";
18
19
  // Enhanced CLI with Professional UX
19
20
  export function initializeCliParser() {
20
21
  return (yargs(hideBin(process.argv))
@@ -199,6 +200,8 @@ export function initializeCliParser() {
199
200
  handler: () => { },
200
201
  })
201
202
  // Evaluate Command Group - Using EvaluateCommandFactory
202
- .command(EvaluateCommandFactory.createEvaluateCommand())); // Close the main return statement
203
+ .command(EvaluateCommandFactory.createEvaluateCommand())
204
+ // Task Command Group - Scheduled and self-running tasks
205
+ .command(TaskCommandFactory.createTaskCommands())); // Close the main return statement
203
206
  }
204
207
  //# sourceMappingURL=parser.js.map
@@ -25,11 +25,14 @@ import type { ObservabilityConfig } from "./types/observability.js";
25
25
  import type { StreamOptions, StreamResult } from "./types/streamTypes.js";
26
26
  import type { ToolExecutionContext, ToolExecutionSummary, ToolInfo, ToolRegistrationOptions } from "./types/tools.js";
27
27
  import type { BatchOperationResult } from "./types/typeAliases.js";
28
+ import { TaskManager } from "./tasks/taskManager.js";
28
29
  export declare class NeuroLink {
29
30
  private mcpInitialized;
30
31
  private mcpSkipped;
31
32
  private mcpInitPromise;
32
33
  private emitter;
34
+ private _taskManager?;
35
+ private _taskManagerConfig?;
33
36
  private toolRegistry;
34
37
  private autoDiscoveredServerInfos;
35
38
  private externalServerManager;
@@ -148,6 +151,13 @@ export declare class NeuroLink {
148
151
  */
149
152
  private get _metricsTraceContext();
150
153
  constructor(config?: NeurolinkConstructorConfig);
154
+ /**
155
+ * TaskManager — scheduled and self-running tasks.
156
+ * Lazy-initialized on first access. Configurable via constructor `tasks` option.
157
+ * The actual async initialization (Redis connect, backend start) happens
158
+ * lazily inside TaskManager on first operation.
159
+ */
160
+ get tasks(): TaskManager;
151
161
  /**
152
162
  * Initialize provider registry with security settings
153
163
  */
@@ -173,6 +183,12 @@ export declare class NeuroLink {
173
183
  * and registers them as direct tools so they're available to LLMs.
174
184
  */
175
185
  private registerFileTools;
186
+ /**
187
+ * Register task management tools bound to a TaskManager instance.
188
+ * Follows the same factory + registry pattern as registerFileTools().
189
+ * Called when TaskManager is created (eagerly or lazily via the `tasks` getter).
190
+ */
191
+ private registerTaskTools;
176
192
  /**
177
193
  * Register memory retrieval tools that allow the AI to access
178
194
  * conversation history, including full tool outputs.
@@ -75,6 +75,8 @@ import { isNonNullObject } from "./utils/typeUtils.js";
75
75
  import { resolveModel } from "./utils/modelAliasResolver.js";
76
76
  import { getWorkflow } from "./workflow/core/workflowRegistry.js";
77
77
  import { runWorkflow } from "./workflow/core/workflowRunner.js";
78
+ import { TaskManager } from "./tasks/taskManager.js";
79
+ import { createTaskTools } from "./tasks/tools/taskTools.js";
78
80
  /**
79
81
  * NL-002: Classify MCP error messages into categories for AI disambiguation.
80
82
  * Returns a human-readable error category based on error message content.
@@ -187,6 +189,9 @@ export class NeuroLink {
187
189
  mcpSkipped = false;
188
190
  mcpInitPromise = null;
189
191
  emitter = new EventEmitter();
192
+ // TaskManager — lazy-initialized on first access via `this.tasks`
193
+ _taskManager;
194
+ _taskManagerConfig;
190
195
  toolRegistry;
191
196
  autoDiscoveredServerInfos = [];
192
197
  // External MCP server management
@@ -459,6 +464,28 @@ export class NeuroLink {
459
464
  if (config?.auth) {
460
465
  this.pendingAuthConfig = config.auth;
461
466
  }
467
+ // Store task config for lazy initialization
468
+ this._taskManagerConfig = config?.tasks;
469
+ // Eagerly create TaskManager and register tools if config is provided
470
+ if (this._taskManagerConfig) {
471
+ this._taskManager = new TaskManager(this, this._taskManagerConfig);
472
+ this._taskManager.setEmitter(this.emitter);
473
+ this.registerTaskTools(this._taskManager);
474
+ }
475
+ }
476
+ /**
477
+ * TaskManager — scheduled and self-running tasks.
478
+ * Lazy-initialized on first access. Configurable via constructor `tasks` option.
479
+ * The actual async initialization (Redis connect, backend start) happens
480
+ * lazily inside TaskManager on first operation.
481
+ */
482
+ get tasks() {
483
+ if (!this._taskManager) {
484
+ this._taskManager = new TaskManager(this, this._taskManagerConfig);
485
+ this._taskManager.setEmitter(this.emitter);
486
+ this.registerTaskTools(this._taskManager);
487
+ }
488
+ return this._taskManager;
462
489
  }
463
490
  /**
464
491
  * Initialize provider registry with security settings
@@ -716,6 +743,52 @@ export class NeuroLink {
716
743
  logger.debug(`[NeuroLink] Registered ${Object.keys(fileTools).length} file reference tools`);
717
744
  });
718
745
  }
746
+ /**
747
+ * Register task management tools bound to a TaskManager instance.
748
+ * Follows the same factory + registry pattern as registerFileTools().
749
+ * Called when TaskManager is created (eagerly or lazily via the `tasks` getter).
750
+ */
751
+ registerTaskTools(manager) {
752
+ const taskTools = createTaskTools(manager);
753
+ for (const [toolName, toolDef] of Object.entries(taskTools)) {
754
+ const toolId = `direct.${toolName}`;
755
+ const toolInfo = {
756
+ name: toolName,
757
+ description: toolDef.description || `Task tool: ${toolName}`,
758
+ inputSchema: {},
759
+ serverId: "direct",
760
+ category: "built-in",
761
+ };
762
+ // registerTool is async but its core logic is synchronous (Map.set).
763
+ // We fire-and-forget here but tools are available immediately after
764
+ // the synchronous validation + map insertion completes.
765
+ void this.toolRegistry.registerTool(toolId, toolInfo, {
766
+ execute: async (params) => {
767
+ try {
768
+ const result = await toolDef.execute(params, {
769
+ toolCallId: "task-tool",
770
+ messages: [],
771
+ });
772
+ return {
773
+ success: true,
774
+ data: result,
775
+ metadata: { toolName, serverId: "direct", executionTime: 0 },
776
+ };
777
+ }
778
+ catch (error) {
779
+ return {
780
+ success: false,
781
+ error: error instanceof Error ? error.message : String(error),
782
+ metadata: { toolName, serverId: "direct", executionTime: 0 },
783
+ };
784
+ }
785
+ },
786
+ description: toolDef.description,
787
+ inputSchema: {},
788
+ });
789
+ }
790
+ logger.debug(`[NeuroLink] Registered ${Object.keys(taskTools).length} task tools`);
791
+ }
719
792
  /**
720
793
  * Register memory retrieval tools that allow the AI to access
721
794
  * conversation history, including full tool outputs.
@@ -1906,6 +1979,18 @@ Current user's request: ${currentInput}`;
1906
1979
  logger.warn("[NeuroLink] MCP servers shutdown failed:", error);
1907
1980
  }
1908
1981
  }
1982
+ // Shutdown TaskManager
1983
+ if (this._taskManager) {
1984
+ try {
1985
+ await withTimeout(this._taskManager.shutdown(), 5000, new Error("TaskManager shutdown timed out"));
1986
+ }
1987
+ catch (error) {
1988
+ logger.warn("[NeuroLink] TaskManager shutdown error:", error);
1989
+ }
1990
+ finally {
1991
+ this._taskManager = undefined;
1992
+ }
1993
+ }
1909
1994
  // Close conversation memory manager (release Redis connections, etc.)
1910
1995
  if (this.conversationMemory?.close) {
1911
1996
  try {
@@ -2521,6 +2606,8 @@ Current user's request: ${currentInput}`;
2521
2606
  abortSignal: options.abortSignal,
2522
2607
  skipToolPromptInjection: options.skipToolPromptInjection,
2523
2608
  middleware: options.middleware,
2609
+ // Pass through conversation messages for task continuation and external callers
2610
+ conversationMessages: options.conversationMessages,
2524
2611
  };
2525
2612
  // Auto-map top-level sessionId/userId to context for convenience
2526
2613
  // Tests and users may pass sessionId/userId as top-level options
@@ -2745,7 +2832,15 @@ Current user's request: ${currentInput}`;
2745
2832
  // Execute workflow
2746
2833
  const workflowResult = await runWorkflow(workflowConfig, {
2747
2834
  prompt: options.input.text,
2748
- conversationHistory: options.conversationHistory,
2835
+ conversationHistory: options.conversationMessages
2836
+ ?.filter((m) => m.role === "user" || m.role === "assistant")
2837
+ .map((m) => ({
2838
+ role: m.role,
2839
+ content: typeof m.content === "string"
2840
+ ? m.content
2841
+ : JSON.stringify(m.content),
2842
+ })) ??
2843
+ options.conversationHistory,
2749
2844
  timeout: options.timeout,
2750
2845
  verbose: false,
2751
2846
  metadata: options.context,
@@ -2836,7 +2931,15 @@ Current user's request: ${currentInput}`;
2836
2931
  // Execute workflow with progressive streaming
2837
2932
  const workflowStream = runWorkflowWithStreaming(workflowConfig, {
2838
2933
  prompt: options.input.text,
2839
- conversationHistory: options.conversationHistory,
2934
+ conversationHistory: options.conversationMessages
2935
+ ?.filter((m) => m.role === "user" || m.role === "assistant")
2936
+ .map((m) => ({
2937
+ role: m.role,
2938
+ content: typeof m.content === "string"
2939
+ ? m.content
2940
+ : JSON.stringify(m.content),
2941
+ })) ??
2942
+ options.conversationHistory,
2840
2943
  timeout: options.timeout,
2841
2944
  verbose: false,
2842
2945
  metadata: options.context,
@@ -5046,6 +5149,7 @@ Current user's request: ${currentInput}`;
5046
5149
  model: options.model,
5047
5150
  temperature: options.temperature,
5048
5151
  maxTokens: options.maxTokens,
5152
+ conversationMessages: options.conversationMessages,
5049
5153
  });
5050
5154
  // Create a wrapper around the fallback stream that accumulates content
5051
5155
  let fallbackAccumulatedContent = "";
@@ -8396,6 +8500,19 @@ Current user's request: ${currentInput}`;
8396
8500
  cleanupErrors.push(err);
8397
8501
  logger.warn("[NeuroLink] Error clearing caches:", error);
8398
8502
  }
8503
+ // 5b. Shutdown TaskManager
8504
+ if (this._taskManager) {
8505
+ try {
8506
+ logger.debug("[NeuroLink] Shutting down TaskManager...");
8507
+ await withTimeout(this._taskManager.shutdown(), 5000, new Error("TaskManager shutdown timed out"));
8508
+ }
8509
+ catch (error) {
8510
+ logger.warn("[NeuroLink] TaskManager shutdown error:", error);
8511
+ }
8512
+ finally {
8513
+ this._taskManager = undefined;
8514
+ }
8515
+ }
8399
8516
  // 6. Reset initialization flags
8400
8517
  try {
8401
8518
  logger.debug("[NeuroLink] Resetting initialization state...");
@@ -0,0 +1,32 @@
1
+ /**
2
+ * BullMQ Backend — Production-grade task scheduling via Redis.
3
+ *
4
+ * - Cron tasks → BullMQ repeatable jobs with cron pattern
5
+ * - Interval tasks → BullMQ repeatable jobs with `every` option
6
+ * - One-shot tasks → BullMQ delayed jobs
7
+ * - Survives process restarts (Redis-persisted)
8
+ */
9
+ import { type Task, type TaskBackend, type TaskExecutorFn, type TaskManagerConfig } from "../../types/taskTypes.js";
10
+ export declare class BullMQBackend implements TaskBackend {
11
+ readonly name = "bullmq";
12
+ private queue;
13
+ private worker;
14
+ private executors;
15
+ private config;
16
+ constructor(config: TaskManagerConfig);
17
+ initialize(): Promise<void>;
18
+ shutdown(): Promise<void>;
19
+ schedule(task: Task, executor: TaskExecutorFn): Promise<void>;
20
+ cancel(taskId: string): Promise<void>;
21
+ pause(taskId: string): Promise<void>;
22
+ resume(taskId: string): Promise<void>;
23
+ isHealthy(): Promise<boolean>;
24
+ /**
25
+ * Returns a connection options object for BullMQ / ioredis.
26
+ * When a URL is provided we parse it fully, preserving TLS (`rediss://`),
27
+ * ACL username, password, db index, and any query-string parameters so
28
+ * nothing is silently dropped.
29
+ */
30
+ private getConnectionConfig;
31
+ private ensureInitialized;
32
+ }
@@ -0,0 +1,189 @@
1
+ /**
2
+ * BullMQ Backend — Production-grade task scheduling via Redis.
3
+ *
4
+ * - Cron tasks → BullMQ repeatable jobs with cron pattern
5
+ * - Interval tasks → BullMQ repeatable jobs with `every` option
6
+ * - One-shot tasks → BullMQ delayed jobs
7
+ * - Survives process restarts (Redis-persisted)
8
+ */
9
+ import { Queue, Worker } from "bullmq";
10
+ import { logger } from "../../utils/logger.js";
11
+ import { TaskError } from "../errors.js";
12
+ import { TASK_DEFAULTS, } from "../../types/taskTypes.js";
13
+ const QUEUE_NAME = "neurolink-tasks";
14
+ export class BullMQBackend {
15
+ name = "bullmq";
16
+ queue = null;
17
+ worker = null;
18
+ executors = new Map();
19
+ config;
20
+ constructor(config) {
21
+ this.config = config;
22
+ }
23
+ async initialize() {
24
+ const connection = this.getConnectionConfig();
25
+ this.queue = new Queue(QUEUE_NAME, { connection });
26
+ this.worker = new Worker(QUEUE_NAME, async (job) => {
27
+ const taskId = job.data.taskId;
28
+ const task = job.data.task;
29
+ const executor = this.executors.get(taskId);
30
+ if (!executor) {
31
+ logger.warn("[BullMQ] No executor found for task", { taskId });
32
+ return;
33
+ }
34
+ logger.info("[BullMQ] Executing task", { taskId, name: task.name });
35
+ const result = await executor(task);
36
+ return result;
37
+ }, {
38
+ connection,
39
+ concurrency: this.config.maxConcurrentRuns ?? TASK_DEFAULTS.maxConcurrentRuns,
40
+ });
41
+ this.worker.on("failed", (job, err) => {
42
+ logger.error("[BullMQ] Job failed", {
43
+ taskId: job?.data?.taskId,
44
+ error: String(err),
45
+ });
46
+ });
47
+ this.worker.on("error", (err) => {
48
+ logger.error("[BullMQ] Worker error", { error: String(err) });
49
+ });
50
+ logger.info("[BullMQ] Backend initialized");
51
+ }
52
+ async shutdown() {
53
+ if (this.worker) {
54
+ await this.worker.close();
55
+ this.worker = null;
56
+ }
57
+ if (this.queue) {
58
+ await this.queue.close();
59
+ this.queue = null;
60
+ }
61
+ this.executors.clear();
62
+ logger.info("[BullMQ] Backend shut down");
63
+ }
64
+ async schedule(task, executor) {
65
+ this.ensureInitialized();
66
+ this.executors.set(task.id, executor);
67
+ const jobData = { taskId: task.id, task };
68
+ const schedule = task.schedule;
69
+ if (schedule.type === "cron") {
70
+ await this.queue.upsertJobScheduler(task.id, {
71
+ pattern: schedule.expression,
72
+ ...(schedule.timezone ? { tz: schedule.timezone } : {}),
73
+ }, { name: task.name, data: jobData });
74
+ }
75
+ else if (schedule.type === "interval") {
76
+ await this.queue.upsertJobScheduler(task.id, { every: schedule.every }, { name: task.name, data: jobData });
77
+ }
78
+ else if (schedule.type === "once") {
79
+ const at = typeof schedule.at === "string" ? new Date(schedule.at) : schedule.at;
80
+ const delay = Math.max(0, at.getTime() - Date.now());
81
+ await this.queue.add(task.name, jobData, {
82
+ jobId: task.id,
83
+ delay,
84
+ });
85
+ }
86
+ logger.info("[BullMQ] Task scheduled", {
87
+ taskId: task.id,
88
+ type: schedule.type,
89
+ });
90
+ }
91
+ async cancel(taskId) {
92
+ this.ensureInitialized();
93
+ this.executors.delete(taskId);
94
+ // Remove repeatable job scheduler
95
+ try {
96
+ await this.queue.removeJobScheduler(taskId);
97
+ }
98
+ catch {
99
+ // May not be a repeatable job — try removing by job ID
100
+ }
101
+ // Remove delayed/waiting job
102
+ try {
103
+ const job = await this.queue.getJob(taskId);
104
+ if (job) {
105
+ await job.remove();
106
+ }
107
+ }
108
+ catch {
109
+ // Job may already be processed/removed
110
+ }
111
+ logger.info("[BullMQ] Task cancelled", { taskId });
112
+ }
113
+ async pause(taskId) {
114
+ // BullMQ doesn't have per-job pause, so we fully cancel the job scheduler
115
+ // and executor. This is intentionally destructive — cancel() removes both
116
+ // the executor from the map and the job/scheduler from Redis.
117
+ //
118
+ // Resume flow (orchestrated by TaskManager):
119
+ // 1. TaskManager.resume() updates task status to "active" in the store
120
+ // 2. TaskManager.resume() calls backend.schedule(task, newExecutor)
121
+ // 3. schedule() re-registers the executor and creates a new job/scheduler
122
+ //
123
+ // Because TaskManager always supplies a fresh executor on schedule(),
124
+ // there is no need to preserve the old executor here.
125
+ await this.cancel(taskId);
126
+ logger.info("[BullMQ] Task paused (cancelled pending jobs; TaskManager will re-schedule on resume)", { taskId });
127
+ }
128
+ async resume(taskId) {
129
+ // No-op: BullMQ resume is handled by TaskManager calling schedule() after
130
+ // this method returns. See TaskManager.resume() which calls:
131
+ // backend.schedule(updatedTask, executor)
132
+ // That call re-registers the executor and creates the job/scheduler in Redis.
133
+ logger.info("[BullMQ] Task resume requested (awaiting re-schedule from TaskManager)", { taskId });
134
+ }
135
+ async isHealthy() {
136
+ if (!this.queue) {
137
+ return false;
138
+ }
139
+ try {
140
+ // Check if the queue can reach Redis
141
+ await this.queue.getJobCounts();
142
+ return true;
143
+ }
144
+ catch {
145
+ return false;
146
+ }
147
+ }
148
+ // ── Internal ──────────────────────────────────────────
149
+ /**
150
+ * Returns a connection options object for BullMQ / ioredis.
151
+ * When a URL is provided we parse it fully, preserving TLS (`rediss://`),
152
+ * ACL username, password, db index, and any query-string parameters so
153
+ * nothing is silently dropped.
154
+ */
155
+ getConnectionConfig() {
156
+ const redis = this.config.redis ?? {};
157
+ if (redis.url) {
158
+ const parsed = new URL(redis.url);
159
+ const opts = {
160
+ host: parsed.hostname || "localhost",
161
+ port: Number(parsed.port) || 6379,
162
+ db: parsed.pathname ? Number(parsed.pathname.slice(1)) || 0 : 0,
163
+ };
164
+ if (parsed.password) {
165
+ opts.password = decodeURIComponent(parsed.password);
166
+ }
167
+ if (parsed.username) {
168
+ opts.username = decodeURIComponent(parsed.username);
169
+ }
170
+ // rediss:// scheme → enable TLS
171
+ if (parsed.protocol === "rediss:") {
172
+ opts.tls = {};
173
+ }
174
+ return opts;
175
+ }
176
+ return {
177
+ host: redis.host ?? TASK_DEFAULTS.redis.host,
178
+ port: redis.port ?? TASK_DEFAULTS.redis.port,
179
+ ...(redis.password ? { password: redis.password } : {}),
180
+ db: redis.db ?? 0,
181
+ };
182
+ }
183
+ ensureInitialized() {
184
+ if (!this.queue) {
185
+ throw TaskError.create("BACKEND_NOT_INITIALIZED", "[BullMQ] Backend not initialized. Call initialize() first.");
186
+ }
187
+ }
188
+ }
189
+ //# sourceMappingURL=bullmqBackend.js.map
@@ -0,0 +1,27 @@
1
+ /**
2
+ * NodeTimeout Backend — Development/zero-dependency task scheduling.
3
+ *
4
+ * - Cron tasks → parsed with `croner`, scheduled via setTimeout chains
5
+ * - Interval tasks → setInterval
6
+ * - One-shot tasks → setTimeout
7
+ * - All timers are in-process — lost on restart
8
+ */
9
+ import { type Task, type TaskBackend, type TaskExecutorFn, type TaskManagerConfig } from "../../types/taskTypes.js";
10
+ export declare class NodeTimeoutBackend implements TaskBackend {
11
+ readonly name = "node-timeout";
12
+ private scheduled;
13
+ private paused;
14
+ private disposed;
15
+ private activeRuns;
16
+ private maxConcurrentRuns;
17
+ constructor(config: TaskManagerConfig);
18
+ initialize(): Promise<void>;
19
+ shutdown(): Promise<void>;
20
+ schedule(task: Task, executor: TaskExecutorFn): Promise<void>;
21
+ cancel(taskId: string): Promise<void>;
22
+ pause(taskId: string): Promise<void>;
23
+ resume(taskId: string): Promise<void>;
24
+ isHealthy(): Promise<boolean>;
25
+ private executeTask;
26
+ private clearEntry;
27
+ }
@@ -0,0 +1,141 @@
1
+ /**
2
+ * NodeTimeout Backend — Development/zero-dependency task scheduling.
3
+ *
4
+ * - Cron tasks → parsed with `croner`, scheduled via setTimeout chains
5
+ * - Interval tasks → setInterval
6
+ * - One-shot tasks → setTimeout
7
+ * - All timers are in-process — lost on restart
8
+ */
9
+ import { Cron } from "croner";
10
+ import { logger } from "../../utils/logger.js";
11
+ import { TASK_DEFAULTS, } from "../../types/taskTypes.js";
12
+ export class NodeTimeoutBackend {
13
+ name = "node-timeout";
14
+ scheduled = new Map();
15
+ paused = new Map();
16
+ disposed = false;
17
+ activeRuns = 0;
18
+ maxConcurrentRuns;
19
+ constructor(config) {
20
+ this.maxConcurrentRuns =
21
+ config.maxConcurrentRuns ?? TASK_DEFAULTS.maxConcurrentRuns;
22
+ }
23
+ async initialize() {
24
+ logger.info("[NodeTimeout] Backend initialized");
25
+ }
26
+ async shutdown() {
27
+ this.disposed = true;
28
+ for (const entry of this.scheduled.values()) {
29
+ this.clearEntry(entry);
30
+ }
31
+ this.scheduled.clear();
32
+ this.paused.clear();
33
+ logger.info("[NodeTimeout] Backend shut down");
34
+ }
35
+ async schedule(task, executor) {
36
+ // Cancel existing schedule for this task if any
37
+ await this.cancel(task.id);
38
+ const entry = { taskId: task.id, executor, task };
39
+ const schedule = task.schedule;
40
+ if (schedule.type === "cron") {
41
+ entry.cronJob = new Cron(schedule.expression, {
42
+ timezone: schedule.timezone,
43
+ catch: (err) => {
44
+ logger.error("[NodeTimeout] Cron execution error", {
45
+ taskId: task.id,
46
+ error: String(err),
47
+ });
48
+ },
49
+ }, () => {
50
+ this.executeTask(entry);
51
+ });
52
+ }
53
+ else if (schedule.type === "interval") {
54
+ // Wait for the first interval tick before executing
55
+ entry.intervalId = setInterval(() => {
56
+ this.executeTask(entry);
57
+ }, schedule.every);
58
+ }
59
+ else if (schedule.type === "once") {
60
+ const at = typeof schedule.at === "string" ? new Date(schedule.at) : schedule.at;
61
+ const delay = Math.max(0, at.getTime() - Date.now());
62
+ entry.timeoutId = setTimeout(() => {
63
+ this.executeTask(entry);
64
+ this.scheduled.delete(task.id);
65
+ }, delay);
66
+ }
67
+ this.scheduled.set(task.id, entry);
68
+ logger.info("[NodeTimeout] Task scheduled", {
69
+ taskId: task.id,
70
+ type: schedule.type,
71
+ });
72
+ }
73
+ async cancel(taskId) {
74
+ const entry = this.scheduled.get(taskId);
75
+ if (entry) {
76
+ this.clearEntry(entry);
77
+ this.scheduled.delete(taskId);
78
+ }
79
+ this.paused.delete(taskId);
80
+ logger.debug("[NodeTimeout] Task cancelled", { taskId });
81
+ }
82
+ async pause(taskId) {
83
+ const entry = this.scheduled.get(taskId);
84
+ if (!entry) {
85
+ return;
86
+ }
87
+ this.clearEntry(entry);
88
+ this.scheduled.delete(taskId);
89
+ // Save the entry so we can re-schedule on resume
90
+ this.paused.set(taskId, entry);
91
+ logger.info("[NodeTimeout] Task paused", { taskId });
92
+ }
93
+ async resume(taskId) {
94
+ const entry = this.paused.get(taskId);
95
+ if (!entry) {
96
+ return;
97
+ }
98
+ this.paused.delete(taskId);
99
+ // Re-schedule with the saved task and executor
100
+ await this.schedule(entry.task, entry.executor);
101
+ logger.info("[NodeTimeout] Task resumed", { taskId });
102
+ }
103
+ async isHealthy() {
104
+ return !this.disposed;
105
+ }
106
+ // ── Internal ──────────────────────────────────────────
107
+ executeTask(entry) {
108
+ if (this.activeRuns >= this.maxConcurrentRuns) {
109
+ logger.warn("[NodeTimeout] Max concurrent runs reached, skipping tick", {
110
+ taskId: entry.taskId,
111
+ activeRuns: this.activeRuns,
112
+ maxConcurrentRuns: this.maxConcurrentRuns,
113
+ });
114
+ return;
115
+ }
116
+ this.activeRuns++;
117
+ entry
118
+ .executor(entry.task)
119
+ .catch((err) => {
120
+ logger.error("[NodeTimeout] Task execution failed", {
121
+ taskId: entry.taskId,
122
+ error: String(err),
123
+ });
124
+ })
125
+ .finally(() => {
126
+ this.activeRuns--;
127
+ });
128
+ }
129
+ clearEntry(entry) {
130
+ if (entry.cronJob) {
131
+ entry.cronJob.stop();
132
+ }
133
+ if (entry.intervalId !== undefined) {
134
+ clearInterval(entry.intervalId);
135
+ }
136
+ if (entry.timeoutId !== undefined) {
137
+ clearTimeout(entry.timeoutId);
138
+ }
139
+ }
140
+ }
141
+ //# sourceMappingURL=nodeTimeoutBackend.js.map
@@ -0,0 +1,31 @@
1
+ /**
2
+ * TaskBackendRegistry — registration point for all task backend implementations.
3
+ * Follows the same pattern as ProviderRegistry: dynamic imports, lazy registration.
4
+ */
5
+ import type { TaskBackendName, TaskBackendFactoryFn, TaskManagerConfig, TaskBackend } from "../../types/taskTypes.js";
6
+ export declare class TaskBackendRegistry {
7
+ private static factories;
8
+ private static registered;
9
+ /**
10
+ * Register a backend factory function.
11
+ * Can be called externally to add custom backends (e.g., "pg-boss").
12
+ */
13
+ static register(name: string, factory: TaskBackendFactoryFn): void;
14
+ /**
15
+ * Register the built-in backends (BullMQ, NodeTimeout).
16
+ * Idempotent — safe to call multiple times.
17
+ */
18
+ static registerDefaults(): void;
19
+ /**
20
+ * Create a backend instance by name.
21
+ */
22
+ static create(name: TaskBackendName | string, config: TaskManagerConfig): Promise<TaskBackend>;
23
+ /**
24
+ * Check if a backend is registered.
25
+ */
26
+ static has(name: string): boolean;
27
+ /**
28
+ * List all registered backend names.
29
+ */
30
+ static getAvailable(): string[];
31
+ }