@pond-ts/react 0.17.0 → 0.18.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (3) hide show
  1. package/CHANGELOG.md +179 -1
  2. package/README.md +142 -382
  3. package/package.json +2 -2
package/CHANGELOG.md CHANGED
@@ -7,10 +7,187 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/).
7
7
  file covers both packages. Pre-1.0: minor bumps may include new features and
8
8
  type-level changes; patch bumps are strictly additive.
9
9
 
10
- [Unreleased]: https://github.com/pjm17971/pond-ts/compare/v0.17.0...HEAD
10
+ [Unreleased]: https://github.com/pjm17971/pond-ts/compare/v0.18.0...HEAD
11
+ [0.18.0]: https://github.com/pjm17971/pond-ts/compare/v0.17.1...v0.18.0
11
12
 
12
13
  ## [Unreleased]
13
14
 
15
+ ## [0.18.0] — 2026-05-30
16
+
17
+ This release graduates the **Phase 4.7 columnar substrate** from
18
+ framework-internal (shipped piecemeal to `main` since v0.17.1) to a
19
+ user-visible **public column API**, plus a column-native live buffer that
20
+ fixes a high-partition-count OOM. Everything is additive except one
21
+ documented breaking change (interval label kinds) and one documented
22
+ behavior change (chunked-backed `pushMany` commit semantics). Pre-1.0: the
23
+ column API is expected to keep moving toward its eventual shape.
24
+
25
+ ### Added
26
+
27
+ - **Public column API (Phase 4.7 step 8).** A column-centric extraction
28
+ surface on `TimeSeries`, for high-throughput and charting consumers that
29
+ want typed-array access instead of per-`Event` iteration. Additive — every
30
+ existing row / `Event` API is unchanged.
31
+ - `series.column(name)` returns a schema-narrowed typed column view, with
32
+ public re-exports of the `Float64Column` / `BooleanColumn` /
33
+ `StringColumn` / `KeyColumn` (time / timeRange / interval) variants
34
+ ([#154](https://github.com/pjm17971/pond-ts/pull/154),
35
+ [#155](https://github.com/pjm17971/pond-ts/pull/155)).
36
+ - `Float64Column`: scalar reductions (`min` / `max` / `sum` / `mean` /
37
+ `count` / …) and `scan`
38
+ ([#155](https://github.com/pjm17971/pond-ts/pull/155)); `bin(...)` for
39
+ histogram / downsample bucketing
40
+ ([#156](https://github.com/pjm17971/pond-ts/pull/156)); and
41
+ `toFloat64Array()` for a storage-agnostic gather into a dense array
42
+ ([#165](https://github.com/pjm17971/pond-ts/pull/165)).
43
+ - `KeyColumn.at(i)` and `.slice(start, end)`
44
+ ([#159](https://github.com/pjm17971/pond-ts/pull/159)).
45
+ - **Columnar substrate (Phase 4.7 step 1, framework layer).** All
46
+ eight sub-steps (1a–1h) shipped to main as PRs #132 / #133 /
47
+ #134 / #135 / #136 / #147 / #148 / #149. See `PLAN.md` and
48
+ [`packages/core/src/columnar/README.md`](packages/core/src/columnar/README.md)
49
+ for the full inventory. Framework-internal — surfaced behind the existing
50
+ `TimeSeries` API at step 2 (below) and the public column API at step 8
51
+ (above).
52
+
53
+ ### Changed
54
+
55
+ - **Chunked columnar live backing for strict time-keyed `LiveSeries`**
56
+ ([#170](https://github.com/pjm17971/pond-ts/pull/170)). A top-level
57
+ `LiveSeries` with `ordering: 'strict'` and a time key now backs its
58
+ retained window with batch-granular columnar chunks instead of an
59
+ `Event[]` window — each `pushMany` validates straight into typed columns,
60
+ retaining **zero `Event` objects** (~4.7× less retained heap in-pond; the
61
+ high-partition-count OOM fix). Two consequences:
62
+ - **`pushMany` commit semantics** on the chunked path: the batch is
63
+ appended atomically _before_ any `'event'` fires, so a listener observes
64
+ the full post-batch `length` (not a row-by-row `1, 2, 3`), and a listener
65
+ that throws mid-batch leaves the whole batch committed. The per-row
66
+ `Event[]` backing (`reorder` / `drop` / interval-keyed /
67
+ internally-created series) keeps per-row commit. Listener _values_ and
68
+ `event → batch → evict` ordering are unchanged.
69
+ - **`LiveReduce` eviction** resolves by event identity (primary) with a
70
+ FIFO-frontier fallback for the chunked backing's materialized evictions —
71
+ correct for both `reorder` and the chunked backing. `min` / `max` /
72
+ `first` / `last` / `samples` over a `reorder` source **with retention**
73
+ remain a documented limitation (see `LiveReduce` JSDoc and PLAN
74
+ "Deferred") — pre-existing, not introduced here.
75
+ - **Internal, behavior-preserving performance work.** Column-native intake
76
+ bypasses per-row `Event` allocation at `TimeSeries` construction
77
+ ([#151](https://github.com/pjm17971/pond-ts/pull/151)); numeric reducers
78
+ (`min` / `max` / `sum` / `avg` / …) compute over typed-array columns where
79
+ available, with NaN parity preserved
80
+ ([#153](https://github.com/pjm17971/pond-ts/pull/153)); the live storage
81
+ strategy was extracted behind an internal interface
82
+ ([#168](https://github.com/pjm17971/pond-ts/pull/168)).
83
+
84
+ ### Changed (BREAKING)
85
+
86
+ - **Interval-keyed series must use one label type throughout**
87
+ ([#150](https://github.com/pjm17971/pond-ts/pull/150)). Pre-2a,
88
+ TimeSeries silently tolerated mixed-kind interval labels —
89
+ rows with `value: 'row-1'` (string) and `value: 2` (number) could
90
+ coexist in a single series because events were stored as a raw
91
+ array with no per-column type alignment. The columnar substrate
92
+ introduced at Phase 4.7 enforces one label kind per column via
93
+ `IntervalKeyColumn`, so mixed-kind labels now throw at series
94
+ construction with a row-pointed error message.
95
+ - **Affected:** Any series built via `new TimeSeries(...)`,
96
+ `TimeSeries.fromJSON(...)`, `TimeSeries.fromEvents(...)`, or
97
+ any transform that produces interval-keyed events, where the
98
+ `value` field of `IntervalInput` rows or `Interval` keys
99
+ mixes `string` and `number` types.
100
+ - **Migration:** Choose one label kind for the whole series.
101
+ Numeric labels can be stringified at intake (`String(label)`)
102
+ if the downstream consumer accepts string equality; string
103
+ labels parseable as integers can be converted to numbers at
104
+ intake. The error message names the first offending row so
105
+ the offending data is easy to find.
106
+ - **Rationale:** Aligns the row-API contract with the columnar
107
+ substrate's per-column kind discipline (matching Polars /
108
+ Arrow / Parquet). The previous behavior produced type-broken
109
+ events that worked only because TimeSeries didn't enforce
110
+ per-column alignment; downstream columnar operators (the
111
+ upcoming reducer adaptation in steps 3+) require it.
112
+ - **Affected types:** `IntervalValue` remains `string | number`
113
+ per the `Interval` class contract. The runtime restriction
114
+ is at the **series** level (all intervals within one series
115
+ must share a kind), not the per-interval level. Type-level
116
+ narrowing of `IntervalKeyedSchema<S>` over label kind is a
117
+ follow-up deferred to a later sub-step.
118
+
119
+ ## [0.17.1] — 2026-05-11
120
+
121
+ Bug fix: `live.partitionBy()` now default-inherits `ordering`,
122
+ `graceWindow`, and `retention` from the source `LiveSeries`. Surfaced
123
+ by the gRPC experiment's
124
+ [M4 late-data friction note](https://github.com/pjm17971/pond-grpc-experiment/blob/main/friction-notes/M4.md),
125
+ which measured `99.5%` of late events crashing the partition router
126
+ under `source = LiveSeries({ ordering: 'reorder', graceWindow })`
127
+ followed by bare `partitionBy('host')`.
128
+
129
+ ### Fixed
130
+
131
+ - **`LiveSeries.partitionBy(by)` default-inherits source config**
132
+ ([#TBD](https://github.com/pjm17971/pond-ts/pull/TBD)). Pre-fix,
133
+ per-partition sub-series were constructed with default
134
+ `ordering: 'strict'` regardless of source mode. Under a `'reorder'`
135
+ source, late events that the source accepted via its reorder path
136
+ were routed into the partition's `#insert` and threw with a
137
+ strict-mode error; the throw propagated back through the source's
138
+ listener fan-out into `live.push()`.
139
+
140
+ Post-fix, `partitionBy(by)` defaults each per-partition sub-series'
141
+ `ordering`, `graceWindow`, and `retention` to the source's values.
142
+ Explicit options on `partitionBy(by, { ordering, ... })` override
143
+ per-field. `graceWindow` inheritance is gated on effective ordering
144
+ being `'reorder'` (LiveSeries rejects strict + graceWindow combos).
145
+
146
+ ```ts
147
+ // Pre-0.17.1: crashed the partition router
148
+ const live = new LiveSeries({
149
+ name: 'metrics',
150
+ schema,
151
+ ordering: 'reorder',
152
+ graceWindow: '30s',
153
+ });
154
+ live.partitionBy('host'); // ← partition was strict regardless
155
+
156
+ // Post-0.17.1: partitions inherit reorder + 30s grace; late events
157
+ // accept correctly via the reorder path.
158
+ ```
159
+
160
+ Existing callers with explicit `partitionBy(by, { ordering, ... })`:
161
+ unchanged. Existing callers on `'strict'` sources: unchanged.
162
+ Existing callers on `'reorder'` sources with bare `partitionBy`:
163
+ the previously-thrown late events now accept correctly — bug fix,
164
+ not a behavior change anyone could rely on.
165
+
166
+ - **`collect()` and `apply()` on `LivePartitionedSeries` default-
167
+ inherit `ordering` and `graceWindow`** from the partitioned series
168
+ (which inherits from source). Pre-fix, the unified buffer defaulted
169
+ to `'strict'`, so partition fan-in on a `'reorder'` source could
170
+ deliver events out-of-order to a strict unified buffer and throw.
171
+ Retention stays caller-explicit on these per the existing append-
172
+ only fan-in semantics.
173
+
174
+ ### Notes
175
+
176
+ - **Six regression tests pin the new defaults** in
177
+ `LivePartitionedSeries.test.ts`: inherited ordering, inherited
178
+ graceWindow within reorder, inherited retention on partitions,
179
+ explicit override of inheritance, strict-source no-change, and the
180
+ edge case where overriding ordering to strict suppresses graceWindow
181
+ inheritance. `collect()` inheritance pinned separately.
182
+ - The gRPC experiment's M4 friction note also surfaced milestone B
183
+ (capability-based late repair) as **driver-light by empirical test**
184
+ after Codex's adversarial pass caught simulator RNG leakage across
185
+ A/B legs. Drift signal collapsed to within noise on every host once
186
+ all randomness sources were seeded — milestone B's library design
187
+ stays sound, but the gRPC experiment's measurement style (last-tick
188
+ `.value()` reads) doesn't surface its payoff. Milestone B sequencing
189
+ updated in PLAN.md to reflect this finding.
190
+
14
191
  ## [0.17.0] — 2026-05-08
15
192
 
16
193
  `sample({...})` operator wave: bounded-memory stream thinning, surfaced
@@ -385,6 +562,7 @@ compaction); any downstream code reading `#entries` directly would
385
562
  break, but those fields are private. Public APIs and types are
386
563
  unchanged.
387
564
 
565
+ [0.17.1]: https://github.com/pjm17971/pond-ts/compare/v0.17.0...v0.17.1
388
566
  [0.17.0]: https://github.com/pjm17971/pond-ts/compare/v0.16.1...v0.17.0
389
567
  [0.16.1]: https://github.com/pjm17971/pond-ts/compare/v0.16.0...v0.16.1
390
568
  [0.16.0]: https://github.com/pjm17971/pond-ts/compare/v0.15.2...v0.16.0
package/README.md CHANGED
@@ -1,134 +1,39 @@
1
- # pond-ts - A modern typescript timeseries library
1
+ # pond-ts
2
2
 
3
- TypeScript-first time series primitives built around typed events, typed schemas, and explicit temporal keys.
3
+ **Highly optimised, fully typed Timeseries library for TypeScript**
4
+
5
+ Schema-driven events, composable batch transforms, push-based streaming
6
+ ingest, multi-entity partitioning, and an optional React integration —
7
+ all strict TypeScript end to end, all immutable.
4
8
 
5
- **pond-ts** is the successor to [pondjs](https://github.com/esnet/pond), rewritten from scratch in TypeScript with a focus on performance, type safety, and composable live streaming.
6
-
7
- - typed `TimeSeries` construction and immutable `Event` objects
8
- - `Time`, `TimeRange`, and `Interval` temporal keys
9
- - alignment, aggregation, joins, rolling windows, and smoothing
10
- - `LiveSeries` with push-based ingestion, retention policies, and subscriptions
11
- - `LiveView`, `LiveAggregation`, and `LiveRollingAggregation` for streaming composition
12
- - timezone-aware calendar sequences and ingest helpers
13
-
14
- The package is intended to work in modern Node and frontend projects.
15
-
16
- ## Performance
17
-
18
- pond-ts is **7.6x faster** than pondjs on average across all comparable operations,
19
- with no regressions. The advantage grows with data size.
20
-
21
- | Category | Speedup (N=16k) | Notes |
22
- | ----------------- | --------------- | --------------------------------------------- |
23
- | **Aggregation** | 25–32x | O(N+B) bucketing vs O(N×B) Pipeline |
24
- | **Alignment** | 32x | Forward cursor vs repeated binary search |
25
- | **Rate/diff** | 18x | Direct array walk vs Pipeline materialization |
26
- | **Fill** | 10–11x | Single-pass vs Pipeline per strategy |
27
- | **Transforms** | 3–16x | Pre-validated constructor skips re-validation |
28
- | **Construction** | 7x | Plain objects vs ImmutableJS wrapping |
29
- | **Statistics** | 7–9x | Direct computation vs ImmutableJS iteration |
30
- | **Serialization** | 4x | Simpler internal representation |
31
- | **Event access** | 23x | Array indexing vs ImmutableJS `get()` |
32
-
33
- See the [full benchmark results](website/docs/reference/benchmarks.mdx) for detailed numbers.
34
- Run locally: `npm run build && node packages/core/bench/vs-pondjs.cjs`
35
-
36
- ## Install
37
-
38
- ```sh
39
- npm install pond-ts
40
- ```
41
-
42
- ## Build
43
-
44
- The repo toolchain should work on Node 18, but use `nvm` to verify against newer stable Node releases when needed.
45
-
46
- ```sh
47
- npm run build
48
- ```
49
-
50
- ## Format
51
-
52
- ```sh
53
- npm run format
54
- ```
55
-
56
- ## Test
57
-
58
- ```sh
59
- npm test
60
- ```
61
-
62
- ## Verify
9
+ **pond-ts** is the TypeScript-first successor to
10
+ [pondjs](https://github.com/esnet/pond), rewritten from scratch with a
11
+ focus on type safety, composability, and the live-streaming patterns
12
+ that pondjs never grew.
63
13
 
64
14
  ```sh
65
- npm run verify
66
- ```
67
-
68
- ## Docs site
69
-
70
- The documentation website lives in [`website/`](./website) and is built with Docusaurus.
71
-
72
- ## Examples
73
-
74
- - **[pond-ts-dashboard](https://github.com/pjm17971/pond-ts-dashboard)**
75
- a working React dashboard that streams synthetic per-host CPU/request
76
- metrics, computes per-host rolling baselines, flags anomalies against
77
- ±σ bands, and renders everything as live line and bar charts (~600
78
- lines of TypeScript). The repo's README is also published as a docs-
79
- site guide: [Building a dashboard](website/docs/how-to-guides/dashboard-guide.mdx).
80
-
81
- ## License
82
-
83
- MIT
84
-
85
- ## Core model
86
-
87
- The key types are:
88
-
89
- - `Time`: a point in time
90
- - `TimeRange`: an unlabeled interval
91
- - `Interval`: a labeled interval
92
-
93
- An `Event` is a key plus typed data.
94
-
95
- A `TimeSeries` is an ordered immutable collection of events sharing one schema.
96
-
97
- ## Quick start
98
-
99
- ```ts
100
- import { TimeSeries } from 'pond-ts';
101
-
102
- const schema = [
103
- { name: 'time', kind: 'time' },
104
- { name: 'cpu', kind: 'number' },
105
- { name: 'host', kind: 'string' },
106
- { name: 'healthy', kind: 'boolean' },
107
- ] as const;
108
-
109
- const series = new TimeSeries({
110
- name: 'cpu',
111
- schema,
112
- rows: [
113
- [new Date('2025-01-01T00:00:00.000Z'), 0.42, 'api-1', true],
114
- [new Date('2025-01-01T00:01:00.000Z'), 0.51, 'api-2', true],
115
- ],
116
- });
117
-
118
- const event = series.at(1);
119
- if (!event) {
120
- throw new Error('missing event');
121
- }
122
-
123
- event.key();
124
- event.timeRange();
125
- event.get('cpu');
126
- event.data().host;
127
- ```
128
-
129
- ## Worked example
130
-
131
- This is the kind of flow `pond-ts` is built for: start with typed events, then derive aligned, aggregated, and smoothed analytical views without mutating the original series.
15
+ npm install pond-ts # core
16
+ npm install @pond-ts/react # React hooks (optional)
17
+ ```
18
+
19
+ - **Typed schemas** — declare once, every transform downstream narrows
20
+ off it. `event.get('cpu')` returns `number | undefined` straight from
21
+ the schema; no `as` casts.
22
+ - **Batch + streaming with the same vocabulary** — `filter`, `map`,
23
+ `aggregate`, `rolling`, `diff`, `rate`, `fill`, `cumulative`,
24
+ `sample`, `reduce` all exist on both `TimeSeries` and `LiveSeries`.
25
+ - **Multi-entity by construction** `partitionBy('host')` routes per
26
+ entity; `rolling` / `aggregate` / `fill` / `sample` over a partitioned
27
+ view all become per-entity automatically.
28
+ - **Bounded-memory streaming** retention policies, eviction-aware
29
+ views, and sampling decouple downstream window length
30
+ from event rate at firehose loads (up to 500k events/sec on a
31
+ single node.js instance.)
32
+ - **Triggers** — for control of rolling emission cadences. Synchronised
33
+ partitioned rolling fires across partitions on every boundary.
34
+ - **No legacy baggage**
35
+
36
+ ## Quick start: batch
132
37
 
133
38
  ```ts
134
39
  import { Sequence, TimeSeries } from 'pond-ts';
@@ -144,316 +49,171 @@ const cpu = TimeSeries.fromJSON({
144
49
  name: 'cpu',
145
50
  schema,
146
51
  rows: [
147
- ['2025-01-01T00:00:00Z', 0.31, 120, 'api-1'],
148
- ['2025-01-01T00:01:00Z', 0.44, 135, 'api-1'],
149
- ['2025-01-01T00:02:00Z', 0.52, 141, 'api-1'],
150
- ['2025-01-01T00:03:00Z', 0.48, 128, 'api-1'],
151
- ['2025-01-01T00:04:00Z', 0.63, 166, 'api-1'],
52
+ ['2025-01-01T00:00:00Z', 0.31, 120, 'host1'],
53
+ ['2025-01-01T00:01:00Z', 0.44, 135, 'host2'],
54
+ ['2025-01-01T00:02:00Z', 0.52, 141, 'host1'],
55
+ ['2025-01-01T00:03:00Z', 0.48, 128, 'host1'],
56
+ ['2025-01-01T00:04:00Z', 0.63, 166, 'host3'],
152
57
  ],
153
58
  });
154
59
 
155
- const perMinute = cpu.align(Sequence.every('1m'), {
156
- method: 'hold',
157
- });
158
-
159
- const fiveMinute = cpu.aggregate(Sequence.every('5m'), {
60
+ const byMinute = cpu.aggregate(Sequence.every('1m'), {
160
61
  cpu: 'avg',
161
62
  requests: 'sum',
162
63
  host: 'last',
163
64
  });
164
65
 
165
- const rolling = cpu.rolling('3m', {
166
- cpu: 'avg',
167
- requests: 'sum',
168
- });
169
-
170
- const smoothed = cpu.smooth('cpu', 'ema', {
171
- alpha: 0.35,
172
- output: 'cpuTrend',
173
- });
66
+ const bands = cpu.baseline('cpu', { window: '2m', sigma: 2 });
67
+ // ^ appends rolling avg / sd / upper / lower in one pass.
174
68
 
175
- console.log(perMinute.first()?.key().asString());
176
- console.log(fiveMinute.first()?.data());
177
- console.log(rolling.last()?.data());
178
- console.log(smoothed.last()?.get('cpuTrend'));
69
+ const anomalies = cpu.outliers('cpu', { window: '2m', sigma: 2 });
70
+ // ^ schema-preserving filter — same columns, just the spikes.
179
71
  ```
180
72
 
181
- From one typed source series, you can derive:
182
-
183
- - aligned interval views for dashboards and joins
184
- - bucketed aggregates for reporting
185
- - rolling metrics for short-term behavior
186
- - smoothed trends for visualization or alerting
187
-
188
- All of those remain fully typed and immutable.
73
+ The full batch surface (`align`, `rolling`, `smooth`, `groupBy`, `join`,
74
+ `reduce`, `diff`, `rate`, `fill`, `dedupe`, `materialize`, `sample`,
75
+ `partitionBy`, `pivotByGroup`, …) follows the same shape: TimeSeries
76
+ in, TimeSeries out, schema preserved.
189
77
 
190
- ## JSON ingest
191
-
192
- Use `TimeSeries.fromJSON(...)` for external data and ambiguous local timestamps.
78
+ ## Quick start: live (streaming)
193
79
 
194
80
  ```ts
195
- import { TimeSeries } from 'pond-ts';
81
+ import { LiveSeries, Sequence } from 'pond-ts';
196
82
 
197
- const schema = [
198
- { name: 'time', kind: 'time' },
199
- { name: 'value', kind: 'number' },
200
- { name: 'status', kind: 'string', required: false },
201
- ] as const;
202
-
203
- const series = TimeSeries.fromJSON({
83
+ // 1. Same schema; this is a live append buffer with retention.
84
+ const live = new LiveSeries({
204
85
  name: 'cpu',
205
86
  schema,
206
- rows: [
207
- ['2025-01-01T09:00', 0.42, 'ok'],
208
- ['2025-01-01T10:00', 0.51, null],
209
- ],
210
- parse: { timeZone: 'Europe/Madrid' },
211
- });
212
- ```
213
-
214
- Export back into the same JSON-friendly shape:
215
-
216
- ```ts
217
- const rows = series.toJSON();
218
- const objectRows = series.toJSON({ rowFormat: 'object' });
219
- ```
220
-
221
- For normalized in-memory export helpers:
222
-
223
- ```ts
224
- const normalizedRows = series.toRows();
225
- const normalizedObjects = series.toObjects();
226
- ```
227
-
228
- ## Event and series transforms
229
-
230
- Event-level transforms:
231
-
232
- - `get(...)`
233
- - `set(...)`
234
- - `merge(...)`
235
- - `select(...)`
236
- - `rename(...)`
237
- - `collapse(...)`
238
- - `asTime(...)`
239
- - `asTimeRange()`
240
- - `asInterval(...)`
241
-
242
- Series-level transforms:
243
-
244
- - `map(...)`
245
- - `select(...)`
246
- - `rename(...)`
247
- - `collapse(...)`
248
- - `asTime(...)`
249
- - `asTimeRange()`
250
- - `asInterval(...)`
251
-
252
- Example:
253
-
254
- ```ts
255
- const renamed = series.rename({ cpu: 'usage' });
256
- const selected = renamed.select('usage', 'healthy');
257
- ```
258
-
259
- ## Temporal selection
260
-
261
- `TimeSeries` includes both positional and temporal selection methods:
262
-
263
- - `slice(...)`
264
- - `filter(...)`
265
- - `find(...)`
266
- - `first()`
267
- - `last()`
268
- - `before(...)`
269
- - `after(...)`
270
- - `within(...)`
271
- - `overlapping(...)`
272
- - `containedBy(...)`
273
- - `trim(...)`
274
- - `includesKey(...)`
275
- - `bisect(...)`
276
- - `atOrBefore(...)`
277
- - `atOrAfter(...)`
278
-
279
- Vocabulary is intentionally distinct:
280
-
281
- - `within(...)`: fully contained
282
- - `overlapping(...)`: intersects without clipping
283
- - `trim(...)`: intersects and clips event extents
284
-
285
- ## Sequences
286
-
287
- Use `Sequence` for unbounded grids and `BoundedSequence` for explicit finite interval lists.
288
-
289
- Fixed-step sequences:
290
-
291
- ```ts
292
- import { Sequence } from 'pond-ts';
293
-
294
- const minuteGrid = Sequence.every('1m');
295
- const hourlyGrid = Sequence.hourly();
296
- ```
297
-
298
- Calendar-aware sequences:
299
-
300
- ```ts
301
- const localDays = Sequence.calendar('day', {
302
- timeZone: 'America/New_York',
87
+ retention: { maxAge: '10m' }, // keep only the last 10 minutes
303
88
  });
304
- ```
305
-
306
- Explicit bounded sequences:
307
-
308
- ```ts
309
- import { BoundedSequence, Interval } from 'pond-ts';
310
-
311
- const buckets = new BoundedSequence([
312
- new Interval({ value: 'a', start: 0, end: 10 }),
313
- new Interval({ value: 'b', start: 20, end: 30 }),
314
- ]);
315
- ```
316
89
 
317
- ## Alignment and aggregation
318
-
319
- Align onto a sequence:
320
-
321
- ```ts
322
- const aligned = series.align(Sequence.every('1m'), {
323
- method: 'hold',
324
- });
325
- ```
90
+ // 2. Push as events arrive. Each push is validated against the schema.
91
+ live.push([Date.now(), 0.45, 128, 'api-1']);
326
92
 
327
- Aggregate into buckets:
93
+ // 3. Compose live views — incremental, push-driven, eviction-aware.
94
+ const recentAvg = live.rolling('5m', { cpu: 'avg' });
95
+ recentAvg.on('event', (e) => render(e.get('cpu')));
328
96
 
329
- ```ts
330
- const aggregated = series.aggregate(Sequence.every('5m'), {
331
- cpu: 'avg',
332
- host: 'last',
333
- });
97
+ // 4. Snapshot to a TimeSeries for batch analytics at any time.
98
+ const snap = live.toTimeSeries();
334
99
  ```
335
100
 
336
- Built-in aggregations:
337
-
338
- - `sum`
339
- - `avg`
340
- - `min`
341
- - `max`
342
- - `count`
343
- - `first`
344
- - `last`
101
+ The full live surface (`filter`, `map`, `select`, `window`, `aggregate`,
102
+ `rolling`, `reduce`, `diff`, `rate`, `pctChange`, `fill`, `cumulative`,
103
+ `sample`) is incremental — events flow, views emit, retention bounds
104
+ memory.
345
105
 
346
- ## Joins
106
+ ## Quick start: multi-entity
347
107
 
348
- Join two aligned or bucketed series:
108
+ `partitionBy` routes events into per-key buffers. Every stateful
109
+ operator downstream of `partitionBy` runs per-partition automatically:
349
110
 
350
111
  ```ts
351
- const joined = left.join(right, { type: 'outer' });
352
- ```
112
+ const perHost = cpu
113
+ .partitionBy('host')
114
+ .rolling('5m', { cpu: 'avg', cpu_sd: 'stdev' });
353
115
 
354
- Supported join types:
355
-
356
- - `outer`
357
- - `left`
358
- - `right`
359
- - `inner`
360
-
361
- Join many:
362
-
363
- ```ts
364
- const wide = TimeSeries.joinMany([cpu, memory, errors], {
365
- type: 'outer',
366
- });
116
+ // .collect() fans the per-partition outputs back into a flat TimeSeries
117
+ // with the partition key auto-injected as a column.
118
+ const flat = perHost.collect();
367
119
  ```
368
120
 
369
- Conflict handling:
121
+ Same shape on the live side — `live.partitionBy('host')` returns a
122
+ `LivePartitionedSeries` whose `rolling` / `fill` / `diff` / `sample`
123
+ methods all maintain per-partition state.
370
124
 
371
- - default: `onConflict: "error"`
372
- - optional prefixing:
125
+ ## Quick start: bounded-memory sampling
373
126
 
374
- ```ts
375
- const joined = left.join(right, {
376
- onConflict: 'prefix',
377
- prefixes: ['left', 'right'] as const,
378
- });
379
- ```
380
-
381
- ## Rolling windows
382
-
383
- Event-driven rolling:
127
+ At firehose rates, a long rolling baseline blows the heap. `sample({
128
+ stride: N })` decouples baseline length from event rate; chain it
129
+ between `partitionBy` and `rolling`:
384
130
 
385
131
  ```ts
386
- const rolled = series.rolling('5m', {
387
- cpu: 'avg',
388
- host: 'last',
389
- });
132
+ // Per-host 1-in-10 stride feeding a per-host 5m baseline.
133
+ live
134
+ .partitionBy('host')
135
+ .sample({ stride: 10 })
136
+ .rolling('5m', { cpu_avg: 'avg', cpu_sd: 'stdev' });
390
137
  ```
391
138
 
392
- Sequence-driven rolling:
139
+ For visualization, the snapshot side ships reservoir sampling too —
140
+ single-pass Algorithm R, sorted by key, fixed point count regardless of
141
+ source size:
393
142
 
394
143
  ```ts
395
- const rolledOnGrid = series.rolling(Sequence.every('1m'), '5m', { cpu: 'avg' });
144
+ const points = series.sample({ reservoir: { size: 500 } }).toRows();
145
+ // 500 uncorrelated points drawn uniformly from the source.
396
146
  ```
397
147
 
398
- Rolling alignment options:
399
-
400
- - `trailing`
401
- - `centered`
402
- - `leading`
148
+ ## Performance
403
149
 
404
- ## Smoothing
150
+ pond-ts is **7.6x faster** than pondjs on average across all comparable
151
+ operations, with no regressions. The advantage grows with data size.
405
152
 
406
- Smoothing targets one numeric column at a time.
153
+ | Category | Speedup (N=16k) | Notes |
154
+ | ----------------- | --------------- | --------------------------------------------- |
155
+ | **Aggregation** | 25–32x | O(N+B) bucketing vs O(N×B) Pipeline |
156
+ | **Alignment** | 32x | Forward cursor vs repeated binary search |
157
+ | **Rate/diff** | 18x | Direct array walk vs Pipeline materialization |
158
+ | **Fill** | 10–11x | Single-pass vs Pipeline per strategy |
159
+ | **Transforms** | 3–16x | Pre-validated constructor skips re-validation |
160
+ | **Construction** | 7x | Plain objects vs ImmutableJS wrapping |
161
+ | **Statistics** | 7–9x | Direct computation vs ImmutableJS iteration |
162
+ | **Serialization** | 4x | Simpler internal representation |
163
+ | **Event access** | 23x | Array indexing vs ImmutableJS `get()` |
407
164
 
408
- Replace the source column:
165
+ See the [full benchmark results](website/docs/reference/benchmarks.mdx)
166
+ for detailed numbers. Run locally:
409
167
 
410
- ```ts
411
- const smoothed = series.smooth('cpu', 'ema', { alpha: 0.2 });
168
+ ```sh
169
+ npm run build && node packages/core/bench/vs-pondjs.cjs
412
170
  ```
413
171
 
414
- Append the smoothed output:
172
+ ## Documentation
415
173
 
416
- ```ts
417
- const smoothed = series.smooth('cpu', 'movingAverage', {
418
- window: '5m',
419
- alignment: 'centered',
420
- output: 'cpuAvg',
421
- });
422
- ```
174
+ The full guide is at **<https://pjm17971.github.io/pond-ts/>**.
423
175
 
424
- Supported smoothing methods:
176
+ - **[Start here](https://pjm17971.github.io/pond-ts/docs/)**
177
+ — five-minute walkthrough with batch, live, and React examples.
178
+ - **[Concepts](https://pjm17971.github.io/pond-ts/docs/start-here/concepts)**
179
+ — temporal keys, sequences, windowing, partitioning, triggers, late
180
+ data.
181
+ - **[Transforms reference](https://pjm17971.github.io/pond-ts/docs/pond-ts/transforms/queries)**
182
+ — every batch operator (queries, aggregation, alignment, rolling,
183
+ smoothing, sampling, cleaning, reshape, anomaly detection).
184
+ - **[Live reference](https://pjm17971.github.io/pond-ts/docs/pond-ts/live/live-series)**
185
+ — `LiveSeries`, live transforms, triggering.
186
+ - **[How-to guides](https://pjm17971.github.io/pond-ts/docs/how-to-guides)**
187
+ — building a dashboard, ingesting messy data.
188
+ - **[API reference (auto-generated)](https://pjm17971.github.io/pond-ts/generated-api/core/)**
189
+ — TypeDoc output, every public class and method.
190
+ - **[CHANGELOG](./CHANGELOG.md)** — what shipped in each release.
425
191
 
426
- - `ema`
427
- - `movingAverage`
428
- - `loess`
192
+ ## Examples
429
193
 
430
- For interval-like keys, smoothing uses the key midpoint as the internal anchor.
194
+ - **[pond-ts-dashboard](https://github.com/pjm17971/pond-ts-dashboard)**
195
+ — a working React dashboard that streams synthetic per-host CPU /
196
+ request metrics, computes per-host rolling baselines, flags anomalies
197
+ against ±σ bands, and renders everything as live line and bar charts
198
+ (~600 lines of TypeScript). Walked through end-to-end in
199
+ [Building a dashboard](website/docs/how-to-guides/dashboard-guide.mdx).
431
200
 
432
- ## Calendar-aware helpers
201
+ ## Develop
433
202
 
434
- Primitive helpers normalize local calendar inputs into absolute time:
203
+ The repo is an npm-workspaces monorepo with two published packages
204
+ (`pond-ts`, `@pond-ts/react`). Node 18+ for runtime; Node 20+ for the
205
+ docs site (Docusaurus).
435
206
 
436
- ```ts
437
- import { Interval, Time, TimeRange } from 'pond-ts';
438
-
439
- const time = Time.parse('2025-01-01T09:00', { timeZone: 'Europe/Madrid' });
440
- const day = TimeRange.fromDate('2025-01-01', { timeZone: 'UTC' });
441
- const month = Interval.fromCalendar('month', '2025-01', {
442
- timeZone: 'UTC',
443
- value: '2025-01',
444
- });
207
+ ```sh
208
+ npm install # one-time, hoists deps for both packages
209
+ npm run build # build both packages
210
+ npm test # runtime + type-level tests on both packages
211
+ npm run format # prettier write across the repo
212
+ npm run verify # format check + build + test (CI parity)
445
213
  ```
446
214
 
447
- ## Current scope
448
-
449
- The library provides both batch analytics (`TimeSeries`) and live streaming
450
- (`LiveSeries`, `LiveView`, `LiveAggregation`, `LiveRollingAggregation`).
451
-
452
- - type-safe construction with schema types that flow through every operation
453
- - temporal modeling with `Time`, `TimeRange`, and `Interval` keys
454
- - composable batch analytics (aggregate, align, join, rolling, smooth, fill, diff, rate, groupBy)
455
- - push-based live ingestion with retention policies and subscriptions
456
- - live composition: filter, map, select, window, diff, rate, fill, cumulative, aggregate, rolling
215
+ `packages/core/` is the `pond-ts` package; `packages/react/` is
216
+ `@pond-ts/react`. Docs live in `website/`.
457
217
 
458
218
  ## License
459
219
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pond-ts/react",
3
- "version": "0.17.0",
3
+ "version": "0.18.0",
4
4
  "description": "React hooks for pond-ts live time series",
5
5
  "license": "MIT",
6
6
  "repository": {
@@ -31,7 +31,7 @@
31
31
  "test:runtime": "vitest run"
32
32
  },
33
33
  "peerDependencies": {
34
- "pond-ts": "^0.17.0",
34
+ "pond-ts": "^0.18.0",
35
35
  "react": "^18.0.0 || ^19.0.0"
36
36
  },
37
37
  "devDependencies": {