()
| 276 | } |
| 277 | |
| 278 | function tick(): void { |
| 279 | if (isKilled?.() === true) return; |
| 280 | if (!isIdle()) return; |
| 281 | |
| 282 | const tasks = source(); |
| 283 | if (tasks.length === 0) return; |
| 284 | |
| 285 | const now = clocks.wallNow(); |
| 286 | |
| 287 | // We clear inFlight at the end of the tick; entry-time defence |
| 288 | // against re-entry handled by the `inFlight.has(id)` skip below. |
| 289 | try { |
| 290 | for (const task of tasks) { |
| 291 | try { |
| 292 | if (inFlight.has(task.id)) continue; |
| 293 | |
| 294 | const parsed = getParsed(task.cron); |
| 295 | |
| 296 | // First time we see this task in this scheduler instance, |
| 297 | // seed `lastSeenAt` from the persisted `task.lastFiredAt` |
| 298 | // (when present and sane). This is the one-line fix for |
| 299 | // "resume replays yesterday's already-fired 09:00 cron": |
| 300 | // without seeding, the baseline below would fall back to |
| 301 | // `task.createdAt` and `countCoalesced` would treat every |
| 302 | // ideal fire since creation as still due. A `lastFiredAt` |
| 303 | // strictly greater than `now` is treated as corrupt (clock |
| 304 | // skew, mis-set bench env) and ignored — never trust a |
| 305 | // stored cursor enough to *skip* a legitimately-due fire. |
| 306 | if ( |
| 307 | !seededFromDisk.has(task.id) && |
| 308 | task.lastFiredAt !== undefined && |
| 309 | Number.isFinite(task.lastFiredAt) && |
| 310 | task.lastFiredAt <= now && |
| 311 | !lastSeenAt.has(task.id) |
| 312 | ) { |
| 313 | lastSeenAt.set(task.id, task.lastFiredAt); |
| 314 | } |
| 315 | seededFromDisk.add(task.id); |
| 316 | |
| 317 | // Base from which to compute the next ideal fire. For a |
| 318 | // freshly-added task this is its createdAt; once we've fired |
| 319 | // (or seen it pass), bump to the wall clock at that moment |
| 320 | // so we don't double-count the same fire on the next tick. |
| 321 | const seen = lastSeenAt.get(task.id); |
| 322 | const baseFromMs = |
| 323 | seen !== undefined && seen > task.createdAt ? seen : task.createdAt; |
| 324 | |
| 325 | const nextFireAt = computeJitteredNext(task, parsed, baseFromMs); |
| 326 | if (nextFireAt === null) continue; |
| 327 | |
| 328 | if (now < nextFireAt) continue; |
| 329 | |
| 330 | // Due — compute coalescedCount starting from the first |
| 331 | // ideal fire (not the jittered one — jitter only shifts the |
| 332 | // delivery point, not the underlying schedule). One-shot |
| 333 | // tasks are removed after a single delivery and must always |
| 334 | // report `coalescedCount: 1`; multi-occurrence semantics |
| 335 | // make no sense for "remind me at X" reminders that were |
nothing calls this directly
no test coverage detected