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