Skip to content

Latest commit

 

History

History
325 lines (252 loc) · 10.9 KB

File metadata and controls

325 lines (252 loc) · 10.9 KB

Timeleap Use Cases

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()

How The Four APIs Map To Problems

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
Loading

1. Idempotent Job Deduplication Window (parse with next)

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 elapsed

Output: next from base with 30 minutes = 2077-01-01T00:30:00.000Z.

2. Feature Flag / Trial Grace Period (next)

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.000Z

Why 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.

3. Office-Hours-Only SLA Clock (next with skip.hour)

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
Loading

4. Next Business Day From A Friday (next with skip.day)

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.

5. Backdated Report Cursor, Skipping Quarter-Hours (prev with skip.minute)

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.

6. Bi-Weekly Payroll That Avoids The First And Last Week (next with skip.week)

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)

7. Maintenance Freeze Across Blocked Months (next with skip.month)

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
Loading

8. Exponential Retry Ladder For A Flaky API (backoff exponential)

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
// ]

9. Escalating Reminder Cadence Before A Deadline (backoff linear)

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
Loading

10. Fixed-Interval Heartbeats / Polling Slots (backoff constant)

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
// ]

11. Previous Billing Cycle Anchor (prev)

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.000Z

Pair 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.

Choosing A Backoff Curve At A Glance

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
Loading
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

Skip Rules In One Picture

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
Loading

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).