Skip to content
Open
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -115,6 +115,7 @@ The job will be sceduled to run at next matching time unless you supply option `
job.nextRun( /*optional*/ startFromDate ); // Get a Date object representing the next run.
job.nextRuns(10, /*optional*/ startFromDate ); // Get an array of Dates, containing the next n runs.
job.previousRuns(10, /*optional*/ referenceDate ); // Get an array of Dates, containing previous n scheduled runs.
job.enumerate( /*optional*/ startFromDate ); // Get a stateful CronIterator for use in for...of / destructuring.
job.msToNext( /*optional*/ startFromDate ); // Get the milliseconds left until the next execution.
job.currentRun(); // Get a Date object showing when the current (or last) run was started.
job.previousRun( ); // Get a Date object showing when the previous job was started.
Expand Down
1 change: 1 addition & 0 deletions docs/src/usage/basics.md
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ Check the status of the job using the following methods:
job.nextRun( /*optional*/ startFromDate ); // Get a Date object representing the next run.
job.nextRuns(10, /*optional*/ startFromDate ); // Get an array of Dates, containing the next n runs.
job.previousRuns(10, /*optional*/ referenceDate ); // Get an array of Dates, containing previous n scheduled runs.
job.enumerate( /*optional*/ startFromDate ); // Get a stateful CronIterator for use in for...of / destructuring.
job.msToNext( /*optional*/ startFromDate ); // Get the milliseconds left until the next execution.
job.currentRun(); // Get a Date object showing when the current (or last) run was started.
job.previousRun( ); // Get a Date object showing when the previous job was started.
Expand Down
39 changes: 39 additions & 0 deletions docs/src/usage/examples.md
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,45 @@ previousRuns.forEach((run, i) => {
// 5. 2024-01-11T00:00:00.000Z
```

### Stateful iterator (enumerate)

`enumerate()` returns a `CronIterator` that implements the ECMAScript Iterator and
Iterable protocols. Use it with `for...of` loops, destructuring, or any other construct
that consumes iterables.

```ts
// for...of — collect the next 3 daily runs starting from a reference date
const job = new Cron("0 0 0 * * *");
const start = new Date("2024-06-01T00:00:00");

let count = 0;
for (const date of job.enumerate(start)) {
console.log(date.toISOString());
// Stop after printing 3 dates
if (++count >= 3) break;
}

// Destructuring — capture the next two occurrences
const [next1, next2] = job.enumerate(start);
console.log("Next: ", next1.toISOString());
console.log("After that: ", next2.toISOString());

// peek() — look at the upcoming date without advancing
const iter = job.enumerate(start);
console.log("Upcoming:", iter.peek()?.toISOString()); // does not advance
console.log("First: ", iter.next().value?.toISOString()); // same date

// reset() — restart from a new point
iter.reset(new Date("2024-07-01T00:00:00"));
console.log("After reset:", iter.next().value?.toISOString()); // 2024-07-02

// Finite schedule — iterator signals done automatically when stopAt is reached
const finite = new Cron("0 0 0 * * *", { stopAt: "2024-06-03T00:00:00" });
for (const date of finite.enumerate(start)) {
console.log(date.toISOString()); // prints 2024-06-02 only (stopAt is exclusive)
}
```

### Get run-once date

```ts
Expand Down
107 changes: 106 additions & 1 deletion src/croner.ts
Original file line number Diff line number Diff line change
Expand Up @@ -260,6 +260,19 @@ class Cron<T = undefined> {
return this._enumerateRuns(n, reference || undefined, "previous");
}

/**
* Create a stateful iterator for sequential date traversal of this schedule.
*
* Returns a {@link CronIterator} that implements both the ECMAScript Iterator and
* Iterable protocols, enabling `for...of` loops and destructuring assignment.
*
* @param startAt - Optional. The date to start iterating from. Defaults to current time.
* @returns A new CronIterator instance
*/
public enumerate(startAt?: Date | string | null): CronIterator<T> {
return new CronIterator<T>(this, startAt);
}

/**
* Internal helper to enumerate runs in either direction.
*
Expand Down Expand Up @@ -731,4 +744,96 @@ class Cron<T = undefined> {
}
}

export { Cron, CronDate, type CronOptions, CronPattern, scheduledJobs };
/**
* Stateful iterator for sequential date traversal of a Cron schedule.
*
* Implements both the ECMAScript Iterator and Iterable protocols, which means it can be
* used directly in `for...of` loops and with destructuring assignment.
*
* Obtain an instance via {@link Cron#enumerate}.
*/
class CronIterator<T = undefined> implements Iterator<Date, undefined>, Iterable<Date> {
private cron: Cron<T>;
private cursor: Date | string | undefined;
private done: boolean;

/**
* @param cron - The Cron instance to iterate over
* @param startAt - Optional starting date for iteration. Defaults to current time if omitted.
*/
constructor(cron: Cron<T>, startAt?: Date | string | null) {
this.cron = cron;
this.cursor = CronIterator._normalizeCursor(startAt);
this.done = false;
}

/**
* Normalizes a cursor argument: Date objects are cloned to prevent external mutation;
* strings are preserved as-is so that nextRun() can parse them using the Cron instance's
* configured timezone (via CronDate/fromTZISO), matching the behaviour of nextRun() and nextRuns().
* @private
*/
private static _normalizeCursor(
d?: Date | string | null,
): Date | string | undefined {
if (d === undefined || d === null) return undefined;
return d instanceof Date ? new Date(d.getTime()) : d;
}

/**
* Returns the next scheduled date and advances the cursor.
* Implements the ECMAScript Iterator protocol.
*
* @returns `{ value: Date, done: false }` for the next occurrence,
* or `{ value: undefined, done: true }` when the schedule is exhausted.
*/
public next(): IteratorResult<Date, undefined> {
if (this.done) {
return { value: undefined, done: true };
}
const nextDate = this.cron.nextRun(this.cursor ?? null);
if (nextDate === null) {
this.done = true;
return { value: undefined, done: true };
}
// The cursor must track the un-offset schedule time so that subsequent
// nextRun() calls find the correct next occurrence. nextRun() applies
// dayOffset on the way out, so we reverse it here before storing.
const dayOffset = this.cron.options.dayOffset;
const offsetDays = dayOffset ?? 0;
const unoffsetTime = nextDate.getTime() - offsetDays * 24 * 60 * 60 * 1000;
this.cursor = new Date(unoffsetTime);
return { value: nextDate, done: false };
}

/**
* Returns the next scheduled date without advancing the cursor.
*
* @returns The next scheduled date, or null if the schedule is exhausted.
*/
public peek(): Date | null {
if (this.done) {
return null;
}
return this.cron.nextRun(this.cursor ?? null);
}

/**
* Resets the cursor, optionally to a new starting date.
*
* @param newStartAt - New starting date. Defaults to current time if omitted.
*/
public reset(newStartAt?: Date | string | null): void {
this.cursor = CronIterator._normalizeCursor(newStartAt);
this.done = false;
}

/**
* Implements the ECMAScript Iterable protocol, enabling `for...of` and destructuring.
*/
[Symbol.iterator](): CronIterator<T> {
return this;
}
}

export { Cron, CronDate, CronIterator, type CronOptions, CronPattern, scheduledJobs };
Loading
Loading