Skip to main content

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 matterDST and month variation are noise
Reports align to local working hoursSampling cadence is independent of wall clock
Cross-region comparisons need same wall hourCross-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:

  1. Pass an explicit range to the operator (aggregate(seq, mapping, { range }))
  2. Use BoundedSequence directly 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

  • Aggregationseries.aggregate(seq, mapping, { range? }) buckets events onto sequence boundaries; output is Interval-keyed. See Aggregation.
  • Alignmentseries.align(seq, { method, sample }) produces one event per sequence point with values resampled from the source. See Alignment.
  • Grid-form rollingseries.rolling(seq, window, mapping) takes a sequence to define output cadence and a window for the reduction range. See Rolling.
  • Clock triggersTrigger.clock(seq) and the Trigger.every(duration) sugar use a sequence to define the emission cadence of live rolling. Calendar sequences are rejected. See Triggering.
  • Interval keys — every operator that consumes a sequence produces Interval-keyed output. See Temporal keys.