@rebasepro/server-core 0.0.1-canary.94dff14 → 0.0.1-canary.bbcb8b4
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/index.es.js +185 -45
- package/dist/index.es.js.map +1 -1
- package/dist/index.umd.js +185 -45
- package/dist/index.umd.js.map +1 -1
- package/dist/server-core/src/cron/cron-scheduler.d.ts +45 -0
- package/dist/server-core/src/cron/index.d.ts +1 -1
- package/package.json +8 -8
- package/src/cron/cron-scheduler.test.ts +301 -175
- package/src/cron/cron-scheduler.ts +220 -57
- package/src/cron/index.ts +1 -1
package/dist/index.es.js
CHANGED
|
@@ -49220,46 +49220,98 @@ const cronLoader = /* @__PURE__ */ Object.freeze(/* @__PURE__ */ Object.definePr
|
|
|
49220
49220
|
__proto__: null,
|
|
49221
49221
|
loadCronJobsFromDirectory
|
|
49222
49222
|
}, Symbol.toStringTag, { value: "Module" }));
|
|
49223
|
+
function expandCronField(field, min, max) {
|
|
49224
|
+
const results = /* @__PURE__ */ new Set();
|
|
49225
|
+
for (const segment of field.split(",")) {
|
|
49226
|
+
const trimmed = segment.trim();
|
|
49227
|
+
if (trimmed === "*") {
|
|
49228
|
+
for (let i = min; i <= max; i++) results.add(i);
|
|
49229
|
+
} else if (trimmed.includes("/")) {
|
|
49230
|
+
const [rangeStr, stepStr] = trimmed.split("/");
|
|
49231
|
+
const step = parseInt(stepStr, 10);
|
|
49232
|
+
if (isNaN(step) || step <= 0) {
|
|
49233
|
+
throw new Error(`Invalid step value "${stepStr}" in cron field "${field}"`);
|
|
49234
|
+
}
|
|
49235
|
+
let start = min;
|
|
49236
|
+
let end2 = max;
|
|
49237
|
+
if (rangeStr !== "*") {
|
|
49238
|
+
if (rangeStr.includes("-")) {
|
|
49239
|
+
const [a2, b] = rangeStr.split("-").map(Number);
|
|
49240
|
+
start = a2;
|
|
49241
|
+
end2 = b;
|
|
49242
|
+
} else {
|
|
49243
|
+
start = parseInt(rangeStr, 10);
|
|
49244
|
+
}
|
|
49245
|
+
}
|
|
49246
|
+
for (let i = start; i <= end2; i += step) results.add(i);
|
|
49247
|
+
} else if (trimmed.includes("-")) {
|
|
49248
|
+
const [a2, b] = trimmed.split("-").map(Number);
|
|
49249
|
+
for (let i = a2; i <= b; i++) results.add(i);
|
|
49250
|
+
} else {
|
|
49251
|
+
const val = parseInt(trimmed, 10);
|
|
49252
|
+
if (isNaN(val)) {
|
|
49253
|
+
throw new Error(`Invalid value "${trimmed}" in cron field "${field}"`);
|
|
49254
|
+
}
|
|
49255
|
+
results.add(val);
|
|
49256
|
+
}
|
|
49257
|
+
}
|
|
49258
|
+
return [...results].sort((a2, b) => a2 - b);
|
|
49259
|
+
}
|
|
49260
|
+
function validateCronExpression(schedule) {
|
|
49261
|
+
if (!schedule || typeof schedule !== "string") {
|
|
49262
|
+
return {
|
|
49263
|
+
valid: false,
|
|
49264
|
+
reason: "Schedule must be a non-empty string"
|
|
49265
|
+
};
|
|
49266
|
+
}
|
|
49267
|
+
const parts = schedule.trim().split(/\s+/);
|
|
49268
|
+
if (parts.length !== 5) {
|
|
49269
|
+
return {
|
|
49270
|
+
valid: false,
|
|
49271
|
+
reason: `Expected 5 fields, got ${parts.length}`
|
|
49272
|
+
};
|
|
49273
|
+
}
|
|
49274
|
+
const fieldRanges = [["minute", 0, 59], ["hour", 0, 23], ["day of month", 1, 31], ["month", 1, 12], ["day of week", 0, 6]];
|
|
49275
|
+
for (let i = 0; i < 5; i++) {
|
|
49276
|
+
const [name2, min, max] = fieldRanges[i];
|
|
49277
|
+
try {
|
|
49278
|
+
const values2 = expandCronField(parts[i], min, max);
|
|
49279
|
+
if (values2.length === 0) {
|
|
49280
|
+
return {
|
|
49281
|
+
valid: false,
|
|
49282
|
+
reason: `${name2} field "${parts[i]}" produces no values`
|
|
49283
|
+
};
|
|
49284
|
+
}
|
|
49285
|
+
for (const v of values2) {
|
|
49286
|
+
if (v < min || v > max) {
|
|
49287
|
+
return {
|
|
49288
|
+
valid: false,
|
|
49289
|
+
reason: `${name2} field value ${v} out of range [${min}–${max}]`
|
|
49290
|
+
};
|
|
49291
|
+
}
|
|
49292
|
+
}
|
|
49293
|
+
} catch (err) {
|
|
49294
|
+
return {
|
|
49295
|
+
valid: false,
|
|
49296
|
+
reason: `${name2} field: ${err instanceof Error ? err.message : String(err)}`
|
|
49297
|
+
};
|
|
49298
|
+
}
|
|
49299
|
+
}
|
|
49300
|
+
return {
|
|
49301
|
+
valid: true
|
|
49302
|
+
};
|
|
49303
|
+
}
|
|
49223
49304
|
function parseCronExpression(expression, after) {
|
|
49224
49305
|
const parts = expression.trim().split(/\s+/);
|
|
49225
49306
|
if (parts.length < 5) {
|
|
49226
49307
|
throw new Error(`Invalid cron expression: "${expression}". Expected 5 fields.`);
|
|
49227
49308
|
}
|
|
49228
49309
|
const [minField, hourField, domField, monField, dowField] = parts;
|
|
49229
|
-
const
|
|
49230
|
-
|
|
49231
|
-
|
|
49232
|
-
|
|
49233
|
-
|
|
49234
|
-
} else if (segment.includes("/")) {
|
|
49235
|
-
const [rangeStr, stepStr] = segment.split("/");
|
|
49236
|
-
const step = parseInt(stepStr, 10);
|
|
49237
|
-
let start = min;
|
|
49238
|
-
let end2 = max;
|
|
49239
|
-
if (rangeStr !== "*") {
|
|
49240
|
-
if (rangeStr.includes("-")) {
|
|
49241
|
-
const [a2, b] = rangeStr.split("-").map(Number);
|
|
49242
|
-
start = a2;
|
|
49243
|
-
end2 = b;
|
|
49244
|
-
} else {
|
|
49245
|
-
start = parseInt(rangeStr, 10);
|
|
49246
|
-
}
|
|
49247
|
-
}
|
|
49248
|
-
for (let i = start; i <= end2; i += step) results.add(i);
|
|
49249
|
-
} else if (segment.includes("-")) {
|
|
49250
|
-
const [a2, b] = segment.split("-").map(Number);
|
|
49251
|
-
for (let i = a2; i <= b; i++) results.add(i);
|
|
49252
|
-
} else {
|
|
49253
|
-
results.add(parseInt(segment, 10));
|
|
49254
|
-
}
|
|
49255
|
-
}
|
|
49256
|
-
return [...results].sort((a2, b) => a2 - b);
|
|
49257
|
-
};
|
|
49258
|
-
const minutes = expand(minField, 0, 59);
|
|
49259
|
-
const hours = expand(hourField, 0, 23);
|
|
49260
|
-
const doms = expand(domField, 1, 31);
|
|
49261
|
-
const months = expand(monField, 1, 12);
|
|
49262
|
-
const dows = expand(dowField, 0, 6);
|
|
49310
|
+
const minutes = expandCronField(minField, 0, 59);
|
|
49311
|
+
const hours = expandCronField(hourField, 0, 23);
|
|
49312
|
+
const doms = expandCronField(domField, 1, 31);
|
|
49313
|
+
const months = expandCronField(monField, 1, 12);
|
|
49314
|
+
const dows = expandCronField(dowField, 0, 6);
|
|
49263
49315
|
const candidate = new Date(after);
|
|
49264
49316
|
candidate.setSeconds(0, 0);
|
|
49265
49317
|
candidate.setMinutes(candidate.getMinutes() + 1);
|
|
@@ -49280,6 +49332,7 @@ function parseCronExpression(expression, after) {
|
|
|
49280
49332
|
return fallback;
|
|
49281
49333
|
}
|
|
49282
49334
|
const MAX_LOGS_PER_JOB = 50;
|
|
49335
|
+
const MIN_SCHEDULE_INTERVAL_MS = 5e3;
|
|
49283
49336
|
class CronScheduler {
|
|
49284
49337
|
jobs = /* @__PURE__ */ new Map();
|
|
49285
49338
|
started = false;
|
|
@@ -49301,23 +49354,39 @@ class CronScheduler {
|
|
|
49301
49354
|
}
|
|
49302
49355
|
/**
|
|
49303
49356
|
* Register a batch of loaded cron jobs.
|
|
49357
|
+
*
|
|
49358
|
+
* If the scheduler is already started, newly registered jobs are
|
|
49359
|
+
* automatically scheduled (so late-registered jobs don't sit idle).
|
|
49360
|
+
*
|
|
49361
|
+
* Validates the cron schedule on registration — invalid schedules
|
|
49362
|
+
* are rejected with a warning and the job is NOT registered.
|
|
49304
49363
|
*/
|
|
49305
49364
|
registerJobs(loadedJobs) {
|
|
49306
49365
|
for (const loaded of loadedJobs) {
|
|
49366
|
+
const validation = validateCronExpression(loaded.definition.schedule);
|
|
49367
|
+
if (!validation.valid) {
|
|
49368
|
+
console.error(`[cron] Rejecting job "${loaded.id}": invalid schedule "${loaded.definition.schedule}" — ${validation.reason}`);
|
|
49369
|
+
continue;
|
|
49370
|
+
}
|
|
49307
49371
|
const existing = this.jobs.get(loaded.id);
|
|
49308
49372
|
if (existing) {
|
|
49309
49373
|
console.warn(`[cron] Duplicate cron job id: "${loaded.id}". Overwriting.`);
|
|
49310
49374
|
this.stopJob(loaded.id);
|
|
49311
49375
|
}
|
|
49376
|
+
const enabled = loaded.definition.enabled !== false;
|
|
49312
49377
|
this.jobs.set(loaded.id, {
|
|
49313
49378
|
id: loaded.id,
|
|
49314
49379
|
definition: loaded.definition,
|
|
49315
|
-
enabled
|
|
49316
|
-
state:
|
|
49380
|
+
enabled,
|
|
49381
|
+
state: enabled ? "idle" : "disabled",
|
|
49317
49382
|
totalRuns: 0,
|
|
49318
49383
|
totalFailures: 0,
|
|
49319
|
-
logs: []
|
|
49384
|
+
logs: [],
|
|
49385
|
+
executing: false
|
|
49320
49386
|
});
|
|
49387
|
+
if (this.started && enabled) {
|
|
49388
|
+
this.scheduleNext(loaded.id);
|
|
49389
|
+
}
|
|
49321
49390
|
}
|
|
49322
49391
|
}
|
|
49323
49392
|
/**
|
|
@@ -49351,6 +49420,9 @@ class CronScheduler {
|
|
|
49351
49420
|
}
|
|
49352
49421
|
/**
|
|
49353
49422
|
* Stop the scheduler and clear all timers.
|
|
49423
|
+
*
|
|
49424
|
+
* Currently-executing handlers run to completion (they are async),
|
|
49425
|
+
* but no further scheduling occurs after stop.
|
|
49354
49426
|
*/
|
|
49355
49427
|
stop() {
|
|
49356
49428
|
this.started = false;
|
|
@@ -49409,32 +49481,81 @@ class CronScheduler {
|
|
|
49409
49481
|
}
|
|
49410
49482
|
/**
|
|
49411
49483
|
* Manually trigger a job execution immediately.
|
|
49484
|
+
*
|
|
49485
|
+
* Returns `undefined` if the job doesn't exist.
|
|
49486
|
+
* If the job is currently executing, returns the log entry with
|
|
49487
|
+
* a `skipped: true` result rather than running concurrently.
|
|
49412
49488
|
*/
|
|
49413
49489
|
async triggerJob(id) {
|
|
49414
49490
|
const job = this.jobs.get(id);
|
|
49415
49491
|
if (!job) return void 0;
|
|
49492
|
+
if (job.executing) {
|
|
49493
|
+
console.warn(`[cron] Skipping manual trigger of "${id}" — already executing`);
|
|
49494
|
+
const logEntry = {
|
|
49495
|
+
jobId: id,
|
|
49496
|
+
startedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
49497
|
+
finishedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
49498
|
+
durationMs: 0,
|
|
49499
|
+
success: true,
|
|
49500
|
+
result: {
|
|
49501
|
+
skipped: true,
|
|
49502
|
+
reason: "already_executing"
|
|
49503
|
+
},
|
|
49504
|
+
logs: ["Skipped: job is already running"],
|
|
49505
|
+
manual: true
|
|
49506
|
+
};
|
|
49507
|
+
job.logs.push(logEntry);
|
|
49508
|
+
if (job.logs.length > MAX_LOGS_PER_JOB) job.logs.shift();
|
|
49509
|
+
return logEntry;
|
|
49510
|
+
}
|
|
49416
49511
|
return this.executeJob(job, true);
|
|
49417
49512
|
}
|
|
49418
49513
|
// ─── Internal ────────────────────────────────────────────────────
|
|
49514
|
+
/**
|
|
49515
|
+
* Schedule the next execution for a job.
|
|
49516
|
+
*
|
|
49517
|
+
* Safety guarantees:
|
|
49518
|
+
* 1. Clears any existing timer first (prevents leaked/duplicate timers)
|
|
49519
|
+
* 2. Enforces a minimum delay to prevent tight loops from jitter
|
|
49520
|
+
* 3. Unref's the timer so it doesn't prevent process exit
|
|
49521
|
+
* 4. Re-checks enabled & started state before executing
|
|
49522
|
+
* 5. Concurrency guard prevents overlapping handler executions
|
|
49523
|
+
*/
|
|
49419
49524
|
scheduleNext(id) {
|
|
49420
49525
|
const job = this.jobs.get(id);
|
|
49421
49526
|
if (!job || !job.enabled || !this.started) return;
|
|
49527
|
+
this.stopJob(id);
|
|
49422
49528
|
try {
|
|
49423
49529
|
const now = /* @__PURE__ */ new Date();
|
|
49424
49530
|
const nextRun = parseCronExpression(job.definition.schedule, now);
|
|
49425
49531
|
job.nextRunAt = nextRun;
|
|
49426
|
-
const
|
|
49427
|
-
|
|
49532
|
+
const rawDelay = nextRun.getTime() - now.getTime();
|
|
49533
|
+
const delay = Math.max(rawDelay, MIN_SCHEDULE_INTERVAL_MS);
|
|
49534
|
+
const timer = setTimeout(async () => {
|
|
49428
49535
|
if (!job.enabled || !this.started) return;
|
|
49536
|
+
if (job.executing) {
|
|
49537
|
+
console.warn(`[cron] Skipping scheduled run of "${id}" — still executing from previous run`);
|
|
49538
|
+
this.scheduleNext(id);
|
|
49539
|
+
return;
|
|
49540
|
+
}
|
|
49429
49541
|
await this.executeJob(job, false);
|
|
49430
|
-
this.
|
|
49542
|
+
if (this.started && job.enabled) {
|
|
49543
|
+
this.scheduleNext(id);
|
|
49544
|
+
}
|
|
49431
49545
|
}, delay);
|
|
49546
|
+
if (timer && typeof timer === "object" && "unref" in timer) {
|
|
49547
|
+
timer.unref();
|
|
49548
|
+
}
|
|
49549
|
+
job.timerId = timer;
|
|
49432
49550
|
} catch (err) {
|
|
49433
49551
|
console.error(`[cron] Failed to schedule "${id}":`, err);
|
|
49434
49552
|
job.state = "error";
|
|
49435
49553
|
job.lastError = err instanceof Error ? err.message : String(err);
|
|
49436
49554
|
}
|
|
49437
49555
|
}
|
|
49556
|
+
/**
|
|
49557
|
+
* Stop a single job's timer and clear its next run state.
|
|
49558
|
+
*/
|
|
49438
49559
|
stopJob(id) {
|
|
49439
49560
|
const job = this.jobs.get(id);
|
|
49440
49561
|
if (job?.timerId) {
|
|
@@ -49443,9 +49564,19 @@ class CronScheduler {
|
|
|
49443
49564
|
job.nextRunAt = void 0;
|
|
49444
49565
|
}
|
|
49445
49566
|
}
|
|
49567
|
+
/**
|
|
49568
|
+
* Execute a job's handler with full isolation and safety.
|
|
49569
|
+
*
|
|
49570
|
+
* - Sets a concurrency flag to prevent overlapping runs
|
|
49571
|
+
* - Wraps handler in a timeout race
|
|
49572
|
+
* - Captures all logs, errors, and results
|
|
49573
|
+
* - Persists to store (non-blocking) if available
|
|
49574
|
+
* - Always restores state even on catastrophic errors
|
|
49575
|
+
*/
|
|
49446
49576
|
async executeJob(job, manual) {
|
|
49447
49577
|
const startedAt = /* @__PURE__ */ new Date();
|
|
49448
49578
|
const capturedLogs = [];
|
|
49579
|
+
job.executing = true;
|
|
49449
49580
|
const ctx = {
|
|
49450
49581
|
jobId: job.id,
|
|
49451
49582
|
scheduledAt: startedAt,
|
|
@@ -49464,14 +49595,21 @@ class CronScheduler {
|
|
|
49464
49595
|
try {
|
|
49465
49596
|
const timeout = (job.definition.timeoutSeconds ?? 300) * 1e3;
|
|
49466
49597
|
const handlerPromise = Promise.resolve(job.definition.handler(ctx));
|
|
49598
|
+
let timeoutHandle;
|
|
49467
49599
|
const timeoutPromise = new Promise((_, reject) => {
|
|
49468
|
-
setTimeout(() => reject(new Error(`Cron job "${job.id}" timed out after ${timeout}ms`)), timeout);
|
|
49600
|
+
timeoutHandle = setTimeout(() => reject(new Error(`Cron job "${job.id}" timed out after ${timeout}ms`)), timeout);
|
|
49469
49601
|
});
|
|
49470
|
-
|
|
49602
|
+
try {
|
|
49603
|
+
result = await Promise.race([handlerPromise, timeoutPromise]);
|
|
49604
|
+
} finally {
|
|
49605
|
+
clearTimeout(timeoutHandle);
|
|
49606
|
+
}
|
|
49471
49607
|
} catch (err) {
|
|
49472
49608
|
success = false;
|
|
49473
49609
|
error2 = err instanceof Error ? err.message : String(err);
|
|
49474
49610
|
job.totalFailures++;
|
|
49611
|
+
} finally {
|
|
49612
|
+
job.executing = false;
|
|
49475
49613
|
}
|
|
49476
49614
|
const finishedAt = /* @__PURE__ */ new Date();
|
|
49477
49615
|
const durationMs = finishedAt.getTime() - startedAt.getTime();
|
|
@@ -49494,8 +49632,8 @@ class CronScheduler {
|
|
|
49494
49632
|
job.logs.shift();
|
|
49495
49633
|
}
|
|
49496
49634
|
if (this.store) {
|
|
49497
|
-
this.store.insertLog(logEntry).catch((
|
|
49498
|
-
console.error(`[cron] Failed to persist log for "${job.id}":`,
|
|
49635
|
+
this.store.insertLog(logEntry).catch((persistErr) => {
|
|
49636
|
+
console.error(`[cron] Failed to persist log for "${job.id}":`, persistErr);
|
|
49499
49637
|
});
|
|
49500
49638
|
}
|
|
49501
49639
|
if (success) {
|
|
@@ -49524,7 +49662,8 @@ class CronScheduler {
|
|
|
49524
49662
|
}
|
|
49525
49663
|
const cronScheduler = /* @__PURE__ */ Object.freeze(/* @__PURE__ */ Object.defineProperty({
|
|
49526
49664
|
__proto__: null,
|
|
49527
|
-
CronScheduler
|
|
49665
|
+
CronScheduler,
|
|
49666
|
+
validateCronExpression
|
|
49528
49667
|
}, Symbol.toStringTag, { value: "Module" }));
|
|
49529
49668
|
function createCronRoutes(scheduler) {
|
|
49530
49669
|
const router = new Hono();
|
|
@@ -49927,6 +50066,7 @@ export {
|
|
|
49927
50066
|
resetConsole,
|
|
49928
50067
|
serveSPA,
|
|
49929
50068
|
strictAuthLimiter,
|
|
50069
|
+
validateCronExpression,
|
|
49930
50070
|
validatePasswordStrength,
|
|
49931
50071
|
verifyAccessToken,
|
|
49932
50072
|
verifyPassword
|