Deeper, real-world scenarios that go beyond the quick table in the README.
All examples share one fixed, deterministic base so results are reproducible:
import timeleap from '@neabyte/timeleap'
const base = Date.UTC(2077, 0, 1) // Friday, 2077-01-01T00:00:00.000Z
const iso = (time: number): string => new Date(time).toISOString()flowchart LR
A["Duration text
'1 hour', '2 months', '15m'"] --> P["parse()
gatekeeper / validation"]
P -->|valid| N["next()
one step forward"]
P -->|valid| V["prev()
one step backward"]
P -->|valid| B["backoff()
many cumulative steps"]
N --> S{"skip?"}
V --> S
S -->|yes| E["evade blocked
minute > hour > day > week > month"]
S -->|no| O["raw UTC timestamp"]
E --> O
B --> L["number[] schedule
constant | linear | exponential"]
style A fill:#e2e8f0,stroke:#475569,color:#0f172a
style P fill:#93c5fd,stroke:#1e40af,color:#0f172a
style N fill:#86efac,stroke:#166534,color:#052e16
style V fill:#86efac,stroke:#166534,color:#052e16
style B fill:#86efac,stroke:#166534,color:#052e16
style S fill:#fde68a,stroke:#92400e,color:#451a03
style E fill:#fdba74,stroke:#9a3412,color:#431407
style O fill:#e2e8f0,stroke:#475569,color:#0f172a
style L fill:#93c5fd,stroke:#1e40af,color:#0f172a
Problem: A worker must reject a duplicate job if an identical one already ran inside a configurable window. The window is user-supplied text, so it has to be validated before it ever touches the clock.
Process: parse guards the input, then next turns the window into a concrete deadline that you compare against the last-seen timestamp.
function withinWindow(lastRun: number, now: number, window: string): boolean {
if (!timeleap.parse(window)) {
throw new Error(`bad dedupe window: ${window}`)
}
const expiresAt = timeleap.next({ time: lastRun, duration: window })
return now < expiresAt
}
withinWindow(base, base + 20 * 60_000, '30 minutes') // true, still inside 30m
withinWindow(base, base + 40 * 60_000, '30 minutes') // false, window elapsedOutput: next from base with 30 minutes = 2077-01-01T00:30:00.000Z.
Problem: A free trial lasts a fixed calendar span and must survive month-length differences (28 vs 31 days).
Process: Use a calendar unit so the end date tracks the real month, not a naive 30-day count.
const trialEnds = timeleap.next({ time: base, duration: '1 month' })
// 2077-02-01T00:00:00.000Z (calendar month, not +30 days)
const annual = timeleap.next({ time: base, duration: '1 year' })
// 2078-01-01T00:00:00.000ZWhy it matters: 1 month from Jan 1 lands on Feb 1, and Jan 31 plus 1 month overflows to Mar 2/3 instead of silently clamping to Feb 28.
Problem: A support SLA of "respond within 1 hour" must only tick during working hours. If a ticket lands at 17:00 and the office closes at 18:00, the remaining hour rolls to the next morning.
Process: Block every non-working hour. When the shift lands on a blocked hour, skip steps hour-by-hour forward until it clears, wrapping across the day boundary automatically.
const officeOnly = {
hour: [0, 1, 2, 3, 4, 5, 6, 7, 8, 18, 19, 20, 21, 22, 23] // allow 09..17 only
}
timeleap.next({
time: Date.UTC(2077, 0, 1, 17, 0, 0), // 17:00
duration: '1 hour',
skip: officeOnly
})
// 2077-01-02T09:00:00.000Z (18:00 blocked -> next open slot next morning)flowchart LR
T0["17:00 ticket"] --> T1["plus 1h = 18:00 (blocked)"]
T1 --> T2["step hourly through night"]
T2 --> T3["09:00 next day (open)"]
style T0 fill:#e2e8f0,stroke:#475569,color:#0f172a
style T1 fill:#fdba74,stroke:#9a3412,color:#431407
style T2 fill:#fde68a,stroke:#92400e,color:#451a03
style T3 fill:#86efac,stroke:#166534,color:#052e16
Problem: A payout scheduled one day after Friday must not land on Saturday or Sunday.
Process: Block the weekend by name; the engine steps forward a day at a time until it reaches Monday.
timeleap.next({
time: base, // Friday 2077-01-01
duration: '1 day',
skip: { day: ['saturday', 'sunday'] }
})
// 2077-01-04T00:00:00.000Z (Sat and Sun skipped -> Monday)Names are case-insensitive and interchangeable with numbers:
day: [6, 0]means the same thing.
Problem: A metrics pipeline stores samples only at :05, :10, :20 ... never on the quarter marks. Walking backward "10 minutes" from 10:15 must not land on a quarter-hour.
Process: prev shifts backward; skip.minute nudges further back by the minute stride until the timestamp is off every blocked minute.
timeleap.prev({
time: Date.UTC(2077, 0, 1, 10, 15, 0),
duration: '10 minutes',
skip: { minute: [0, 15, 30, 45] }
})
// 2077-01-01T10:05:00.000Z (10:05 is a valid, non-quarter slot)Note how skip always steps in the shift direction: prev steps backward, next steps forward.
Problem: Finance runs payroll only in the middle weeks of a month, never in week 1 or the trailing week 5.
Process: week is the week-of-month (ceil(dayOfMonth / 7), range 1..5). Blocking weeks 1 and 5 forces the result into weeks 2-4.
timeleap.next({
time: Date.UTC(2077, 0, 1), // week 1
duration: '1 week',
skip: { week: [1, 5] }
})
// 2077-01-08T00:00:00.000Z (Jan 8 is week 2)Problem: A rollout scheduler advances "1 month" at a time but must never schedule during the December/January change freeze.
Process: skip.month steps by whole calendar months (from the original anchor) until it clears the frozen months, preserving the day-of-month.
timeleap.next({
time: Date.UTC(2077, 10, 15), // Nov 15
duration: '1 month',
skip: { month: ['december', 'january'] }
})
// 2078-02-15T00:00:00.000Z (Dec and Jan frozen -> Feb 15 next year)timeline
title Month stepping under a Dec/Jan freeze
Nov 15 : anchor
Dec 15 : blocked (freeze)
Jan 15 : blocked (freeze)
Feb 15 : scheduled
Problem: After a failed request, retries should back off as 1x, 3x, 7x, 15x, 31x the base delay (cumulative 2^n - 1).
Process: backoff with type: 'exponential' returns absolute UTC timestamps, each already offset from the base, ready to feed a scheduler.
timeleap.backoff({
time: base,
duration: '2 seconds',
type: 'exponential',
limit: 5
})
// [
// 2077-01-01T00:00:02.000Z, // +2s
// 2077-01-01T00:00:06.000Z, // +6s
// 2077-01-01T00:00:14.000Z, // +14s
// 2077-01-01T00:00:30.000Z, // +30s
// 2077-01-01T00:01:02.000Z // +62s
// ]Problem: Send reminders that spread out over time: day 1, then day 3, then day 6, then day 10 (cumulative n(n+1)/2).
Process: type: 'linear' grows the gap on every step, so early reminders are frequent and later ones taper off.
timeleap.backoff({
time: base,
duration: '1 day',
type: 'linear',
limit: 4
})
// [
// 2077-01-02T00:00:00.000Z, // +1d
// 2077-01-04T00:00:00.000Z, // +3d
// 2077-01-07T00:00:00.000Z, // +6d
// 2077-01-11T00:00:00.000Z // +10d
// ]flowchart LR
D0["base"] -->|+1d| D1["day 1"]
D1 -->|+2d| D2["day 3"]
D2 -->|+3d| D3["day 6"]
D3 -->|+4d| D4["day 10"]
style D0 fill:#e2e8f0,stroke:#475569,color:#0f172a
style D1 fill:#86efac,stroke:#166534,color:#052e16
style D2 fill:#86efac,stroke:#166534,color:#052e16
style D3 fill:#86efac,stroke:#166534,color:#052e16
style D4 fill:#86efac,stroke:#166534,color:#052e16
Problem: A health check must fire on an even cadence: every 15 minutes, four slots ahead.
Process: type: 'constant' keeps a flat gap, giving evenly spaced timestamps you can enqueue directly.
timeleap.backoff({
time: base,
duration: '15 minutes',
type: 'constant',
limit: 4
})
// [
// 2077-01-01T00:15:00.000Z,
// 2077-01-01T00:30:00.000Z,
// 2077-01-01T00:45:00.000Z,
// 2077-01-01T01:00:00.000Z
// ]Problem: To generate an invoice for "last month," you need the start of the previous calendar cycle from any point in the current one.
Process: prev with a calendar unit rewinds one full month, respecting real month lengths.
timeleap.prev({ time: base, duration: '1 month' })
// 2076-12-01T00:00:00.000ZPair it with next on the same base to bracket a full range: [prev(1 month), base) is the previous cycle, [base, next(1 month)) is the current one.
flowchart TD
Q{"How should the gap grow?"}
Q -->|"flat, even polling"| C["constant -> n"]
Q -->|"gently widening reminders"| L["linear -> n(n+1)/2"]
Q -->|"aggressive retry backoff"| E["exponential -> 2^n - 1"]
style Q fill:#fde68a,stroke:#92400e,color:#451a03
style C fill:#93c5fd,stroke:#1e40af,color:#0f172a
style L fill:#86efac,stroke:#166534,color:#052e16
style E fill:#fca5a5,stroke:#991b1b,color:#450a0a
| Curve | Cumulative factor | Feels like | Good for |
|---|---|---|---|
constant |
n |
even ticks | heartbeats, fixed polling |
linear |
n(n+1)/2 |
slowly spreading out | staged reminders |
exponential |
2^n - 1 |
doubling each attempt | retry storms, rate limiting |
flowchart TD
R["shifted timestamp"] --> C{"blocked by any rule?"}
C -->|no| DONE["return timestamp"]
C -->|yes| P["pick smallest active grain
minute > hour > day > week > month"]
P --> STEP["step one grain in shift direction
(next = forward, prev = backward)"]
STEP --> C
C -->|"all points blocked"| ERR["RangeError"]
style R fill:#e2e8f0,stroke:#475569,color:#0f172a
style C fill:#fde68a,stroke:#92400e,color:#451a03
style DONE fill:#86efac,stroke:#166534,color:#052e16
style P fill:#fdba74,stroke:#9a3412,color:#431407
style STEP fill:#fdba74,stroke:#9a3412,color:#431407
style ERR fill:#fca5a5,stroke:#991b1b,color:#450a0a
Guard rails to remember:
- Every skip field is optional and evaluated in UTC.
- Only the smallest active dimension drives the stepping stride.
- Values out of range throw
RangeError, and so does a rule that blocks an entire dimension (every minute, hour, day, week, or month).