( cron: string, fromMs: number, taskId: string, cfg: CronJitterConfig = DEFAULT_CRON_JITTER_CONFIG, )
| 379 | * {@link oneShotJitteredNextCronRunMs} (backward jitter, minute-gated). |
| 380 | */ |
| 381 | export function jitteredNextCronRunMs( |
| 382 | cron: string, |
| 383 | fromMs: number, |
| 384 | taskId: string, |
| 385 | cfg: CronJitterConfig = DEFAULT_CRON_JITTER_CONFIG, |
| 386 | ): number | null { |
| 387 | const t1 = nextCronRunMs(cron, fromMs) |
| 388 | if (t1 === null) return null |
| 389 | const t2 = nextCronRunMs(cron, t1) |
| 390 | // No second match in the next year (e.g. pinned date) → nothing to |
| 391 | // proportion against, and near-certainly not a herd risk. Fire on t1. |
| 392 | if (t2 === null) return t1 |
| 393 | const jitter = Math.min( |
| 394 | jitterFrac(taskId) * cfg.recurringFrac * (t2 - t1), |
| 395 | cfg.recurringCapMs, |
| 396 | ) |
| 397 | return t1 + jitter |
| 398 | } |
| 399 | |
| 400 | /** |
| 401 | * Same as {@link nextCronRunMs}, minus a deterministic per-task lead time |
no test coverage detected