@karmaniverous/jeeves-runner 0.5.0 → 0.6.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/dist/cli/jeeves-runner/index.js +526 -79
- package/dist/index.d.ts +68 -7
- package/dist/mjs/index.js +529 -76
- package/package.json +2 -1
package/dist/mjs/index.js
CHANGED
|
@@ -1,15 +1,17 @@
|
|
|
1
1
|
import { z } from 'zod';
|
|
2
|
-
import { mkdirSync, existsSync, readFileSync } from 'node:fs';
|
|
2
|
+
import { mkdirSync, mkdtempSync, writeFileSync, unlinkSync, existsSync, readFileSync } from 'node:fs';
|
|
3
3
|
import { pino } from 'pino';
|
|
4
4
|
import Fastify from 'fastify';
|
|
5
5
|
import { createConfigQueryHandler } from '@karmaniverous/jeeves';
|
|
6
|
-
import {
|
|
6
|
+
import { RRStack } from '@karmaniverous/rrstack';
|
|
7
|
+
import { Cron } from 'croner';
|
|
8
|
+
import { JSONPath } from 'jsonpath-plus';
|
|
9
|
+
import { dirname, join, extname } from 'node:path';
|
|
7
10
|
import { DatabaseSync } from 'node:sqlite';
|
|
8
11
|
import { request as request$1 } from 'node:http';
|
|
9
12
|
import { request } from 'node:https';
|
|
10
13
|
import { spawn } from 'node:child_process';
|
|
11
|
-
import {
|
|
12
|
-
import { JSONPath } from 'jsonpath-plus';
|
|
14
|
+
import { tmpdir } from 'node:os';
|
|
13
15
|
|
|
14
16
|
/**
|
|
15
17
|
* Runner configuration schema and types.
|
|
@@ -45,6 +47,8 @@ const gatewaySchema = z.object({
|
|
|
45
47
|
const runnerConfigSchema = z.object({
|
|
46
48
|
/** HTTP server port for the runner API. */
|
|
47
49
|
port: z.number().default(1937),
|
|
50
|
+
/** Bind address for the HTTP server. */
|
|
51
|
+
host: z.string().default('127.0.0.1'),
|
|
48
52
|
/** Path to SQLite database file. */
|
|
49
53
|
dbPath: z.string().default('./data/runner.sqlite'),
|
|
50
54
|
/** Maximum number of concurrent job executions. */
|
|
@@ -91,8 +95,8 @@ const jobSchema = z.object({
|
|
|
91
95
|
enabled: z.boolean().default(true),
|
|
92
96
|
/** Optional execution timeout in milliseconds. */
|
|
93
97
|
timeoutMs: z.number().optional(),
|
|
94
|
-
/** Policy for handling overlapping job executions (skip
|
|
95
|
-
overlapPolicy: z.enum(['skip', '
|
|
98
|
+
/** Policy for handling overlapping job executions (skip or allow). */
|
|
99
|
+
overlapPolicy: z.enum(['skip', 'allow']).default('skip'),
|
|
96
100
|
/** Slack channel ID for failure notifications. */
|
|
97
101
|
onFailure: z.string().nullable().default(null),
|
|
98
102
|
/** Slack channel ID for success notifications. */
|
|
@@ -170,6 +174,385 @@ const runSchema = z.object({
|
|
|
170
174
|
trigger: runTriggerSchema.default('schedule'),
|
|
171
175
|
});
|
|
172
176
|
|
|
177
|
+
/**
|
|
178
|
+
* Schedule parsing, validation, and next-fire-time computation for cron and RRStack formats.
|
|
179
|
+
*
|
|
180
|
+
* @module
|
|
181
|
+
*/
|
|
182
|
+
/**
|
|
183
|
+
* Attempt to parse a schedule string as RRStack JSON (non-null, non-array
|
|
184
|
+
* object). Returns the parsed options on success, or null if the string
|
|
185
|
+
* is not a JSON object.
|
|
186
|
+
*/
|
|
187
|
+
function tryParseRRStack(schedule) {
|
|
188
|
+
try {
|
|
189
|
+
const parsed = JSON.parse(schedule);
|
|
190
|
+
if (typeof parsed === 'object' &&
|
|
191
|
+
parsed !== null &&
|
|
192
|
+
!Array.isArray(parsed)) {
|
|
193
|
+
return parsed;
|
|
194
|
+
}
|
|
195
|
+
return null;
|
|
196
|
+
}
|
|
197
|
+
catch {
|
|
198
|
+
return null;
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
/**
|
|
202
|
+
* Compute the next fire time for a schedule string.
|
|
203
|
+
* Supports cron expressions (via croner) and RRStack JSON (via nextEvent).
|
|
204
|
+
*
|
|
205
|
+
* @param schedule - Cron expression or RRStack JSON string.
|
|
206
|
+
* @returns Next fire time as a Date, or null if none can be determined.
|
|
207
|
+
*/
|
|
208
|
+
function getNextFireTime(schedule) {
|
|
209
|
+
const rrOpts = tryParseRRStack(schedule);
|
|
210
|
+
if (rrOpts) {
|
|
211
|
+
const stack = new RRStack(rrOpts);
|
|
212
|
+
const next = stack.nextEvent();
|
|
213
|
+
if (!next)
|
|
214
|
+
return null;
|
|
215
|
+
const unit = stack.timeUnit;
|
|
216
|
+
return new Date(unit === 's' ? next.at * 1000 : next.at);
|
|
217
|
+
}
|
|
218
|
+
const cron = new Cron(schedule);
|
|
219
|
+
return cron.nextRun() ?? null;
|
|
220
|
+
}
|
|
221
|
+
/**
|
|
222
|
+
* Validate a schedule string as either a cron expression or RRStack JSON.
|
|
223
|
+
*
|
|
224
|
+
* @param schedule - Cron expression or RRStack JSON string.
|
|
225
|
+
* @returns Validation result with format on success, or error message on failure.
|
|
226
|
+
*/
|
|
227
|
+
function validateSchedule(schedule) {
|
|
228
|
+
const rrOpts = tryParseRRStack(schedule);
|
|
229
|
+
if (rrOpts) {
|
|
230
|
+
try {
|
|
231
|
+
new RRStack(rrOpts);
|
|
232
|
+
return { valid: true, format: 'rrstack' };
|
|
233
|
+
}
|
|
234
|
+
catch (err) {
|
|
235
|
+
return {
|
|
236
|
+
valid: false,
|
|
237
|
+
error: `Invalid RRStack schedule: ${err instanceof Error ? err.message : String(err)}`,
|
|
238
|
+
};
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
try {
|
|
242
|
+
new Cron(schedule);
|
|
243
|
+
return { valid: true, format: 'cron' };
|
|
244
|
+
}
|
|
245
|
+
catch (err) {
|
|
246
|
+
return {
|
|
247
|
+
valid: false,
|
|
248
|
+
error: `Invalid cron expression: ${err instanceof Error ? err.message : String(err)}`,
|
|
249
|
+
};
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
/**
|
|
254
|
+
* Job CRUD routes: create, update, delete, enable/disable, and script update endpoints.
|
|
255
|
+
*
|
|
256
|
+
* @module
|
|
257
|
+
*/
|
|
258
|
+
/** Zod schema for job creation/update request body. */
|
|
259
|
+
const createJobSchema = z.object({
|
|
260
|
+
id: z.string().min(1),
|
|
261
|
+
name: z.string().min(1),
|
|
262
|
+
schedule: z.string().min(1),
|
|
263
|
+
script: z.string().min(1),
|
|
264
|
+
source_type: z.enum(['path', 'inline']).default('path'),
|
|
265
|
+
type: z.enum(['script', 'session']).default('script'),
|
|
266
|
+
timeout_seconds: z.number().positive().optional(),
|
|
267
|
+
overlap_policy: z.enum(['skip', 'allow']).default('skip'),
|
|
268
|
+
enabled: z.boolean().default(true),
|
|
269
|
+
description: z.string().optional(),
|
|
270
|
+
on_failure: z.string().nullable().optional(),
|
|
271
|
+
on_success: z.string().nullable().optional(),
|
|
272
|
+
});
|
|
273
|
+
/** Zod schema for job update (all fields optional except what's set). */
|
|
274
|
+
const updateJobSchema = createJobSchema.omit({ id: true }).partial();
|
|
275
|
+
/** Zod schema for script update request body. */
|
|
276
|
+
const updateScriptSchema = z.object({
|
|
277
|
+
script: z.string().min(1),
|
|
278
|
+
source_type: z.enum(['path', 'inline']).optional(),
|
|
279
|
+
});
|
|
280
|
+
/** Standard error message for missing job resources. */
|
|
281
|
+
const JOB_NOT_FOUND = 'Job not found';
|
|
282
|
+
/**
|
|
283
|
+
* Register job CRUD routes on the Fastify instance.
|
|
284
|
+
*/
|
|
285
|
+
function registerJobRoutes(app, deps) {
|
|
286
|
+
const { db, scheduler } = deps;
|
|
287
|
+
/** POST /jobs — Create a new job. */
|
|
288
|
+
app.post('/jobs', (request, reply) => {
|
|
289
|
+
const parsed = createJobSchema.safeParse(request.body);
|
|
290
|
+
if (!parsed.success) {
|
|
291
|
+
reply.code(400);
|
|
292
|
+
return { error: parsed.error.message };
|
|
293
|
+
}
|
|
294
|
+
const data = parsed.data;
|
|
295
|
+
const validation = validateSchedule(data.schedule);
|
|
296
|
+
if (!validation.valid) {
|
|
297
|
+
reply.code(400);
|
|
298
|
+
return { error: validation.error };
|
|
299
|
+
}
|
|
300
|
+
const timeoutMs = data.timeout_seconds ? data.timeout_seconds * 1000 : null;
|
|
301
|
+
db.prepare(`INSERT INTO jobs (id, name, schedule, script, source_type, type, timeout_ms,
|
|
302
|
+
overlap_policy, enabled, description, on_failure, on_success)
|
|
303
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`).run(data.id, data.name, data.schedule, data.script, data.source_type, data.type, timeoutMs, data.overlap_policy, data.enabled ? 1 : 0, data.description ?? null, data.on_failure ?? null, data.on_success ?? null);
|
|
304
|
+
scheduler.reconcileNow();
|
|
305
|
+
reply.code(201);
|
|
306
|
+
return { ok: true, id: data.id };
|
|
307
|
+
});
|
|
308
|
+
/** PATCH /jobs/:id — Partial update of an existing job. */
|
|
309
|
+
app.patch('/jobs/:id', (request, reply) => {
|
|
310
|
+
const parsed = updateJobSchema.safeParse(request.body);
|
|
311
|
+
if (!parsed.success) {
|
|
312
|
+
reply.code(400);
|
|
313
|
+
return { error: parsed.error.message };
|
|
314
|
+
}
|
|
315
|
+
const existing = db
|
|
316
|
+
.prepare('SELECT id FROM jobs WHERE id = ?')
|
|
317
|
+
.get(request.params.id);
|
|
318
|
+
if (!existing) {
|
|
319
|
+
reply.code(404);
|
|
320
|
+
return { error: JOB_NOT_FOUND };
|
|
321
|
+
}
|
|
322
|
+
const data = parsed.data;
|
|
323
|
+
if (data.schedule) {
|
|
324
|
+
const validation = validateSchedule(data.schedule);
|
|
325
|
+
if (!validation.valid) {
|
|
326
|
+
reply.code(400);
|
|
327
|
+
return { error: validation.error };
|
|
328
|
+
}
|
|
329
|
+
}
|
|
330
|
+
/** Map input field → DB column + value transform. */
|
|
331
|
+
const fieldMap = [
|
|
332
|
+
{ input: 'name', column: 'name' },
|
|
333
|
+
{ input: 'schedule', column: 'schedule' },
|
|
334
|
+
{ input: 'script', column: 'script' },
|
|
335
|
+
{ input: 'source_type', column: 'source_type' },
|
|
336
|
+
{ input: 'type', column: 'type' },
|
|
337
|
+
{
|
|
338
|
+
input: 'timeout_seconds',
|
|
339
|
+
column: 'timeout_ms',
|
|
340
|
+
transform: (v) => v * 1000,
|
|
341
|
+
},
|
|
342
|
+
{ input: 'overlap_policy', column: 'overlap_policy' },
|
|
343
|
+
{
|
|
344
|
+
input: 'enabled',
|
|
345
|
+
column: 'enabled',
|
|
346
|
+
transform: (v) => (v ? 1 : 0),
|
|
347
|
+
},
|
|
348
|
+
{ input: 'description', column: 'description' },
|
|
349
|
+
{ input: 'on_failure', column: 'on_failure' },
|
|
350
|
+
{ input: 'on_success', column: 'on_success' },
|
|
351
|
+
];
|
|
352
|
+
const sets = [];
|
|
353
|
+
const values = [];
|
|
354
|
+
for (const { input, column, transform } of fieldMap) {
|
|
355
|
+
if (data[input] !== undefined) {
|
|
356
|
+
sets.push(`${column} = ?`);
|
|
357
|
+
values.push(transform ? transform(data[input]) : data[input]);
|
|
358
|
+
}
|
|
359
|
+
}
|
|
360
|
+
if (sets.length > 0) {
|
|
361
|
+
sets.push("updated_at = datetime('now')");
|
|
362
|
+
values.push(request.params.id);
|
|
363
|
+
db.prepare(`UPDATE jobs SET ${sets.join(', ')} WHERE id = ?`).run(...values);
|
|
364
|
+
}
|
|
365
|
+
scheduler.reconcileNow();
|
|
366
|
+
return { ok: true };
|
|
367
|
+
});
|
|
368
|
+
/** DELETE /jobs/:id — Delete a job and its runs. */
|
|
369
|
+
app.delete('/jobs/:id', (request, reply) => {
|
|
370
|
+
db.prepare('DELETE FROM runs WHERE job_id = ?').run(request.params.id);
|
|
371
|
+
const result = db
|
|
372
|
+
.prepare('DELETE FROM jobs WHERE id = ?')
|
|
373
|
+
.run(request.params.id);
|
|
374
|
+
if (result.changes === 0) {
|
|
375
|
+
reply.code(404);
|
|
376
|
+
return { error: JOB_NOT_FOUND };
|
|
377
|
+
}
|
|
378
|
+
scheduler.reconcileNow();
|
|
379
|
+
return { ok: true };
|
|
380
|
+
});
|
|
381
|
+
/** Register a PATCH toggle endpoint (enable or disable). */
|
|
382
|
+
function registerToggle(path, enabledValue) {
|
|
383
|
+
app.patch(path, (request, reply) => {
|
|
384
|
+
const result = db
|
|
385
|
+
.prepare('UPDATE jobs SET enabled = ? WHERE id = ?')
|
|
386
|
+
.run(enabledValue, request.params.id);
|
|
387
|
+
if (result.changes === 0) {
|
|
388
|
+
reply.code(404);
|
|
389
|
+
return { error: JOB_NOT_FOUND };
|
|
390
|
+
}
|
|
391
|
+
scheduler.reconcileNow();
|
|
392
|
+
return { ok: true };
|
|
393
|
+
});
|
|
394
|
+
}
|
|
395
|
+
registerToggle('/jobs/:id/enable', 1);
|
|
396
|
+
registerToggle('/jobs/:id/disable', 0);
|
|
397
|
+
/** PUT /jobs/:id/script — Update job script and source_type. */
|
|
398
|
+
app.put('/jobs/:id/script', (request, reply) => {
|
|
399
|
+
const parsed = updateScriptSchema.safeParse(request.body);
|
|
400
|
+
if (!parsed.success) {
|
|
401
|
+
reply.code(400);
|
|
402
|
+
return { error: parsed.error.message };
|
|
403
|
+
}
|
|
404
|
+
const data = parsed.data;
|
|
405
|
+
const result = data.source_type
|
|
406
|
+
? db
|
|
407
|
+
.prepare('UPDATE jobs SET script = ?, source_type = ? WHERE id = ?')
|
|
408
|
+
.run(data.script, data.source_type, request.params.id)
|
|
409
|
+
: db
|
|
410
|
+
.prepare('UPDATE jobs SET script = ? WHERE id = ?')
|
|
411
|
+
.run(data.script, request.params.id);
|
|
412
|
+
if (result.changes === 0) {
|
|
413
|
+
reply.code(404);
|
|
414
|
+
return { error: JOB_NOT_FOUND };
|
|
415
|
+
}
|
|
416
|
+
scheduler.reconcileNow();
|
|
417
|
+
return { ok: true };
|
|
418
|
+
});
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
/**
|
|
422
|
+
* Queue inspection routes: list queues, queue status, and peek at pending items.
|
|
423
|
+
*
|
|
424
|
+
* @module
|
|
425
|
+
*/
|
|
426
|
+
/**
|
|
427
|
+
* Register queue inspection routes on the Fastify instance.
|
|
428
|
+
*/
|
|
429
|
+
function registerQueueRoutes(app, deps) {
|
|
430
|
+
const { db } = deps;
|
|
431
|
+
/** GET /queues — List all distinct queues that have items. */
|
|
432
|
+
app.get('/queues', () => {
|
|
433
|
+
const rows = db
|
|
434
|
+
.prepare('SELECT DISTINCT queue_id FROM queue_items')
|
|
435
|
+
.all();
|
|
436
|
+
return { queues: rows.map((r) => r.queue_id) };
|
|
437
|
+
});
|
|
438
|
+
/** GET /queues/:name/status — Queue depth, claimed count, failed count, oldest age. */
|
|
439
|
+
app.get('/queues/:name/status', (request) => {
|
|
440
|
+
const row = db
|
|
441
|
+
.prepare(`SELECT
|
|
442
|
+
SUM(CASE WHEN status = 'pending' THEN 1 ELSE 0 END) AS depth,
|
|
443
|
+
SUM(CASE WHEN status = 'processing' THEN 1 ELSE 0 END) AS claimed,
|
|
444
|
+
SUM(CASE WHEN status = 'failed' THEN 1 ELSE 0 END) AS failed,
|
|
445
|
+
MIN(CASE WHEN status = 'pending' THEN created_at END) AS oldest
|
|
446
|
+
FROM queue_items
|
|
447
|
+
WHERE queue_id = ?`)
|
|
448
|
+
.get(request.params.name);
|
|
449
|
+
return {
|
|
450
|
+
depth: row.depth ?? 0,
|
|
451
|
+
claimedCount: row.claimed ?? 0,
|
|
452
|
+
failedCount: row.failed ?? 0,
|
|
453
|
+
oldestAge: row.oldest
|
|
454
|
+
? Date.now() - new Date(row.oldest).getTime()
|
|
455
|
+
: null,
|
|
456
|
+
};
|
|
457
|
+
});
|
|
458
|
+
/** GET /queues/:name/peek — Non-claiming read of pending items. */
|
|
459
|
+
app.get('/queues/:name/peek', (request) => {
|
|
460
|
+
const name = request.params.name;
|
|
461
|
+
const limit = parseInt(request.query.limit ?? '10', 10);
|
|
462
|
+
const items = db
|
|
463
|
+
.prepare(`SELECT id, payload, priority, created_at FROM queue_items
|
|
464
|
+
WHERE queue_id = ? AND status = 'pending'
|
|
465
|
+
ORDER BY priority DESC, created_at
|
|
466
|
+
LIMIT ?`)
|
|
467
|
+
.all(name, limit);
|
|
468
|
+
return {
|
|
469
|
+
items: items.map((item) => ({
|
|
470
|
+
id: item.id,
|
|
471
|
+
payload: JSON.parse(item.payload),
|
|
472
|
+
priority: item.priority,
|
|
473
|
+
createdAt: item.created_at,
|
|
474
|
+
})),
|
|
475
|
+
};
|
|
476
|
+
});
|
|
477
|
+
}
|
|
478
|
+
|
|
479
|
+
/**
|
|
480
|
+
* State inspection routes: list namespaces, read scalar state, and read collection items.
|
|
481
|
+
*
|
|
482
|
+
* @module
|
|
483
|
+
*/
|
|
484
|
+
/**
|
|
485
|
+
* Register state inspection routes on the Fastify instance.
|
|
486
|
+
*/
|
|
487
|
+
function registerStateRoutes(app, deps) {
|
|
488
|
+
const { db } = deps;
|
|
489
|
+
/** GET /state — List all distinct namespaces. */
|
|
490
|
+
app.get('/state', () => {
|
|
491
|
+
const rows = db
|
|
492
|
+
.prepare('SELECT DISTINCT namespace FROM state')
|
|
493
|
+
.all();
|
|
494
|
+
return { namespaces: rows.map((r) => r.namespace) };
|
|
495
|
+
});
|
|
496
|
+
/** GET /state/:namespace — Materialise all scalar state as key-value map. */
|
|
497
|
+
app.get('/state/:namespace', (request) => {
|
|
498
|
+
const { namespace } = request.params;
|
|
499
|
+
const rows = db
|
|
500
|
+
.prepare(`SELECT key, value FROM state
|
|
501
|
+
WHERE namespace = ?
|
|
502
|
+
AND (expires_at IS NULL OR expires_at > datetime('now'))`)
|
|
503
|
+
.all(namespace);
|
|
504
|
+
const state = {};
|
|
505
|
+
for (const row of rows) {
|
|
506
|
+
state[row.key] = row.value;
|
|
507
|
+
}
|
|
508
|
+
if (request.query.path) {
|
|
509
|
+
const result = JSONPath({
|
|
510
|
+
path: request.query.path,
|
|
511
|
+
json: state,
|
|
512
|
+
});
|
|
513
|
+
return { result, count: Array.isArray(result) ? result.length : 1 };
|
|
514
|
+
}
|
|
515
|
+
return state;
|
|
516
|
+
});
|
|
517
|
+
/** GET /state/:namespace/:key — Read collection items for a state key. */
|
|
518
|
+
app.get('/state/:namespace/:key', (request) => {
|
|
519
|
+
const { namespace, key } = request.params;
|
|
520
|
+
const limit = parseInt(request.query.limit ?? '100', 10);
|
|
521
|
+
const order = request.query.order === 'asc' ? 'ASC' : 'DESC';
|
|
522
|
+
// First check scalar state value
|
|
523
|
+
const scalar = db
|
|
524
|
+
.prepare(`SELECT value FROM state
|
|
525
|
+
WHERE namespace = ? AND key = ?
|
|
526
|
+
AND (expires_at IS NULL OR expires_at > datetime('now'))`)
|
|
527
|
+
.get(namespace, key);
|
|
528
|
+
// Get collection items
|
|
529
|
+
const items = db
|
|
530
|
+
.prepare(`SELECT item_key, value, updated_at FROM state_items
|
|
531
|
+
WHERE namespace = ? AND key = ?
|
|
532
|
+
ORDER BY updated_at ${order}
|
|
533
|
+
LIMIT ?`)
|
|
534
|
+
.all(namespace, key, limit);
|
|
535
|
+
const mappedItems = items.map((item) => ({
|
|
536
|
+
itemKey: item.item_key,
|
|
537
|
+
value: item.value,
|
|
538
|
+
updatedAt: item.updated_at,
|
|
539
|
+
}));
|
|
540
|
+
const body = {
|
|
541
|
+
value: scalar?.value ?? null,
|
|
542
|
+
items: mappedItems,
|
|
543
|
+
count: mappedItems.length,
|
|
544
|
+
};
|
|
545
|
+
if (request.query.path) {
|
|
546
|
+
const result = JSONPath({
|
|
547
|
+
path: request.query.path,
|
|
548
|
+
json: body,
|
|
549
|
+
});
|
|
550
|
+
return { result, count: Array.isArray(result) ? result.length : 1 };
|
|
551
|
+
}
|
|
552
|
+
return body;
|
|
553
|
+
});
|
|
554
|
+
}
|
|
555
|
+
|
|
173
556
|
/**
|
|
174
557
|
* Fastify API routes for job management and monitoring. Provides endpoints for job CRUD, run history, manual triggers, system stats, and config queries.
|
|
175
558
|
*
|
|
@@ -199,7 +582,7 @@ function registerRoutes(app, deps) {
|
|
|
199
582
|
return { jobs: rows };
|
|
200
583
|
});
|
|
201
584
|
/** GET /jobs/:id — Single job detail. */
|
|
202
|
-
app.get('/jobs/:id',
|
|
585
|
+
app.get('/jobs/:id', (request, reply) => {
|
|
203
586
|
const job = db
|
|
204
587
|
.prepare('SELECT * FROM jobs WHERE id = ?')
|
|
205
588
|
.get(request.params.id);
|
|
@@ -228,30 +611,6 @@ function registerRoutes(app, deps) {
|
|
|
228
611
|
return { error: err instanceof Error ? err.message : 'Unknown error' };
|
|
229
612
|
}
|
|
230
613
|
});
|
|
231
|
-
/** POST /jobs/:id/enable — Enable a job. */
|
|
232
|
-
app.post('/jobs/:id/enable', (request, reply) => {
|
|
233
|
-
const result = db
|
|
234
|
-
.prepare('UPDATE jobs SET enabled = 1 WHERE id = ?')
|
|
235
|
-
.run(request.params.id);
|
|
236
|
-
if (result.changes === 0) {
|
|
237
|
-
reply.code(404);
|
|
238
|
-
return { error: 'Job not found' };
|
|
239
|
-
}
|
|
240
|
-
scheduler.reconcileNow();
|
|
241
|
-
return { ok: true };
|
|
242
|
-
});
|
|
243
|
-
/** POST /jobs/:id/disable — Disable a job. */
|
|
244
|
-
app.post('/jobs/:id/disable', (request, reply) => {
|
|
245
|
-
const result = db
|
|
246
|
-
.prepare('UPDATE jobs SET enabled = 0 WHERE id = ?')
|
|
247
|
-
.run(request.params.id);
|
|
248
|
-
if (result.changes === 0) {
|
|
249
|
-
reply.code(404);
|
|
250
|
-
return { error: 'Job not found' };
|
|
251
|
-
}
|
|
252
|
-
scheduler.reconcileNow();
|
|
253
|
-
return { ok: true };
|
|
254
|
-
});
|
|
255
614
|
/** GET /stats — Aggregate job statistics. */
|
|
256
615
|
app.get('/stats', () => {
|
|
257
616
|
const totalJobs = db
|
|
@@ -283,10 +642,17 @@ function registerRoutes(app, deps) {
|
|
|
283
642
|
});
|
|
284
643
|
return reply.status(result.status).send(result.body);
|
|
285
644
|
});
|
|
645
|
+
// Register job CRUD routes (POST, PATCH, DELETE, PATCH enable/disable, PUT script)
|
|
646
|
+
registerJobRoutes(app, { db, scheduler });
|
|
647
|
+
// Register queue and state inspection routes
|
|
648
|
+
registerQueueRoutes(app, { db });
|
|
649
|
+
registerStateRoutes(app, { db });
|
|
286
650
|
}
|
|
287
651
|
|
|
288
652
|
/**
|
|
289
653
|
* Fastify HTTP server for runner API. Creates server instance with logging, registers routes, listens on configured port (localhost only).
|
|
654
|
+
*
|
|
655
|
+
* @module
|
|
290
656
|
*/
|
|
291
657
|
/**
|
|
292
658
|
* Create and configure the Fastify server. Routes are registered but server is not started.
|
|
@@ -317,6 +683,8 @@ function createServer(deps) {
|
|
|
317
683
|
|
|
318
684
|
/**
|
|
319
685
|
* SQLite connection manager. Creates DB file with parent directories, enables WAL mode for concurrency.
|
|
686
|
+
*
|
|
687
|
+
* @module
|
|
320
688
|
*/
|
|
321
689
|
/**
|
|
322
690
|
* Create and configure a SQLite database connection.
|
|
@@ -345,6 +713,8 @@ function closeConnection(db) {
|
|
|
345
713
|
|
|
346
714
|
/**
|
|
347
715
|
* Database maintenance tasks: run retention pruning and expired state cleanup.
|
|
716
|
+
*
|
|
717
|
+
* @module
|
|
348
718
|
*/
|
|
349
719
|
/** Delete runs older than the configured retention period. */
|
|
350
720
|
function pruneOldRuns(db, days, logger) {
|
|
@@ -411,6 +781,8 @@ function createMaintenance(db, config, logger) {
|
|
|
411
781
|
|
|
412
782
|
/**
|
|
413
783
|
* Schema migration runner. Tracks applied migrations via schema_version table, applies pending migrations idempotently.
|
|
784
|
+
*
|
|
785
|
+
* @module
|
|
414
786
|
*/
|
|
415
787
|
/** Initial schema migration SQL (embedded to avoid runtime file resolution issues). */
|
|
416
788
|
const MIGRATION_001 = `
|
|
@@ -544,11 +916,16 @@ CREATE TABLE state_items (
|
|
|
544
916
|
);
|
|
545
917
|
CREATE INDEX idx_state_items_ns_key ON state_items(namespace, key);
|
|
546
918
|
`;
|
|
919
|
+
/** Migration 004: Add source_type column to jobs. */
|
|
920
|
+
const MIGRATION_004 = `
|
|
921
|
+
ALTER TABLE jobs ADD COLUMN source_type TEXT DEFAULT 'path';
|
|
922
|
+
`;
|
|
547
923
|
/** Registry of all migrations keyed by version number. */
|
|
548
924
|
const MIGRATIONS = {
|
|
549
925
|
1: MIGRATION_001,
|
|
550
926
|
2: MIGRATION_002,
|
|
551
927
|
3: MIGRATION_003,
|
|
928
|
+
4: MIGRATION_004,
|
|
552
929
|
};
|
|
553
930
|
/**
|
|
554
931
|
* Run all pending migrations. Creates schema_version table if needed, applies migrations in order.
|
|
@@ -579,6 +956,8 @@ function runMigrations(db) {
|
|
|
579
956
|
|
|
580
957
|
/**
|
|
581
958
|
* Shared HTTP utility for making POST requests.
|
|
959
|
+
*
|
|
960
|
+
* @module
|
|
582
961
|
*/
|
|
583
962
|
/** Make an HTTP/HTTPS POST request. Returns the parsed JSON response body. */
|
|
584
963
|
function httpPost(url, headers, body, timeoutMs = 30000) {
|
|
@@ -626,6 +1005,8 @@ function httpPost(url, headers, body, timeoutMs = 30000) {
|
|
|
626
1005
|
|
|
627
1006
|
/**
|
|
628
1007
|
* OpenClaw Gateway HTTP client for spawning and monitoring sessions.
|
|
1008
|
+
*
|
|
1009
|
+
* @module
|
|
629
1010
|
*/
|
|
630
1011
|
/** Make an HTTP POST request to the Gateway /tools/invoke endpoint. */
|
|
631
1012
|
function invokeGateway(url, token, tool, args, timeoutMs = 30000) {
|
|
@@ -692,6 +1073,8 @@ function createGatewayClient(options) {
|
|
|
692
1073
|
|
|
693
1074
|
/**
|
|
694
1075
|
* Slack notification module. Sends job completion/failure messages via Slack Web API (chat.postMessage). Falls back gracefully if no token.
|
|
1076
|
+
*
|
|
1077
|
+
* @module
|
|
695
1078
|
*/
|
|
696
1079
|
/** Post a message to Slack via chat.postMessage API. */
|
|
697
1080
|
async function postToSlack(token, channel, text) {
|
|
@@ -709,21 +1092,21 @@ function createNotifier(config) {
|
|
|
709
1092
|
return {
|
|
710
1093
|
async notifySuccess(jobName, durationMs, channel) {
|
|
711
1094
|
if (!slackToken) {
|
|
712
|
-
console.warn(`No Slack token configured
|
|
1095
|
+
console.warn(`No Slack token configured \u2014 skipping success notification for ${jobName}`);
|
|
713
1096
|
return;
|
|
714
1097
|
}
|
|
715
1098
|
const durationSec = (durationMs / 1000).toFixed(1);
|
|
716
|
-
const text =
|
|
1099
|
+
const text = `\u2705 *${jobName}* completed (${durationSec}s)`;
|
|
717
1100
|
await postToSlack(slackToken, channel, text);
|
|
718
1101
|
},
|
|
719
1102
|
async notifyFailure(jobName, durationMs, error, channel) {
|
|
720
1103
|
if (!slackToken) {
|
|
721
|
-
console.warn(`No Slack token configured
|
|
1104
|
+
console.warn(`No Slack token configured \u2014 skipping failure notification for ${jobName}`);
|
|
722
1105
|
return;
|
|
723
1106
|
}
|
|
724
1107
|
const durationSec = (durationMs / 1000).toFixed(1);
|
|
725
1108
|
const errorMsg = error ? `: ${error}` : '';
|
|
726
|
-
const text =
|
|
1109
|
+
const text = `\u274c *${jobName}* failed (${durationSec}s)${errorMsg}`;
|
|
727
1110
|
await postToSlack(slackToken, channel, text);
|
|
728
1111
|
},
|
|
729
1112
|
async dispatchResult(result, jobName, onSuccess, onFailure, logger) {
|
|
@@ -743,6 +1126,8 @@ function createNotifier(config) {
|
|
|
743
1126
|
|
|
744
1127
|
/**
|
|
745
1128
|
* Job executor. Spawns job scripts as child processes, captures output, parses result metadata, enforces timeouts.
|
|
1129
|
+
*
|
|
1130
|
+
* @module
|
|
746
1131
|
*/
|
|
747
1132
|
/** Ring buffer for capturing last N lines of output. */
|
|
748
1133
|
class RingBuffer {
|
|
@@ -804,14 +1189,23 @@ function resolveCommand(script) {
|
|
|
804
1189
|
* Execute a job script as a child process. Captures output, parses metadata, enforces timeout.
|
|
805
1190
|
*/
|
|
806
1191
|
function executeJob(options) {
|
|
807
|
-
const { script, dbPath, jobId, runId, timeoutMs, commandResolver } = options;
|
|
1192
|
+
const { script, dbPath, jobId, runId, timeoutMs, commandResolver, sourceType = 'path', } = options;
|
|
808
1193
|
const startTime = Date.now();
|
|
1194
|
+
// For inline scripts, write to a temp file and clean up after.
|
|
1195
|
+
let tempFile = null;
|
|
1196
|
+
let effectiveScript = script;
|
|
1197
|
+
if (sourceType === 'inline') {
|
|
1198
|
+
const tempDir = mkdtempSync(join(tmpdir(), 'jr-inline-'));
|
|
1199
|
+
tempFile = join(tempDir, 'inline.js');
|
|
1200
|
+
writeFileSync(tempFile, script);
|
|
1201
|
+
effectiveScript = tempFile;
|
|
1202
|
+
}
|
|
809
1203
|
return new Promise((resolve) => {
|
|
810
1204
|
const stdoutBuffer = new RingBuffer(100);
|
|
811
1205
|
const stderrBuffer = new RingBuffer(100);
|
|
812
1206
|
const { command, args } = commandResolver
|
|
813
|
-
? commandResolver(
|
|
814
|
-
: resolveCommand(
|
|
1207
|
+
? commandResolver(effectiveScript)
|
|
1208
|
+
: resolveCommand(effectiveScript);
|
|
815
1209
|
const child = spawn(command, args, {
|
|
816
1210
|
env: {
|
|
817
1211
|
...process.env,
|
|
@@ -847,6 +1241,7 @@ function executeJob(options) {
|
|
|
847
1241
|
child.on('close', (exitCode) => {
|
|
848
1242
|
if (timeoutHandle)
|
|
849
1243
|
clearTimeout(timeoutHandle);
|
|
1244
|
+
cleanupTempFile(tempFile);
|
|
850
1245
|
const durationMs = Date.now() - startTime;
|
|
851
1246
|
const stdoutTail = stdoutBuffer.getAll();
|
|
852
1247
|
const stderrTail = stderrBuffer.getAll();
|
|
@@ -891,6 +1286,7 @@ function executeJob(options) {
|
|
|
891
1286
|
child.on('error', (err) => {
|
|
892
1287
|
if (timeoutHandle)
|
|
893
1288
|
clearTimeout(timeoutHandle);
|
|
1289
|
+
cleanupTempFile(tempFile);
|
|
894
1290
|
const durationMs = Date.now() - startTime;
|
|
895
1291
|
resolve({
|
|
896
1292
|
status: 'error',
|
|
@@ -905,31 +1301,73 @@ function executeJob(options) {
|
|
|
905
1301
|
});
|
|
906
1302
|
});
|
|
907
1303
|
}
|
|
1304
|
+
/** Remove a temp file created for inline script execution. */
|
|
1305
|
+
function cleanupTempFile(tempFile) {
|
|
1306
|
+
if (!tempFile)
|
|
1307
|
+
return;
|
|
1308
|
+
try {
|
|
1309
|
+
unlinkSync(tempFile);
|
|
1310
|
+
}
|
|
1311
|
+
catch {
|
|
1312
|
+
// Ignore cleanup errors
|
|
1313
|
+
}
|
|
1314
|
+
}
|
|
908
1315
|
|
|
909
1316
|
/**
|
|
910
|
-
*
|
|
1317
|
+
* Schedule registration and reconciliation. Supports cron (croner) and RRStack schedule formats via unified setTimeout-based scheduling.
|
|
1318
|
+
*
|
|
1319
|
+
* @module
|
|
911
1320
|
*/
|
|
1321
|
+
/** Maximum setTimeout delay (Node.js limit: ~24.8 days). */
|
|
1322
|
+
const MAX_TIMEOUT_MS = 2_147_483_647;
|
|
912
1323
|
function createCronRegistry(deps) {
|
|
913
1324
|
const { db, logger, onScheduledRun } = deps;
|
|
914
|
-
const
|
|
915
|
-
const
|
|
1325
|
+
const handles = new Map();
|
|
1326
|
+
const scheduleStrings = new Map();
|
|
916
1327
|
const failedRegistrations = new Set();
|
|
917
|
-
|
|
1328
|
+
/**
|
|
1329
|
+
* Schedule the next fire for a job. Computes next fire time, sets a
|
|
1330
|
+
* setTimeout, and re-arms after each fire.
|
|
1331
|
+
*/
|
|
1332
|
+
function scheduleNext(jobId, schedule) {
|
|
1333
|
+
const nextDate = getNextFireTime(schedule);
|
|
1334
|
+
if (!nextDate) {
|
|
1335
|
+
logger.warn({ jobId }, 'No upcoming fire time, job will not fire');
|
|
1336
|
+
return;
|
|
1337
|
+
}
|
|
1338
|
+
const delayMs = Math.max(0, nextDate.getTime() - Date.now());
|
|
1339
|
+
// Node.js setTimeout max is ~24.8 days. If delay exceeds that,
|
|
1340
|
+
// set an intermediate wakeup and re-check.
|
|
1341
|
+
const effectiveDelay = Math.min(delayMs, MAX_TIMEOUT_MS);
|
|
1342
|
+
const isIntermediate = delayMs > MAX_TIMEOUT_MS;
|
|
1343
|
+
const timeout = setTimeout(() => {
|
|
1344
|
+
if (isIntermediate) {
|
|
1345
|
+
// Woke up early just to re-check; schedule again.
|
|
1346
|
+
scheduleNext(jobId, schedule);
|
|
1347
|
+
return;
|
|
1348
|
+
}
|
|
1349
|
+
// Re-read job from DB to get current configuration
|
|
1350
|
+
const currentJob = db
|
|
1351
|
+
.prepare('SELECT * FROM jobs WHERE id = ? AND enabled = 1')
|
|
1352
|
+
.get(jobId);
|
|
1353
|
+
if (!currentJob) {
|
|
1354
|
+
logger.warn({ jobId }, 'Job no longer exists or disabled, skipping');
|
|
1355
|
+
return;
|
|
1356
|
+
}
|
|
1357
|
+
onScheduledRun(currentJob);
|
|
1358
|
+
// Re-arm for the next occurrence
|
|
1359
|
+
scheduleNext(jobId, schedule);
|
|
1360
|
+
}, effectiveDelay);
|
|
1361
|
+
handles.set(jobId, {
|
|
1362
|
+
cancel: () => {
|
|
1363
|
+
clearTimeout(timeout);
|
|
1364
|
+
},
|
|
1365
|
+
});
|
|
1366
|
+
}
|
|
1367
|
+
function registerSchedule(job) {
|
|
918
1368
|
try {
|
|
919
|
-
|
|
920
|
-
|
|
921
|
-
// Re-read job from DB to get current configuration
|
|
922
|
-
const currentJob = db
|
|
923
|
-
.prepare('SELECT * FROM jobs WHERE id = ? AND enabled = 1')
|
|
924
|
-
.get(jobId);
|
|
925
|
-
if (!currentJob) {
|
|
926
|
-
logger.warn({ jobId }, 'Job no longer exists or disabled, skipping');
|
|
927
|
-
return;
|
|
928
|
-
}
|
|
929
|
-
onScheduledRun(currentJob);
|
|
930
|
-
});
|
|
931
|
-
crons.set(job.id, cron);
|
|
932
|
-
cronSchedules.set(job.id, job.schedule);
|
|
1369
|
+
scheduleNext(job.id, job.schedule);
|
|
1370
|
+
scheduleStrings.set(job.id, job.schedule);
|
|
933
1371
|
failedRegistrations.delete(job.id);
|
|
934
1372
|
logger.info({ jobId: job.id, schedule: job.schedule }, 'Scheduled job');
|
|
935
1373
|
return true;
|
|
@@ -946,39 +1384,39 @@ function createCronRegistry(deps) {
|
|
|
946
1384
|
.all();
|
|
947
1385
|
const enabledById = new Map(enabledJobs.map((j) => [j.id, j]));
|
|
948
1386
|
// Remove disabled/deleted jobs
|
|
949
|
-
for (const [jobId,
|
|
1387
|
+
for (const [jobId, handle] of handles.entries()) {
|
|
950
1388
|
if (!enabledById.has(jobId)) {
|
|
951
|
-
|
|
952
|
-
|
|
953
|
-
|
|
1389
|
+
handle.cancel();
|
|
1390
|
+
handles.delete(jobId);
|
|
1391
|
+
scheduleStrings.delete(jobId);
|
|
954
1392
|
}
|
|
955
1393
|
}
|
|
956
1394
|
const failedIds = [];
|
|
957
1395
|
// Add or update enabled jobs
|
|
958
1396
|
for (const job of enabledJobs) {
|
|
959
|
-
const
|
|
960
|
-
const existingSchedule =
|
|
961
|
-
if (!
|
|
962
|
-
if (!
|
|
1397
|
+
const existingHandle = handles.get(job.id);
|
|
1398
|
+
const existingSchedule = scheduleStrings.get(job.id);
|
|
1399
|
+
if (!existingHandle) {
|
|
1400
|
+
if (!registerSchedule(job))
|
|
963
1401
|
failedIds.push(job.id);
|
|
964
1402
|
continue;
|
|
965
1403
|
}
|
|
966
1404
|
if (existingSchedule !== job.schedule) {
|
|
967
|
-
|
|
968
|
-
|
|
969
|
-
|
|
970
|
-
if (!
|
|
1405
|
+
existingHandle.cancel();
|
|
1406
|
+
handles.delete(job.id);
|
|
1407
|
+
scheduleStrings.delete(job.id);
|
|
1408
|
+
if (!registerSchedule(job))
|
|
971
1409
|
failedIds.push(job.id);
|
|
972
1410
|
}
|
|
973
1411
|
}
|
|
974
1412
|
return { totalEnabled: enabledJobs.length, failedIds };
|
|
975
1413
|
}
|
|
976
1414
|
function stopAll() {
|
|
977
|
-
for (const
|
|
978
|
-
|
|
1415
|
+
for (const handle of handles.values()) {
|
|
1416
|
+
handle.cancel();
|
|
979
1417
|
}
|
|
980
|
-
|
|
981
|
-
|
|
1418
|
+
handles.clear();
|
|
1419
|
+
scheduleStrings.clear();
|
|
982
1420
|
}
|
|
983
1421
|
return {
|
|
984
1422
|
reconcile,
|
|
@@ -991,6 +1429,8 @@ function createCronRegistry(deps) {
|
|
|
991
1429
|
|
|
992
1430
|
/**
|
|
993
1431
|
* Run record repository for managing job execution records.
|
|
1432
|
+
*
|
|
1433
|
+
* @module
|
|
994
1434
|
*/
|
|
995
1435
|
/** Create a run repository for the given database connection. */
|
|
996
1436
|
function createRunRepository(db) {
|
|
@@ -1012,6 +1452,8 @@ function createRunRepository(db) {
|
|
|
1012
1452
|
|
|
1013
1453
|
/**
|
|
1014
1454
|
* Session executor for job type='session'. Spawns OpenClaw Gateway sessions and polls for completion.
|
|
1455
|
+
*
|
|
1456
|
+
* @module
|
|
1015
1457
|
*/
|
|
1016
1458
|
/** File extensions that indicate a script rather than a prompt. */
|
|
1017
1459
|
const SCRIPT_EXTENSIONS = ['.js', '.mjs', '.cjs', '.ps1', '.cmd', '.bat'];
|
|
@@ -1111,7 +1553,9 @@ async function executeSession(options) {
|
|
|
1111
1553
|
}
|
|
1112
1554
|
|
|
1113
1555
|
/**
|
|
1114
|
-
*
|
|
1556
|
+
* Job scheduler. Loads enabled jobs, registers schedules (cron or rrstack), manages execution, respects overlap policies and concurrency limits.
|
|
1557
|
+
*
|
|
1558
|
+
* @module
|
|
1115
1559
|
*/
|
|
1116
1560
|
// JobRow is imported from cron-registry
|
|
1117
1561
|
/**
|
|
@@ -1131,7 +1575,7 @@ function createScheduler(deps) {
|
|
|
1131
1575
|
let reconcileInterval = null;
|
|
1132
1576
|
/** Execute a job: create run record, run script, update record, send notifications. */
|
|
1133
1577
|
async function runJob(job, trigger) {
|
|
1134
|
-
const { id, name, script, type, timeout_ms, on_success, on_failure } = job;
|
|
1578
|
+
const { id, name, script, type, timeout_ms, on_success, on_failure, source_type, } = job;
|
|
1135
1579
|
// Check concurrency limit
|
|
1136
1580
|
if (runningJobs.size >= config.maxConcurrency) {
|
|
1137
1581
|
logger.warn({ jobId: id }, 'Max concurrency reached, skipping job');
|
|
@@ -1162,6 +1606,7 @@ function createScheduler(deps) {
|
|
|
1162
1606
|
jobId: id,
|
|
1163
1607
|
runId,
|
|
1164
1608
|
timeoutMs: timeout_ms ?? undefined,
|
|
1609
|
+
sourceType: source_type ?? 'path',
|
|
1165
1610
|
});
|
|
1166
1611
|
}
|
|
1167
1612
|
runRepository.finishRun(runId, result);
|
|
@@ -1251,6 +1696,8 @@ function createScheduler(deps) {
|
|
|
1251
1696
|
|
|
1252
1697
|
/**
|
|
1253
1698
|
* Main runner orchestrator. Wires up database, scheduler, API server, and handles graceful shutdown on SIGTERM/SIGINT.
|
|
1699
|
+
*
|
|
1700
|
+
* @module
|
|
1254
1701
|
*/
|
|
1255
1702
|
/**
|
|
1256
1703
|
* Create the runner. Initializes database, scheduler, API server, and sets up graceful shutdown.
|
|
@@ -1322,7 +1769,7 @@ function createRunner(config, deps) {
|
|
|
1322
1769
|
getConfig: () => config,
|
|
1323
1770
|
loggerConfig: { level: config.log.level, file: config.log.file },
|
|
1324
1771
|
});
|
|
1325
|
-
await server.listen({ port: config.port, host:
|
|
1772
|
+
await server.listen({ port: config.port, host: config.host });
|
|
1326
1773
|
logger.info({ port: config.port }, 'API server listening');
|
|
1327
1774
|
// Graceful shutdown
|
|
1328
1775
|
const shutdown = async (signal) => {
|
|
@@ -1361,6 +1808,8 @@ function createRunner(config, deps) {
|
|
|
1361
1808
|
|
|
1362
1809
|
/**
|
|
1363
1810
|
* Queue operations module for runner client. Provides enqueue, dequeue, done, and fail operations.
|
|
1811
|
+
*
|
|
1812
|
+
* @module
|
|
1364
1813
|
*/
|
|
1365
1814
|
/** Create queue operations for the given database connection. */
|
|
1366
1815
|
function createQueueOps(db) {
|
|
@@ -1463,6 +1912,8 @@ function createQueueOps(db) {
|
|
|
1463
1912
|
|
|
1464
1913
|
/**
|
|
1465
1914
|
* State operations module for runner client. Provides scalar state (key-value) and collection state (grouped items).
|
|
1915
|
+
*
|
|
1916
|
+
* @module
|
|
1466
1917
|
*/
|
|
1467
1918
|
/** Parse TTL string (e.g., '30d', '24h', '60m') into ISO datetime offset from now. */
|
|
1468
1919
|
function parseTtl(ttl) {
|
|
@@ -1568,6 +2019,8 @@ function createCollectionOps(db) {
|
|
|
1568
2019
|
|
|
1569
2020
|
/**
|
|
1570
2021
|
* Job client library for runner jobs. Provides state and queue operations. Opens its own DB connection via JR_DB_PATH env var.
|
|
2022
|
+
*
|
|
2023
|
+
* @module
|
|
1571
2024
|
*/
|
|
1572
2025
|
/**
|
|
1573
2026
|
* Create a runner client for job scripts. Opens its own DB connection.
|
|
@@ -1590,4 +2043,4 @@ function createClient(dbPath) {
|
|
|
1590
2043
|
};
|
|
1591
2044
|
}
|
|
1592
2045
|
|
|
1593
|
-
export { closeConnection, createClient, createConnection, createGatewayClient, createMaintenance, createNotifier, createRunner, createScheduler, executeJob, executeSession, jobSchema, queueSchema, runMigrations, runSchema, runStatusSchema, runTriggerSchema, runnerConfigSchema };
|
|
2046
|
+
export { closeConnection, createClient, createConnection, createGatewayClient, createMaintenance, createNotifier, createRunner, createScheduler, executeJob, executeSession, getNextFireTime, jobSchema, queueSchema, runMigrations, runSchema, runStatusSchema, runTriggerSchema, runnerConfigSchema, validateSchedule };
|