@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.umd.js
CHANGED
|
@@ -49254,46 +49254,98 @@ export default ${safeId}Collection;
|
|
|
49254
49254
|
__proto__: null,
|
|
49255
49255
|
loadCronJobsFromDirectory
|
|
49256
49256
|
}, Symbol.toStringTag, { value: "Module" }));
|
|
49257
|
+
function expandCronField(field, min, max) {
|
|
49258
|
+
const results = /* @__PURE__ */ new Set();
|
|
49259
|
+
for (const segment of field.split(",")) {
|
|
49260
|
+
const trimmed = segment.trim();
|
|
49261
|
+
if (trimmed === "*") {
|
|
49262
|
+
for (let i2 = min; i2 <= max; i2++) results.add(i2);
|
|
49263
|
+
} else if (trimmed.includes("/")) {
|
|
49264
|
+
const [rangeStr, stepStr] = trimmed.split("/");
|
|
49265
|
+
const step = parseInt(stepStr, 10);
|
|
49266
|
+
if (isNaN(step) || step <= 0) {
|
|
49267
|
+
throw new Error(`Invalid step value "${stepStr}" in cron field "${field}"`);
|
|
49268
|
+
}
|
|
49269
|
+
let start = min;
|
|
49270
|
+
let end = max;
|
|
49271
|
+
if (rangeStr !== "*") {
|
|
49272
|
+
if (rangeStr.includes("-")) {
|
|
49273
|
+
const [a2, b] = rangeStr.split("-").map(Number);
|
|
49274
|
+
start = a2;
|
|
49275
|
+
end = b;
|
|
49276
|
+
} else {
|
|
49277
|
+
start = parseInt(rangeStr, 10);
|
|
49278
|
+
}
|
|
49279
|
+
}
|
|
49280
|
+
for (let i2 = start; i2 <= end; i2 += step) results.add(i2);
|
|
49281
|
+
} else if (trimmed.includes("-")) {
|
|
49282
|
+
const [a2, b] = trimmed.split("-").map(Number);
|
|
49283
|
+
for (let i2 = a2; i2 <= b; i2++) results.add(i2);
|
|
49284
|
+
} else {
|
|
49285
|
+
const val = parseInt(trimmed, 10);
|
|
49286
|
+
if (isNaN(val)) {
|
|
49287
|
+
throw new Error(`Invalid value "${trimmed}" in cron field "${field}"`);
|
|
49288
|
+
}
|
|
49289
|
+
results.add(val);
|
|
49290
|
+
}
|
|
49291
|
+
}
|
|
49292
|
+
return [...results].sort((a2, b) => a2 - b);
|
|
49293
|
+
}
|
|
49294
|
+
function validateCronExpression(schedule) {
|
|
49295
|
+
if (!schedule || typeof schedule !== "string") {
|
|
49296
|
+
return {
|
|
49297
|
+
valid: false,
|
|
49298
|
+
reason: "Schedule must be a non-empty string"
|
|
49299
|
+
};
|
|
49300
|
+
}
|
|
49301
|
+
const parts = schedule.trim().split(/\s+/);
|
|
49302
|
+
if (parts.length !== 5) {
|
|
49303
|
+
return {
|
|
49304
|
+
valid: false,
|
|
49305
|
+
reason: `Expected 5 fields, got ${parts.length}`
|
|
49306
|
+
};
|
|
49307
|
+
}
|
|
49308
|
+
const fieldRanges = [["minute", 0, 59], ["hour", 0, 23], ["day of month", 1, 31], ["month", 1, 12], ["day of week", 0, 6]];
|
|
49309
|
+
for (let i2 = 0; i2 < 5; i2++) {
|
|
49310
|
+
const [name2, min, max] = fieldRanges[i2];
|
|
49311
|
+
try {
|
|
49312
|
+
const values2 = expandCronField(parts[i2], min, max);
|
|
49313
|
+
if (values2.length === 0) {
|
|
49314
|
+
return {
|
|
49315
|
+
valid: false,
|
|
49316
|
+
reason: `${name2} field "${parts[i2]}" produces no values`
|
|
49317
|
+
};
|
|
49318
|
+
}
|
|
49319
|
+
for (const v of values2) {
|
|
49320
|
+
if (v < min || v > max) {
|
|
49321
|
+
return {
|
|
49322
|
+
valid: false,
|
|
49323
|
+
reason: `${name2} field value ${v} out of range [${min}–${max}]`
|
|
49324
|
+
};
|
|
49325
|
+
}
|
|
49326
|
+
}
|
|
49327
|
+
} catch (err) {
|
|
49328
|
+
return {
|
|
49329
|
+
valid: false,
|
|
49330
|
+
reason: `${name2} field: ${err instanceof Error ? err.message : String(err)}`
|
|
49331
|
+
};
|
|
49332
|
+
}
|
|
49333
|
+
}
|
|
49334
|
+
return {
|
|
49335
|
+
valid: true
|
|
49336
|
+
};
|
|
49337
|
+
}
|
|
49257
49338
|
function parseCronExpression(expression, after) {
|
|
49258
49339
|
const parts = expression.trim().split(/\s+/);
|
|
49259
49340
|
if (parts.length < 5) {
|
|
49260
49341
|
throw new Error(`Invalid cron expression: "${expression}". Expected 5 fields.`);
|
|
49261
49342
|
}
|
|
49262
49343
|
const [minField, hourField, domField, monField, dowField] = parts;
|
|
49263
|
-
const
|
|
49264
|
-
|
|
49265
|
-
|
|
49266
|
-
|
|
49267
|
-
|
|
49268
|
-
} else if (segment.includes("/")) {
|
|
49269
|
-
const [rangeStr, stepStr] = segment.split("/");
|
|
49270
|
-
const step = parseInt(stepStr, 10);
|
|
49271
|
-
let start = min;
|
|
49272
|
-
let end = max;
|
|
49273
|
-
if (rangeStr !== "*") {
|
|
49274
|
-
if (rangeStr.includes("-")) {
|
|
49275
|
-
const [a2, b] = rangeStr.split("-").map(Number);
|
|
49276
|
-
start = a2;
|
|
49277
|
-
end = b;
|
|
49278
|
-
} else {
|
|
49279
|
-
start = parseInt(rangeStr, 10);
|
|
49280
|
-
}
|
|
49281
|
-
}
|
|
49282
|
-
for (let i2 = start; i2 <= end; i2 += step) results.add(i2);
|
|
49283
|
-
} else if (segment.includes("-")) {
|
|
49284
|
-
const [a2, b] = segment.split("-").map(Number);
|
|
49285
|
-
for (let i2 = a2; i2 <= b; i2++) results.add(i2);
|
|
49286
|
-
} else {
|
|
49287
|
-
results.add(parseInt(segment, 10));
|
|
49288
|
-
}
|
|
49289
|
-
}
|
|
49290
|
-
return [...results].sort((a2, b) => a2 - b);
|
|
49291
|
-
};
|
|
49292
|
-
const minutes = expand(minField, 0, 59);
|
|
49293
|
-
const hours = expand(hourField, 0, 23);
|
|
49294
|
-
const doms = expand(domField, 1, 31);
|
|
49295
|
-
const months = expand(monField, 1, 12);
|
|
49296
|
-
const dows = expand(dowField, 0, 6);
|
|
49344
|
+
const minutes = expandCronField(minField, 0, 59);
|
|
49345
|
+
const hours = expandCronField(hourField, 0, 23);
|
|
49346
|
+
const doms = expandCronField(domField, 1, 31);
|
|
49347
|
+
const months = expandCronField(monField, 1, 12);
|
|
49348
|
+
const dows = expandCronField(dowField, 0, 6);
|
|
49297
49349
|
const candidate = new Date(after);
|
|
49298
49350
|
candidate.setSeconds(0, 0);
|
|
49299
49351
|
candidate.setMinutes(candidate.getMinutes() + 1);
|
|
@@ -49314,6 +49366,7 @@ export default ${safeId}Collection;
|
|
|
49314
49366
|
return fallback;
|
|
49315
49367
|
}
|
|
49316
49368
|
const MAX_LOGS_PER_JOB = 50;
|
|
49369
|
+
const MIN_SCHEDULE_INTERVAL_MS = 5e3;
|
|
49317
49370
|
class CronScheduler {
|
|
49318
49371
|
jobs = /* @__PURE__ */ new Map();
|
|
49319
49372
|
started = false;
|
|
@@ -49335,23 +49388,39 @@ export default ${safeId}Collection;
|
|
|
49335
49388
|
}
|
|
49336
49389
|
/**
|
|
49337
49390
|
* Register a batch of loaded cron jobs.
|
|
49391
|
+
*
|
|
49392
|
+
* If the scheduler is already started, newly registered jobs are
|
|
49393
|
+
* automatically scheduled (so late-registered jobs don't sit idle).
|
|
49394
|
+
*
|
|
49395
|
+
* Validates the cron schedule on registration — invalid schedules
|
|
49396
|
+
* are rejected with a warning and the job is NOT registered.
|
|
49338
49397
|
*/
|
|
49339
49398
|
registerJobs(loadedJobs) {
|
|
49340
49399
|
for (const loaded of loadedJobs) {
|
|
49400
|
+
const validation = validateCronExpression(loaded.definition.schedule);
|
|
49401
|
+
if (!validation.valid) {
|
|
49402
|
+
console.error(`[cron] Rejecting job "${loaded.id}": invalid schedule "${loaded.definition.schedule}" — ${validation.reason}`);
|
|
49403
|
+
continue;
|
|
49404
|
+
}
|
|
49341
49405
|
const existing = this.jobs.get(loaded.id);
|
|
49342
49406
|
if (existing) {
|
|
49343
49407
|
console.warn(`[cron] Duplicate cron job id: "${loaded.id}". Overwriting.`);
|
|
49344
49408
|
this.stopJob(loaded.id);
|
|
49345
49409
|
}
|
|
49410
|
+
const enabled = loaded.definition.enabled !== false;
|
|
49346
49411
|
this.jobs.set(loaded.id, {
|
|
49347
49412
|
id: loaded.id,
|
|
49348
49413
|
definition: loaded.definition,
|
|
49349
|
-
enabled
|
|
49350
|
-
state:
|
|
49414
|
+
enabled,
|
|
49415
|
+
state: enabled ? "idle" : "disabled",
|
|
49351
49416
|
totalRuns: 0,
|
|
49352
49417
|
totalFailures: 0,
|
|
49353
|
-
logs: []
|
|
49418
|
+
logs: [],
|
|
49419
|
+
executing: false
|
|
49354
49420
|
});
|
|
49421
|
+
if (this.started && enabled) {
|
|
49422
|
+
this.scheduleNext(loaded.id);
|
|
49423
|
+
}
|
|
49355
49424
|
}
|
|
49356
49425
|
}
|
|
49357
49426
|
/**
|
|
@@ -49385,6 +49454,9 @@ export default ${safeId}Collection;
|
|
|
49385
49454
|
}
|
|
49386
49455
|
/**
|
|
49387
49456
|
* Stop the scheduler and clear all timers.
|
|
49457
|
+
*
|
|
49458
|
+
* Currently-executing handlers run to completion (they are async),
|
|
49459
|
+
* but no further scheduling occurs after stop.
|
|
49388
49460
|
*/
|
|
49389
49461
|
stop() {
|
|
49390
49462
|
this.started = false;
|
|
@@ -49443,32 +49515,81 @@ export default ${safeId}Collection;
|
|
|
49443
49515
|
}
|
|
49444
49516
|
/**
|
|
49445
49517
|
* Manually trigger a job execution immediately.
|
|
49518
|
+
*
|
|
49519
|
+
* Returns `undefined` if the job doesn't exist.
|
|
49520
|
+
* If the job is currently executing, returns the log entry with
|
|
49521
|
+
* a `skipped: true` result rather than running concurrently.
|
|
49446
49522
|
*/
|
|
49447
49523
|
async triggerJob(id) {
|
|
49448
49524
|
const job = this.jobs.get(id);
|
|
49449
49525
|
if (!job) return void 0;
|
|
49526
|
+
if (job.executing) {
|
|
49527
|
+
console.warn(`[cron] Skipping manual trigger of "${id}" — already executing`);
|
|
49528
|
+
const logEntry = {
|
|
49529
|
+
jobId: id,
|
|
49530
|
+
startedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
49531
|
+
finishedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
49532
|
+
durationMs: 0,
|
|
49533
|
+
success: true,
|
|
49534
|
+
result: {
|
|
49535
|
+
skipped: true,
|
|
49536
|
+
reason: "already_executing"
|
|
49537
|
+
},
|
|
49538
|
+
logs: ["Skipped: job is already running"],
|
|
49539
|
+
manual: true
|
|
49540
|
+
};
|
|
49541
|
+
job.logs.push(logEntry);
|
|
49542
|
+
if (job.logs.length > MAX_LOGS_PER_JOB) job.logs.shift();
|
|
49543
|
+
return logEntry;
|
|
49544
|
+
}
|
|
49450
49545
|
return this.executeJob(job, true);
|
|
49451
49546
|
}
|
|
49452
49547
|
// ─── Internal ────────────────────────────────────────────────────
|
|
49548
|
+
/**
|
|
49549
|
+
* Schedule the next execution for a job.
|
|
49550
|
+
*
|
|
49551
|
+
* Safety guarantees:
|
|
49552
|
+
* 1. Clears any existing timer first (prevents leaked/duplicate timers)
|
|
49553
|
+
* 2. Enforces a minimum delay to prevent tight loops from jitter
|
|
49554
|
+
* 3. Unref's the timer so it doesn't prevent process exit
|
|
49555
|
+
* 4. Re-checks enabled & started state before executing
|
|
49556
|
+
* 5. Concurrency guard prevents overlapping handler executions
|
|
49557
|
+
*/
|
|
49453
49558
|
scheduleNext(id) {
|
|
49454
49559
|
const job = this.jobs.get(id);
|
|
49455
49560
|
if (!job || !job.enabled || !this.started) return;
|
|
49561
|
+
this.stopJob(id);
|
|
49456
49562
|
try {
|
|
49457
49563
|
const now = /* @__PURE__ */ new Date();
|
|
49458
49564
|
const nextRun = parseCronExpression(job.definition.schedule, now);
|
|
49459
49565
|
job.nextRunAt = nextRun;
|
|
49460
|
-
const
|
|
49461
|
-
|
|
49566
|
+
const rawDelay = nextRun.getTime() - now.getTime();
|
|
49567
|
+
const delay = Math.max(rawDelay, MIN_SCHEDULE_INTERVAL_MS);
|
|
49568
|
+
const timer = setTimeout(async () => {
|
|
49462
49569
|
if (!job.enabled || !this.started) return;
|
|
49570
|
+
if (job.executing) {
|
|
49571
|
+
console.warn(`[cron] Skipping scheduled run of "${id}" — still executing from previous run`);
|
|
49572
|
+
this.scheduleNext(id);
|
|
49573
|
+
return;
|
|
49574
|
+
}
|
|
49463
49575
|
await this.executeJob(job, false);
|
|
49464
|
-
this.
|
|
49576
|
+
if (this.started && job.enabled) {
|
|
49577
|
+
this.scheduleNext(id);
|
|
49578
|
+
}
|
|
49465
49579
|
}, delay);
|
|
49580
|
+
if (timer && typeof timer === "object" && "unref" in timer) {
|
|
49581
|
+
timer.unref();
|
|
49582
|
+
}
|
|
49583
|
+
job.timerId = timer;
|
|
49466
49584
|
} catch (err) {
|
|
49467
49585
|
console.error(`[cron] Failed to schedule "${id}":`, err);
|
|
49468
49586
|
job.state = "error";
|
|
49469
49587
|
job.lastError = err instanceof Error ? err.message : String(err);
|
|
49470
49588
|
}
|
|
49471
49589
|
}
|
|
49590
|
+
/**
|
|
49591
|
+
* Stop a single job's timer and clear its next run state.
|
|
49592
|
+
*/
|
|
49472
49593
|
stopJob(id) {
|
|
49473
49594
|
const job = this.jobs.get(id);
|
|
49474
49595
|
if (job?.timerId) {
|
|
@@ -49477,9 +49598,19 @@ export default ${safeId}Collection;
|
|
|
49477
49598
|
job.nextRunAt = void 0;
|
|
49478
49599
|
}
|
|
49479
49600
|
}
|
|
49601
|
+
/**
|
|
49602
|
+
* Execute a job's handler with full isolation and safety.
|
|
49603
|
+
*
|
|
49604
|
+
* - Sets a concurrency flag to prevent overlapping runs
|
|
49605
|
+
* - Wraps handler in a timeout race
|
|
49606
|
+
* - Captures all logs, errors, and results
|
|
49607
|
+
* - Persists to store (non-blocking) if available
|
|
49608
|
+
* - Always restores state even on catastrophic errors
|
|
49609
|
+
*/
|
|
49480
49610
|
async executeJob(job, manual) {
|
|
49481
49611
|
const startedAt = /* @__PURE__ */ new Date();
|
|
49482
49612
|
const capturedLogs = [];
|
|
49613
|
+
job.executing = true;
|
|
49483
49614
|
const ctx = {
|
|
49484
49615
|
jobId: job.id,
|
|
49485
49616
|
scheduledAt: startedAt,
|
|
@@ -49498,14 +49629,21 @@ export default ${safeId}Collection;
|
|
|
49498
49629
|
try {
|
|
49499
49630
|
const timeout = (job.definition.timeoutSeconds ?? 300) * 1e3;
|
|
49500
49631
|
const handlerPromise = Promise.resolve(job.definition.handler(ctx));
|
|
49632
|
+
let timeoutHandle;
|
|
49501
49633
|
const timeoutPromise = new Promise((_, reject) => {
|
|
49502
|
-
setTimeout(() => reject(new Error(`Cron job "${job.id}" timed out after ${timeout}ms`)), timeout);
|
|
49634
|
+
timeoutHandle = setTimeout(() => reject(new Error(`Cron job "${job.id}" timed out after ${timeout}ms`)), timeout);
|
|
49503
49635
|
});
|
|
49504
|
-
|
|
49636
|
+
try {
|
|
49637
|
+
result = await Promise.race([handlerPromise, timeoutPromise]);
|
|
49638
|
+
} finally {
|
|
49639
|
+
clearTimeout(timeoutHandle);
|
|
49640
|
+
}
|
|
49505
49641
|
} catch (err) {
|
|
49506
49642
|
success = false;
|
|
49507
49643
|
error2 = err instanceof Error ? err.message : String(err);
|
|
49508
49644
|
job.totalFailures++;
|
|
49645
|
+
} finally {
|
|
49646
|
+
job.executing = false;
|
|
49509
49647
|
}
|
|
49510
49648
|
const finishedAt = /* @__PURE__ */ new Date();
|
|
49511
49649
|
const durationMs = finishedAt.getTime() - startedAt.getTime();
|
|
@@ -49528,8 +49666,8 @@ export default ${safeId}Collection;
|
|
|
49528
49666
|
job.logs.shift();
|
|
49529
49667
|
}
|
|
49530
49668
|
if (this.store) {
|
|
49531
|
-
this.store.insertLog(logEntry).catch((
|
|
49532
|
-
console.error(`[cron] Failed to persist log for "${job.id}":`,
|
|
49669
|
+
this.store.insertLog(logEntry).catch((persistErr) => {
|
|
49670
|
+
console.error(`[cron] Failed to persist log for "${job.id}":`, persistErr);
|
|
49533
49671
|
});
|
|
49534
49672
|
}
|
|
49535
49673
|
if (success) {
|
|
@@ -49558,7 +49696,8 @@ export default ${safeId}Collection;
|
|
|
49558
49696
|
}
|
|
49559
49697
|
const cronScheduler = /* @__PURE__ */ Object.freeze(/* @__PURE__ */ Object.defineProperty({
|
|
49560
49698
|
__proto__: null,
|
|
49561
|
-
CronScheduler
|
|
49699
|
+
CronScheduler,
|
|
49700
|
+
validateCronExpression
|
|
49562
49701
|
}, Symbol.toStringTag, { value: "Module" }));
|
|
49563
49702
|
function createCronRoutes(scheduler) {
|
|
49564
49703
|
const router = new hono.Hono();
|
|
@@ -49960,6 +50099,7 @@ export default ${safeId}Collection;
|
|
|
49960
50099
|
exports2.resetConsole = resetConsole;
|
|
49961
50100
|
exports2.serveSPA = serveSPA;
|
|
49962
50101
|
exports2.strictAuthLimiter = strictAuthLimiter;
|
|
50102
|
+
exports2.validateCronExpression = validateCronExpression;
|
|
49963
50103
|
exports2.validatePasswordStrength = validatePasswordStrength;
|
|
49964
50104
|
exports2.verifyAccessToken = verifyAccessToken;
|
|
49965
50105
|
exports2.verifyPassword = verifyPassword;
|