@smythos/sre 1.6.14 → 1.7.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/CHANGELOG +15 -0
- package/dist/index.js +52 -46
- package/dist/index.js.map +1 -1
- package/dist/types/Components/APIEndpoint.class.d.ts +2 -8
- package/dist/types/Components/Component.class.d.ts +9 -0
- package/dist/types/Components/Triggers/Gmail.trigger.d.ts +0 -17
- package/dist/types/Components/Triggers/JobScheduler.trigger.d.ts +10 -0
- package/dist/types/Components/Triggers/Trigger.class.d.ts +11 -0
- package/dist/types/Components/index.d.ts +6 -0
- package/dist/types/Core/Connector.class.d.ts +1 -0
- package/dist/types/Core/ConnectorsService.d.ts +2 -0
- package/dist/types/Core/HookService.d.ts +1 -1
- package/dist/types/helpers/Conversation.helper.d.ts +2 -0
- package/dist/types/helpers/Crypto.helper.d.ts +8 -0
- package/dist/types/index.d.ts +13 -0
- package/dist/types/subsystems/AgentManager/Agent.class.d.ts +4 -2
- package/dist/types/subsystems/AgentManager/AgentData.service/AgentDataConnector.d.ts +13 -0
- package/dist/types/subsystems/AgentManager/AgentData.service/connectors/NullAgentData.class.d.ts +1 -4
- package/dist/types/subsystems/AgentManager/Scheduler.service/Job.class.d.ts +29 -6
- package/dist/types/subsystems/AgentManager/Scheduler.service/SchedulerConnector.d.ts +11 -3
- package/dist/types/subsystems/AgentManager/Scheduler.service/connectors/LocalScheduler.class.d.ts +31 -7
- package/dist/types/subsystems/LLMManager/LLM.service/connectors/Perplexity.class.d.ts +2 -5
- package/dist/types/subsystems/LLMManager/LLM.service/connectors/openai/OpenAIConnector.class.d.ts +3 -6
- package/dist/types/subsystems/LLMManager/LLM.service/connectors/openai/apiInterfaces/ResponsesApiInterface.d.ts +7 -0
- package/dist/types/subsystems/LLMManager/LLM.service/connectors/xAI.class.d.ts +2 -5
- package/dist/types/types/Agent.types.d.ts +1 -0
- package/dist/types/types/LLM.types.d.ts +2 -5
- package/dist/types/types/SRE.types.d.ts +4 -1
- package/package.json +6 -2
- package/src/Components/APICall/OAuth.helper.ts +30 -35
- package/src/Components/APIEndpoint.class.ts +25 -6
- package/src/Components/Component.class.ts +11 -0
- package/src/Components/Triggers/Gmail.trigger.ts +282 -0
- package/src/Components/Triggers/JobScheduler.trigger.ts +45 -0
- package/src/Components/Triggers/README.md +3 -0
- package/src/Components/Triggers/Trigger.class.ts +101 -0
- package/src/Components/Triggers/WhatsApp.trigger.ts +219 -0
- package/src/Components/index.ts +8 -0
- package/src/Core/AgentProcess.helper.ts +4 -6
- package/src/Core/Connector.class.ts +11 -3
- package/src/Core/ConnectorsService.ts +5 -0
- package/src/Core/ExternalEventsReceiver.ts +317 -0
- package/src/Core/HookService.ts +20 -6
- package/src/Core/SmythRuntime.class.ts +7 -0
- package/src/Core/SystemEvents.ts +17 -0
- package/src/Core/boot.ts +2 -0
- package/src/helpers/Conversation.helper.ts +35 -11
- package/src/helpers/Crypto.helper.ts +28 -0
- package/src/index.ts +208 -195
- package/src/index.ts.bak +208 -195
- package/src/subsystems/AGENTS.md +594 -0
- package/src/subsystems/AgentManager/Agent.class.ts +71 -21
- package/src/subsystems/AgentManager/AgentData.service/AgentDataConnector.ts +24 -1
- package/src/subsystems/AgentManager/AgentData.service/connectors/NullAgentData.class.ts +2 -2
- package/src/subsystems/AgentManager/AgentRuntime.class.ts +34 -5
- package/src/subsystems/AgentManager/Scheduler.service/Job.class.ts +414 -0
- package/src/subsystems/AgentManager/Scheduler.service/Schedule.class.ts +200 -0
- package/src/subsystems/AgentManager/Scheduler.service/SchedulerConnector.ts +200 -0
- package/src/subsystems/AgentManager/Scheduler.service/connectors/LocalScheduler.class.ts +767 -0
- package/src/subsystems/AgentManager/Scheduler.service/index.ts +11 -0
- package/src/subsystems/IO/VectorDB.service/connectors/MilvusVectorDB.class.ts +1 -1
- package/src/subsystems/LLMManager/LLM.service/LLMCredentials.helper.ts +61 -2
- package/src/subsystems/LLMManager/LLM.service/connectors/Anthropic.class.ts +3 -0
- package/src/subsystems/LLMManager/LLM.service/connectors/Bedrock.class.ts +3 -1
- package/src/subsystems/LLMManager/LLM.service/connectors/Echo.class.ts +5 -1
- package/src/subsystems/LLMManager/LLM.service/connectors/GoogleAI.class.ts +247 -56
- package/src/subsystems/LLMManager/LLM.service/connectors/Groq.class.ts +3 -0
- package/src/subsystems/LLMManager/LLM.service/connectors/Ollama.class.ts +28 -21
- package/src/subsystems/LLMManager/LLM.service/connectors/Perplexity.class.ts +3 -0
- package/src/subsystems/LLMManager/LLM.service/connectors/VertexAI.class.ts +121 -33
- package/src/subsystems/LLMManager/LLM.service/connectors/openai/OpenAIConnector.class.ts +38 -27
- package/src/subsystems/LLMManager/LLM.service/connectors/openai/apiInterfaces/ResponsesApiInterface.ts +115 -18
- package/src/subsystems/LLMManager/LLM.service/connectors/xAI.class.ts +3 -0
- package/src/subsystems/LLMManager/ModelsProvider.service/ModelsProviderConnector.ts +1 -6
- package/src/subsystems/MemoryManager/LLMContext.ts +3 -8
- package/src/subsystems/MemoryManager/RuntimeContext.ts +10 -9
- package/src/subsystems/Security/Credentials/Credentials.class.ts +1 -0
- package/src/subsystems/Security/Credentials/ManagedOAuth2Credentials.class.ts +106 -0
- package/src/types/Agent.types.ts +1 -0
- package/src/types/LLM.types.ts +2 -2
- package/src/types/SRE.types.ts +3 -0
|
@@ -0,0 +1,414 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Job - Wrapper for agent-based scheduled tasks
|
|
3
|
+
*
|
|
4
|
+
* Jobs can execute in three ways:
|
|
5
|
+
* 1. **Skill Execution**: Call a specific agent skill with arguments
|
|
6
|
+
* 2. **Trigger Execution**: Invoke an agent trigger (no arguments)
|
|
7
|
+
* 3. **Prompt Execution**: Send a prompt to an agent for processing
|
|
8
|
+
*
|
|
9
|
+
* All job data is fully serializable, allowing jobs to persist and resume after restart.
|
|
10
|
+
*
|
|
11
|
+
* @example
|
|
12
|
+
* ```typescript
|
|
13
|
+
* // Skill execution job
|
|
14
|
+
* const skillJob = new Job({
|
|
15
|
+
* type: 'skill',
|
|
16
|
+
* agentId: 'my-agent',
|
|
17
|
+
* skillName: 'process_data',
|
|
18
|
+
* args: { input: 'test data' },
|
|
19
|
+
* metadata: {
|
|
20
|
+
* name: 'Data Processing',
|
|
21
|
+
* description: 'Process data every hour',
|
|
22
|
+
* retryOnFailure: true,
|
|
23
|
+
* maxRetries: 3
|
|
24
|
+
* }
|
|
25
|
+
* });
|
|
26
|
+
*
|
|
27
|
+
* // Trigger execution job
|
|
28
|
+
* const triggerJob = new Job({
|
|
29
|
+
* type: 'trigger',
|
|
30
|
+
* agentId: 'my-agent',
|
|
31
|
+
* triggerName: 'daily_sync',
|
|
32
|
+
* metadata: {
|
|
33
|
+
* name: 'Daily Sync',
|
|
34
|
+
* description: 'Sync data every day at midnight'
|
|
35
|
+
* }
|
|
36
|
+
* });
|
|
37
|
+
*
|
|
38
|
+
* // Prompt execution job
|
|
39
|
+
* const promptJob = new Job({
|
|
40
|
+
* type: 'prompt',
|
|
41
|
+
* agentId: 'my-agent',
|
|
42
|
+
* prompt: 'Generate daily report',
|
|
43
|
+
* metadata: {
|
|
44
|
+
* name: 'Daily Report',
|
|
45
|
+
* description: 'Generate report every day'
|
|
46
|
+
* }
|
|
47
|
+
* });
|
|
48
|
+
* ```
|
|
49
|
+
*/
|
|
50
|
+
|
|
51
|
+
import { AgentProcess } from '@sre/Core/AgentProcess.helper';
|
|
52
|
+
import { Conversation } from '@sre/helpers/Conversation.helper';
|
|
53
|
+
import { ConnectorService } from '@sre/Core/ConnectorsService';
|
|
54
|
+
import { hookAsync, HookService } from '@sre/Core/HookService';
|
|
55
|
+
import { Logger } from '@sre/helpers/Log.helper';
|
|
56
|
+
const console = Logger('Scheduler/Job');
|
|
57
|
+
export interface IJobMetadata {
|
|
58
|
+
name?: string;
|
|
59
|
+
description?: string;
|
|
60
|
+
tags?: string[];
|
|
61
|
+
retryOnFailure?: boolean;
|
|
62
|
+
maxRetries?: number;
|
|
63
|
+
timeout?: number; // in milliseconds
|
|
64
|
+
[key: string]: any; // Additional custom metadata
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* Configuration for a skill-based job
|
|
69
|
+
*/
|
|
70
|
+
export interface ISkillJobConfig {
|
|
71
|
+
type: 'skill';
|
|
72
|
+
agentId: string;
|
|
73
|
+
skillName: string;
|
|
74
|
+
args?: Record<string, any> | any[];
|
|
75
|
+
metadata?: IJobMetadata;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
export interface ITriggerJobConfig {
|
|
79
|
+
type: 'trigger';
|
|
80
|
+
agentId: string;
|
|
81
|
+
triggerName: string;
|
|
82
|
+
metadata?: IJobMetadata;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* Configuration for a prompt-based job
|
|
87
|
+
*/
|
|
88
|
+
export interface IPromptJobConfig {
|
|
89
|
+
type: 'prompt';
|
|
90
|
+
agentId: string;
|
|
91
|
+
prompt: string;
|
|
92
|
+
metadata?: IJobMetadata;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
export type IJobConfig = ISkillJobConfig | IPromptJobConfig | ITriggerJobConfig;
|
|
96
|
+
|
|
97
|
+
export class Job {
|
|
98
|
+
private config: IJobConfig;
|
|
99
|
+
public get agentId(): string {
|
|
100
|
+
return this.config.agentId;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
constructor(config: IJobConfig) {
|
|
104
|
+
// Validate configuration
|
|
105
|
+
if (!config.type || (config.type !== 'skill' && config.type !== 'prompt' && config.type !== 'trigger')) {
|
|
106
|
+
throw new Error('Job type must be either "skill", "prompt", or "trigger"');
|
|
107
|
+
}
|
|
108
|
+
if (!config.agentId) {
|
|
109
|
+
throw new Error('Job must have an agentId');
|
|
110
|
+
}
|
|
111
|
+
if (config.type === 'skill' && !(config as ISkillJobConfig).skillName) {
|
|
112
|
+
throw new Error('Skill job must have a skillName');
|
|
113
|
+
}
|
|
114
|
+
if (config.type === 'prompt' && !(config as IPromptJobConfig).prompt) {
|
|
115
|
+
throw new Error('Prompt job must have a prompt');
|
|
116
|
+
}
|
|
117
|
+
if (config.type === 'trigger' && !(config as ITriggerJobConfig).triggerName) {
|
|
118
|
+
throw new Error('Trigger job must have a triggerName');
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
this.config = {
|
|
122
|
+
...config,
|
|
123
|
+
metadata: {
|
|
124
|
+
retryOnFailure: false,
|
|
125
|
+
maxRetries: 0,
|
|
126
|
+
tags: [],
|
|
127
|
+
...config.metadata,
|
|
128
|
+
},
|
|
129
|
+
};
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
/**
|
|
133
|
+
* Get the job metadata
|
|
134
|
+
*/
|
|
135
|
+
public getMetadata(): IJobMetadata {
|
|
136
|
+
return { ...this.config.metadata };
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
/**
|
|
140
|
+
* Get the job configuration
|
|
141
|
+
*/
|
|
142
|
+
public getConfig(): IJobConfig {
|
|
143
|
+
return { ...this.config };
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
/**
|
|
147
|
+
* Execute the job with error handling and timeout support
|
|
148
|
+
* @returns Execution result
|
|
149
|
+
*/
|
|
150
|
+
@hookAsync('Scheduler/Job.execute')
|
|
151
|
+
public async execute(): Promise<{ success: boolean; error?: Error; executionTime: number; result?: any }> {
|
|
152
|
+
const startTime = Date.now();
|
|
153
|
+
|
|
154
|
+
try {
|
|
155
|
+
let result: any;
|
|
156
|
+
|
|
157
|
+
// Execute with timeout if specified
|
|
158
|
+
if (this.config.metadata.timeout && this.config.metadata.timeout > 0) {
|
|
159
|
+
result = await this.executeWithTimeout(this.config.metadata.timeout);
|
|
160
|
+
} else {
|
|
161
|
+
result = await this.executeInternal();
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
const executionTime = Date.now() - startTime;
|
|
165
|
+
return { success: true, executionTime, result };
|
|
166
|
+
} catch (error) {
|
|
167
|
+
const executionTime = Date.now() - startTime;
|
|
168
|
+
return {
|
|
169
|
+
success: false,
|
|
170
|
+
error: error instanceof Error ? error : new Error(String(error)),
|
|
171
|
+
executionTime,
|
|
172
|
+
};
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
/**
|
|
177
|
+
* Internal execution logic based on job type
|
|
178
|
+
*/
|
|
179
|
+
private async executeInternal(): Promise<any> {
|
|
180
|
+
if (this.config.type === 'skill') {
|
|
181
|
+
return await this.executeSkill();
|
|
182
|
+
} else if (this.config.type === 'trigger') {
|
|
183
|
+
return await this.executeTrigger();
|
|
184
|
+
} else {
|
|
185
|
+
return await this.executePrompt();
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
/**
|
|
190
|
+
* Execute a skill-based job
|
|
191
|
+
*/
|
|
192
|
+
private async executeSkill(): Promise<any> {
|
|
193
|
+
const config = this.config as ISkillJobConfig;
|
|
194
|
+
|
|
195
|
+
// Get agent data
|
|
196
|
+
const agentDataConnector = ConnectorService.getAgentDataConnector();
|
|
197
|
+
const agentData = (await agentDataConnector.getEphemeralAgentData(config.agentId)) || (await agentDataConnector.getAgentData(config.agentId));
|
|
198
|
+
|
|
199
|
+
if (!agentData) {
|
|
200
|
+
console.debug('Job execusion skiped, agent not found: ', config.agentId);
|
|
201
|
+
throw new Error(`Job execution skipped, agent not found: ${config.agentId}`);
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
// Handle different agent data structures
|
|
205
|
+
// AgentData can be: { data: { components, connections }, version } or { components, connections }
|
|
206
|
+
const actualData = agentData.data || agentData;
|
|
207
|
+
const components = actualData.components;
|
|
208
|
+
|
|
209
|
+
if (!components || !Array.isArray(components)) {
|
|
210
|
+
console.debug('Job execusion skiped, agent not found: ', config.agentId);
|
|
211
|
+
|
|
212
|
+
throw new Error(`Job execution skipped, agent not found: ${config.agentId}`);
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
// Find the skill in agent data
|
|
216
|
+
const skill = components.find((c: any) => {
|
|
217
|
+
const endpoint = c.data?.endpoint || c.endpoint;
|
|
218
|
+
return endpoint === config.skillName;
|
|
219
|
+
});
|
|
220
|
+
|
|
221
|
+
if (!skill) {
|
|
222
|
+
throw new Error(`Skill ${config.skillName} not found in agent ${config.agentId}`);
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
// Prepare request based on skill method
|
|
226
|
+
const method = (skill.data?.method || skill.method || 'POST').toUpperCase();
|
|
227
|
+
const path = `/api/${config.skillName}`;
|
|
228
|
+
const headers = {
|
|
229
|
+
'Content-Type': 'application/json',
|
|
230
|
+
};
|
|
231
|
+
|
|
232
|
+
const args = config.args || {};
|
|
233
|
+
const body = method === 'POST' ? args : undefined;
|
|
234
|
+
const query = method === 'GET' ? args : undefined;
|
|
235
|
+
|
|
236
|
+
// Load agent and execute
|
|
237
|
+
const agent = AgentProcess.load(agentData);
|
|
238
|
+
await agent.ready();
|
|
239
|
+
|
|
240
|
+
const result = await agent.run({ method, path, body, query, headers });
|
|
241
|
+
return result.data;
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
/**
|
|
245
|
+
* Execute a trigger-based job
|
|
246
|
+
*/
|
|
247
|
+
private async executeTrigger(): Promise<any> {
|
|
248
|
+
const config = this.config as ITriggerJobConfig;
|
|
249
|
+
|
|
250
|
+
// Get agent data
|
|
251
|
+
const agentDataConnector = ConnectorService.getAgentDataConnector();
|
|
252
|
+
const agentData = (await agentDataConnector.getEphemeralAgentData(config.agentId)) || (await agentDataConnector.getAgentData(config.agentId));
|
|
253
|
+
|
|
254
|
+
if (!agentData) {
|
|
255
|
+
console.debug('Job execusion skiped, agent not found: ', config.agentId);
|
|
256
|
+
throw new Error(`Job execution skipped, agent not found: ${config.agentId}`);
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
// Handle different agent data structures
|
|
260
|
+
const actualData = agentData.data || agentData;
|
|
261
|
+
const components = actualData.components;
|
|
262
|
+
|
|
263
|
+
if (!components || !Array.isArray(components)) {
|
|
264
|
+
console.debug('Job execusion skiped, agent not found: ', config.agentId);
|
|
265
|
+
throw new Error(`Job execution skipped, agent not found: ${config.agentId}`);
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
// Find the trigger in agent data (triggers have triggerEndpoint property)
|
|
269
|
+
const trigger = components.find((c: any) => {
|
|
270
|
+
const triggerEndpoint = c.data?.triggerEndpoint || c.triggerEndpoint;
|
|
271
|
+
return triggerEndpoint === config.triggerName;
|
|
272
|
+
});
|
|
273
|
+
|
|
274
|
+
if (!trigger) {
|
|
275
|
+
throw new Error(`Trigger ${config.triggerName} not found in agent ${config.agentId}`);
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
// Prepare request for trigger
|
|
279
|
+
// Triggers don't use HTTP methods like skills, they're just invoked via path
|
|
280
|
+
const path = `/trigger/${config.triggerName}`;
|
|
281
|
+
const headers = {
|
|
282
|
+
'Content-Type': 'application/json',
|
|
283
|
+
};
|
|
284
|
+
|
|
285
|
+
// Load agent and execute trigger (triggers don't take arguments)
|
|
286
|
+
const agent = AgentProcess.load(agentData);
|
|
287
|
+
await agent.ready();
|
|
288
|
+
|
|
289
|
+
const result = await agent.run({ method: 'POST', path, body: {}, query: {}, headers });
|
|
290
|
+
return result.data;
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
/**
|
|
294
|
+
* Execute a prompt-based job
|
|
295
|
+
*/
|
|
296
|
+
private async executePrompt(): Promise<any> {
|
|
297
|
+
const config = this.config as IPromptJobConfig;
|
|
298
|
+
|
|
299
|
+
// Get agent data
|
|
300
|
+
const agentDataConnector = ConnectorService.getAgentDataConnector();
|
|
301
|
+
const agentData = (await agentDataConnector.getEphemeralAgentData(config.agentId)) || (await agentDataConnector.getAgentData(config.agentId));
|
|
302
|
+
|
|
303
|
+
if (!agentData) {
|
|
304
|
+
console.debug('Job execusion skiped, agent not found: ', config.agentId);
|
|
305
|
+
throw new Error(`Job execution skipped, agent not found: ${config.agentId}`);
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
// Handle different agent data structures
|
|
309
|
+
const actualData = agentData.data || agentData;
|
|
310
|
+
const defaultModel = actualData.defaultModel;
|
|
311
|
+
const behavior = actualData.behavior || '';
|
|
312
|
+
|
|
313
|
+
if (!defaultModel) {
|
|
314
|
+
throw new Error(`No model configured for agent ${config.agentId}`);
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
// Create conversation with agent
|
|
318
|
+
const conversation = new Conversation(
|
|
319
|
+
defaultModel,
|
|
320
|
+
agentData, // spec source (agent data)
|
|
321
|
+
{
|
|
322
|
+
systemPrompt: behavior,
|
|
323
|
+
agentId: config.agentId,
|
|
324
|
+
}
|
|
325
|
+
);
|
|
326
|
+
|
|
327
|
+
await conversation.ready;
|
|
328
|
+
|
|
329
|
+
// Execute prompt
|
|
330
|
+
const result = await conversation.prompt(config.prompt);
|
|
331
|
+
return result;
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
/**
|
|
335
|
+
* Execute the job with retry logic
|
|
336
|
+
* @param retryCount - Current retry attempt
|
|
337
|
+
* @returns Execution result
|
|
338
|
+
*/
|
|
339
|
+
public async executeWithRetry(
|
|
340
|
+
retryCount: number = 0
|
|
341
|
+
): Promise<{ success: boolean; error?: Error; executionTime: number; retries: number; result?: any }> {
|
|
342
|
+
let lastError: Error | undefined;
|
|
343
|
+
let totalExecutionTime = 0;
|
|
344
|
+
let lastResult: any;
|
|
345
|
+
const maxRetries = this.config.metadata.retryOnFailure ? this.config.metadata.maxRetries || 0 : 0;
|
|
346
|
+
|
|
347
|
+
for (let attempt = 0; attempt <= maxRetries; attempt++) {
|
|
348
|
+
const result = await this.execute();
|
|
349
|
+
totalExecutionTime += result.executionTime;
|
|
350
|
+
lastResult = result.result;
|
|
351
|
+
|
|
352
|
+
if (result.success) {
|
|
353
|
+
return { success: true, executionTime: totalExecutionTime, retries: attempt, result: lastResult };
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
lastError = result.error;
|
|
357
|
+
|
|
358
|
+
// Don't retry on the last attempt
|
|
359
|
+
if (attempt < maxRetries) {
|
|
360
|
+
// Wait before retrying (exponential backoff)
|
|
361
|
+
await this.sleep(Math.min(1000 * Math.pow(2, attempt), 30000));
|
|
362
|
+
}
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
return {
|
|
366
|
+
success: false,
|
|
367
|
+
error: lastError,
|
|
368
|
+
executionTime: totalExecutionTime,
|
|
369
|
+
retries: maxRetries,
|
|
370
|
+
};
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
/**
|
|
374
|
+
* Execute with timeout
|
|
375
|
+
*/
|
|
376
|
+
private async executeWithTimeout(timeoutMs: number): Promise<any> {
|
|
377
|
+
return new Promise((resolve, reject) => {
|
|
378
|
+
const timer = setTimeout(() => {
|
|
379
|
+
reject(new Error(`Job execution timed out after ${timeoutMs}ms`));
|
|
380
|
+
}, timeoutMs);
|
|
381
|
+
|
|
382
|
+
this.executeInternal()
|
|
383
|
+
.then((result) => {
|
|
384
|
+
clearTimeout(timer);
|
|
385
|
+
resolve(result);
|
|
386
|
+
})
|
|
387
|
+
.catch((error) => {
|
|
388
|
+
clearTimeout(timer);
|
|
389
|
+
reject(error);
|
|
390
|
+
});
|
|
391
|
+
});
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
/**
|
|
395
|
+
* Sleep utility for retry delays
|
|
396
|
+
*/
|
|
397
|
+
private sleep(ms: number): Promise<void> {
|
|
398
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
/**
|
|
402
|
+
* Serialize job to JSON (everything is serializable now!)
|
|
403
|
+
*/
|
|
404
|
+
public toJSON(): IJobConfig {
|
|
405
|
+
return { ...this.config };
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
/**
|
|
409
|
+
* Create a Job instance from JSON
|
|
410
|
+
*/
|
|
411
|
+
public static fromJSON(config: IJobConfig): Job {
|
|
412
|
+
return new Job(config);
|
|
413
|
+
}
|
|
414
|
+
}
|
|
@@ -0,0 +1,200 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Schedule - Fluent API for building schedule definitions
|
|
3
|
+
*
|
|
4
|
+
* Supports interval-based scheduling and cron expressions.
|
|
5
|
+
* Serializable to JSON for persistence.
|
|
6
|
+
*
|
|
7
|
+
* @example
|
|
8
|
+
* ```typescript
|
|
9
|
+
* // Interval-based scheduling
|
|
10
|
+
* const schedule = Schedule.every('10m');
|
|
11
|
+
* const schedule2 = Schedule.every('30s').starts(new Date('2025-01-01')).ends(new Date('2025-12-31'));
|
|
12
|
+
*
|
|
13
|
+
* // Cron-based scheduling
|
|
14
|
+
* const schedule3 = Schedule.cron('0 0 * * *'); // Daily at midnight
|
|
15
|
+
*
|
|
16
|
+
* // Serialization
|
|
17
|
+
* const json = schedule.toJSON();
|
|
18
|
+
* const restored = Schedule.fromJSON(json);
|
|
19
|
+
* ```
|
|
20
|
+
*/
|
|
21
|
+
|
|
22
|
+
export interface IScheduleData {
|
|
23
|
+
interval?: string; // e.g., "10m", "30s", "2h"
|
|
24
|
+
startDate?: string; // ISO 8601
|
|
25
|
+
endDate?: string; // ISO 8601
|
|
26
|
+
cron?: string; // Cron expression
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export class Schedule {
|
|
30
|
+
private data: IScheduleData = {};
|
|
31
|
+
|
|
32
|
+
private constructor(data?: IScheduleData) {
|
|
33
|
+
if (data) {
|
|
34
|
+
this.data = { ...data };
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Create a schedule with an interval
|
|
40
|
+
* @param interval - Time interval (e.g., "10m", "30s", "2h", "1d")
|
|
41
|
+
*/
|
|
42
|
+
public static every(interval: string): Schedule {
|
|
43
|
+
const schedule = new Schedule();
|
|
44
|
+
schedule.data.interval = interval;
|
|
45
|
+
return schedule;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Create a schedule with a cron expression
|
|
50
|
+
* @param cronExpression - Cron expression (e.g., "0 0 * * *")
|
|
51
|
+
*/
|
|
52
|
+
public static cron(cronExpression: string): Schedule {
|
|
53
|
+
const schedule = new Schedule();
|
|
54
|
+
schedule.data.cron = cronExpression;
|
|
55
|
+
return schedule;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Set the start date for the schedule
|
|
60
|
+
* @param date - Start date
|
|
61
|
+
*/
|
|
62
|
+
public starts(date: Date): Schedule {
|
|
63
|
+
this.data.startDate = date.toISOString();
|
|
64
|
+
return this;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* Set the end date for the schedule
|
|
69
|
+
* @param date - End date
|
|
70
|
+
*/
|
|
71
|
+
public ends(date: Date): Schedule {
|
|
72
|
+
this.data.endDate = date.toISOString();
|
|
73
|
+
return this;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* Convert schedule to JSON representation
|
|
78
|
+
*/
|
|
79
|
+
public toJSON(): IScheduleData {
|
|
80
|
+
return { ...this.data };
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* Create a Schedule instance from JSON data
|
|
85
|
+
* @param json - Schedule data
|
|
86
|
+
*/
|
|
87
|
+
public static fromJSON(json: IScheduleData): Schedule {
|
|
88
|
+
return new Schedule(json);
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* Get the schedule data
|
|
93
|
+
*/
|
|
94
|
+
public getData(): IScheduleData {
|
|
95
|
+
return { ...this.data };
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* Validate the schedule configuration
|
|
100
|
+
*/
|
|
101
|
+
public validate(): { valid: boolean; error?: string } {
|
|
102
|
+
// Must have either interval or cron
|
|
103
|
+
if (!this.data.interval && !this.data.cron) {
|
|
104
|
+
return { valid: false, error: 'Schedule must have either interval or cron expression' };
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
// Cannot have both
|
|
108
|
+
if (this.data.interval && this.data.cron) {
|
|
109
|
+
return { valid: false, error: 'Schedule cannot have both interval and cron expression' };
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
// Validate interval format
|
|
113
|
+
if (this.data.interval) {
|
|
114
|
+
const intervalRegex = /^(\d+)(s|m|h|d|w)$/;
|
|
115
|
+
if (!intervalRegex.test(this.data.interval)) {
|
|
116
|
+
return { valid: false, error: 'Invalid interval format. Use format like "10m", "30s", "2h"' };
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
// Validate date range
|
|
121
|
+
if (this.data.startDate && this.data.endDate) {
|
|
122
|
+
const start = new Date(this.data.startDate);
|
|
123
|
+
const end = new Date(this.data.endDate);
|
|
124
|
+
if (start >= end) {
|
|
125
|
+
return { valid: false, error: 'Start date must be before end date' };
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
return { valid: true };
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
/**
|
|
133
|
+
* Parse interval string to milliseconds
|
|
134
|
+
* @param interval - Interval string (e.g., "10m")
|
|
135
|
+
*/
|
|
136
|
+
public static parseInterval(interval: string): number {
|
|
137
|
+
const match = interval.match(/^(\d+)(s|m|h|d|w)$/);
|
|
138
|
+
if (!match) {
|
|
139
|
+
throw new Error(`Invalid interval format: ${interval}`);
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
const value = parseInt(match[1], 10);
|
|
143
|
+
const unit = match[2];
|
|
144
|
+
|
|
145
|
+
const multipliers: Record<string, number> = {
|
|
146
|
+
s: 1000, // seconds
|
|
147
|
+
m: 60 * 1000, // minutes
|
|
148
|
+
h: 60 * 60 * 1000, // hours
|
|
149
|
+
d: 24 * 60 * 60 * 1000, // days
|
|
150
|
+
w: 7 * 24 * 60 * 60 * 1000, // weeks
|
|
151
|
+
};
|
|
152
|
+
|
|
153
|
+
return value * multipliers[unit];
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
/**
|
|
157
|
+
* Check if schedule should run at the given time
|
|
158
|
+
* @param now - Current time
|
|
159
|
+
*/
|
|
160
|
+
public shouldRun(now: Date = new Date()): boolean {
|
|
161
|
+
// Check date range
|
|
162
|
+
if (this.data.startDate && now < new Date(this.data.startDate)) {
|
|
163
|
+
return false;
|
|
164
|
+
}
|
|
165
|
+
if (this.data.endDate && now > new Date(this.data.endDate)) {
|
|
166
|
+
return false;
|
|
167
|
+
}
|
|
168
|
+
return true;
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
/**
|
|
172
|
+
* Calculate next run time based on last run
|
|
173
|
+
* @param lastRun - Last execution time
|
|
174
|
+
*/
|
|
175
|
+
public calculateNextRun(lastRun?: Date): Date | null {
|
|
176
|
+
const now = new Date();
|
|
177
|
+
|
|
178
|
+
// If schedule has ended, no next run
|
|
179
|
+
if (this.data.endDate && now > new Date(this.data.endDate)) {
|
|
180
|
+
return null;
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
// For interval-based scheduling
|
|
184
|
+
if (this.data.interval) {
|
|
185
|
+
const intervalMs = Schedule.parseInterval(this.data.interval);
|
|
186
|
+
const nextRun = lastRun ? new Date(lastRun.getTime() + intervalMs) : now;
|
|
187
|
+
|
|
188
|
+
// Ensure next run is not before start date
|
|
189
|
+
if (this.data.startDate) {
|
|
190
|
+
const startDate = new Date(this.data.startDate);
|
|
191
|
+
return nextRun < startDate ? startDate : nextRun;
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
return nextRun;
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
// For cron-based scheduling, return null (will be handled by node-cron)
|
|
198
|
+
return null;
|
|
199
|
+
}
|
|
200
|
+
}
|