@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 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 expand = (field, min, max) => {
49230
- const results = /* @__PURE__ */ new Set();
49231
- for (const segment of field.split(",")) {
49232
- if (segment === "*") {
49233
- for (let i = min; i <= max; i++) results.add(i);
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: loaded.definition.enabled !== false,
49316
- state: loaded.definition.enabled !== false ? "idle" : "disabled",
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 delay = Math.max(nextRun.getTime() - now.getTime(), 0);
49427
- job.timerId = setTimeout(async () => {
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.scheduleNext(id);
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
- result = await Promise.race([handlerPromise, timeoutPromise]);
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((err) => {
49498
- console.error(`[cron] Failed to persist log for "${job.id}":`, err);
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