@juspay/neurolink 9.39.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.
- package/CHANGELOG.md +12 -0
- package/dist/browser/neurolink.min.js +445 -431
- package/dist/cli/commands/task.d.ts +56 -0
- package/dist/cli/commands/task.js +835 -0
- package/dist/cli/parser.js +4 -1
- package/dist/lib/neurolink.d.ts +22 -1
- package/dist/lib/neurolink.js +195 -14
- package/dist/lib/tasks/backends/bullmqBackend.d.ts +32 -0
- package/dist/lib/tasks/backends/bullmqBackend.js +189 -0
- package/dist/lib/tasks/backends/nodeTimeoutBackend.d.ts +27 -0
- package/dist/lib/tasks/backends/nodeTimeoutBackend.js +141 -0
- package/dist/lib/tasks/backends/taskBackendRegistry.d.ts +31 -0
- package/dist/lib/tasks/backends/taskBackendRegistry.js +66 -0
- package/dist/lib/tasks/errors.d.ts +31 -0
- package/dist/lib/tasks/errors.js +18 -0
- package/dist/lib/tasks/store/fileTaskStore.d.ts +43 -0
- package/dist/lib/tasks/store/fileTaskStore.js +179 -0
- package/dist/lib/tasks/store/redisTaskStore.d.ts +42 -0
- package/dist/lib/tasks/store/redisTaskStore.js +189 -0
- package/dist/lib/tasks/taskExecutor.d.ts +21 -0
- package/dist/lib/tasks/taskExecutor.js +166 -0
- package/dist/lib/tasks/taskManager.d.ts +60 -0
- package/dist/lib/tasks/taskManager.js +393 -0
- package/dist/lib/tasks/tools/taskTools.d.ts +135 -0
- package/dist/lib/tasks/tools/taskTools.js +274 -0
- package/dist/lib/types/configTypes.d.ts +3 -0
- package/dist/lib/types/generateTypes.d.ts +42 -0
- package/dist/lib/types/index.d.ts +2 -1
- package/dist/lib/types/streamTypes.d.ts +7 -0
- package/dist/lib/types/taskTypes.d.ts +275 -0
- package/dist/lib/types/taskTypes.js +37 -0
- package/dist/neurolink.d.ts +22 -1
- package/dist/neurolink.js +195 -14
- package/dist/tasks/backends/bullmqBackend.d.ts +32 -0
- package/dist/tasks/backends/bullmqBackend.js +188 -0
- package/dist/tasks/backends/nodeTimeoutBackend.d.ts +27 -0
- package/dist/tasks/backends/nodeTimeoutBackend.js +140 -0
- package/dist/tasks/backends/taskBackendRegistry.d.ts +31 -0
- package/dist/tasks/backends/taskBackendRegistry.js +65 -0
- package/dist/tasks/errors.d.ts +31 -0
- package/dist/tasks/errors.js +17 -0
- package/dist/tasks/store/fileTaskStore.d.ts +43 -0
- package/dist/tasks/store/fileTaskStore.js +178 -0
- package/dist/tasks/store/redisTaskStore.d.ts +42 -0
- package/dist/tasks/store/redisTaskStore.js +188 -0
- package/dist/tasks/taskExecutor.d.ts +21 -0
- package/dist/tasks/taskExecutor.js +165 -0
- package/dist/tasks/taskManager.d.ts +60 -0
- package/dist/tasks/taskManager.js +392 -0
- package/dist/tasks/tools/taskTools.d.ts +135 -0
- package/dist/tasks/tools/taskTools.js +273 -0
- package/dist/types/configTypes.d.ts +3 -0
- package/dist/types/generateTypes.d.ts +42 -0
- package/dist/types/index.d.ts +2 -1
- package/dist/types/streamTypes.d.ts +7 -0
- package/dist/types/taskTypes.d.ts +275 -0
- package/dist/types/taskTypes.js +36 -0
- package/package.json +4 -2
|
@@ -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,140 @@
|
|
|
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
|
+
}
|
|
@@ -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
|
+
}
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* TaskBackendRegistry — registration point for all task backend implementations.
|
|
3
|
+
* Follows the same pattern as ProviderRegistry: dynamic imports, lazy registration.
|
|
4
|
+
*/
|
|
5
|
+
import { logger } from "../../utils/logger.js";
|
|
6
|
+
import { TaskError } from "../errors.js";
|
|
7
|
+
export class TaskBackendRegistry {
|
|
8
|
+
static factories = new Map();
|
|
9
|
+
static registered = false;
|
|
10
|
+
/**
|
|
11
|
+
* Register a backend factory function.
|
|
12
|
+
* Can be called externally to add custom backends (e.g., "pg-boss").
|
|
13
|
+
*/
|
|
14
|
+
static register(name, factory) {
|
|
15
|
+
TaskBackendRegistry.factories.set(name, factory);
|
|
16
|
+
logger.debug(`[TaskBackendRegistry] Registered backend: ${name}`);
|
|
17
|
+
}
|
|
18
|
+
/**
|
|
19
|
+
* Register the built-in backends (BullMQ, NodeTimeout).
|
|
20
|
+
* Idempotent — safe to call multiple times.
|
|
21
|
+
*/
|
|
22
|
+
static registerDefaults() {
|
|
23
|
+
if (TaskBackendRegistry.registered) {
|
|
24
|
+
return;
|
|
25
|
+
}
|
|
26
|
+
TaskBackendRegistry.registered = true;
|
|
27
|
+
// BullMQ backend (production, Redis-backed)
|
|
28
|
+
TaskBackendRegistry.register("bullmq", async (config) => {
|
|
29
|
+
const { BullMQBackend } = await import("./bullmqBackend.js");
|
|
30
|
+
return new BullMQBackend(config);
|
|
31
|
+
});
|
|
32
|
+
// NodeTimeout backend (development, in-process timers)
|
|
33
|
+
TaskBackendRegistry.register("node-timeout", async (config) => {
|
|
34
|
+
const { NodeTimeoutBackend } = await import("./nodeTimeoutBackend.js");
|
|
35
|
+
return new NodeTimeoutBackend(config);
|
|
36
|
+
});
|
|
37
|
+
logger.debug("[TaskBackendRegistry] Registered default backends");
|
|
38
|
+
}
|
|
39
|
+
/**
|
|
40
|
+
* Create a backend instance by name.
|
|
41
|
+
*/
|
|
42
|
+
static async create(name, config) {
|
|
43
|
+
TaskBackendRegistry.registerDefaults();
|
|
44
|
+
const factory = TaskBackendRegistry.factories.get(name);
|
|
45
|
+
if (!factory) {
|
|
46
|
+
const available = Array.from(TaskBackendRegistry.factories.keys());
|
|
47
|
+
throw TaskError.create("BACKEND_UNKNOWN", `Unknown task backend: "${name}". Available: ${available.join(", ")}`);
|
|
48
|
+
}
|
|
49
|
+
return factory(config);
|
|
50
|
+
}
|
|
51
|
+
/**
|
|
52
|
+
* Check if a backend is registered.
|
|
53
|
+
*/
|
|
54
|
+
static has(name) {
|
|
55
|
+
TaskBackendRegistry.registerDefaults();
|
|
56
|
+
return TaskBackendRegistry.factories.has(name);
|
|
57
|
+
}
|
|
58
|
+
/**
|
|
59
|
+
* List all registered backend names.
|
|
60
|
+
*/
|
|
61
|
+
static getAvailable() {
|
|
62
|
+
TaskBackendRegistry.registerDefaults();
|
|
63
|
+
return Array.from(TaskBackendRegistry.factories.keys());
|
|
64
|
+
}
|
|
65
|
+
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* TaskError — Typed error factory for the TaskManager system.
|
|
3
|
+
*
|
|
4
|
+
* Uses the standard NeuroLink createErrorFactory pattern so every task-related
|
|
5
|
+
* error carries a structured code, feature tag, and optional retryable flag.
|
|
6
|
+
*/
|
|
7
|
+
export declare const TaskErrorCodes: {
|
|
8
|
+
readonly TASK_NOT_FOUND: "TASK-001";
|
|
9
|
+
readonly BACKEND_NOT_INITIALIZED: "TASK-002";
|
|
10
|
+
readonly BACKEND_UNKNOWN: "TASK-003";
|
|
11
|
+
readonly INVALID_TASK_STATUS: "TASK-004";
|
|
12
|
+
readonly TASK_LIMIT_REACHED: "TASK-005";
|
|
13
|
+
readonly TASK_DISABLED: "TASK-006";
|
|
14
|
+
readonly SCHEDULE_FAILED: "TASK-007";
|
|
15
|
+
};
|
|
16
|
+
export declare const TaskError: {
|
|
17
|
+
codes: {
|
|
18
|
+
readonly TASK_NOT_FOUND: "TASK-001";
|
|
19
|
+
readonly BACKEND_NOT_INITIALIZED: "TASK-002";
|
|
20
|
+
readonly BACKEND_UNKNOWN: "TASK-003";
|
|
21
|
+
readonly INVALID_TASK_STATUS: "TASK-004";
|
|
22
|
+
readonly TASK_LIMIT_REACHED: "TASK-005";
|
|
23
|
+
readonly TASK_DISABLED: "TASK-006";
|
|
24
|
+
readonly SCHEDULE_FAILED: "TASK-007";
|
|
25
|
+
};
|
|
26
|
+
create: (code: "TASK_NOT_FOUND" | "BACKEND_NOT_INITIALIZED" | "BACKEND_UNKNOWN" | "INVALID_TASK_STATUS" | "TASK_LIMIT_REACHED" | "TASK_DISABLED" | "SCHEDULE_FAILED", message: string, options?: {
|
|
27
|
+
retryable?: boolean;
|
|
28
|
+
details?: Record<string, unknown>;
|
|
29
|
+
cause?: Error;
|
|
30
|
+
} | undefined) => import("../index.js").NeuroLinkFeatureError;
|
|
31
|
+
};
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* TaskError — Typed error factory for the TaskManager system.
|
|
3
|
+
*
|
|
4
|
+
* Uses the standard NeuroLink createErrorFactory pattern so every task-related
|
|
5
|
+
* error carries a structured code, feature tag, and optional retryable flag.
|
|
6
|
+
*/
|
|
7
|
+
import { createErrorFactory } from "../core/infrastructure/baseError.js";
|
|
8
|
+
export const TaskErrorCodes = {
|
|
9
|
+
TASK_NOT_FOUND: "TASK-001",
|
|
10
|
+
BACKEND_NOT_INITIALIZED: "TASK-002",
|
|
11
|
+
BACKEND_UNKNOWN: "TASK-003",
|
|
12
|
+
INVALID_TASK_STATUS: "TASK-004",
|
|
13
|
+
TASK_LIMIT_REACHED: "TASK-005",
|
|
14
|
+
TASK_DISABLED: "TASK-006",
|
|
15
|
+
SCHEDULE_FAILED: "TASK-007",
|
|
16
|
+
};
|
|
17
|
+
export const TaskError = createErrorFactory("Task", TaskErrorCodes);
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* FileTaskStore — File-based persistence for TaskManager.
|
|
3
|
+
* Used automatically when backend is "node-timeout".
|
|
4
|
+
*
|
|
5
|
+
* Storage layout:
|
|
6
|
+
* {storePath} — tasks.json (all task definitions)
|
|
7
|
+
* {logsPath}/{taskId}.jsonl — run log per task (append-only)
|
|
8
|
+
* Continuation history is in-memory only (lost on restart).
|
|
9
|
+
*/
|
|
10
|
+
import { type Task, type TaskStatus, type TaskRunResult, type TaskStore, type TaskManagerConfig, type ConversationEntry } from "../../types/taskTypes.js";
|
|
11
|
+
export declare class FileTaskStore implements TaskStore {
|
|
12
|
+
readonly type: "file";
|
|
13
|
+
private storePath;
|
|
14
|
+
private logsPath;
|
|
15
|
+
private maxRunLogs;
|
|
16
|
+
private maxHistoryEntries;
|
|
17
|
+
private tasks;
|
|
18
|
+
/** In-memory only — lost on restart */
|
|
19
|
+
private history;
|
|
20
|
+
private flushQueue;
|
|
21
|
+
constructor(config: TaskManagerConfig);
|
|
22
|
+
initialize(): Promise<void>;
|
|
23
|
+
shutdown(): Promise<void>;
|
|
24
|
+
save(task: Task): Promise<void>;
|
|
25
|
+
get(taskId: string): Promise<Task | null>;
|
|
26
|
+
list(filter?: {
|
|
27
|
+
status?: TaskStatus;
|
|
28
|
+
}): Promise<Task[]>;
|
|
29
|
+
update(taskId: string, updates: Partial<Task>): Promise<Task>;
|
|
30
|
+
delete(taskId: string): Promise<void>;
|
|
31
|
+
appendRun(taskId: string, run: TaskRunResult): Promise<void>;
|
|
32
|
+
getRuns(taskId: string, options?: {
|
|
33
|
+
limit?: number;
|
|
34
|
+
status?: string;
|
|
35
|
+
}): Promise<TaskRunResult[]>;
|
|
36
|
+
appendHistory(taskId: string, messages: ConversationEntry[]): Promise<void>;
|
|
37
|
+
getHistory(taskId: string): Promise<ConversationEntry[]>;
|
|
38
|
+
clearHistory(taskId: string): Promise<void>;
|
|
39
|
+
/** Write all tasks to disk atomically, serialized via promise queue */
|
|
40
|
+
private flush;
|
|
41
|
+
/** Prune run log if it exceeds maxRunLogs entries */
|
|
42
|
+
private pruneRunLog;
|
|
43
|
+
}
|
|
@@ -0,0 +1,178 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* FileTaskStore — File-based persistence for TaskManager.
|
|
3
|
+
* Used automatically when backend is "node-timeout".
|
|
4
|
+
*
|
|
5
|
+
* Storage layout:
|
|
6
|
+
* {storePath} — tasks.json (all task definitions)
|
|
7
|
+
* {logsPath}/{taskId}.jsonl — run log per task (append-only)
|
|
8
|
+
* Continuation history is in-memory only (lost on restart).
|
|
9
|
+
*/
|
|
10
|
+
import { existsSync, mkdirSync, readFileSync } from "node:fs";
|
|
11
|
+
import { appendFile, readFile, rename, unlink, writeFile, } from "node:fs/promises";
|
|
12
|
+
import { dirname, join } from "node:path";
|
|
13
|
+
import { logger } from "../../utils/logger.js";
|
|
14
|
+
import { TaskError } from "../errors.js";
|
|
15
|
+
import { TASK_DEFAULTS, } from "../../types/taskTypes.js";
|
|
16
|
+
export class FileTaskStore {
|
|
17
|
+
type = "file";
|
|
18
|
+
storePath;
|
|
19
|
+
logsPath;
|
|
20
|
+
maxRunLogs;
|
|
21
|
+
maxHistoryEntries;
|
|
22
|
+
tasks = new Map();
|
|
23
|
+
/** In-memory only — lost on restart */
|
|
24
|
+
history = new Map();
|
|
25
|
+
flushQueue = Promise.resolve();
|
|
26
|
+
constructor(config) {
|
|
27
|
+
this.storePath = config.storePath ?? TASK_DEFAULTS.storePath;
|
|
28
|
+
this.logsPath = config.logsPath ?? TASK_DEFAULTS.logsPath;
|
|
29
|
+
this.maxRunLogs = config.maxRunLogs ?? TASK_DEFAULTS.maxRunLogs;
|
|
30
|
+
this.maxHistoryEntries =
|
|
31
|
+
config.maxHistoryEntries ?? TASK_DEFAULTS.maxHistoryEntries;
|
|
32
|
+
}
|
|
33
|
+
async initialize() {
|
|
34
|
+
// Ensure directories exist
|
|
35
|
+
mkdirSync(dirname(this.storePath), { recursive: true });
|
|
36
|
+
mkdirSync(this.logsPath, { recursive: true });
|
|
37
|
+
// Load existing tasks
|
|
38
|
+
if (existsSync(this.storePath)) {
|
|
39
|
+
try {
|
|
40
|
+
const raw = readFileSync(this.storePath, "utf-8");
|
|
41
|
+
const data = JSON.parse(raw);
|
|
42
|
+
for (const [id, task] of Object.entries(data.tasks)) {
|
|
43
|
+
this.tasks.set(id, task);
|
|
44
|
+
}
|
|
45
|
+
logger.info("[TaskStore:File] Loaded tasks", {
|
|
46
|
+
count: this.tasks.size,
|
|
47
|
+
});
|
|
48
|
+
}
|
|
49
|
+
catch (err) {
|
|
50
|
+
logger.error("[TaskStore:File] Failed to load tasks file", {
|
|
51
|
+
error: String(err),
|
|
52
|
+
});
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
async shutdown() {
|
|
57
|
+
await this.flush();
|
|
58
|
+
this.tasks.clear();
|
|
59
|
+
this.history.clear();
|
|
60
|
+
}
|
|
61
|
+
// ── Task CRUD ───────────────────────────────────────────
|
|
62
|
+
async save(task) {
|
|
63
|
+
this.tasks.set(task.id, task);
|
|
64
|
+
await this.flush();
|
|
65
|
+
}
|
|
66
|
+
async get(taskId) {
|
|
67
|
+
return this.tasks.get(taskId) ?? null;
|
|
68
|
+
}
|
|
69
|
+
async list(filter) {
|
|
70
|
+
let tasks = Array.from(this.tasks.values());
|
|
71
|
+
if (filter?.status) {
|
|
72
|
+
tasks = tasks.filter((t) => t.status === filter.status);
|
|
73
|
+
}
|
|
74
|
+
return tasks.sort((a, b) => new Date(b.updatedAt).getTime() - new Date(a.updatedAt).getTime());
|
|
75
|
+
}
|
|
76
|
+
async update(taskId, updates) {
|
|
77
|
+
const existing = this.tasks.get(taskId);
|
|
78
|
+
if (!existing) {
|
|
79
|
+
throw TaskError.create("TASK_NOT_FOUND", `Task not found: ${taskId}`);
|
|
80
|
+
}
|
|
81
|
+
const updated = {
|
|
82
|
+
...existing,
|
|
83
|
+
...updates,
|
|
84
|
+
id: existing.id, // ID is immutable
|
|
85
|
+
updatedAt: new Date().toISOString(),
|
|
86
|
+
};
|
|
87
|
+
this.tasks.set(taskId, updated);
|
|
88
|
+
await this.flush();
|
|
89
|
+
return updated;
|
|
90
|
+
}
|
|
91
|
+
async delete(taskId) {
|
|
92
|
+
this.tasks.delete(taskId);
|
|
93
|
+
this.history.delete(taskId);
|
|
94
|
+
// Delete run log file
|
|
95
|
+
const logPath = join(this.logsPath, `${taskId}.jsonl`);
|
|
96
|
+
try {
|
|
97
|
+
await unlink(logPath);
|
|
98
|
+
}
|
|
99
|
+
catch {
|
|
100
|
+
// File may not exist if task never ran
|
|
101
|
+
}
|
|
102
|
+
await this.flush();
|
|
103
|
+
}
|
|
104
|
+
// ── Run Logs ──────────────────────────────────────────
|
|
105
|
+
async appendRun(taskId, run) {
|
|
106
|
+
const logPath = join(this.logsPath, `${taskId}.jsonl`);
|
|
107
|
+
mkdirSync(dirname(logPath), { recursive: true });
|
|
108
|
+
await appendFile(logPath, JSON.stringify(run) + "\n", "utf-8");
|
|
109
|
+
await this.pruneRunLog(logPath);
|
|
110
|
+
}
|
|
111
|
+
async getRuns(taskId, options) {
|
|
112
|
+
const logPath = join(this.logsPath, `${taskId}.jsonl`);
|
|
113
|
+
if (!existsSync(logPath)) {
|
|
114
|
+
return [];
|
|
115
|
+
}
|
|
116
|
+
const content = await readFile(logPath, "utf-8");
|
|
117
|
+
const lines = content.trim().split("\n").filter(Boolean);
|
|
118
|
+
let runs = lines.map((line) => JSON.parse(line));
|
|
119
|
+
if (options?.status) {
|
|
120
|
+
runs = runs.filter((r) => r.status === options.status);
|
|
121
|
+
}
|
|
122
|
+
// Return newest first, limited
|
|
123
|
+
runs.reverse();
|
|
124
|
+
const limit = options?.limit ?? 20;
|
|
125
|
+
return runs.slice(0, limit);
|
|
126
|
+
}
|
|
127
|
+
// ── Continuation History (in-memory only) ─────────────
|
|
128
|
+
async appendHistory(taskId, messages) {
|
|
129
|
+
const existing = this.history.get(taskId) ?? [];
|
|
130
|
+
existing.push(...messages);
|
|
131
|
+
// Trim to keep only the most recent entries, preventing unbounded growth
|
|
132
|
+
if (existing.length > this.maxHistoryEntries) {
|
|
133
|
+
const trimmed = existing.slice(-this.maxHistoryEntries);
|
|
134
|
+
this.history.set(taskId, trimmed);
|
|
135
|
+
}
|
|
136
|
+
else {
|
|
137
|
+
this.history.set(taskId, existing);
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
async getHistory(taskId) {
|
|
141
|
+
return this.history.get(taskId) ?? [];
|
|
142
|
+
}
|
|
143
|
+
async clearHistory(taskId) {
|
|
144
|
+
this.history.delete(taskId);
|
|
145
|
+
}
|
|
146
|
+
// ── Internal ──────────────────────────────────────────
|
|
147
|
+
/** Write all tasks to disk atomically, serialized via promise queue */
|
|
148
|
+
async flush() {
|
|
149
|
+
this.flushQueue = this.flushQueue.then(async () => {
|
|
150
|
+
const data = {
|
|
151
|
+
version: 1,
|
|
152
|
+
tasks: Object.fromEntries(this.tasks),
|
|
153
|
+
};
|
|
154
|
+
const dir = dirname(this.storePath);
|
|
155
|
+
mkdirSync(dir, { recursive: true });
|
|
156
|
+
// Write to temp file first, then atomic rename
|
|
157
|
+
const tmpPath = this.storePath + ".tmp";
|
|
158
|
+
await writeFile(tmpPath, JSON.stringify(data, null, 2), "utf-8");
|
|
159
|
+
await rename(tmpPath, this.storePath);
|
|
160
|
+
});
|
|
161
|
+
await this.flushQueue;
|
|
162
|
+
}
|
|
163
|
+
/** Prune run log if it exceeds maxRunLogs entries */
|
|
164
|
+
async pruneRunLog(logPath) {
|
|
165
|
+
try {
|
|
166
|
+
const content = await readFile(logPath, "utf-8");
|
|
167
|
+
const lines = content.trim().split("\n").filter(Boolean);
|
|
168
|
+
if (lines.length > this.maxRunLogs) {
|
|
169
|
+
// Keep the most recent entries
|
|
170
|
+
const trimmed = lines.slice(-this.maxRunLogs);
|
|
171
|
+
await writeFile(logPath, trimmed.join("\n") + "\n", "utf-8");
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
catch {
|
|
175
|
+
// Pruning is best-effort
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
}
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* RedisTaskStore — Redis-backed persistence for TaskManager.
|
|
3
|
+
* Used automatically when backend is "bullmq".
|
|
4
|
+
*
|
|
5
|
+
* Key patterns:
|
|
6
|
+
* neurolink:tasks (Hash) — all task definitions
|
|
7
|
+
* neurolink:task:{id}:runs (List) — run log entries (newest first)
|
|
8
|
+
* neurolink:task:{id}:history (List) — continuation mode conversation history
|
|
9
|
+
*/
|
|
10
|
+
import { type Task, type TaskStatus, type TaskRunResult, type TaskStore, type TaskManagerConfig, type ConversationEntry } from "../../types/taskTypes.js";
|
|
11
|
+
export declare class RedisTaskStore implements TaskStore {
|
|
12
|
+
private config;
|
|
13
|
+
readonly type: "redis";
|
|
14
|
+
private client;
|
|
15
|
+
private maxRunLogs;
|
|
16
|
+
private maxHistoryEntries;
|
|
17
|
+
private retentionConfig;
|
|
18
|
+
constructor(config: TaskManagerConfig);
|
|
19
|
+
initialize(): Promise<void>;
|
|
20
|
+
shutdown(): Promise<void>;
|
|
21
|
+
save(task: Task): Promise<void>;
|
|
22
|
+
get(taskId: string): Promise<Task | null>;
|
|
23
|
+
list(filter?: {
|
|
24
|
+
status?: TaskStatus;
|
|
25
|
+
}): Promise<Task[]>;
|
|
26
|
+
update(taskId: string, updates: Partial<Task>): Promise<Task>;
|
|
27
|
+
delete(taskId: string): Promise<void>;
|
|
28
|
+
appendRun(taskId: string, run: TaskRunResult): Promise<void>;
|
|
29
|
+
getRuns(taskId: string, options?: {
|
|
30
|
+
limit?: number;
|
|
31
|
+
status?: string;
|
|
32
|
+
}): Promise<TaskRunResult[]>;
|
|
33
|
+
appendHistory(taskId: string, messages: ConversationEntry[]): Promise<void>;
|
|
34
|
+
getHistory(taskId: string): Promise<ConversationEntry[]>;
|
|
35
|
+
clearHistory(taskId: string): Promise<void>;
|
|
36
|
+
private ensureConnected;
|
|
37
|
+
/**
|
|
38
|
+
* Set Redis TTL on terminal-state tasks so they auto-expire.
|
|
39
|
+
* Active and paused tasks never expire.
|
|
40
|
+
*/
|
|
41
|
+
private applyRetentionTTL;
|
|
42
|
+
}
|