@karmaniverous/jeeves-runner 0.4.1 → 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 +545 -82
- package/dist/index.d.ts +65 -4
- package/dist/mjs/index.js +548 -79
- package/package.json +5 -3
package/dist/mjs/index.js
CHANGED
|
@@ -1,14 +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
|
-
import {
|
|
5
|
+
import { createConfigQueryHandler } from '@karmaniverous/jeeves';
|
|
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';
|
|
6
10
|
import { DatabaseSync } from 'node:sqlite';
|
|
7
11
|
import { request as request$1 } from 'node:http';
|
|
8
12
|
import { request } from 'node:https';
|
|
9
13
|
import { spawn } from 'node:child_process';
|
|
10
|
-
import {
|
|
11
|
-
import { JSONPath } from 'jsonpath-plus';
|
|
14
|
+
import { tmpdir } from 'node:os';
|
|
12
15
|
|
|
13
16
|
/**
|
|
14
17
|
* Runner configuration schema and types.
|
|
@@ -44,6 +47,8 @@ const gatewaySchema = z.object({
|
|
|
44
47
|
const runnerConfigSchema = z.object({
|
|
45
48
|
/** HTTP server port for the runner API. */
|
|
46
49
|
port: z.number().default(1937),
|
|
50
|
+
/** Bind address for the HTTP server. */
|
|
51
|
+
host: z.string().default('127.0.0.1'),
|
|
47
52
|
/** Path to SQLite database file. */
|
|
48
53
|
dbPath: z.string().default('./data/runner.sqlite'),
|
|
49
54
|
/** Maximum number of concurrent job executions. */
|
|
@@ -90,8 +95,8 @@ const jobSchema = z.object({
|
|
|
90
95
|
enabled: z.boolean().default(true),
|
|
91
96
|
/** Optional execution timeout in milliseconds. */
|
|
92
97
|
timeoutMs: z.number().optional(),
|
|
93
|
-
/** Policy for handling overlapping job executions (skip
|
|
94
|
-
overlapPolicy: z.enum(['skip', '
|
|
98
|
+
/** Policy for handling overlapping job executions (skip or allow). */
|
|
99
|
+
overlapPolicy: z.enum(['skip', 'allow']).default('skip'),
|
|
95
100
|
/** Slack channel ID for failure notifications. */
|
|
96
101
|
onFailure: z.string().nullable().default(null),
|
|
97
102
|
/** Slack channel ID for success notifications. */
|
|
@@ -170,13 +175,394 @@ const runSchema = z.object({
|
|
|
170
175
|
});
|
|
171
176
|
|
|
172
177
|
/**
|
|
173
|
-
*
|
|
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
|
+
|
|
556
|
+
/**
|
|
557
|
+
* Fastify API routes for job management and monitoring. Provides endpoints for job CRUD, run history, manual triggers, system stats, and config queries.
|
|
558
|
+
*
|
|
559
|
+
* @module routes
|
|
174
560
|
*/
|
|
175
561
|
/**
|
|
176
562
|
* Register all API routes on the Fastify instance.
|
|
177
563
|
*/
|
|
178
564
|
function registerRoutes(app, deps) {
|
|
179
|
-
const { db, scheduler } = deps;
|
|
565
|
+
const { db, scheduler, getConfig } = deps;
|
|
180
566
|
/** GET /health — Health check. */
|
|
181
567
|
app.get('/health', () => {
|
|
182
568
|
return {
|
|
@@ -196,7 +582,7 @@ function registerRoutes(app, deps) {
|
|
|
196
582
|
return { jobs: rows };
|
|
197
583
|
});
|
|
198
584
|
/** GET /jobs/:id — Single job detail. */
|
|
199
|
-
app.get('/jobs/:id',
|
|
585
|
+
app.get('/jobs/:id', (request, reply) => {
|
|
200
586
|
const job = db
|
|
201
587
|
.prepare('SELECT * FROM jobs WHERE id = ?')
|
|
202
588
|
.get(request.params.id);
|
|
@@ -225,30 +611,6 @@ function registerRoutes(app, deps) {
|
|
|
225
611
|
return { error: err instanceof Error ? err.message : 'Unknown error' };
|
|
226
612
|
}
|
|
227
613
|
});
|
|
228
|
-
/** POST /jobs/:id/enable — Enable a job. */
|
|
229
|
-
app.post('/jobs/:id/enable', (request, reply) => {
|
|
230
|
-
const result = db
|
|
231
|
-
.prepare('UPDATE jobs SET enabled = 1 WHERE id = ?')
|
|
232
|
-
.run(request.params.id);
|
|
233
|
-
if (result.changes === 0) {
|
|
234
|
-
reply.code(404);
|
|
235
|
-
return { error: 'Job not found' };
|
|
236
|
-
}
|
|
237
|
-
scheduler.reconcileNow();
|
|
238
|
-
return { ok: true };
|
|
239
|
-
});
|
|
240
|
-
/** POST /jobs/:id/disable — Disable a job. */
|
|
241
|
-
app.post('/jobs/:id/disable', (request, reply) => {
|
|
242
|
-
const result = db
|
|
243
|
-
.prepare('UPDATE jobs SET enabled = 0 WHERE id = ?')
|
|
244
|
-
.run(request.params.id);
|
|
245
|
-
if (result.changes === 0) {
|
|
246
|
-
reply.code(404);
|
|
247
|
-
return { error: 'Job not found' };
|
|
248
|
-
}
|
|
249
|
-
scheduler.reconcileNow();
|
|
250
|
-
return { ok: true };
|
|
251
|
-
});
|
|
252
614
|
/** GET /stats — Aggregate job statistics. */
|
|
253
615
|
app.get('/stats', () => {
|
|
254
616
|
const totalJobs = db
|
|
@@ -272,10 +634,25 @@ function registerRoutes(app, deps) {
|
|
|
272
634
|
errorsLastHour: errorsLastHour.count,
|
|
273
635
|
};
|
|
274
636
|
});
|
|
637
|
+
/** GET /config — Query effective configuration via JSONPath. */
|
|
638
|
+
const configHandler = createConfigQueryHandler(getConfig);
|
|
639
|
+
app.get('/config', async (request, reply) => {
|
|
640
|
+
const result = await configHandler({
|
|
641
|
+
path: request.query.path,
|
|
642
|
+
});
|
|
643
|
+
return reply.status(result.status).send(result.body);
|
|
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 });
|
|
275
650
|
}
|
|
276
651
|
|
|
277
652
|
/**
|
|
278
653
|
* Fastify HTTP server for runner API. Creates server instance with logging, registers routes, listens on configured port (localhost only).
|
|
654
|
+
*
|
|
655
|
+
* @module
|
|
279
656
|
*/
|
|
280
657
|
/**
|
|
281
658
|
* Create and configure the Fastify server. Routes are registered but server is not started.
|
|
@@ -296,12 +673,18 @@ function createServer(deps) {
|
|
|
296
673
|
}
|
|
297
674
|
: false,
|
|
298
675
|
});
|
|
299
|
-
registerRoutes(app,
|
|
676
|
+
registerRoutes(app, {
|
|
677
|
+
db: deps.db,
|
|
678
|
+
scheduler: deps.scheduler,
|
|
679
|
+
getConfig: deps.getConfig,
|
|
680
|
+
});
|
|
300
681
|
return app;
|
|
301
682
|
}
|
|
302
683
|
|
|
303
684
|
/**
|
|
304
685
|
* SQLite connection manager. Creates DB file with parent directories, enables WAL mode for concurrency.
|
|
686
|
+
*
|
|
687
|
+
* @module
|
|
305
688
|
*/
|
|
306
689
|
/**
|
|
307
690
|
* Create and configure a SQLite database connection.
|
|
@@ -330,6 +713,8 @@ function closeConnection(db) {
|
|
|
330
713
|
|
|
331
714
|
/**
|
|
332
715
|
* Database maintenance tasks: run retention pruning and expired state cleanup.
|
|
716
|
+
*
|
|
717
|
+
* @module
|
|
333
718
|
*/
|
|
334
719
|
/** Delete runs older than the configured retention period. */
|
|
335
720
|
function pruneOldRuns(db, days, logger) {
|
|
@@ -396,6 +781,8 @@ function createMaintenance(db, config, logger) {
|
|
|
396
781
|
|
|
397
782
|
/**
|
|
398
783
|
* Schema migration runner. Tracks applied migrations via schema_version table, applies pending migrations idempotently.
|
|
784
|
+
*
|
|
785
|
+
* @module
|
|
399
786
|
*/
|
|
400
787
|
/** Initial schema migration SQL (embedded to avoid runtime file resolution issues). */
|
|
401
788
|
const MIGRATION_001 = `
|
|
@@ -529,11 +916,16 @@ CREATE TABLE state_items (
|
|
|
529
916
|
);
|
|
530
917
|
CREATE INDEX idx_state_items_ns_key ON state_items(namespace, key);
|
|
531
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
|
+
`;
|
|
532
923
|
/** Registry of all migrations keyed by version number. */
|
|
533
924
|
const MIGRATIONS = {
|
|
534
925
|
1: MIGRATION_001,
|
|
535
926
|
2: MIGRATION_002,
|
|
536
927
|
3: MIGRATION_003,
|
|
928
|
+
4: MIGRATION_004,
|
|
537
929
|
};
|
|
538
930
|
/**
|
|
539
931
|
* Run all pending migrations. Creates schema_version table if needed, applies migrations in order.
|
|
@@ -564,6 +956,8 @@ function runMigrations(db) {
|
|
|
564
956
|
|
|
565
957
|
/**
|
|
566
958
|
* Shared HTTP utility for making POST requests.
|
|
959
|
+
*
|
|
960
|
+
* @module
|
|
567
961
|
*/
|
|
568
962
|
/** Make an HTTP/HTTPS POST request. Returns the parsed JSON response body. */
|
|
569
963
|
function httpPost(url, headers, body, timeoutMs = 30000) {
|
|
@@ -611,6 +1005,8 @@ function httpPost(url, headers, body, timeoutMs = 30000) {
|
|
|
611
1005
|
|
|
612
1006
|
/**
|
|
613
1007
|
* OpenClaw Gateway HTTP client for spawning and monitoring sessions.
|
|
1008
|
+
*
|
|
1009
|
+
* @module
|
|
614
1010
|
*/
|
|
615
1011
|
/** Make an HTTP POST request to the Gateway /tools/invoke endpoint. */
|
|
616
1012
|
function invokeGateway(url, token, tool, args, timeoutMs = 30000) {
|
|
@@ -677,6 +1073,8 @@ function createGatewayClient(options) {
|
|
|
677
1073
|
|
|
678
1074
|
/**
|
|
679
1075
|
* Slack notification module. Sends job completion/failure messages via Slack Web API (chat.postMessage). Falls back gracefully if no token.
|
|
1076
|
+
*
|
|
1077
|
+
* @module
|
|
680
1078
|
*/
|
|
681
1079
|
/** Post a message to Slack via chat.postMessage API. */
|
|
682
1080
|
async function postToSlack(token, channel, text) {
|
|
@@ -694,21 +1092,21 @@ function createNotifier(config) {
|
|
|
694
1092
|
return {
|
|
695
1093
|
async notifySuccess(jobName, durationMs, channel) {
|
|
696
1094
|
if (!slackToken) {
|
|
697
|
-
console.warn(`No Slack token configured
|
|
1095
|
+
console.warn(`No Slack token configured \u2014 skipping success notification for ${jobName}`);
|
|
698
1096
|
return;
|
|
699
1097
|
}
|
|
700
1098
|
const durationSec = (durationMs / 1000).toFixed(1);
|
|
701
|
-
const text =
|
|
1099
|
+
const text = `\u2705 *${jobName}* completed (${durationSec}s)`;
|
|
702
1100
|
await postToSlack(slackToken, channel, text);
|
|
703
1101
|
},
|
|
704
1102
|
async notifyFailure(jobName, durationMs, error, channel) {
|
|
705
1103
|
if (!slackToken) {
|
|
706
|
-
console.warn(`No Slack token configured
|
|
1104
|
+
console.warn(`No Slack token configured \u2014 skipping failure notification for ${jobName}`);
|
|
707
1105
|
return;
|
|
708
1106
|
}
|
|
709
1107
|
const durationSec = (durationMs / 1000).toFixed(1);
|
|
710
1108
|
const errorMsg = error ? `: ${error}` : '';
|
|
711
|
-
const text =
|
|
1109
|
+
const text = `\u274c *${jobName}* failed (${durationSec}s)${errorMsg}`;
|
|
712
1110
|
await postToSlack(slackToken, channel, text);
|
|
713
1111
|
},
|
|
714
1112
|
async dispatchResult(result, jobName, onSuccess, onFailure, logger) {
|
|
@@ -728,6 +1126,8 @@ function createNotifier(config) {
|
|
|
728
1126
|
|
|
729
1127
|
/**
|
|
730
1128
|
* Job executor. Spawns job scripts as child processes, captures output, parses result metadata, enforces timeouts.
|
|
1129
|
+
*
|
|
1130
|
+
* @module
|
|
731
1131
|
*/
|
|
732
1132
|
/** Ring buffer for capturing last N lines of output. */
|
|
733
1133
|
class RingBuffer {
|
|
@@ -789,14 +1189,23 @@ function resolveCommand(script) {
|
|
|
789
1189
|
* Execute a job script as a child process. Captures output, parses metadata, enforces timeout.
|
|
790
1190
|
*/
|
|
791
1191
|
function executeJob(options) {
|
|
792
|
-
const { script, dbPath, jobId, runId, timeoutMs, commandResolver } = options;
|
|
1192
|
+
const { script, dbPath, jobId, runId, timeoutMs, commandResolver, sourceType = 'path', } = options;
|
|
793
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
|
+
}
|
|
794
1203
|
return new Promise((resolve) => {
|
|
795
1204
|
const stdoutBuffer = new RingBuffer(100);
|
|
796
1205
|
const stderrBuffer = new RingBuffer(100);
|
|
797
1206
|
const { command, args } = commandResolver
|
|
798
|
-
? commandResolver(
|
|
799
|
-
: resolveCommand(
|
|
1207
|
+
? commandResolver(effectiveScript)
|
|
1208
|
+
: resolveCommand(effectiveScript);
|
|
800
1209
|
const child = spawn(command, args, {
|
|
801
1210
|
env: {
|
|
802
1211
|
...process.env,
|
|
@@ -832,6 +1241,7 @@ function executeJob(options) {
|
|
|
832
1241
|
child.on('close', (exitCode) => {
|
|
833
1242
|
if (timeoutHandle)
|
|
834
1243
|
clearTimeout(timeoutHandle);
|
|
1244
|
+
cleanupTempFile(tempFile);
|
|
835
1245
|
const durationMs = Date.now() - startTime;
|
|
836
1246
|
const stdoutTail = stdoutBuffer.getAll();
|
|
837
1247
|
const stderrTail = stderrBuffer.getAll();
|
|
@@ -876,6 +1286,7 @@ function executeJob(options) {
|
|
|
876
1286
|
child.on('error', (err) => {
|
|
877
1287
|
if (timeoutHandle)
|
|
878
1288
|
clearTimeout(timeoutHandle);
|
|
1289
|
+
cleanupTempFile(tempFile);
|
|
879
1290
|
const durationMs = Date.now() - startTime;
|
|
880
1291
|
resolve({
|
|
881
1292
|
status: 'error',
|
|
@@ -890,31 +1301,73 @@ function executeJob(options) {
|
|
|
890
1301
|
});
|
|
891
1302
|
});
|
|
892
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
|
+
}
|
|
893
1315
|
|
|
894
1316
|
/**
|
|
895
|
-
*
|
|
1317
|
+
* Schedule registration and reconciliation. Supports cron (croner) and RRStack schedule formats via unified setTimeout-based scheduling.
|
|
1318
|
+
*
|
|
1319
|
+
* @module
|
|
896
1320
|
*/
|
|
1321
|
+
/** Maximum setTimeout delay (Node.js limit: ~24.8 days). */
|
|
1322
|
+
const MAX_TIMEOUT_MS = 2_147_483_647;
|
|
897
1323
|
function createCronRegistry(deps) {
|
|
898
1324
|
const { db, logger, onScheduledRun } = deps;
|
|
899
|
-
const
|
|
900
|
-
const
|
|
1325
|
+
const handles = new Map();
|
|
1326
|
+
const scheduleStrings = new Map();
|
|
901
1327
|
const failedRegistrations = new Set();
|
|
902
|
-
|
|
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) {
|
|
903
1368
|
try {
|
|
904
|
-
|
|
905
|
-
|
|
906
|
-
// Re-read job from DB to get current configuration
|
|
907
|
-
const currentJob = db
|
|
908
|
-
.prepare('SELECT * FROM jobs WHERE id = ? AND enabled = 1')
|
|
909
|
-
.get(jobId);
|
|
910
|
-
if (!currentJob) {
|
|
911
|
-
logger.warn({ jobId }, 'Job no longer exists or disabled, skipping');
|
|
912
|
-
return;
|
|
913
|
-
}
|
|
914
|
-
onScheduledRun(currentJob);
|
|
915
|
-
});
|
|
916
|
-
crons.set(job.id, cron);
|
|
917
|
-
cronSchedules.set(job.id, job.schedule);
|
|
1369
|
+
scheduleNext(job.id, job.schedule);
|
|
1370
|
+
scheduleStrings.set(job.id, job.schedule);
|
|
918
1371
|
failedRegistrations.delete(job.id);
|
|
919
1372
|
logger.info({ jobId: job.id, schedule: job.schedule }, 'Scheduled job');
|
|
920
1373
|
return true;
|
|
@@ -931,39 +1384,39 @@ function createCronRegistry(deps) {
|
|
|
931
1384
|
.all();
|
|
932
1385
|
const enabledById = new Map(enabledJobs.map((j) => [j.id, j]));
|
|
933
1386
|
// Remove disabled/deleted jobs
|
|
934
|
-
for (const [jobId,
|
|
1387
|
+
for (const [jobId, handle] of handles.entries()) {
|
|
935
1388
|
if (!enabledById.has(jobId)) {
|
|
936
|
-
|
|
937
|
-
|
|
938
|
-
|
|
1389
|
+
handle.cancel();
|
|
1390
|
+
handles.delete(jobId);
|
|
1391
|
+
scheduleStrings.delete(jobId);
|
|
939
1392
|
}
|
|
940
1393
|
}
|
|
941
1394
|
const failedIds = [];
|
|
942
1395
|
// Add or update enabled jobs
|
|
943
1396
|
for (const job of enabledJobs) {
|
|
944
|
-
const
|
|
945
|
-
const existingSchedule =
|
|
946
|
-
if (!
|
|
947
|
-
if (!
|
|
1397
|
+
const existingHandle = handles.get(job.id);
|
|
1398
|
+
const existingSchedule = scheduleStrings.get(job.id);
|
|
1399
|
+
if (!existingHandle) {
|
|
1400
|
+
if (!registerSchedule(job))
|
|
948
1401
|
failedIds.push(job.id);
|
|
949
1402
|
continue;
|
|
950
1403
|
}
|
|
951
1404
|
if (existingSchedule !== job.schedule) {
|
|
952
|
-
|
|
953
|
-
|
|
954
|
-
|
|
955
|
-
if (!
|
|
1405
|
+
existingHandle.cancel();
|
|
1406
|
+
handles.delete(job.id);
|
|
1407
|
+
scheduleStrings.delete(job.id);
|
|
1408
|
+
if (!registerSchedule(job))
|
|
956
1409
|
failedIds.push(job.id);
|
|
957
1410
|
}
|
|
958
1411
|
}
|
|
959
1412
|
return { totalEnabled: enabledJobs.length, failedIds };
|
|
960
1413
|
}
|
|
961
1414
|
function stopAll() {
|
|
962
|
-
for (const
|
|
963
|
-
|
|
1415
|
+
for (const handle of handles.values()) {
|
|
1416
|
+
handle.cancel();
|
|
964
1417
|
}
|
|
965
|
-
|
|
966
|
-
|
|
1418
|
+
handles.clear();
|
|
1419
|
+
scheduleStrings.clear();
|
|
967
1420
|
}
|
|
968
1421
|
return {
|
|
969
1422
|
reconcile,
|
|
@@ -976,6 +1429,8 @@ function createCronRegistry(deps) {
|
|
|
976
1429
|
|
|
977
1430
|
/**
|
|
978
1431
|
* Run record repository for managing job execution records.
|
|
1432
|
+
*
|
|
1433
|
+
* @module
|
|
979
1434
|
*/
|
|
980
1435
|
/** Create a run repository for the given database connection. */
|
|
981
1436
|
function createRunRepository(db) {
|
|
@@ -997,6 +1452,8 @@ function createRunRepository(db) {
|
|
|
997
1452
|
|
|
998
1453
|
/**
|
|
999
1454
|
* Session executor for job type='session'. Spawns OpenClaw Gateway sessions and polls for completion.
|
|
1455
|
+
*
|
|
1456
|
+
* @module
|
|
1000
1457
|
*/
|
|
1001
1458
|
/** File extensions that indicate a script rather than a prompt. */
|
|
1002
1459
|
const SCRIPT_EXTENSIONS = ['.js', '.mjs', '.cjs', '.ps1', '.cmd', '.bat'];
|
|
@@ -1096,7 +1553,9 @@ async function executeSession(options) {
|
|
|
1096
1553
|
}
|
|
1097
1554
|
|
|
1098
1555
|
/**
|
|
1099
|
-
*
|
|
1556
|
+
* Job scheduler. Loads enabled jobs, registers schedules (cron or rrstack), manages execution, respects overlap policies and concurrency limits.
|
|
1557
|
+
*
|
|
1558
|
+
* @module
|
|
1100
1559
|
*/
|
|
1101
1560
|
// JobRow is imported from cron-registry
|
|
1102
1561
|
/**
|
|
@@ -1116,7 +1575,7 @@ function createScheduler(deps) {
|
|
|
1116
1575
|
let reconcileInterval = null;
|
|
1117
1576
|
/** Execute a job: create run record, run script, update record, send notifications. */
|
|
1118
1577
|
async function runJob(job, trigger) {
|
|
1119
|
-
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;
|
|
1120
1579
|
// Check concurrency limit
|
|
1121
1580
|
if (runningJobs.size >= config.maxConcurrency) {
|
|
1122
1581
|
logger.warn({ jobId: id }, 'Max concurrency reached, skipping job');
|
|
@@ -1147,6 +1606,7 @@ function createScheduler(deps) {
|
|
|
1147
1606
|
jobId: id,
|
|
1148
1607
|
runId,
|
|
1149
1608
|
timeoutMs: timeout_ms ?? undefined,
|
|
1609
|
+
sourceType: source_type ?? 'path',
|
|
1150
1610
|
});
|
|
1151
1611
|
}
|
|
1152
1612
|
runRepository.finishRun(runId, result);
|
|
@@ -1236,6 +1696,8 @@ function createScheduler(deps) {
|
|
|
1236
1696
|
|
|
1237
1697
|
/**
|
|
1238
1698
|
* Main runner orchestrator. Wires up database, scheduler, API server, and handles graceful shutdown on SIGTERM/SIGINT.
|
|
1699
|
+
*
|
|
1700
|
+
* @module
|
|
1239
1701
|
*/
|
|
1240
1702
|
/**
|
|
1241
1703
|
* Create the runner. Initializes database, scheduler, API server, and sets up graceful shutdown.
|
|
@@ -1304,9 +1766,10 @@ function createRunner(config, deps) {
|
|
|
1304
1766
|
server = createServer({
|
|
1305
1767
|
db,
|
|
1306
1768
|
scheduler,
|
|
1769
|
+
getConfig: () => config,
|
|
1307
1770
|
loggerConfig: { level: config.log.level, file: config.log.file },
|
|
1308
1771
|
});
|
|
1309
|
-
await server.listen({ port: config.port, host:
|
|
1772
|
+
await server.listen({ port: config.port, host: config.host });
|
|
1310
1773
|
logger.info({ port: config.port }, 'API server listening');
|
|
1311
1774
|
// Graceful shutdown
|
|
1312
1775
|
const shutdown = async (signal) => {
|
|
@@ -1345,6 +1808,8 @@ function createRunner(config, deps) {
|
|
|
1345
1808
|
|
|
1346
1809
|
/**
|
|
1347
1810
|
* Queue operations module for runner client. Provides enqueue, dequeue, done, and fail operations.
|
|
1811
|
+
*
|
|
1812
|
+
* @module
|
|
1348
1813
|
*/
|
|
1349
1814
|
/** Create queue operations for the given database connection. */
|
|
1350
1815
|
function createQueueOps(db) {
|
|
@@ -1447,6 +1912,8 @@ function createQueueOps(db) {
|
|
|
1447
1912
|
|
|
1448
1913
|
/**
|
|
1449
1914
|
* State operations module for runner client. Provides scalar state (key-value) and collection state (grouped items).
|
|
1915
|
+
*
|
|
1916
|
+
* @module
|
|
1450
1917
|
*/
|
|
1451
1918
|
/** Parse TTL string (e.g., '30d', '24h', '60m') into ISO datetime offset from now. */
|
|
1452
1919
|
function parseTtl(ttl) {
|
|
@@ -1552,6 +2019,8 @@ function createCollectionOps(db) {
|
|
|
1552
2019
|
|
|
1553
2020
|
/**
|
|
1554
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
|
|
1555
2024
|
*/
|
|
1556
2025
|
/**
|
|
1557
2026
|
* Create a runner client for job scripts. Opens its own DB connection.
|
|
@@ -1574,4 +2043,4 @@ function createClient(dbPath) {
|
|
|
1574
2043
|
};
|
|
1575
2044
|
}
|
|
1576
2045
|
|
|
1577
|
-
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 };
|