@blokjs/trigger-cron 0.2.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 +22 -0
- package/dist/CronTrigger.d.ts +124 -0
- package/dist/CronTrigger.js +305 -0
- package/dist/index.d.ts +64 -0
- package/dist/index.js +69 -0
- package/package.json +35 -0
- package/src/CronTrigger.test.ts +119 -0
- package/src/CronTrigger.ts +432 -0
- package/src/index.ts +72 -0
- package/tsconfig.json +32 -0
package/CHANGELOG.md
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
# @blokjs/trigger-cron
|
|
2
|
+
|
|
3
|
+
## 0.2.0
|
|
4
|
+
|
|
5
|
+
### Minor Changes
|
|
6
|
+
|
|
7
|
+
- Initial public release of Blok packages.
|
|
8
|
+
|
|
9
|
+
This release includes:
|
|
10
|
+
|
|
11
|
+
- Core packages: @blokjs/shared, @blokjs/helper, @blokjs/runner
|
|
12
|
+
- Node packages: @blokjs/api-call, @blokjs/if-else, @blokjs/react
|
|
13
|
+
- Trigger packages: pubsub, queue, webhook, websocket, worker, cron, grpc
|
|
14
|
+
- CLI tool: blokctl
|
|
15
|
+
- Editor support: @blokjs/lsp-server, @blokjs/syntax
|
|
16
|
+
|
|
17
|
+
### Patch Changes
|
|
18
|
+
|
|
19
|
+
- Updated dependencies
|
|
20
|
+
- @blokjs/shared@0.2.0
|
|
21
|
+
- @blokjs/helper@0.2.0
|
|
22
|
+
- @blokjs/runner@0.2.0
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* CronTrigger - Scheduled workflow execution based on cron expressions
|
|
3
|
+
*
|
|
4
|
+
* Extends TriggerBase to support scheduled triggers:
|
|
5
|
+
* - Cron expressions (e.g., "0 * * * *" for hourly)
|
|
6
|
+
* - Timezone-aware scheduling
|
|
7
|
+
* - Overlap prevention
|
|
8
|
+
* - Manual trigger support
|
|
9
|
+
*
|
|
10
|
+
* Uses the 'cron' package for cron parsing and scheduling.
|
|
11
|
+
*/
|
|
12
|
+
import type { CronTriggerOpts, HelperResponse } from "@blok/helper";
|
|
13
|
+
import { DefaultLogger, type GlobalOptions, type BlokService, TriggerBase, type TriggerResponse } from "@blok/runner";
|
|
14
|
+
import { CronJob } from "cron";
|
|
15
|
+
/**
|
|
16
|
+
* Scheduled job information
|
|
17
|
+
*/
|
|
18
|
+
export interface ScheduledJob {
|
|
19
|
+
/** Unique job ID */
|
|
20
|
+
id: string;
|
|
21
|
+
/** Workflow path */
|
|
22
|
+
workflowPath: string;
|
|
23
|
+
/** Cron expression */
|
|
24
|
+
schedule: string;
|
|
25
|
+
/** Timezone */
|
|
26
|
+
timezone: string;
|
|
27
|
+
/** Allow overlapping executions */
|
|
28
|
+
overlap: boolean;
|
|
29
|
+
/** Whether the job is currently running */
|
|
30
|
+
running: boolean;
|
|
31
|
+
/** Last execution time */
|
|
32
|
+
lastRun?: Date;
|
|
33
|
+
/** Next scheduled time */
|
|
34
|
+
nextRun?: Date;
|
|
35
|
+
/** Internal CronJob instance */
|
|
36
|
+
job: CronJob;
|
|
37
|
+
}
|
|
38
|
+
/**
|
|
39
|
+
* Execution context passed to the workflow
|
|
40
|
+
*/
|
|
41
|
+
export interface CronExecutionContext {
|
|
42
|
+
/** Job ID */
|
|
43
|
+
jobId: string;
|
|
44
|
+
/** Scheduled time (when it was supposed to run) */
|
|
45
|
+
scheduledTime: Date;
|
|
46
|
+
/** Actual execution time */
|
|
47
|
+
executionTime: Date;
|
|
48
|
+
/** Cron expression */
|
|
49
|
+
schedule: string;
|
|
50
|
+
/** Timezone */
|
|
51
|
+
timezone: string;
|
|
52
|
+
/** Whether this is a manual trigger */
|
|
53
|
+
manual: boolean;
|
|
54
|
+
}
|
|
55
|
+
/**
|
|
56
|
+
* Workflow model with cron trigger configuration
|
|
57
|
+
*/
|
|
58
|
+
interface CronWorkflowModel {
|
|
59
|
+
path: string;
|
|
60
|
+
config: {
|
|
61
|
+
name: string;
|
|
62
|
+
version: string;
|
|
63
|
+
trigger?: {
|
|
64
|
+
cron?: CronTriggerOpts;
|
|
65
|
+
[key: string]: unknown;
|
|
66
|
+
};
|
|
67
|
+
[key: string]: unknown;
|
|
68
|
+
};
|
|
69
|
+
}
|
|
70
|
+
/**
|
|
71
|
+
* CronTrigger - Scheduled workflow execution
|
|
72
|
+
*/
|
|
73
|
+
export declare abstract class CronTrigger extends TriggerBase {
|
|
74
|
+
protected nodeMap: GlobalOptions;
|
|
75
|
+
protected readonly tracer: import("@opentelemetry/api").Tracer;
|
|
76
|
+
protected readonly logger: DefaultLogger;
|
|
77
|
+
protected jobs: Map<string, ScheduledJob>;
|
|
78
|
+
protected abstract nodes: Record<string, BlokService<unknown>>;
|
|
79
|
+
protected abstract workflows: Record<string, HelperResponse>;
|
|
80
|
+
constructor();
|
|
81
|
+
/**
|
|
82
|
+
* Load nodes into the node map
|
|
83
|
+
*/
|
|
84
|
+
loadNodes(): void;
|
|
85
|
+
/**
|
|
86
|
+
* Load workflows into the workflow map
|
|
87
|
+
*/
|
|
88
|
+
loadWorkflows(): void;
|
|
89
|
+
/**
|
|
90
|
+
* Convert cron DateTime to Date
|
|
91
|
+
* The cron package uses luxon DateTime which has toJSDate()
|
|
92
|
+
*/
|
|
93
|
+
protected toDate(dateTime: unknown): Date;
|
|
94
|
+
/**
|
|
95
|
+
* Start the cron scheduler - main entry point
|
|
96
|
+
*/
|
|
97
|
+
listen(): Promise<number>;
|
|
98
|
+
/**
|
|
99
|
+
* Stop all cron jobs
|
|
100
|
+
*/
|
|
101
|
+
stop(): Promise<void>;
|
|
102
|
+
protected onHmrWorkflowChange(): Promise<void>;
|
|
103
|
+
/**
|
|
104
|
+
* Manually trigger a specific job
|
|
105
|
+
*/
|
|
106
|
+
triggerJob(jobId: string): Promise<TriggerResponse | null>;
|
|
107
|
+
/**
|
|
108
|
+
* Get all scheduled jobs
|
|
109
|
+
*/
|
|
110
|
+
getJobs(): ScheduledJob[];
|
|
111
|
+
/**
|
|
112
|
+
* Get all workflows that have cron triggers
|
|
113
|
+
*/
|
|
114
|
+
protected getCronWorkflows(): CronWorkflowModel[];
|
|
115
|
+
/**
|
|
116
|
+
* Get workflow by path
|
|
117
|
+
*/
|
|
118
|
+
protected getWorkflowByPath(path: string): CronWorkflowModel | null;
|
|
119
|
+
/**
|
|
120
|
+
* Execute a workflow
|
|
121
|
+
*/
|
|
122
|
+
protected executeWorkflow(jobId: string, workflow: CronWorkflowModel, config: CronTriggerOpts, manual: boolean): Promise<TriggerResponse>;
|
|
123
|
+
}
|
|
124
|
+
export default CronTrigger;
|
|
@@ -0,0 +1,305 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* CronTrigger - Scheduled workflow execution based on cron expressions
|
|
4
|
+
*
|
|
5
|
+
* Extends TriggerBase to support scheduled triggers:
|
|
6
|
+
* - Cron expressions (e.g., "0 * * * *" for hourly)
|
|
7
|
+
* - Timezone-aware scheduling
|
|
8
|
+
* - Overlap prevention
|
|
9
|
+
* - Manual trigger support
|
|
10
|
+
*
|
|
11
|
+
* Uses the 'cron' package for cron parsing and scheduling.
|
|
12
|
+
*/
|
|
13
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
14
|
+
exports.CronTrigger = void 0;
|
|
15
|
+
const runner_1 = require("@blok/runner");
|
|
16
|
+
const api_1 = require("@opentelemetry/api");
|
|
17
|
+
const cron_1 = require("cron");
|
|
18
|
+
const uuid_1 = require("uuid");
|
|
19
|
+
/**
|
|
20
|
+
* CronTrigger - Scheduled workflow execution
|
|
21
|
+
*/
|
|
22
|
+
class CronTrigger extends runner_1.TriggerBase {
|
|
23
|
+
nodeMap = {};
|
|
24
|
+
tracer = api_1.trace.getTracer(process.env.PROJECT_NAME || "trigger-cron-workflow", process.env.PROJECT_VERSION || "0.0.1");
|
|
25
|
+
logger = new runner_1.DefaultLogger();
|
|
26
|
+
jobs = new Map();
|
|
27
|
+
constructor() {
|
|
28
|
+
super();
|
|
29
|
+
this.loadNodes();
|
|
30
|
+
this.loadWorkflows();
|
|
31
|
+
}
|
|
32
|
+
/**
|
|
33
|
+
* Load nodes into the node map
|
|
34
|
+
*/
|
|
35
|
+
loadNodes() {
|
|
36
|
+
this.nodeMap.nodes = new runner_1.NodeMap();
|
|
37
|
+
const nodeKeys = Object.keys(this.nodes);
|
|
38
|
+
for (const key of nodeKeys) {
|
|
39
|
+
this.nodeMap.nodes.addNode(key, this.nodes[key]);
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
/**
|
|
43
|
+
* Load workflows into the workflow map
|
|
44
|
+
*/
|
|
45
|
+
loadWorkflows() {
|
|
46
|
+
this.nodeMap.workflows = this.workflows;
|
|
47
|
+
}
|
|
48
|
+
/**
|
|
49
|
+
* Convert cron DateTime to Date
|
|
50
|
+
* The cron package uses luxon DateTime which has toJSDate()
|
|
51
|
+
*/
|
|
52
|
+
toDate(dateTime) {
|
|
53
|
+
if (dateTime && typeof dateTime === "object" && "toJSDate" in dateTime) {
|
|
54
|
+
return dateTime.toJSDate();
|
|
55
|
+
}
|
|
56
|
+
return dateTime instanceof Date ? dateTime : new Date(dateTime);
|
|
57
|
+
}
|
|
58
|
+
/**
|
|
59
|
+
* Start the cron scheduler - main entry point
|
|
60
|
+
*/
|
|
61
|
+
async listen() {
|
|
62
|
+
const startTime = this.startCounter();
|
|
63
|
+
try {
|
|
64
|
+
// Find all workflows with cron triggers
|
|
65
|
+
const cronWorkflows = this.getCronWorkflows();
|
|
66
|
+
if (cronWorkflows.length === 0) {
|
|
67
|
+
this.logger.log("No workflows with cron triggers found");
|
|
68
|
+
return this.endCounter(startTime);
|
|
69
|
+
}
|
|
70
|
+
// Create and start cron jobs for each workflow
|
|
71
|
+
for (const workflow of cronWorkflows) {
|
|
72
|
+
const config = workflow.config.trigger?.cron;
|
|
73
|
+
const jobId = `cron-${workflow.path}-${(0, uuid_1.v4)().slice(0, 8)}`;
|
|
74
|
+
this.logger.log(`Scheduling workflow: ${workflow.path} with schedule: ${config.schedule} (${config.timezone})`);
|
|
75
|
+
const job = new cron_1.CronJob(config.schedule, async () => {
|
|
76
|
+
await this.executeWorkflow(jobId, workflow, config, false);
|
|
77
|
+
}, null, // onComplete
|
|
78
|
+
false, // start
|
|
79
|
+
config.timezone);
|
|
80
|
+
const scheduledJob = {
|
|
81
|
+
id: jobId,
|
|
82
|
+
workflowPath: workflow.path,
|
|
83
|
+
schedule: config.schedule,
|
|
84
|
+
timezone: config.timezone,
|
|
85
|
+
overlap: config.overlap ?? false,
|
|
86
|
+
running: false,
|
|
87
|
+
nextRun: this.toDate(job.nextDate()),
|
|
88
|
+
job,
|
|
89
|
+
};
|
|
90
|
+
this.jobs.set(jobId, scheduledJob);
|
|
91
|
+
// Start the job
|
|
92
|
+
job.start();
|
|
93
|
+
this.logger.log(`Job ${jobId} started. Next run: ${scheduledJob.nextRun}`);
|
|
94
|
+
}
|
|
95
|
+
this.logger.log(`Cron trigger started. ${this.jobs.size} job(s) scheduled`);
|
|
96
|
+
// Enable HMR in development mode
|
|
97
|
+
if (process.env.BLOK_HMR === "true" || process.env.NODE_ENV === "development") {
|
|
98
|
+
await this.enableHotReload();
|
|
99
|
+
}
|
|
100
|
+
return this.endCounter(startTime);
|
|
101
|
+
}
|
|
102
|
+
catch (error) {
|
|
103
|
+
this.logger.error(`Failed to start cron trigger: ${error.message}`);
|
|
104
|
+
throw error;
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
/**
|
|
108
|
+
* Stop all cron jobs
|
|
109
|
+
*/
|
|
110
|
+
async stop() {
|
|
111
|
+
for (const [jobId, scheduledJob] of this.jobs) {
|
|
112
|
+
scheduledJob.job.stop();
|
|
113
|
+
this.logger.log(`Job ${jobId} stopped`);
|
|
114
|
+
}
|
|
115
|
+
this.jobs.clear();
|
|
116
|
+
this.logger.log("Cron trigger stopped");
|
|
117
|
+
}
|
|
118
|
+
async onHmrWorkflowChange() {
|
|
119
|
+
this.logger.log("[HMR] Cron workflow changed, reloading...");
|
|
120
|
+
await this.waitForInFlightRequests();
|
|
121
|
+
await this.stop();
|
|
122
|
+
this.loadWorkflows();
|
|
123
|
+
await this.listen();
|
|
124
|
+
}
|
|
125
|
+
/**
|
|
126
|
+
* Manually trigger a specific job
|
|
127
|
+
*/
|
|
128
|
+
async triggerJob(jobId) {
|
|
129
|
+
const scheduledJob = this.jobs.get(jobId);
|
|
130
|
+
if (!scheduledJob) {
|
|
131
|
+
this.logger.error(`Job not found: ${jobId}`);
|
|
132
|
+
return null;
|
|
133
|
+
}
|
|
134
|
+
// Get the workflow
|
|
135
|
+
const workflow = this.getWorkflowByPath(scheduledJob.workflowPath);
|
|
136
|
+
if (!workflow) {
|
|
137
|
+
this.logger.error(`Workflow not found: ${scheduledJob.workflowPath}`);
|
|
138
|
+
return null;
|
|
139
|
+
}
|
|
140
|
+
const config = workflow.config.trigger?.cron;
|
|
141
|
+
return this.executeWorkflow(jobId, workflow, config, true);
|
|
142
|
+
}
|
|
143
|
+
/**
|
|
144
|
+
* Get all scheduled jobs
|
|
145
|
+
*/
|
|
146
|
+
getJobs() {
|
|
147
|
+
return Array.from(this.jobs.values()).map((job) => ({
|
|
148
|
+
...job,
|
|
149
|
+
nextRun: this.toDate(job.job.nextDate()),
|
|
150
|
+
}));
|
|
151
|
+
}
|
|
152
|
+
/**
|
|
153
|
+
* Get all workflows that have cron triggers
|
|
154
|
+
*/
|
|
155
|
+
getCronWorkflows() {
|
|
156
|
+
const workflows = [];
|
|
157
|
+
for (const [path, workflow] of Object.entries(this.nodeMap.workflows || {})) {
|
|
158
|
+
const workflowConfig = workflow._config;
|
|
159
|
+
if (workflowConfig?.trigger) {
|
|
160
|
+
const triggerType = Object.keys(workflowConfig.trigger)[0];
|
|
161
|
+
if (triggerType === "cron" && workflowConfig.trigger.cron) {
|
|
162
|
+
workflows.push({
|
|
163
|
+
path,
|
|
164
|
+
config: workflowConfig,
|
|
165
|
+
});
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
return workflows;
|
|
170
|
+
}
|
|
171
|
+
/**
|
|
172
|
+
* Get workflow by path
|
|
173
|
+
*/
|
|
174
|
+
getWorkflowByPath(path) {
|
|
175
|
+
const workflow = this.nodeMap.workflows?.[path];
|
|
176
|
+
if (!workflow)
|
|
177
|
+
return null;
|
|
178
|
+
const workflowConfig = workflow._config;
|
|
179
|
+
return {
|
|
180
|
+
path,
|
|
181
|
+
config: workflowConfig,
|
|
182
|
+
};
|
|
183
|
+
}
|
|
184
|
+
/**
|
|
185
|
+
* Execute a workflow
|
|
186
|
+
*/
|
|
187
|
+
async executeWorkflow(jobId, workflow, config, manual) {
|
|
188
|
+
const scheduledJob = this.jobs.get(jobId);
|
|
189
|
+
if (!scheduledJob) {
|
|
190
|
+
throw new Error(`Job not found: ${jobId}`);
|
|
191
|
+
}
|
|
192
|
+
// Check for overlap
|
|
193
|
+
if (scheduledJob.running && !scheduledJob.overlap) {
|
|
194
|
+
this.logger.log(`Skipping ${jobId}: previous execution still running (overlap disabled)`);
|
|
195
|
+
return { ctx: {}, metrics: {} };
|
|
196
|
+
}
|
|
197
|
+
const executionId = (0, uuid_1.v4)();
|
|
198
|
+
const lastDate = scheduledJob.job.lastDate();
|
|
199
|
+
const scheduledTime = lastDate ? new Date(lastDate) : new Date();
|
|
200
|
+
const executionTime = new Date();
|
|
201
|
+
const defaultMeter = api_1.metrics.getMeter("default");
|
|
202
|
+
const cronExecutions = defaultMeter.createCounter("cron_executions", {
|
|
203
|
+
description: "Cron job executions",
|
|
204
|
+
});
|
|
205
|
+
const cronErrors = defaultMeter.createCounter("cron_errors", {
|
|
206
|
+
description: "Cron job execution errors",
|
|
207
|
+
});
|
|
208
|
+
return new Promise((resolve) => {
|
|
209
|
+
this.tracer.startActiveSpan(`cron:${workflow.path}`, async (span) => {
|
|
210
|
+
scheduledJob.running = true;
|
|
211
|
+
try {
|
|
212
|
+
const start = performance.now();
|
|
213
|
+
// Initialize configuration for this workflow
|
|
214
|
+
await this.configuration.init(workflow.path, this.nodeMap);
|
|
215
|
+
// Create context
|
|
216
|
+
const ctx = this.createContext(undefined, workflow.path, executionId);
|
|
217
|
+
// Create execution context
|
|
218
|
+
const cronContext = {
|
|
219
|
+
jobId,
|
|
220
|
+
scheduledTime,
|
|
221
|
+
executionTime,
|
|
222
|
+
schedule: config.schedule,
|
|
223
|
+
timezone: config.timezone,
|
|
224
|
+
manual,
|
|
225
|
+
};
|
|
226
|
+
// Populate request with cron context
|
|
227
|
+
ctx.request = {
|
|
228
|
+
body: cronContext,
|
|
229
|
+
headers: {
|
|
230
|
+
"x-cron-job-id": jobId,
|
|
231
|
+
"x-cron-schedule": config.schedule,
|
|
232
|
+
"x-cron-timezone": config.timezone,
|
|
233
|
+
"x-cron-manual": String(manual),
|
|
234
|
+
},
|
|
235
|
+
query: {},
|
|
236
|
+
params: {
|
|
237
|
+
jobId,
|
|
238
|
+
schedule: config.schedule,
|
|
239
|
+
},
|
|
240
|
+
};
|
|
241
|
+
// Store cron context in vars
|
|
242
|
+
if (!ctx.vars)
|
|
243
|
+
ctx.vars = {};
|
|
244
|
+
ctx.vars._cron_context = {
|
|
245
|
+
jobId,
|
|
246
|
+
scheduledTime: scheduledTime.toISOString(),
|
|
247
|
+
executionTime: executionTime.toISOString(),
|
|
248
|
+
schedule: config.schedule,
|
|
249
|
+
timezone: config.timezone,
|
|
250
|
+
manual: String(manual),
|
|
251
|
+
};
|
|
252
|
+
ctx.logger.log(`Executing cron job: ${jobId} (${manual ? "manual" : "scheduled"})`);
|
|
253
|
+
// Execute workflow
|
|
254
|
+
const response = await this.run(ctx);
|
|
255
|
+
const end = performance.now();
|
|
256
|
+
// Update job state
|
|
257
|
+
scheduledJob.lastRun = executionTime;
|
|
258
|
+
scheduledJob.nextRun = this.toDate(scheduledJob.job.nextDate());
|
|
259
|
+
// Set span attributes
|
|
260
|
+
span.setAttribute("success", true);
|
|
261
|
+
span.setAttribute("job_id", jobId);
|
|
262
|
+
span.setAttribute("workflow_path", workflow.path);
|
|
263
|
+
span.setAttribute("schedule", config.schedule);
|
|
264
|
+
span.setAttribute("timezone", config.timezone);
|
|
265
|
+
span.setAttribute("manual", manual);
|
|
266
|
+
span.setAttribute("elapsed_ms", end - start);
|
|
267
|
+
span.setStatus({ code: api_1.SpanStatusCode.OK });
|
|
268
|
+
// Record metrics
|
|
269
|
+
cronExecutions.add(1, {
|
|
270
|
+
env: process.env.NODE_ENV,
|
|
271
|
+
job_id: jobId,
|
|
272
|
+
workflow_name: this.configuration.name,
|
|
273
|
+
manual: String(manual),
|
|
274
|
+
success: "true",
|
|
275
|
+
});
|
|
276
|
+
ctx.logger.log(`Cron job completed in ${(end - start).toFixed(2)}ms: ${jobId}`);
|
|
277
|
+
resolve(response);
|
|
278
|
+
}
|
|
279
|
+
catch (error) {
|
|
280
|
+
const errorMessage = error.message;
|
|
281
|
+
// Set span error
|
|
282
|
+
span.setAttribute("success", false);
|
|
283
|
+
span.recordException(error);
|
|
284
|
+
span.setStatus({ code: api_1.SpanStatusCode.ERROR, message: errorMessage });
|
|
285
|
+
// Record error metrics
|
|
286
|
+
cronErrors.add(1, {
|
|
287
|
+
env: process.env.NODE_ENV,
|
|
288
|
+
job_id: jobId,
|
|
289
|
+
workflow_name: this.configuration?.name || "unknown",
|
|
290
|
+
manual: String(manual),
|
|
291
|
+
});
|
|
292
|
+
this.logger.error(`Cron job failed ${jobId}: ${errorMessage}`, error.stack);
|
|
293
|
+
resolve({ ctx: {}, metrics: {} });
|
|
294
|
+
}
|
|
295
|
+
finally {
|
|
296
|
+
scheduledJob.running = false;
|
|
297
|
+
span.end();
|
|
298
|
+
}
|
|
299
|
+
});
|
|
300
|
+
});
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
exports.CronTrigger = CronTrigger;
|
|
304
|
+
exports.default = CronTrigger;
|
|
305
|
+
//# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiQ3JvblRyaWdnZXIuanMiLCJzb3VyY2VSb290IjoiIiwic291cmNlcyI6WyIuLi9zcmMvQ3JvblRyaWdnZXIudHMiXSwibmFtZXMiOltdLCJtYXBwaW5ncyI6IjtBQUFBOzs7Ozs7Ozs7O0dBVUc7OztBQUdILHlDQU9zQjtBQUV0Qiw0Q0FBK0U7QUFDL0UsK0JBQStCO0FBQy9CLCtCQUFrQztBQTREbEM7O0dBRUc7QUFDSCxNQUFzQixXQUFZLFNBQVEsb0JBQVc7SUFDMUMsT0FBTyxHQUFrQixFQUFtQixDQUFDO0lBQ3BDLE1BQU0sR0FBRyxXQUFLLENBQUMsU0FBUyxDQUMxQyxPQUFPLENBQUMsR0FBRyxDQUFDLFlBQVksSUFBSSx1QkFBdUIsRUFDbkQsT0FBTyxDQUFDLEdBQUcsQ0FBQyxlQUFlLElBQUksT0FBTyxDQUN0QyxDQUFDO0lBQ2lCLE1BQU0sR0FBRyxJQUFJLHNCQUFhLEVBQUUsQ0FBQztJQUN0QyxJQUFJLEdBQThCLElBQUksR0FBRyxFQUFFLENBQUM7SUFNdEQ7UUFDQyxLQUFLLEVBQUUsQ0FBQztRQUNSLElBQUksQ0FBQyxTQUFTLEVBQUUsQ0FBQztRQUNqQixJQUFJLENBQUMsYUFBYSxFQUFFLENBQUM7SUFDdEIsQ0FBQztJQUVEOztPQUVHO0lBQ0gsU0FBUztRQUNSLElBQUksQ0FBQyxPQUFPLENBQUMsS0FBSyxHQUFHLElBQUksZ0JBQU8sRUFBRSxDQUFDO1FBQ25DLE1BQU0sUUFBUSxHQUFHLE1BQU0sQ0FBQyxJQUFJLENBQUMsSUFBSSxDQUFDLEtBQUssQ0FBQyxDQUFDO1FBQ3pDLEtBQUssTUFBTSxHQUFHLElBQUksUUFBUSxFQUFFLENBQUM7WUFDNUIsSUFBSSxDQUFDLE9BQU8sQ0FBQyxLQUFLLENBQUMsT0FBTyxDQUFDLEdBQUcsRUFBRSxJQUFJLENBQUMsS0FBSyxDQUFDLEdBQUcsQ0FBQyxDQUFDLENBQUM7UUFDbEQsQ0FBQztJQUNGLENBQUM7SUFFRDs7T0FFRztJQUNILGFBQWE7UUFDWixJQUFJLENBQUMsT0FBTyxDQUFDLFNB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QUFJLFNBQVM7d0JBQ3BELE1BQU0sRUFBRSxNQUFNLENBQUMsTUFBTSxDQUFDO3FCQUN0QixDQUFDLENBQUM7b0JBRUgsSUFBSSxDQUFDLE1BQU0sQ0FBQyxLQUFLLENBQUMsbUJBQW1CLEtBQUssS0FBSyxZQUFZLEVBQUUsRUFBRyxLQUFlLENBQUMsS0FBSyxDQUFDLENBQUM7b0JBRXZGLE9BQU8sQ0FBQyxFQUFFLEdBQUcsRUFBRSxFQUFhLEVBQUUsT0FBTyxFQUFFLEVBQWlCLEVBQUUsQ0FBQyxDQUFDO2dCQUM3RCxDQUFDO3dCQUFTLENBQUM7b0JBQ1YsWUFBWSxDQUFDLE9BQU8sR0FBRyxLQUFLLENBQUM7b0JBQzdCLElBQUksQ0FBQyxHQUFHLEVBQUUsQ0FBQztnQkFDWixDQUFDO1lBQ0YsQ0FBQyxDQUFDLENBQUM7UUFDSixDQUFDLENBQUMsQ0FBQztJQUNKLENBQUM7Q0FDRDtBQXRWRCxrQ0FzVkM7QUFFRCxrQkFBZSxXQUFXLENBQUMiLCJzb3VyY2VzQ29udGVudCI6WyIvKipcbiAqIENyb25UcmlnZ2VyIC0gU2NoZWR1bGVkIHdvcmtmbG93IGV4ZWN1dGlvbiBiYXNlZCBvbiBjcm9uIGV4cHJlc3Npb25zXG4gKlxuICogRXh0ZW5kcyBUcmlnZ2VyQmFzZSB0byBzdXBwb3J0IHNjaGVkdWxlZCB0cmlnZ2VyczpcbiAqIC0gQ3JvbiBleHByZXNzaW9ucyAoZS5nLiwgXCIwICogKiAqICpcIiBmb3IgaG91cmx5KVxuICogLSBUaW1lem9uZS1hd2FyZSBzY2hlZHVsaW5nXG4gKiAtIE92ZXJsYXAgcHJldmVudGlvblxuICogLSBNYW51YWwgdHJpZ2dlciBzdXBwb3J0XG4gKlxuICogVXNlcyB0aGUgJ2Nyb24nIHBhY2thZ2UgZm9yIGNyb24gcGFyc2luZyBhbmQgc2NoZWR1bGluZy5cbiAqL1xuXG5pbXBvcnQgdHlwZSB7IENyb25UcmlnZ2VyT3B0cywgSGVscGVyUmVzcG9uc2UgfSBmcm9tIFwiQGJsb2svaGVscGVyXCI7XG5pbXBvcnQge1xuXHREZWZhdWx0TG9nZ2VyLFxuXHR0eXBlIEdsb2JhbE9wdGlvbnMsXG5cdHR5cGUgQmxva1NlcnZpY2UsXG5cdE5vZGVNYXAsXG5cdFRyaWdnZXJCYXNlLFxuXHR0eXBlIFRyaWdnZXJSZXNwb25zZSxcbn0gZnJvbSBcIkBibG9rL3J1bm5lclwiO1xuaW1wb3J0IHR5cGUgeyBDb250ZXh0LCBNZXRyaWNzVHlwZSwgUmVxdWVzdENvbnRleHQgfSBmcm9tIFwiQGJsb2svc2hhcmVkXCI7XG5pbXBvcnQgeyB0eXBlIFNwYW4sIFNwYW5TdGF0dXNDb2RlLCBtZXRyaWNzLCB0cmFjZSB9IGZyb20gXCJAb3BlbnRlbGVtZXRyeS9hcGlcIjtcbmltcG9ydCB7IENyb25Kb2IgfSBmcm9tIFwiY3JvblwiO1xuaW1wb3J0IHsgdjQgYXMgdXVpZCB9IGZyb20gXCJ1dWlkXCI7XG5cbi8qKlxuICogU2NoZWR1bGVkIGpvYiBpbmZvcm1hdGlvblxuICovXG5leHBvcnQgaW50ZXJmYWNlIFNjaGVkdWxlZEpvYiB7XG5cdC8qKiBVbmlxdWUgam9iIElEICovXG5cdGlkOiBzdHJpbmc7XG5cdC8qKiBXb3JrZmxvdyBwYXRoICovXG5cdHdvcmtmbG93UGF0aDogc3RyaW5nO1xuXHQvKiogQ3JvbiBleHByZXNzaW9uICovXG5cdHNjaGVkdWxlOiBzdHJpbmc7XG5cdC8qKiBUaW1lem9uZSAqL1xuXHR0aW1lem9uZTogc3RyaW5nO1xuXHQvKiogQWxsb3cgb3ZlcmxhcHBpbmcgZXhlY3V0aW9ucyAqL1xuXHRvdmVybGFwOiBib29sZWFuO1xuXHQvKiogV2hldGhlciB0aGUgam9iIGlzIGN1cnJlbnRseSBydW5uaW5nICovXG5cdHJ1bm5pbmc6IGJvb2xlYW47XG5cdC8qKiBMYXN0IGV4ZWN1dGlvbiB0aW1lICovXG5cdGxhc3RSdW4/OiBEYXRlO1xuXHQvKiogTmV4dCBzY2hlZHVsZWQgdGltZSAqL1xuXHRuZXh0UnVuPzogRGF0ZTtcblx0LyoqIEludGVybmFsIENyb25Kb2IgaW5zdGFuY2UgKi9cblx0am9iOiBDcm9uSm9iO1xufVxuXG4vKipcbiAqIEV4ZWN1dGlvbiBjb250ZXh0IHBhc3NlZCB0byB0aGUgd29ya2Zsb3dcbiAqL1xuZXhwb3J0IGludGVyZmFjZSBDcm9uRXhlY3V0aW9uQ29udGV4dCB7XG5cdC8qKiBKb2IgSUQgKi9cblx0am9iSWQ6IHN0cmluZztcblx0LyoqIFNjaGVkdWxlZCB0aW1lICh3aGVuIGl0IHdhcyBzdXBwb3NlZCB0byBydW4pICovXG5cdHNjaGVkdWxlZFRpbWU6IERhdGU7XG5cdC8qKiBBY3R1YWwgZXhlY3V0aW9uIHRpbWUgKi9cblx0ZXhlY3V0aW9uVGltZTogRGF0ZTtcblx0LyoqIENyb24gZXhwcmVzc2lvbiAqL1xuXHRzY2hlZHVsZTogc3RyaW5nO1xuXHQvKiogVGltZXpvbmUgKi9cblx0dGltZXpvbmU6IHN0cmluZztcblx0LyoqIFdoZXRoZXIgdGhpcyBpcyBhIG1hbnVhbCB0cmlnZ2VyICovXG5cdG1hbnVhbDogYm9vbGVhbjtcbn1cblxuLyoqXG4gKiBXb3JrZmxvdyBtb2RlbCB3aXRoIGNyb24gdHJpZ2dlciBjb25maWd1cmF0aW9uXG4gKi9cbmludGVyZmFjZSBDcm9uV29ya2Zsb3dNb2RlbCB7XG5cdHBhdGg6IHN0cmluZztcblx0Y29uZmlnOiB7XG5cdFx0bmFtZTogc3RyaW5nO1xuXHRcdHZlcnNpb246IHN0cmluZztcblx0XHR0cmlnZ2VyPzoge1xuXHRcdFx0Y3Jvbj86IENyb25UcmlnZ2VyT3B0cztcblx0XHRcdFtrZXk6IHN0cmluZ106IHVua25vd247XG5cdFx0fTtcblx0XHRba2V5OiBzdHJpbmddOiB1bmtub3duO1xuXHR9O1xufVxuXG4vKipcbiAqIENyb25UcmlnZ2VyIC0gU2NoZWR1bGVkIHdvcmtmbG93IGV4ZWN1dGlvblxuICovXG5leHBvcnQgYWJzdHJhY3QgY2xhc3MgQ3JvblRyaWdnZXIgZXh0ZW5kcyBUcmlnZ2VyQmFzZSB7XG5cdHByb3RlY3RlZCBub2RlTWFwOiBHbG9iYWxPcHRpb25zID0ge30gYXMgR2xvYmFsT3B0aW9ucztcblx0cHJvdGVjdGVkIHJlYWRvbmx5IHRyYWNlciA9IHRyYWNlLmdldFRyYWNlcihcblx0XHRwcm9jZXNzLmVudi5QUk9KRUNUX05BTUUgfHwgXCJ0cmlnZ2VyLWNyb24td29ya2Zsb3dcIixcblx0XHRwcm9jZXNzLmVudi5QUk9KRUNUX1ZFUlNJT04gfHwgXCIwLjAuMVwiLFxuXHQpO1xuXHRwcm90ZWN0ZWQgcmVhZG9ubHkgbG9nZ2VyID0gbmV3IERlZmF1bHRMb2dnZXIoKTtcblx0cHJvdGVjdGVkIGpvYnM6IE1hcDxzdHJpbmcsIFNjaGVkdWxlZEpvYj4gPSBuZXcgTWFwKCk7XG5cblx0Ly8gU3ViY2xhc3NlcyBwcm92aWRlIHRoZXNlXG5cdHByb3RlY3RlZCBhYnN0cmFjdCBub2RlczogUmVjb3JkPHN0cmluZywgQmxva1NlcnZpY2U8dW5rbm93bj4+O1xuXHRwcm90ZWN0ZWQgYWJzdHJhY3Qgd29ya2Zsb3dzOiBSZWNvcmQ8c3RyaW5nLCBIZWxwZXJSZXNwb25zZT47XG5cblx0Y29uc3RydWN0b3IoKSB7XG5cdFx0c3VwZXIoKTtcblx0XHR0aGlzLmxvYWROb2RlcygpO1xuXHRcdHRoaXMubG9hZFdvcmtmbG93cygpO1xuXHR9XG5cblx0LyoqXG5cdCAqIExvYWQgbm9kZXMgaW50byB0aGUgbm9kZSBtYXBcblx0ICovXG5cdGxvYWROb2RlcygpOiB2b2lkIHtcblx0XHR0aGlzLm5vZGVNYXAubm9kZXMgPSBuZXcgTm9kZU1hcCgpO1xuXHRcdGNvbnN0IG5vZGVLZXlzID0gT2JqZWN0LmtleXModGhpcy5ub2Rlcyk7XG5cdFx0Zm9yIChjb25zdCBrZXkgb2Ygbm9kZUtleXMpIHtcblx0XHRcdHRoaXMubm9kZU1hcC5ub2Rlcy5hZGROb2RlKGtleSwgdGhpcy5ub2Rlc1trZXldKTtcblx0XHR9XG5cdH1cblxuXHQvKipcblx0ICogTG9hZCB3b3JrZmxvd3MgaW50byB0aGUgd29ya2Zsb3cgbWFwXG5cdCAqL1xuXHRsb2FkV29ya2Zsb3dzKCk6IHZvaWQge1xuXHRcdHRoaXMubm9kZU1hcC53b3JrZmxvd3MgPSB0aGlzLndvcmtmbG93cztcblx0fVxuXG5cdC8qKlxuXHQgKiBDb252ZXJ0IGNyb24gRGF0ZVRpbWUgdG8gRGF0ZVxuXHQgKiBUaGUgY3JvbiBwYWNrYWdlIHVzZXMgbHV4b24gRGF0ZVRpbWUgd2hpY2ggaGFzIHRvSlNEYXRlKClcblx0ICovXG5cdHByb3RlY3RlZCB0b0RhdGUoZGF0ZVRpbWU6IHVua25vd24pOiBEYXRlIHtcblx0XHRpZiAoZGF0ZVRpbWUgJiYgdHlwZW9mIGRhdGVUaW1lID09PSBcIm9iamVjdFwiICYmIFwidG9KU0RhdGVcIiBpbiBkYXRlVGltZSkge1xuXHRcdFx0cmV0dXJuIChkYXRlVGltZSBhcyB7IHRvSlNEYXRlOiAoKSA9PiBEYXRlIH0pLnRvSlNEYXRlKCk7XG5cdFx0fVxuXHRcdHJldHVybiBkYXRlVGltZSBpbnN0YW5jZW9mIERhdGUgPyBkYXRlVGltZSA6IG5ldyBEYXRlKGRhdGVUaW1lIGFzIHN0cmluZyB8IG51bWJlcik7XG5cdH1cblxuXHQvKipcblx0ICogU3RhcnQgdGhlIGNyb24gc2NoZWR1bGVyIC0gbWFpbiBlbnRyeSBwb2ludFxuXHQgKi9cblx0YXN5bmMgbGlzdGVuKCk6IFByb21pc2U8bnVtYmVyPiB7XG5cdFx0Y29uc3Qgc3RhcnRUaW1lID0gdGhpcy5zdGFydENvdW50ZXIoKTtcblxuXHRcdHRyeSB7XG5cdFx0XHQvLyBGaW5kIGFsbCB3b3JrZmxvd3Mgd2l0aCBjcm9uIHRyaWdnZXJzXG5cdFx0XHRjb25zdCBjcm9uV29ya2Zsb3dzID0gdGhpcy5nZXRDcm9uV29ya2Zsb3dzKCk7XG5cblx0XHRcdGlmIChjcm9uV29ya2Zsb3dzLmxlbmd0aCA9PT0gMCkge1xuXHRcdFx0XHR0aGlzLmxvZ2dlci5sb2coXCJObyB3b3JrZmxvd3Mgd2l0aCBjcm9uIHRyaWdnZXJzIGZvdW5kXCIpO1xuXHRcdFx0XHRyZXR1cm4gdGhpcy5lbmRDb3VudGVyKHN0YXJ0VGltZSk7XG5cdFx0XHR9XG5cblx0XHRcdC8vIENyZWF0ZSBhbmQgc3RhcnQgY3JvbiBqb2JzIGZvciBlYWNoIHdvcmtmbG93XG5cdFx0XHRmb3IgKGNvbnN0IHdvcmtmbG93IG9mIGNyb25Xb3JrZmxvd3MpIHtcblx0XHRcdFx0Y29uc3QgY29uZmlnID0gd29ya2Zsb3cuY29uZmlnLnRyaWdnZXI/LmNyb24gYXMgQ3JvblRyaWdnZXJPcHRzO1xuXHRcdFx0XHRjb25zdCBqb2JJZCA9IGBjcm9uLSR7d29ya2Zsb3cucGF0aH0tJHt1dWlkKCkuc2xpY2UoMCwgOCl9YDtcblxuXHRcdFx0XHR0aGlzLmxvZ2dlci5sb2coYFNjaGVkdWxpbmcgd29ya2Zsb3c6ICR7d29ya2Zsb3cucGF0aH0gd2l0aCBzY2hlZHVsZTogJHtjb25maWcuc2NoZWR1bGV9ICgke2NvbmZpZy50aW1lem9uZX0pYCk7XG5cblx0XHRcdFx0Y29uc3Qgam9iID0gbmV3IENyb25Kb2IoXG5cdFx0XHRcdFx0Y29uZmlnLnNjaGVkdWxlLFxuXHRcdFx0XHRcdGFzeW5jICgpID0+IHtcblx0XHRcdFx0XHRcdGF3YWl0IHRoaXMuZXhlY3V0ZVdvcmtmbG93KGpvYklkLCB3b3JrZmxvdywgY29uZmlnLCBmYWxzZSk7XG5cdFx0XHRcdFx0fSxcblx0XHRcdFx0XHRudWxsLCAvLyBvbkNvbXBsZXRlXG5cdFx0XHRcdFx0ZmFsc2UsIC8vIHN0YXJ0XG5cdFx0XHRcdFx0Y29uZmlnLnRpbWV6b25lLFxuXHRcdFx0XHQpO1xuXG5cdFx0XHRcdGNvbnN0IHNjaGVkdWxlZEpvYjogU2NoZWR1bGVkSm9iID0ge1xuXHRcdFx0XHRcdGlkOiBqb2JJZCxcblx0XHRcdFx0XHR3b3JrZmxvd1BhdGg6IHdvcmtmbG93LnBhdGgsXG5cdFx0XHRcdFx0c2NoZWR1bGU6IGNvbmZpZy5zY2hlZHVsZSxcblx0XHRcdFx0XHR0aW1lem9uZTogY29uZmlnLnRpbWV6b25lLFxuXHRcdFx0XHRcdG92ZXJsYXA6IGNvbmZpZy5vdmVybGFwID8/IGZhbHNlLFxuXHRcdFx0XHRcdHJ1bm5pbmc6IGZhbHNlLFxuXHRcdFx0XHRcdG5leHRSdW46IHRoaXMudG9EYXRlKGpvYi5uZXh0RGF0ZSgpKSxcblx0XHRcdFx0XHRqb2IsXG5cdFx0XHRcdH07XG5cblx0XHRcdFx0dGhpcy5qb2JzLnNldChqb2JJZCwgc2NoZWR1bGVkSm9iKTtcblxuXHRcdFx0XHQvLyBTdGFydCB0aGUgam9iXG5cdFx0XHRcdGpvYi5zdGFydCgpO1xuXHRcdFx0XHR0aGlzLmxvZ2dlci5sb2coYEpvYiAke2pvYklkfSBzdGFydGVkLiBOZXh0IHJ1bjogJHtzY2hlZHVsZWRKb2IubmV4dFJ1bn1gKTtcblx0XHRcdH1cblxuXHRcdFx0dGhpcy5sb2dnZXIubG9nKGBDcm9uIHRyaWdnZXIgc3RhcnRlZC4gJHt0aGlzLmpvYnMuc2l6ZX0gam9iKHMpIHNjaGVkdWxlZGApO1xuXG5cdFx0XHQvLyBFbmFibGUgSE1SIGluIGRldmVsb3BtZW50IG1vZGVcblx0XHRcdGlmIChwcm9jZXNzLmVudi5CTE9LX0hNUiA9PT0gXCJ0cnVlXCIgfHwgcHJvY2Vzcy5lbnYuTk9ERV9FTlYgPT09IFwiZGV2ZWxvcG1lbnRcIikge1xuXHRcdFx0XHRhd2FpdCB0aGlzLmVuYWJsZUhvdFJlbG9hZCgpO1xuXHRcdFx0fVxuXG5cdFx0XHRyZXR1cm4gdGhpcy5lbmRDb3VudGVyKHN0YXJ0VGltZSk7XG5cdFx0fSBjYXRjaCAoZXJyb3IpIHtcblx0XHRcdHRoaXMubG9nZ2VyLmVycm9yKGBGYWlsZWQgdG8gc3RhcnQgY3JvbiB0cmlnZ2VyOiAkeyhlcnJvciBhcyBFcnJvcikubWVzc2FnZX1gKTtcblx0XHRcdHRocm93IGVycm9yO1xuXHRcdH1cblx0fVxuXG5cdC8qKlxuXHQgKiBTdG9wIGFsbCBjcm9uIGpvYnNcblx0ICovXG5cdGFzeW5jIHN0b3AoKTogUHJvbWlzZTx2b2lkPiB7XG5cdFx0Zm9yIChjb25zdCBbam9iSWQsIHNjaGVkdWxlZEpvYl0gb2YgdGhpcy5qb2JzKSB7XG5cdFx0XHRzY2hlZHVsZWRKb2Iuam9iLnN0b3AoKTtcblx0XHRcdHRoaXMubG9nZ2VyLmxvZyhgSm9iICR7am9iSWR9IHN0b3BwZWRgKTtcblx0XHR9XG5cdFx0dGhpcy5qb2JzLmNsZWFyKCk7XG5cdFx0dGhpcy5sb2dnZXIubG9nKFwiQ3JvbiB0cmlnZ2VyIHN0b3BwZWRcIik7XG5cdH1cblxuXHRwcm90ZWN0ZWQgb3ZlcnJpZGUgYXN5bmMgb25IbXJXb3JrZmxvd0NoYW5nZSgpOiBQcm9taXNlPHZvaWQ+IHtcblx0XHR0aGlzLmxvZ2dlci5sb2coXCJbSE1SXSBDcm9uIHdvcmtmbG93IGNoYW5nZWQsIHJlbG9hZGluZy4uLlwiKTtcblx0XHRhd2FpdCB0aGlzLndhaXRGb3JJbkZsaWdodFJlcXVlc3RzKCk7XG5cdFx0YXdhaXQgdGhpcy5zdG9wKCk7XG5cdFx0dGhpcy5sb2FkV29ya2Zsb3dzKCk7XG5cdFx0YXdhaXQgdGhpcy5saXN0ZW4oKTtcblx0fVxuXG5cdC8qKlxuXHQgKiBNYW51YWxseSB0cmlnZ2VyIGEgc3BlY2lmaWMgam9iXG5cdCAqL1xuXHRhc3luYyB0cmlnZ2VySm9iKGpvYklkOiBzdHJpbmcpOiBQcm9taXNlPFRyaWdnZXJSZXNwb25zZSB8IG51bGw+IHtcblx0XHRjb25zdCBzY2hlZHVsZWRKb2IgPSB0aGlzLmpvYnMuZ2V0KGpvYklkKTtcblx0XHRpZiAoIXNjaGVkdWxlZEpvYikge1xuXHRcdFx0dGhpcy5sb2dnZXIuZXJyb3IoYEpvYiBub3QgZm91bmQ6ICR7am9iSWR9YCk7XG5cdFx0XHRyZXR1cm4gbnVsbDtcblx0XHR9XG5cblx0XHQvLyBHZXQgdGhlIHdvcmtmbG93XG5cdFx0Y29uc3Qgd29ya2Zsb3cgPSB0aGlzLmdldFdvcmtmbG93QnlQYXRoKHNjaGVkdWxlZEpvYi53b3JrZmxvd1BhdGgpO1xuXHRcdGlmICghd29ya2Zsb3cpIHtcblx0XHRcdHRoaXMubG9nZ2VyLmVycm9yKGBXb3JrZmxvdyBub3QgZm91bmQ6ICR7c2NoZWR1bGVkSm9iLndvcmtmbG93UGF0aH1gKTtcblx0XHRcdHJldHVybiBudWxsO1xuXHRcdH1cblxuXHRcdGNvbnN0IGNvbmZpZyA9IHdvcmtmbG93LmNvbmZpZy50cmlnZ2VyPy5jcm9uIGFzIENyb25UcmlnZ2VyT3B0cztcblx0XHRyZXR1cm4gdGhpcy5leGVjdXRlV29ya2Zsb3coam9iSWQsIHdvcmtmbG93LCBjb25maWcsIHRydWUpO1xuXHR9XG5cblx0LyoqXG5cdCAqIEdldCBhbGwgc2NoZWR1bGVkIGpvYnNcblx0ICovXG5cdGdldEpvYnMoKTogU2NoZWR1bGVkSm9iW10ge1xuXHRcdHJldHVybiBBcnJheS5mcm9tKHRoaXMuam9icy52YWx1ZXMoKSkubWFwKChqb2IpID0+ICh7XG5cdFx0XHQuLi5qb2IsXG5cdFx0XHRuZXh0UnVuOiB0aGlzLnRvRGF0ZShqb2Iuam9iLm5leHREYXRlKCkpLFxuXHRcdH0pKTtcblx0fVxuXG5cdC8qKlxuXHQgKiBHZXQgYWxsIHdvcmtmbG93cyB0aGF0IGhhdmUgY3JvbiB0cmlnZ2Vyc1xuXHQgKi9cblx0cHJvdGVjdGVkIGdldENyb25Xb3JrZmxvd3MoKTogQ3JvbldvcmtmbG93TW9kZWxbXSB7XG5cdFx0Y29uc3Qgd29ya2Zsb3dzOiBDcm9uV29ya2Zsb3dNb2RlbFtdID0gW107XG5cblx0XHRmb3IgKGNvbnN0IFtwYXRoLCB3b3JrZmxvd10gb2YgT2JqZWN0LmVudHJpZXModGhpcy5ub2RlTWFwLndvcmtmbG93cyB8fCB7fSkpIHtcblx0XHRcdGNvbnN0IHdvcmtmbG93Q29uZmlnID0gKHdvcmtmbG93IGFzIHVua25vd24gYXMgeyBfY29uZmlnOiBDcm9uV29ya2Zsb3dNb2RlbFtcImNvbmZpZ1wiXSB9KS5fY29uZmlnO1xuXG5cdFx0XHRpZiAod29ya2Zsb3dDb25maWc/LnRyaWdnZXIpIHtcblx0XHRcdFx0Y29uc3QgdHJpZ2dlclR5cGUgPSBPYmplY3Qua2V5cyh3b3JrZmxvd0NvbmZpZy50cmlnZ2VyKVswXTtcblxuXHRcdFx0XHRpZiAodHJpZ2dlclR5cGUgPT09IFwiY3JvblwiICYmIHdvcmtmbG93Q29uZmlnLnRyaWdnZXIuY3Jvbikge1xuXHRcdFx0XHRcdHdvcmtmbG93cy5wdXNoKHtcblx0XHRcdFx0XHRcdHBhdGgsXG5cdFx0XHRcdFx0XHRjb25maWc6IHdvcmtmbG93Q29uZmlnLFxuXHRcdFx0XHRcdH0pO1xuXHRcdFx0XHR9XG5cdFx0XHR9XG5cdFx0fVxuXG5cdFx0cmV0dXJuIHdvcmtmbG93cztcblx0fVxuXG5cdC8qKlxuXHQgKiBHZXQgd29ya2Zsb3cgYnkgcGF0aFxuXHQgKi9cblx0cHJvdGVjdGVkIGdldFdvcmtmbG93QnlQYXRoKHBhdGg6IHN0cmluZyk6IENyb25Xb3JrZmxvd01vZGVsIHwgbnVsbCB7XG5cdFx0Y29uc3Qgd29ya2Zsb3cgPSB0aGlzLm5vZGVNYXAud29ya2Zsb3dzPy5bcGF0aF07XG5cdFx0aWYgKCF3b3JrZmxvdykgcmV0dXJuIG51bGw7XG5cblx0XHRjb25zdCB3b3JrZmxvd0NvbmZpZyA9ICh3b3JrZmxvdyBhcyB1bmtub3duIGFzIHsgX2NvbmZpZzogQ3JvbldvcmtmbG93TW9kZWxbXCJjb25maWdcIl0gfSkuX2NvbmZpZztcblx0XHRyZXR1cm4ge1xuXHRcdFx0cGF0aCxcblx0XHRcdGNvbmZpZzogd29ya2Zsb3dDb25maWcsXG5cdFx0fTtcblx0fVxuXG5cdC8qKlxuXHQgKiBFeGVjdXRlIGEgd29ya2Zsb3dcblx0ICovXG5cdHByb3RlY3RlZCBhc3luYyBleGVjdXRlV29ya2Zsb3coXG5cdFx0am9iSWQ6IHN0cmluZyxcblx0XHR3b3JrZmxvdzogQ3JvbldvcmtmbG93TW9kZWwsXG5cdFx0Y29uZmlnOiBDcm9uVHJpZ2dlck9wdHMsXG5cdFx0bWFudWFsOiBib29sZWFuLFxuXHQpOiBQcm9taXNlPFRyaWdnZXJSZXNwb25zZT4ge1xuXHRcdGNvbnN0IHNjaGVkdWxlZEpvYiA9IHRoaXMuam9icy5nZXQoam9iSWQpO1xuXHRcdGlmICghc2NoZWR1bGVkSm9iKSB7XG5cdFx0XHR0aHJvdyBuZXcgRXJyb3IoYEpvYiBub3QgZm91bmQ6ICR7am9iSWR9YCk7XG5cdFx0fVxuXG5cdFx0Ly8gQ2hlY2sgZm9yIG92ZXJsYXBcblx0XHRpZiAoc2NoZWR1bGVkSm9iLnJ1bm5pbmcgJiYgIXNjaGVkdWxlZEpvYi5vdmVybGFwKSB7XG5cdFx0XHR0aGlzLmxvZ2dlci5sb2coYFNraXBwaW5nICR7am9iSWR9OiBwcmV2aW91cyBleGVjdXRpb24gc3RpbGwgcnVubmluZyAob3ZlcmxhcCBkaXNhYmxlZClgKTtcblx0XHRcdHJldHVybiB7IGN0eDoge30gYXMgQ29udGV4dCwgbWV0cmljczoge30gYXMgTWV0cmljc1R5cGUgfTtcblx0XHR9XG5cblx0XHRjb25zdCBleGVjdXRpb25JZCA9IHV1aWQoKTtcblx0XHRjb25zdCBsYXN0RGF0ZSA9IHNjaGVkdWxlZEpvYi5qb2IubGFzdERhdGUoKTtcblx0XHRjb25zdCBzY2hlZHVsZWRUaW1lID0gbGFzdERhdGUgPyBuZXcgRGF0ZShsYXN0RGF0ZSBhcyB1bmtub3duIGFzIHN0cmluZyB8IG51bWJlcikgOiBuZXcgRGF0ZSgpO1xuXHRcdGNvbnN0IGV4ZWN1dGlvblRpbWUgPSBuZXcgRGF0ZSgpO1xuXG5cdFx0Y29uc3QgZGVmYXVsdE1ldGVyID0gbWV0cmljcy5nZXRNZXRlcihcImRlZmF1bHRcIik7XG5cdFx0Y29uc3QgY3JvbkV4ZWN1dGlvbnMgPSBkZWZhdWx0TWV0ZXIuY3JlYXRlQ291bnRlcihcImNyb25fZXhlY3V0aW9uc1wiLCB7XG5cdFx0XHRkZXNjcmlwdGlvbjogXCJDcm9uIGpvYiBleGVjdXRpb25zXCIsXG5cdFx0fSk7XG5cdFx0Y29uc3QgY3JvbkVycm9ycyA9IGRlZmF1bHRNZXRlci5jcmVhdGVDb3VudGVyKFwiY3Jvbl9lcnJvcnNcIiwge1xuXHRcdFx0ZGVzY3JpcHRpb246IFwiQ3JvbiBqb2IgZXhlY3V0aW9uIGVycm9yc1wiLFxuXHRcdH0pO1xuXG5cdFx0cmV0dXJuIG5ldyBQcm9taXNlKChyZXNvbHZlKSA9PiB7XG5cdFx0XHR0aGlzLnRyYWNlci5zdGFydEFjdGl2ZVNwYW4oYGNyb246JHt3b3JrZmxvdy5wYXRofWAsIGFzeW5jIChzcGFuOiBTcGFuKSA9PiB7XG5cdFx0XHRcdHNjaGVkdWxlZEpvYi5ydW5uaW5nID0gdHJ1ZTtcblxuXHRcdFx0XHR0cnkge1xuXHRcdFx0XHRcdGNvbnN0IHN0YXJ0ID0gcGVyZm9ybWFuY2Uubm93KCk7XG5cblx0XHRcdFx0XHQvLyBJbml0aWFsaXplIGNvbmZpZ3VyYXRpb24gZm9yIHRoaXMgd29ya2Zsb3dcblx0XHRcdFx0XHRhd2FpdCB0aGlzLmNvbmZpZ3VyYXRpb24uaW5pdCh3b3JrZmxvdy5wYXRoLCB0aGlzLm5vZGVNYXApO1xuXG5cdFx0XHRcdFx0Ly8gQ3JlYXRlIGNvbnRleHRcblx0XHRcdFx0XHRjb25zdCBjdHg6IENvbnRleHQgPSB0aGlzLmNyZWF0ZUNvbnRleHQodW5kZWZpbmVkLCB3b3JrZmxvdy5wYXRoLCBleGVjdXRpb25JZCk7XG5cblx0XHRcdFx0XHQvLyBDcmVhdGUgZXhlY3V0aW9uIGNvbnRleHRcblx0XHRcdFx0XHRjb25zdCBjcm9uQ29udGV4dDogQ3JvbkV4ZWN1dGlvbkNvbnRleHQgPSB7XG5cdFx0XHRcdFx0XHRqb2JJZCxcblx0XHRcdFx0XHRcdHNjaGVkdWxlZFRpbWUsXG5cdFx0XHRcdFx0XHRleGVjdXRpb25UaW1lLFxuXHRcdFx0XHRcdFx0c2NoZWR1bGU6IGNvbmZpZy5zY2hlZHVsZSxcblx0XHRcdFx0XHRcdHRpbWV6b25lOiBjb25maWcudGltZXpvbmUsXG5cdFx0XHRcdFx0XHRtYW51YWwsXG5cdFx0XHRcdFx0fTtcblxuXHRcdFx0XHRcdC8vIFBvcHVsYXRlIHJlcXVlc3Qgd2l0aCBjcm9uIGNvbnRleHRcblx0XHRcdFx0XHRjdHgucmVxdWVzdCA9IHtcblx0XHRcdFx0XHRcdGJvZHk6IGNyb25Db250ZXh0LFxuXHRcdFx0XHRcdFx0aGVhZGVyczoge1xuXHRcdFx0XHRcdFx0XHRcIngtY3Jvbi1qb2ItaWRcIjogam9iSWQsXG5cdFx0XHRcdFx0XHRcdFwieC1jcm9uLXNjaGVkdWxlXCI6IGNvbmZpZy5zY2hlZHVsZSxcblx0XHRcdFx0XHRcdFx0XCJ4LWNyb24tdGltZXpvbmVcIjogY29uZmlnLnRpbWV6b25lLFxuXHRcdFx0XHRcdFx0XHRcIngtY3Jvbi1tYW51YWxcIjogU3RyaW5nKG1hbnVhbCksXG5cdFx0XHRcdFx0XHR9LFxuXHRcdFx0XHRcdFx0cXVlcnk6IHt9LFxuXHRcdFx0XHRcdFx0cGFyYW1zOiB7XG5cdFx0XHRcdFx0XHRcdGpvYklkLFxuXHRcdFx0XHRcdFx0XHRzY2hlZHVsZTogY29uZmlnLnNjaGVkdWxlLFxuXHRcdFx0XHRcdFx0fSxcblx0XHRcdFx0XHR9IGFzIHVua25vd24gYXMgUmVxdWVzdENvbnRleHQ7XG5cblx0XHRcdFx0XHQvLyBTdG9yZSBjcm9uIGNvbnRleHQgaW4gdmFyc1xuXHRcdFx0XHRcdGlmICghY3R4LnZhcnMpIGN0eC52YXJzID0ge307XG5cdFx0XHRcdFx0Y3R4LnZhcnMuX2Nyb25fY29udGV4dCA9IHtcblx0XHRcdFx0XHRcdGpvYklkLFxuXHRcdFx0XHRcdFx0c2NoZWR1bGVkVGltZTogc2NoZWR1bGVkVGltZS50b0lTT1N0cmluZygpLFxuXHRcdFx0XHRcdFx0ZXhlY3V0aW9uVGltZTogZXhlY3V0aW9uVGltZS50b0lTT1N0cmluZygpLFxuXHRcdFx0XHRcdFx0c2NoZWR1bGU6IGNvbmZpZy5zY2hlZHVsZSxcblx0XHRcdFx0XHRcdHRpbWV6b25lOiBjb25maWcudGltZXpvbmUsXG5cdFx0XHRcdFx0XHRtYW51YWw6IFN0cmluZyhtYW51YWwpLFxuXHRcdFx0XHRcdH07XG5cblx0XHRcdFx0XHRjdHgubG9nZ2VyLmxvZyhgRXhlY3V0aW5nIGNyb24gam9iOiAke2pvYklkfSAoJHttYW51YWwgPyBcIm1hbnVhbFwiIDogXCJzY2hlZHVsZWRcIn0pYCk7XG5cblx0XHRcdFx0XHQvLyBFeGVjdXRlIHdvcmtmbG93XG5cdFx0XHRcdFx0Y29uc3QgcmVzcG9uc2U6IFRyaWdnZXJSZXNwb25zZSA9IGF3YWl0IHRoaXMucnVuKGN0eCk7XG5cdFx0XHRcdFx0Y29uc3QgZW5kID0gcGVyZm9ybWFuY2Uubm93KCk7XG5cblx0XHRcdFx0XHQvLyBVcGRhdGUgam9iIHN0YXRlXG5cdFx0XHRcdFx0c2NoZWR1bGVkSm9iLmxhc3RSdW4gPSBleGVjdXRpb25UaW1lO1xuXHRcdFx0XHRcdHNjaGVkdWxlZEpvYi5uZXh0UnVuID0gdGhpcy50b0RhdGUoc2NoZWR1bGVkSm9iLmpvYi5uZXh0RGF0ZSgpKTtcblxuXHRcdFx0XHRcdC8vIFNldCBzcGFuIGF0dHJpYnV0ZXNcblx0XHRcdFx0XHRzcGFuLnNldEF0dHJpYnV0ZShcInN1Y2Nlc3NcIiwgdHJ1ZSk7XG5cdFx0XHRcdFx0c3Bhbi5zZXRBdHRyaWJ1dGUoXCJqb2JfaWRcIiwgam9iSWQpO1xuXHRcdFx0XHRcdHNwYW4uc2V0QXR0cmlidXRlKFwid29ya2Zsb3dfcGF0aFwiLCB3b3JrZmxvdy5wYXRoKTtcblx0XHRcdFx0XHRzcGFuLnNldEF0dHJpYnV0ZShcInNjaGVkdWxlXCIsIGNvbmZpZy5zY2hlZHVsZSk7XG5cdFx0XHRcdFx0c3Bhbi5zZXRBdHRyaWJ1dGUoXCJ0aW1lem9uZVwiLCBjb25maWcudGltZXpvbmUpO1xuXHRcdFx0XHRcdHNwYW4uc2V0QXR0cmlidXRlKFwibWFudWFsXCIsIG1hbnVhbCk7XG5cdFx0XHRcdFx0c3Bhbi5zZXRBdHRyaWJ1dGUoXCJlbGFwc2VkX21zXCIsIGVuZCAtIHN0YXJ0KTtcblx0XHRcdFx0XHRzcGFuLnNldFN0YXR1cyh7IGNvZGU6IFNwYW5TdGF0dXNDb2RlLk9LIH0pO1xuXG5cdFx0XHRcdFx0Ly8gUmVjb3JkIG1ldHJpY3Ncblx0XHRcdFx0XHRjcm9uRXhlY3V0aW9ucy5hZGQoMSwge1xuXHRcdFx0XHRcdFx0ZW52OiBwcm9jZXNzLmVudi5OT0RFX0VOVixcblx0XHRcdFx0XHRcdGpvYl9pZDogam9iSWQsXG5cdFx0XHRcdFx0XHR3b3JrZmxvd19uYW1lOiB0aGlzLmNvbmZpZ3VyYXRpb24ubmFtZSxcblx0XHRcdFx0XHRcdG1hbnVhbDogU3RyaW5nKG1hbnVhbCksXG5cdFx0XHRcdFx0XHRzdWNjZXNzOiBcInRydWVcIixcblx0XHRcdFx0XHR9KTtcblxuXHRcdFx0XHRcdGN0eC5sb2dnZXIubG9nKGBDcm9uIGpvYiBjb21wbGV0ZWQgaW4gJHsoZW5kIC0gc3RhcnQpLnRvRml4ZWQoMil9bXM6ICR7am9iSWR9YCk7XG5cblx0XHRcdFx0XHRyZXNvbHZlKHJlc3BvbnNlKTtcblx0XHRcdFx0fSBjYXRjaCAoZXJyb3IpIHtcblx0XHRcdFx0XHRjb25zdCBlcnJvck1lc3NhZ2UgPSAoZXJyb3IgYXMgRXJyb3IpLm1lc3NhZ2U7XG5cblx0XHRcdFx0XHQvLyBTZXQgc3BhbiBlcnJvclxuXHRcdFx0XHRcdHNwYW4uc2V0QXR0cmlidXRlKFwic3VjY2Vzc1wiLCBmYWxzZSk7XG5cdFx0XHRcdFx0c3Bhbi5yZWNvcmRFeGNlcHRpb24oZXJyb3IgYXMgRXJyb3IpO1xuXHRcdFx0XHRcdHNwYW4uc2V0U3RhdHVzKHsgY29kZTogU3BhblN0YXR1c0NvZGUuRVJST1IsIG1lc3NhZ2U6IGVycm9yTWVzc2FnZSB9KTtcblxuXHRcdFx0XHRcdC8vIFJlY29yZCBlcnJvciBtZXRyaWNzXG5cdFx0XHRcdFx0Y3JvbkVycm9ycy5hZGQoMSwge1xuXHRcdFx0XHRcdFx0ZW52OiBwcm9jZXNzLmVudi5OT0RFX0VOVixcblx0XHRcdFx0XHRcdGpvYl9pZDogam9iSWQsXG5cdFx0XHRcdFx0XHR3b3JrZmxvd19uYW1lOiB0aGlzLmNvbmZpZ3VyYXRpb24/Lm5hbWUgfHwgXCJ1bmtub3duXCIsXG5cdFx0XHRcdFx0XHRtYW51YWw6IFN0cmluZyhtYW51YWwpLFxuXHRcdFx0XHRcdH0pO1xuXG5cdFx0XHRcdFx0dGhpcy5sb2dnZXIuZXJyb3IoYENyb24gam9iIGZhaWxlZCAke2pvYklkfTogJHtlcnJvck1lc3NhZ2V9YCwgKGVycm9yIGFzIEVycm9yKS5zdGFjayk7XG5cblx0XHRcdFx0XHRyZXNvbHZlKHsgY3R4OiB7fSBhcyBDb250ZXh0LCBtZXRyaWNzOiB7fSBhcyBNZXRyaWNzVHlwZSB9KTtcblx0XHRcdFx0fSBmaW5hbGx5IHtcblx0XHRcdFx0XHRzY2hlZHVsZWRKb2IucnVubmluZyA9IGZhbHNlO1xuXHRcdFx0XHRcdHNwYW4uZW5kKCk7XG5cdFx0XHRcdH1cblx0XHRcdH0pO1xuXHRcdH0pO1xuXHR9XG59XG5cbmV4cG9ydCBkZWZhdWx0IENyb25UcmlnZ2VyO1xuIl19
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @blok/trigger-cron
|
|
3
|
+
*
|
|
4
|
+
* Cron/scheduled trigger for Blok workflows.
|
|
5
|
+
* Execute workflows on a schedule using cron expressions.
|
|
6
|
+
*
|
|
7
|
+
* Features:
|
|
8
|
+
* - Standard cron expression syntax
|
|
9
|
+
* - Timezone-aware scheduling
|
|
10
|
+
* - Overlap prevention
|
|
11
|
+
* - Manual trigger support
|
|
12
|
+
* - Job management API
|
|
13
|
+
*
|
|
14
|
+
* @example
|
|
15
|
+
* ```typescript
|
|
16
|
+
* import { CronTrigger } from "@blok/trigger-cron";
|
|
17
|
+
*
|
|
18
|
+
* class MyCronTrigger extends CronTrigger {
|
|
19
|
+
* protected nodes = myNodes;
|
|
20
|
+
* protected workflows = myWorkflows;
|
|
21
|
+
* }
|
|
22
|
+
*
|
|
23
|
+
* const trigger = new MyCronTrigger();
|
|
24
|
+
* await trigger.listen();
|
|
25
|
+
*
|
|
26
|
+
* // List all scheduled jobs
|
|
27
|
+
* const jobs = trigger.getJobs();
|
|
28
|
+
*
|
|
29
|
+
* // Manually trigger a job
|
|
30
|
+
* await trigger.triggerJob("cron-my-workflow-abc123");
|
|
31
|
+
* ```
|
|
32
|
+
*
|
|
33
|
+
* Workflow Definition:
|
|
34
|
+
* ```typescript
|
|
35
|
+
* Workflow({ name: "daily-cleanup", version: "1.0.0" })
|
|
36
|
+
* .addTrigger("cron", {
|
|
37
|
+
* schedule: "0 2 * * *", // Run at 2 AM daily
|
|
38
|
+
* timezone: "America/New_York",
|
|
39
|
+
* overlap: false,
|
|
40
|
+
* })
|
|
41
|
+
* .addStep({ ... });
|
|
42
|
+
* ```
|
|
43
|
+
*
|
|
44
|
+
* Cron Expression Format:
|
|
45
|
+
* ```
|
|
46
|
+
* * * * * * *
|
|
47
|
+
* │ │ │ │ │ │
|
|
48
|
+
* │ │ │ │ │ └── Day of week (0-7, Sun=0,7)
|
|
49
|
+
* │ │ │ │ └──── Month (1-12)
|
|
50
|
+
* │ │ │ └────── Day of month (1-31)
|
|
51
|
+
* │ │ └──────── Hour (0-23)
|
|
52
|
+
* │ └────────── Minute (0-59)
|
|
53
|
+
* └──────────── Second (0-59, optional)
|
|
54
|
+
* ```
|
|
55
|
+
*
|
|
56
|
+
* Common schedules:
|
|
57
|
+
* - "* * * * *" - Every minute
|
|
58
|
+
* - "0 * * * *" - Every hour
|
|
59
|
+
* - "0 0 * * *" - Every day at midnight
|
|
60
|
+
* - "0 0 * * 0" - Every Sunday at midnight
|
|
61
|
+
* - "0 0 1 * *" - First day of every month
|
|
62
|
+
*/
|
|
63
|
+
export { CronTrigger, type ScheduledJob, type CronExecutionContext, } from "./CronTrigger";
|
|
64
|
+
export type { CronTriggerOpts } from "@blok/helper";
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* @blok/trigger-cron
|
|
4
|
+
*
|
|
5
|
+
* Cron/scheduled trigger for Blok workflows.
|
|
6
|
+
* Execute workflows on a schedule using cron expressions.
|
|
7
|
+
*
|
|
8
|
+
* Features:
|
|
9
|
+
* - Standard cron expression syntax
|
|
10
|
+
* - Timezone-aware scheduling
|
|
11
|
+
* - Overlap prevention
|
|
12
|
+
* - Manual trigger support
|
|
13
|
+
* - Job management API
|
|
14
|
+
*
|
|
15
|
+
* @example
|
|
16
|
+
* ```typescript
|
|
17
|
+
* import { CronTrigger } from "@blok/trigger-cron";
|
|
18
|
+
*
|
|
19
|
+
* class MyCronTrigger extends CronTrigger {
|
|
20
|
+
* protected nodes = myNodes;
|
|
21
|
+
* protected workflows = myWorkflows;
|
|
22
|
+
* }
|
|
23
|
+
*
|
|
24
|
+
* const trigger = new MyCronTrigger();
|
|
25
|
+
* await trigger.listen();
|
|
26
|
+
*
|
|
27
|
+
* // List all scheduled jobs
|
|
28
|
+
* const jobs = trigger.getJobs();
|
|
29
|
+
*
|
|
30
|
+
* // Manually trigger a job
|
|
31
|
+
* await trigger.triggerJob("cron-my-workflow-abc123");
|
|
32
|
+
* ```
|
|
33
|
+
*
|
|
34
|
+
* Workflow Definition:
|
|
35
|
+
* ```typescript
|
|
36
|
+
* Workflow({ name: "daily-cleanup", version: "1.0.0" })
|
|
37
|
+
* .addTrigger("cron", {
|
|
38
|
+
* schedule: "0 2 * * *", // Run at 2 AM daily
|
|
39
|
+
* timezone: "America/New_York",
|
|
40
|
+
* overlap: false,
|
|
41
|
+
* })
|
|
42
|
+
* .addStep({ ... });
|
|
43
|
+
* ```
|
|
44
|
+
*
|
|
45
|
+
* Cron Expression Format:
|
|
46
|
+
* ```
|
|
47
|
+
* * * * * * *
|
|
48
|
+
* │ │ │ │ │ │
|
|
49
|
+
* │ │ │ │ │ └── Day of week (0-7, Sun=0,7)
|
|
50
|
+
* │ │ │ │ └──── Month (1-12)
|
|
51
|
+
* │ │ │ └────── Day of month (1-31)
|
|
52
|
+
* │ │ └──────── Hour (0-23)
|
|
53
|
+
* │ └────────── Minute (0-59)
|
|
54
|
+
* └──────────── Second (0-59, optional)
|
|
55
|
+
* ```
|
|
56
|
+
*
|
|
57
|
+
* Common schedules:
|
|
58
|
+
* - "* * * * *" - Every minute
|
|
59
|
+
* - "0 * * * *" - Every hour
|
|
60
|
+
* - "0 0 * * *" - Every day at midnight
|
|
61
|
+
* - "0 0 * * 0" - Every Sunday at midnight
|
|
62
|
+
* - "0 0 1 * *" - First day of every month
|
|
63
|
+
*/
|
|
64
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
65
|
+
exports.CronTrigger = void 0;
|
|
66
|
+
// Core exports
|
|
67
|
+
var CronTrigger_1 = require("./CronTrigger");
|
|
68
|
+
Object.defineProperty(exports, "CronTrigger", { enumerable: true, get: function () { return CronTrigger_1.CronTrigger; } });
|
|
69
|
+
//# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiaW5kZXguanMiLCJzb3VyY2VSb290IjoiIiwic291cmNlcyI6WyIuLi9zcmMvaW5kZXgudHMiXSwibmFtZXMiOltdLCJtYXBwaW5ncyI6IjtBQUFBOzs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7O0dBNkRHOzs7QUFFSCxlQUFlO0FBQ2YsNkNBSXVCO0FBSHRCLDBHQUFBLFdBQVcsT0FBQSIsInNvdXJjZXNDb250ZW50IjpbIi8qKlxuICogQGJsb2svdHJpZ2dlci1jcm9uXG4gKlxuICogQ3Jvbi9zY2hlZHVsZWQgdHJpZ2dlciBmb3IgQmxvayB3b3JrZmxvd3MuXG4gKiBFeGVjdXRlIHdvcmtmbG93cyBvbiBhIHNjaGVkdWxlIHVzaW5nIGNyb24gZXhwcmVzc2lvbnMuXG4gKlxuICogRmVhdHVyZXM6XG4gKiAtIFN0YW5kYXJkIGNyb24gZXhwcmVzc2lvbiBzeW50YXhcbiAqIC0gVGltZXpvbmUtYXdhcmUgc2NoZWR1bGluZ1xuICogLSBPdmVybGFwIHByZXZlbnRpb25cbiAqIC0gTWFudWFsIHRyaWdnZXIgc3VwcG9ydFxuICogLSBKb2IgbWFuYWdlbWVudCBBUElcbiAqXG4gKiBAZXhhbXBsZVxuICogYGBgdHlwZXNjcmlwdFxuICogaW1wb3J0IHsgQ3JvblRyaWdnZXIgfSBmcm9tIFwiQGJsb2svdHJpZ2dlci1jcm9uXCI7XG4gKlxuICogY2xhc3MgTXlDcm9uVHJpZ2dlciBleHRlbmRzIENyb25UcmlnZ2VyIHtcbiAqICAgcHJvdGVjdGVkIG5vZGVzID0gbXlOb2RlcztcbiAqICAgcHJvdGVjdGVkIHdvcmtmbG93cyA9IG15V29ya2Zsb3dzO1xuICogfVxuICpcbiAqIGNvbnN0IHRyaWdnZXIgPSBuZXcgTXlDcm9uVHJpZ2dlcigpO1xuICogYXdhaXQgdHJpZ2dlci5saXN0ZW4oKTtcbiAqXG4gKiAvLyBMaXN0IGFsbCBzY2hlZHVsZWQgam9ic1xuICogY29uc3Qgam9icyA9IHRyaWdnZXIuZ2V0Sm9icygpO1xuICpcbiAqIC8vIE1hbnVhbGx5IHRyaWdnZXIgYSBqb2JcbiAqIGF3YWl0IHRyaWdnZXIudHJpZ2dlckpvYihcImNyb24tbXktd29ya2Zsb3ctYWJjMTIzXCIpO1xuICogYGBgXG4gKlxuICogV29ya2Zsb3cgRGVmaW5pdGlvbjpcbiAqIGBgYHR5cGVzY3JpcHRcbiAqIFdvcmtmbG93KHsgbmFtZTogXCJkYWlseS1jbGVhbnVwXCIsIHZlcnNpb246IFwiMS4wLjBcIiB9KVxuICogICAuYWRkVHJpZ2dlcihcImNyb25cIiwge1xuICogICAgIHNjaGVkdWxlOiBcIjAgMiAqICogKlwiLCAgLy8gUnVuIGF0IDIgQU0gZGFpbHlcbiAqICAgICB0aW1lem9uZTogXCJBbWVyaWNhL05ld19Zb3JrXCIsXG4gKiAgICAgb3ZlcmxhcDogZmFsc2UsXG4gKiAgIH0pXG4gKiAgIC5hZGRTdGVwKHsgLi4uIH0pO1xuICogYGBgXG4gKlxuICogQ3JvbiBFeHByZXNzaW9uIEZvcm1hdDpcbiAqIGBgYFxuICogKiAqICogKiAqICpcbiAqIOKUgiDilIIg4pSCIOKUgiDilIIg4pSCXG4gKiDilIIg4pSCIOKUgiDilIIg4pSCIOKUlOKUgOKUgCBEYXkgb2Ygd2VlayAoMC03LCBTdW49MCw3KVxuICog4pSCIOKUgiDilIIg4pSCIOKUlOKUgOKUgOKUgOKUgCBNb250aCAoMS0xMilcbiAqIOKUgiDilIIg4pSCIOKUlOKUgOKUgOKUgOKUgOKUgOKUgCBEYXkgb2YgbW9udGggKDEtMzEpXG4gKiDilIIg4pSCIOKUlOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgCBIb3VyICgwLTIzKVxuICog4pSCIOKUlOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgCBNaW51dGUgKDAtNTkpXG4gKiDilJTilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIAgU2Vjb25kICgwLTU5LCBvcHRpb25hbClcbiAqIGBgYFxuICpcbiAqIENvbW1vbiBzY2hlZHVsZXM6XG4gKiAtIFwiKiAqICogKiAqXCIgLSBFdmVyeSBtaW51dGVcbiAqIC0gXCIwICogKiAqICpcIiAtIEV2ZXJ5IGhvdXJcbiAqIC0gXCIwIDAgKiAqICpcIiAtIEV2ZXJ5IGRheSBhdCBtaWRuaWdodFxuICogLSBcIjAgMCAqICogMFwiIC0gRXZlcnkgU3VuZGF5IGF0IG1pZG5pZ2h0XG4gKiAtIFwiMCAwIDEgKiAqXCIgLSBGaXJzdCBkYXkgb2YgZXZlcnkgbW9udGhcbiAqL1xuXG4vLyBDb3JlIGV4cG9ydHNcbmV4cG9ydCB7XG5cdENyb25UcmlnZ2VyLFxuXHR0eXBlIFNjaGVkdWxlZEpvYixcblx0dHlwZSBDcm9uRXhlY3V0aW9uQ29udGV4dCxcbn0gZnJvbSBcIi4vQ3JvblRyaWdnZXJcIjtcblxuLy8gUmUtZXhwb3J0IHR5cGVzIGZyb20gaGVscGVyIGZvciBjb252ZW5pZW5jZVxuZXhwb3J0IHR5cGUgeyBDcm9uVHJpZ2dlck9wdHMgfSBmcm9tIFwiQGJsb2svaGVscGVyXCI7XG4iXX0=
|
package/package.json
ADDED
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@blokjs/trigger-cron",
|
|
3
|
+
"version": "0.2.0",
|
|
4
|
+
"description": "Cron/scheduled trigger for Blok workflows - supports cron expressions and interval-based scheduling",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "dist/index.js",
|
|
7
|
+
"types": "dist/index.d.ts",
|
|
8
|
+
"scripts": {
|
|
9
|
+
"build": "rm -rf dist && bun run tsc",
|
|
10
|
+
"build:dev": "tsc --watch",
|
|
11
|
+
"test": "vitest run",
|
|
12
|
+
"test:dev": "vitest"
|
|
13
|
+
},
|
|
14
|
+
"author": "Deskree Technologies Inc.",
|
|
15
|
+
"license": "Apache-2.0",
|
|
16
|
+
"dependencies": {
|
|
17
|
+
"@blokjs/helper": "workspace:*",
|
|
18
|
+
"@blokjs/runner": "workspace:*",
|
|
19
|
+
"@blokjs/shared": "workspace:*",
|
|
20
|
+
"@opentelemetry/api": "^1.9.0",
|
|
21
|
+
"cron": "^4.4.0",
|
|
22
|
+
"uuid": "^11.1.0"
|
|
23
|
+
},
|
|
24
|
+
"devDependencies": {
|
|
25
|
+
"@types/cron": "^2.4.3",
|
|
26
|
+
"@types/node": "^22.15.21",
|
|
27
|
+
"@types/uuid": "^11.0.0",
|
|
28
|
+
"typescript": "^5.8.3",
|
|
29
|
+
"vitest": "^4.0.18"
|
|
30
|
+
},
|
|
31
|
+
"private": false,
|
|
32
|
+
"publishConfig": {
|
|
33
|
+
"access": "public"
|
|
34
|
+
}
|
|
35
|
+
}
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* CronTrigger Tests
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { describe, expect, it, vi } from "vitest";
|
|
6
|
+
|
|
7
|
+
describe("CronTrigger", () => {
|
|
8
|
+
describe("ScheduledJob Interface", () => {
|
|
9
|
+
it("should accept valid scheduled job structure", () => {
|
|
10
|
+
const mockJob = {
|
|
11
|
+
nextDate: () => new Date(),
|
|
12
|
+
lastDate: () => new Date(),
|
|
13
|
+
start: vi.fn(),
|
|
14
|
+
stop: vi.fn(),
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
const scheduledJob = {
|
|
18
|
+
id: "cron-test-abc123",
|
|
19
|
+
workflowPath: "daily-cleanup",
|
|
20
|
+
schedule: "0 2 * * *",
|
|
21
|
+
timezone: "America/New_York",
|
|
22
|
+
overlap: false,
|
|
23
|
+
running: false,
|
|
24
|
+
lastRun: new Date(),
|
|
25
|
+
nextRun: new Date(),
|
|
26
|
+
job: mockJob,
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
expect(scheduledJob.id).toBe("cron-test-abc123");
|
|
30
|
+
expect(scheduledJob.schedule).toBe("0 2 * * *");
|
|
31
|
+
expect(scheduledJob.timezone).toBe("America/New_York");
|
|
32
|
+
expect(scheduledJob.overlap).toBe(false);
|
|
33
|
+
});
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
describe("CronExecutionContext Interface", () => {
|
|
37
|
+
it("should create valid execution context", () => {
|
|
38
|
+
const context = {
|
|
39
|
+
jobId: "cron-job-123",
|
|
40
|
+
scheduledTime: new Date("2024-01-01T02:00:00Z"),
|
|
41
|
+
executionTime: new Date("2024-01-01T02:00:01Z"),
|
|
42
|
+
schedule: "0 2 * * *",
|
|
43
|
+
timezone: "UTC",
|
|
44
|
+
manual: false,
|
|
45
|
+
};
|
|
46
|
+
|
|
47
|
+
expect(context.jobId).toBe("cron-job-123");
|
|
48
|
+
expect(context.scheduledTime.toISOString()).toBe("2024-01-01T02:00:00.000Z");
|
|
49
|
+
expect(context.manual).toBe(false);
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
it("should support manual trigger context", () => {
|
|
53
|
+
const context = {
|
|
54
|
+
jobId: "cron-job-456",
|
|
55
|
+
scheduledTime: new Date(),
|
|
56
|
+
executionTime: new Date(),
|
|
57
|
+
schedule: "0 * * * *",
|
|
58
|
+
timezone: "Europe/London",
|
|
59
|
+
manual: true,
|
|
60
|
+
};
|
|
61
|
+
|
|
62
|
+
expect(context.manual).toBe(true);
|
|
63
|
+
});
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
describe("CronTriggerOpts Schema", () => {
|
|
67
|
+
it("should validate cron trigger configuration", () => {
|
|
68
|
+
const validConfig = {
|
|
69
|
+
schedule: "0 * * * *",
|
|
70
|
+
timezone: "America/Los_Angeles",
|
|
71
|
+
overlap: false,
|
|
72
|
+
};
|
|
73
|
+
|
|
74
|
+
expect(validConfig.schedule).toBe("0 * * * *");
|
|
75
|
+
expect(validConfig.timezone).toBe("America/Los_Angeles");
|
|
76
|
+
expect(validConfig.overlap).toBe(false);
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
it("should support common cron expressions", () => {
|
|
80
|
+
const schedules = [
|
|
81
|
+
{ expr: "* * * * *", desc: "Every minute" },
|
|
82
|
+
{ expr: "0 * * * *", desc: "Every hour" },
|
|
83
|
+
{ expr: "0 0 * * *", desc: "Every day at midnight" },
|
|
84
|
+
{ expr: "0 0 * * 0", desc: "Every Sunday" },
|
|
85
|
+
{ expr: "0 0 1 * *", desc: "First day of month" },
|
|
86
|
+
{ expr: "*/5 * * * *", desc: "Every 5 minutes" },
|
|
87
|
+
{ expr: "0 9-17 * * 1-5", desc: "Hourly, 9-5 on weekdays" },
|
|
88
|
+
];
|
|
89
|
+
|
|
90
|
+
for (const schedule of schedules) {
|
|
91
|
+
expect(schedule.expr).toBeDefined();
|
|
92
|
+
expect(schedule.desc).toBeDefined();
|
|
93
|
+
}
|
|
94
|
+
});
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
describe("Timezone Support", () => {
|
|
98
|
+
it("should support common timezones", () => {
|
|
99
|
+
const timezones = [
|
|
100
|
+
"UTC",
|
|
101
|
+
"America/New_York",
|
|
102
|
+
"America/Los_Angeles",
|
|
103
|
+
"Europe/London",
|
|
104
|
+
"Europe/Paris",
|
|
105
|
+
"Asia/Tokyo",
|
|
106
|
+
"Australia/Sydney",
|
|
107
|
+
];
|
|
108
|
+
|
|
109
|
+
for (const tz of timezones) {
|
|
110
|
+
const config = {
|
|
111
|
+
schedule: "0 0 * * *",
|
|
112
|
+
timezone: tz,
|
|
113
|
+
overlap: false,
|
|
114
|
+
};
|
|
115
|
+
expect(config.timezone).toBe(tz);
|
|
116
|
+
}
|
|
117
|
+
});
|
|
118
|
+
});
|
|
119
|
+
});
|
|
@@ -0,0 +1,432 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* CronTrigger - Scheduled workflow execution based on cron expressions
|
|
3
|
+
*
|
|
4
|
+
* Extends TriggerBase to support scheduled triggers:
|
|
5
|
+
* - Cron expressions (e.g., "0 * * * *" for hourly)
|
|
6
|
+
* - Timezone-aware scheduling
|
|
7
|
+
* - Overlap prevention
|
|
8
|
+
* - Manual trigger support
|
|
9
|
+
*
|
|
10
|
+
* Uses the 'cron' package for cron parsing and scheduling.
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
import type { CronTriggerOpts, HelperResponse } from "@blokjs/helper";
|
|
14
|
+
import {
|
|
15
|
+
DefaultLogger,
|
|
16
|
+
type GlobalOptions,
|
|
17
|
+
type BlokService,
|
|
18
|
+
NodeMap,
|
|
19
|
+
TriggerBase,
|
|
20
|
+
type TriggerResponse,
|
|
21
|
+
} from "@blokjs/runner";
|
|
22
|
+
import type { Context, MetricsType, RequestContext } from "@blokjs/shared";
|
|
23
|
+
import { type Span, SpanStatusCode, metrics, trace } from "@opentelemetry/api";
|
|
24
|
+
import { CronJob } from "cron";
|
|
25
|
+
import { v4 as uuid } from "uuid";
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Scheduled job information
|
|
29
|
+
*/
|
|
30
|
+
export interface ScheduledJob {
|
|
31
|
+
/** Unique job ID */
|
|
32
|
+
id: string;
|
|
33
|
+
/** Workflow path */
|
|
34
|
+
workflowPath: string;
|
|
35
|
+
/** Cron expression */
|
|
36
|
+
schedule: string;
|
|
37
|
+
/** Timezone */
|
|
38
|
+
timezone: string;
|
|
39
|
+
/** Allow overlapping executions */
|
|
40
|
+
overlap: boolean;
|
|
41
|
+
/** Whether the job is currently running */
|
|
42
|
+
running: boolean;
|
|
43
|
+
/** Last execution time */
|
|
44
|
+
lastRun?: Date;
|
|
45
|
+
/** Next scheduled time */
|
|
46
|
+
nextRun?: Date;
|
|
47
|
+
/** Internal CronJob instance */
|
|
48
|
+
job: CronJob;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Execution context passed to the workflow
|
|
53
|
+
*/
|
|
54
|
+
export interface CronExecutionContext {
|
|
55
|
+
/** Job ID */
|
|
56
|
+
jobId: string;
|
|
57
|
+
/** Scheduled time (when it was supposed to run) */
|
|
58
|
+
scheduledTime: Date;
|
|
59
|
+
/** Actual execution time */
|
|
60
|
+
executionTime: Date;
|
|
61
|
+
/** Cron expression */
|
|
62
|
+
schedule: string;
|
|
63
|
+
/** Timezone */
|
|
64
|
+
timezone: string;
|
|
65
|
+
/** Whether this is a manual trigger */
|
|
66
|
+
manual: boolean;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* Workflow model with cron trigger configuration
|
|
71
|
+
*/
|
|
72
|
+
interface CronWorkflowModel {
|
|
73
|
+
path: string;
|
|
74
|
+
config: {
|
|
75
|
+
name: string;
|
|
76
|
+
version: string;
|
|
77
|
+
trigger?: {
|
|
78
|
+
cron?: CronTriggerOpts;
|
|
79
|
+
[key: string]: unknown;
|
|
80
|
+
};
|
|
81
|
+
[key: string]: unknown;
|
|
82
|
+
};
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* CronTrigger - Scheduled workflow execution
|
|
87
|
+
*/
|
|
88
|
+
export abstract class CronTrigger extends TriggerBase {
|
|
89
|
+
protected nodeMap: GlobalOptions = {} as GlobalOptions;
|
|
90
|
+
protected readonly tracer = trace.getTracer(
|
|
91
|
+
process.env.PROJECT_NAME || "trigger-cron-workflow",
|
|
92
|
+
process.env.PROJECT_VERSION || "0.0.1",
|
|
93
|
+
);
|
|
94
|
+
protected readonly logger = new DefaultLogger();
|
|
95
|
+
protected jobs: Map<string, ScheduledJob> = new Map();
|
|
96
|
+
|
|
97
|
+
// Subclasses provide these
|
|
98
|
+
protected abstract nodes: Record<string, BlokService<unknown>>;
|
|
99
|
+
protected abstract workflows: Record<string, HelperResponse>;
|
|
100
|
+
|
|
101
|
+
constructor() {
|
|
102
|
+
super();
|
|
103
|
+
this.loadNodes();
|
|
104
|
+
this.loadWorkflows();
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/**
|
|
108
|
+
* Load nodes into the node map
|
|
109
|
+
*/
|
|
110
|
+
loadNodes(): void {
|
|
111
|
+
this.nodeMap.nodes = new NodeMap();
|
|
112
|
+
const nodeKeys = Object.keys(this.nodes);
|
|
113
|
+
for (const key of nodeKeys) {
|
|
114
|
+
this.nodeMap.nodes.addNode(key, this.nodes[key]);
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/**
|
|
119
|
+
* Load workflows into the workflow map
|
|
120
|
+
*/
|
|
121
|
+
loadWorkflows(): void {
|
|
122
|
+
this.nodeMap.workflows = this.workflows;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
/**
|
|
126
|
+
* Convert cron DateTime to Date
|
|
127
|
+
* The cron package uses luxon DateTime which has toJSDate()
|
|
128
|
+
*/
|
|
129
|
+
protected toDate(dateTime: unknown): Date {
|
|
130
|
+
if (dateTime && typeof dateTime === "object" && "toJSDate" in dateTime) {
|
|
131
|
+
return (dateTime as { toJSDate: () => Date }).toJSDate();
|
|
132
|
+
}
|
|
133
|
+
return dateTime instanceof Date ? dateTime : new Date(dateTime as string | number);
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
/**
|
|
137
|
+
* Start the cron scheduler - main entry point
|
|
138
|
+
*/
|
|
139
|
+
async listen(): Promise<number> {
|
|
140
|
+
const startTime = this.startCounter();
|
|
141
|
+
|
|
142
|
+
try {
|
|
143
|
+
// Find all workflows with cron triggers
|
|
144
|
+
const cronWorkflows = this.getCronWorkflows();
|
|
145
|
+
|
|
146
|
+
if (cronWorkflows.length === 0) {
|
|
147
|
+
this.logger.log("No workflows with cron triggers found");
|
|
148
|
+
return this.endCounter(startTime);
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
// Create and start cron jobs for each workflow
|
|
152
|
+
for (const workflow of cronWorkflows) {
|
|
153
|
+
const config = workflow.config.trigger?.cron as CronTriggerOpts;
|
|
154
|
+
const jobId = `cron-${workflow.path}-${uuid().slice(0, 8)}`;
|
|
155
|
+
|
|
156
|
+
this.logger.log(`Scheduling workflow: ${workflow.path} with schedule: ${config.schedule} (${config.timezone})`);
|
|
157
|
+
|
|
158
|
+
const job = new CronJob(
|
|
159
|
+
config.schedule,
|
|
160
|
+
async () => {
|
|
161
|
+
await this.executeWorkflow(jobId, workflow, config, false);
|
|
162
|
+
},
|
|
163
|
+
null, // onComplete
|
|
164
|
+
false, // start
|
|
165
|
+
config.timezone,
|
|
166
|
+
);
|
|
167
|
+
|
|
168
|
+
const scheduledJob: ScheduledJob = {
|
|
169
|
+
id: jobId,
|
|
170
|
+
workflowPath: workflow.path,
|
|
171
|
+
schedule: config.schedule,
|
|
172
|
+
timezone: config.timezone,
|
|
173
|
+
overlap: config.overlap ?? false,
|
|
174
|
+
running: false,
|
|
175
|
+
nextRun: this.toDate(job.nextDate()),
|
|
176
|
+
job,
|
|
177
|
+
};
|
|
178
|
+
|
|
179
|
+
this.jobs.set(jobId, scheduledJob);
|
|
180
|
+
|
|
181
|
+
// Start the job
|
|
182
|
+
job.start();
|
|
183
|
+
this.logger.log(`Job ${jobId} started. Next run: ${scheduledJob.nextRun}`);
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
this.logger.log(`Cron trigger started. ${this.jobs.size} job(s) scheduled`);
|
|
187
|
+
|
|
188
|
+
// Enable HMR in development mode
|
|
189
|
+
if (process.env.BLOK_HMR === "true" || process.env.NODE_ENV === "development") {
|
|
190
|
+
await this.enableHotReload();
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
return this.endCounter(startTime);
|
|
194
|
+
} catch (error) {
|
|
195
|
+
this.logger.error(`Failed to start cron trigger: ${(error as Error).message}`);
|
|
196
|
+
throw error;
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
/**
|
|
201
|
+
* Stop all cron jobs
|
|
202
|
+
*/
|
|
203
|
+
async stop(): Promise<void> {
|
|
204
|
+
for (const [jobId, scheduledJob] of this.jobs) {
|
|
205
|
+
scheduledJob.job.stop();
|
|
206
|
+
this.logger.log(`Job ${jobId} stopped`);
|
|
207
|
+
}
|
|
208
|
+
this.jobs.clear();
|
|
209
|
+
this.logger.log("Cron trigger stopped");
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
protected override async onHmrWorkflowChange(): Promise<void> {
|
|
213
|
+
this.logger.log("[HMR] Cron workflow changed, reloading...");
|
|
214
|
+
await this.waitForInFlightRequests();
|
|
215
|
+
await this.stop();
|
|
216
|
+
this.loadWorkflows();
|
|
217
|
+
await this.listen();
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
/**
|
|
221
|
+
* Manually trigger a specific job
|
|
222
|
+
*/
|
|
223
|
+
async triggerJob(jobId: string): Promise<TriggerResponse | null> {
|
|
224
|
+
const scheduledJob = this.jobs.get(jobId);
|
|
225
|
+
if (!scheduledJob) {
|
|
226
|
+
this.logger.error(`Job not found: ${jobId}`);
|
|
227
|
+
return null;
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
// Get the workflow
|
|
231
|
+
const workflow = this.getWorkflowByPath(scheduledJob.workflowPath);
|
|
232
|
+
if (!workflow) {
|
|
233
|
+
this.logger.error(`Workflow not found: ${scheduledJob.workflowPath}`);
|
|
234
|
+
return null;
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
const config = workflow.config.trigger?.cron as CronTriggerOpts;
|
|
238
|
+
return this.executeWorkflow(jobId, workflow, config, true);
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
/**
|
|
242
|
+
* Get all scheduled jobs
|
|
243
|
+
*/
|
|
244
|
+
getJobs(): ScheduledJob[] {
|
|
245
|
+
return Array.from(this.jobs.values()).map((job) => ({
|
|
246
|
+
...job,
|
|
247
|
+
nextRun: this.toDate(job.job.nextDate()),
|
|
248
|
+
}));
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
/**
|
|
252
|
+
* Get all workflows that have cron triggers
|
|
253
|
+
*/
|
|
254
|
+
protected getCronWorkflows(): CronWorkflowModel[] {
|
|
255
|
+
const workflows: CronWorkflowModel[] = [];
|
|
256
|
+
|
|
257
|
+
for (const [path, workflow] of Object.entries(this.nodeMap.workflows || {})) {
|
|
258
|
+
const workflowConfig = (workflow as unknown as { _config: CronWorkflowModel["config"] })._config;
|
|
259
|
+
|
|
260
|
+
if (workflowConfig?.trigger) {
|
|
261
|
+
const triggerType = Object.keys(workflowConfig.trigger)[0];
|
|
262
|
+
|
|
263
|
+
if (triggerType === "cron" && workflowConfig.trigger.cron) {
|
|
264
|
+
workflows.push({
|
|
265
|
+
path,
|
|
266
|
+
config: workflowConfig,
|
|
267
|
+
});
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
return workflows;
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
/**
|
|
276
|
+
* Get workflow by path
|
|
277
|
+
*/
|
|
278
|
+
protected getWorkflowByPath(path: string): CronWorkflowModel | null {
|
|
279
|
+
const workflow = this.nodeMap.workflows?.[path];
|
|
280
|
+
if (!workflow) return null;
|
|
281
|
+
|
|
282
|
+
const workflowConfig = (workflow as unknown as { _config: CronWorkflowModel["config"] })._config;
|
|
283
|
+
return {
|
|
284
|
+
path,
|
|
285
|
+
config: workflowConfig,
|
|
286
|
+
};
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
/**
|
|
290
|
+
* Execute a workflow
|
|
291
|
+
*/
|
|
292
|
+
protected async executeWorkflow(
|
|
293
|
+
jobId: string,
|
|
294
|
+
workflow: CronWorkflowModel,
|
|
295
|
+
config: CronTriggerOpts,
|
|
296
|
+
manual: boolean,
|
|
297
|
+
): Promise<TriggerResponse> {
|
|
298
|
+
const scheduledJob = this.jobs.get(jobId);
|
|
299
|
+
if (!scheduledJob) {
|
|
300
|
+
throw new Error(`Job not found: ${jobId}`);
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
// Check for overlap
|
|
304
|
+
if (scheduledJob.running && !scheduledJob.overlap) {
|
|
305
|
+
this.logger.log(`Skipping ${jobId}: previous execution still running (overlap disabled)`);
|
|
306
|
+
return { ctx: {} as Context, metrics: {} as MetricsType };
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
const executionId = uuid();
|
|
310
|
+
const lastDate = scheduledJob.job.lastDate();
|
|
311
|
+
const scheduledTime = lastDate ? new Date(lastDate as unknown as string | number) : new Date();
|
|
312
|
+
const executionTime = new Date();
|
|
313
|
+
|
|
314
|
+
const defaultMeter = metrics.getMeter("default");
|
|
315
|
+
const cronExecutions = defaultMeter.createCounter("cron_executions", {
|
|
316
|
+
description: "Cron job executions",
|
|
317
|
+
});
|
|
318
|
+
const cronErrors = defaultMeter.createCounter("cron_errors", {
|
|
319
|
+
description: "Cron job execution errors",
|
|
320
|
+
});
|
|
321
|
+
|
|
322
|
+
return new Promise((resolve) => {
|
|
323
|
+
this.tracer.startActiveSpan(`cron:${workflow.path}`, async (span: Span) => {
|
|
324
|
+
scheduledJob.running = true;
|
|
325
|
+
|
|
326
|
+
try {
|
|
327
|
+
const start = performance.now();
|
|
328
|
+
|
|
329
|
+
// Initialize configuration for this workflow
|
|
330
|
+
await this.configuration.init(workflow.path, this.nodeMap);
|
|
331
|
+
|
|
332
|
+
// Create context
|
|
333
|
+
const ctx: Context = this.createContext(undefined, workflow.path, executionId);
|
|
334
|
+
|
|
335
|
+
// Create execution context
|
|
336
|
+
const cronContext: CronExecutionContext = {
|
|
337
|
+
jobId,
|
|
338
|
+
scheduledTime,
|
|
339
|
+
executionTime,
|
|
340
|
+
schedule: config.schedule,
|
|
341
|
+
timezone: config.timezone,
|
|
342
|
+
manual,
|
|
343
|
+
};
|
|
344
|
+
|
|
345
|
+
// Populate request with cron context
|
|
346
|
+
ctx.request = {
|
|
347
|
+
body: cronContext,
|
|
348
|
+
headers: {
|
|
349
|
+
"x-cron-job-id": jobId,
|
|
350
|
+
"x-cron-schedule": config.schedule,
|
|
351
|
+
"x-cron-timezone": config.timezone,
|
|
352
|
+
"x-cron-manual": String(manual),
|
|
353
|
+
},
|
|
354
|
+
query: {},
|
|
355
|
+
params: {
|
|
356
|
+
jobId,
|
|
357
|
+
schedule: config.schedule,
|
|
358
|
+
},
|
|
359
|
+
} as unknown as RequestContext;
|
|
360
|
+
|
|
361
|
+
// Store cron context in vars
|
|
362
|
+
if (!ctx.vars) ctx.vars = {};
|
|
363
|
+
ctx.vars._cron_context = {
|
|
364
|
+
jobId,
|
|
365
|
+
scheduledTime: scheduledTime.toISOString(),
|
|
366
|
+
executionTime: executionTime.toISOString(),
|
|
367
|
+
schedule: config.schedule,
|
|
368
|
+
timezone: config.timezone,
|
|
369
|
+
manual: String(manual),
|
|
370
|
+
};
|
|
371
|
+
|
|
372
|
+
ctx.logger.log(`Executing cron job: ${jobId} (${manual ? "manual" : "scheduled"})`);
|
|
373
|
+
|
|
374
|
+
// Execute workflow
|
|
375
|
+
const response: TriggerResponse = await this.run(ctx);
|
|
376
|
+
const end = performance.now();
|
|
377
|
+
|
|
378
|
+
// Update job state
|
|
379
|
+
scheduledJob.lastRun = executionTime;
|
|
380
|
+
scheduledJob.nextRun = this.toDate(scheduledJob.job.nextDate());
|
|
381
|
+
|
|
382
|
+
// Set span attributes
|
|
383
|
+
span.setAttribute("success", true);
|
|
384
|
+
span.setAttribute("job_id", jobId);
|
|
385
|
+
span.setAttribute("workflow_path", workflow.path);
|
|
386
|
+
span.setAttribute("schedule", config.schedule);
|
|
387
|
+
span.setAttribute("timezone", config.timezone);
|
|
388
|
+
span.setAttribute("manual", manual);
|
|
389
|
+
span.setAttribute("elapsed_ms", end - start);
|
|
390
|
+
span.setStatus({ code: SpanStatusCode.OK });
|
|
391
|
+
|
|
392
|
+
// Record metrics
|
|
393
|
+
cronExecutions.add(1, {
|
|
394
|
+
env: process.env.NODE_ENV,
|
|
395
|
+
job_id: jobId,
|
|
396
|
+
workflow_name: this.configuration.name,
|
|
397
|
+
manual: String(manual),
|
|
398
|
+
success: "true",
|
|
399
|
+
});
|
|
400
|
+
|
|
401
|
+
ctx.logger.log(`Cron job completed in ${(end - start).toFixed(2)}ms: ${jobId}`);
|
|
402
|
+
|
|
403
|
+
resolve(response);
|
|
404
|
+
} catch (error) {
|
|
405
|
+
const errorMessage = (error as Error).message;
|
|
406
|
+
|
|
407
|
+
// Set span error
|
|
408
|
+
span.setAttribute("success", false);
|
|
409
|
+
span.recordException(error as Error);
|
|
410
|
+
span.setStatus({ code: SpanStatusCode.ERROR, message: errorMessage });
|
|
411
|
+
|
|
412
|
+
// Record error metrics
|
|
413
|
+
cronErrors.add(1, {
|
|
414
|
+
env: process.env.NODE_ENV,
|
|
415
|
+
job_id: jobId,
|
|
416
|
+
workflow_name: this.configuration?.name || "unknown",
|
|
417
|
+
manual: String(manual),
|
|
418
|
+
});
|
|
419
|
+
|
|
420
|
+
this.logger.error(`Cron job failed ${jobId}: ${errorMessage}`, (error as Error).stack);
|
|
421
|
+
|
|
422
|
+
resolve({ ctx: {} as Context, metrics: {} as MetricsType });
|
|
423
|
+
} finally {
|
|
424
|
+
scheduledJob.running = false;
|
|
425
|
+
span.end();
|
|
426
|
+
}
|
|
427
|
+
});
|
|
428
|
+
});
|
|
429
|
+
}
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
export default CronTrigger;
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @blokjs/trigger-cron
|
|
3
|
+
*
|
|
4
|
+
* Cron/scheduled trigger for Blok workflows.
|
|
5
|
+
* Execute workflows on a schedule using cron expressions.
|
|
6
|
+
*
|
|
7
|
+
* Features:
|
|
8
|
+
* - Standard cron expression syntax
|
|
9
|
+
* - Timezone-aware scheduling
|
|
10
|
+
* - Overlap prevention
|
|
11
|
+
* - Manual trigger support
|
|
12
|
+
* - Job management API
|
|
13
|
+
*
|
|
14
|
+
* @example
|
|
15
|
+
* ```typescript
|
|
16
|
+
* import { CronTrigger } from "@blokjs/trigger-cron";
|
|
17
|
+
*
|
|
18
|
+
* class MyCronTrigger extends CronTrigger {
|
|
19
|
+
* protected nodes = myNodes;
|
|
20
|
+
* protected workflows = myWorkflows;
|
|
21
|
+
* }
|
|
22
|
+
*
|
|
23
|
+
* const trigger = new MyCronTrigger();
|
|
24
|
+
* await trigger.listen();
|
|
25
|
+
*
|
|
26
|
+
* // List all scheduled jobs
|
|
27
|
+
* const jobs = trigger.getJobs();
|
|
28
|
+
*
|
|
29
|
+
* // Manually trigger a job
|
|
30
|
+
* await trigger.triggerJob("cron-my-workflow-abc123");
|
|
31
|
+
* ```
|
|
32
|
+
*
|
|
33
|
+
* Workflow Definition:
|
|
34
|
+
* ```typescript
|
|
35
|
+
* Workflow({ name: "daily-cleanup", version: "1.0.0" })
|
|
36
|
+
* .addTrigger("cron", {
|
|
37
|
+
* schedule: "0 2 * * *", // Run at 2 AM daily
|
|
38
|
+
* timezone: "America/New_York",
|
|
39
|
+
* overlap: false,
|
|
40
|
+
* })
|
|
41
|
+
* .addStep({ ... });
|
|
42
|
+
* ```
|
|
43
|
+
*
|
|
44
|
+
* Cron Expression Format:
|
|
45
|
+
* ```
|
|
46
|
+
* * * * * * *
|
|
47
|
+
* │ │ │ │ │ │
|
|
48
|
+
* │ │ │ │ │ └── Day of week (0-7, Sun=0,7)
|
|
49
|
+
* │ │ │ │ └──── Month (1-12)
|
|
50
|
+
* │ │ │ └────── Day of month (1-31)
|
|
51
|
+
* │ │ └──────── Hour (0-23)
|
|
52
|
+
* │ └────────── Minute (0-59)
|
|
53
|
+
* └──────────── Second (0-59, optional)
|
|
54
|
+
* ```
|
|
55
|
+
*
|
|
56
|
+
* Common schedules:
|
|
57
|
+
* - "* * * * *" - Every minute
|
|
58
|
+
* - "0 * * * *" - Every hour
|
|
59
|
+
* - "0 0 * * *" - Every day at midnight
|
|
60
|
+
* - "0 0 * * 0" - Every Sunday at midnight
|
|
61
|
+
* - "0 0 1 * *" - First day of every month
|
|
62
|
+
*/
|
|
63
|
+
|
|
64
|
+
// Core exports
|
|
65
|
+
export {
|
|
66
|
+
CronTrigger,
|
|
67
|
+
type ScheduledJob,
|
|
68
|
+
type CronExecutionContext,
|
|
69
|
+
} from "./CronTrigger";
|
|
70
|
+
|
|
71
|
+
// Re-export types from helper for convenience
|
|
72
|
+
export type { CronTriggerOpts } from "@blokjs/helper";
|
package/tsconfig.json
ADDED
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
{
|
|
2
|
+
"ts-node": {
|
|
3
|
+
"transpileOnly": true
|
|
4
|
+
},
|
|
5
|
+
"compilerOptions": {
|
|
6
|
+
"target": "ES2022",
|
|
7
|
+
"module": "es2022",
|
|
8
|
+
"lib": ["ES2022"],
|
|
9
|
+
"declaration": true,
|
|
10
|
+
"strict": true,
|
|
11
|
+
"noImplicitAny": true,
|
|
12
|
+
"strictNullChecks": true,
|
|
13
|
+
"noImplicitThis": true,
|
|
14
|
+
"alwaysStrict": true,
|
|
15
|
+
"noUnusedLocals": false,
|
|
16
|
+
"noUnusedParameters": false,
|
|
17
|
+
"noImplicitReturns": true,
|
|
18
|
+
"noFallthroughCasesInSwitch": false,
|
|
19
|
+
"inlineSourceMap": true,
|
|
20
|
+
"inlineSources": true,
|
|
21
|
+
"experimentalDecorators": true,
|
|
22
|
+
"emitDecoratorMetadata": true,
|
|
23
|
+
"skipLibCheck": true,
|
|
24
|
+
"esModuleInterop": true,
|
|
25
|
+
"resolveJsonModule": true,
|
|
26
|
+
"outDir": "./dist",
|
|
27
|
+
"rootDir": "./src",
|
|
28
|
+
"moduleResolution": "bundler"
|
|
29
|
+
},
|
|
30
|
+
"include": ["src/**/*"],
|
|
31
|
+
"exclude": ["node_modules", "dist", "**/*.test.ts"]
|
|
32
|
+
}
|