Sequences
A Sequence defines a grid over the timeline — a set of equally
or calendar-spaced boundaries that aggregate, align, and clock
triggers consume to bucket or sample events.
A sequence is a grid definition, not a finite bucket list. By
default it extends infinitely in both directions, anchored at the
Unix epoch. Operators that consume a sequence either intersect it
with the data (aggregate(seq, mapping)) or with an explicit range
to produce a finite slice.
![Time-keyed cpu events bucketed by Sequence.every('5m'); aggregate produces one Interval-keyed event per bucket. Buckets are half-open start, end), so an event at exactly the boundary lands in the later bucket
Sequence.every(duration) — fixed-step
Equally-spaced boundaries every duration milliseconds.
import { Sequence } from 'pond-ts';
const minutely = Sequence.every('1m');
const second = Sequence.every('1s');
const tick = Sequence.every('200ms');
duration accepts the standard duration string format
(ms / s / m / h / d suffixes) or a plain millisecond number.
The step is millisecond-exact regardless of calendar boundaries —
Sequence.every('1d') produces 86,400,000-ms-spaced boundaries, not
local-calendar days.
Anchoring
By default a fixed-step sequence is anchored at Unix epoch (0),
which means independently-created sequences with the same step align
exactly. Override with the anchor option when you need a different
phase:
Sequence.every('30s', { anchor: 5_000 });
// boundaries at 5_000, 35_000, 65_000, … instead of 0, 30_000, 60_000
The anchor is a number | Date; sequences with the same step but
different anchors produce different bucket boundaries and will not
align across operators.
Convenience aliases
Sequence.hourly(); // = Sequence.every('1h')
Sequence.hourly({ anchor: 1800_000 });
Sequence.daily(); // = Sequence.every('1d')
Sequence.daily({ anchor: 5 * 3600_000 }); // anchor at 05:00 UTC
Same shape as Sequence.every, just named for the common cases.
Note that daily() here is fixed-step (86,400,000 ms exactly,
no DST awareness) — for local-calendar daily boundaries reach for
Sequence.calendar('day', { timeZone }).
Sequence.calendar(unit, options) — calendar-aligned
Boundaries that snap to local-calendar units (day / week / month / quarter / year) in a chosen time zone. Use these when calendar boundaries matter — daily reports, monthly aggregates, weekday-vs- weekend rollups.
Sequence.calendar('day', { timeZone: 'America/New_York' });
Sequence.calendar('week', { timeZone: 'Europe/Madrid', weekStartsOn: 1 });
Sequence.calendar('month', { timeZone: 'UTC' });
The timeZone option is required because calendar boundaries are not
millisecond-fixed — a "day" in America/New_York is 24 hours except
on DST transition days, where it's 23 or 25. The library uses the
@js-temporal/polyfill to compute the boundaries correctly.
When calendar vs fixed-step matters
| Use calendar when… | Use fixed-step when… |
|---|---|
| Bucket means a calendar day / month / etc. | Bucket means N seconds / minutes / hours |
| DST and varying month lengths matter | DST and month variation are noise |
| Reports align to local working hours | Sampling cadence is independent of wall clock |
| Cross-region comparisons need same wall hour | Cross-region comparisons need same elapsed time |
Calendar sequences are rejected by some operators that require
constant-step boundaries — notably clock-triggered live rolling
(Trigger.clock(seq) / Trigger.every(duration)). The boundary
indexing math assumes a constant step. See
Triggering.
Sequence.kind() — discriminator
seq.kind(); // 'fixed' | 'calendar'
Operators that need to know whether the boundaries are constant-step read this. Most application code doesn't.
Bounded vs unbounded
The Sequence types above are unbounded — they describe a grid
that extends in both directions. To work with a finite list of
buckets, either:
- Pass an explicit range to the operator (
aggregate(seq, mapping, { range })) - Use
BoundedSequencedirectly when the buckets are irregular
BoundedSequence
An explicit finite list of Intervals. Use when the buckets are
not regular — business quarters, predefined report ranges,
boundaries supplied by an external system.
import { BoundedSequence, Interval } from 'pond-ts';
const parse = Date.parse;
const quarters = new BoundedSequence([
new Interval({ value: 'Q1', start: parse('2025-01-01'), end: parse('2025-04-01') }),
new Interval({ value: 'Q2', start: parse('2025-04-01'), end: parse('2025-07-01') }),
new Interval({ value: 'Q3', start: parse('2025-07-01'), end: parse('2025-10-01') }),
new Interval({ value: 'Q4', start: parse('2025-10-01'), end: parse('2026-01-01') }),
]);
The index value of each Interval becomes the bucket label in the
output — cpu.aggregate(quarters, { cpu: 'avg' }).at(0).key().index()
returns 'Q1'. This is what the bucket round-trips through JSON as.
aggregate and align accept Sequence | BoundedSequence
interchangeably; downstream operators don't care which produced the
bucket.
Half-open boundaries
Every sequence — fixed-step, calendar, or bounded — produces
half-open [begin, end) bucket extents. An event whose key
timestamp equals a boundary lands in the later bucket, not the
earlier one.
const seq = Sequence.every('1m');
// Boundary at t=60_000 belongs to bucket [60_000, 120_000), not [0, 60_000)
This is consistent across all temporal-relation operations
(within, overlapping, trim) and matters when event timestamps
land exactly on boundaries — typical for synthetic data, less so for
real telemetry.
Where this shows up
- Aggregation —
series.aggregate(seq, mapping, { range? })buckets events onto sequence boundaries; output isInterval-keyed. See Aggregation. - Alignment —
series.align(seq, { method, sample })produces one event per sequence point with values resampled from the source. See Alignment. - Grid-form rolling —
series.rolling(seq, window, mapping)takes a sequence to define output cadence and a window for the reduction range. See Rolling. - Clock triggers —
Trigger.clock(seq)and theTrigger.every(duration)sugar use a sequence to define the emission cadence of live rolling. Calendar sequences are rejected. See Triggering. Intervalkeys — every operator that consumes a sequence producesInterval-keyed output. See Temporal keys.