@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.
- package/CHANGELOG.md +179 -1
- package/README.md +142 -382
- 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.
|
|
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
|
|
1
|
+
# pond-ts
|
|
2
2
|
|
|
3
|
-
|
|
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
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
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
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
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, '
|
|
148
|
-
['2025-01-01T00:01:00Z', 0.44, 135, '
|
|
149
|
-
['2025-01-01T00:02:00Z', 0.52, 141, '
|
|
150
|
-
['2025-01-01T00:03:00Z', 0.48, 128, '
|
|
151
|
-
['2025-01-01T00:04:00Z', 0.63, 166, '
|
|
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
|
|
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
|
|
166
|
-
|
|
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
|
-
|
|
176
|
-
|
|
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
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
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
|
-
##
|
|
191
|
-
|
|
192
|
-
Use `TimeSeries.fromJSON(...)` for external data and ambiguous local timestamps.
|
|
78
|
+
## Quick start: live (streaming)
|
|
193
79
|
|
|
194
80
|
```ts
|
|
195
|
-
import {
|
|
81
|
+
import { LiveSeries, Sequence } from 'pond-ts';
|
|
196
82
|
|
|
197
|
-
|
|
198
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
330
|
-
const
|
|
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
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
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
|
-
##
|
|
106
|
+
## Quick start: multi-entity
|
|
347
107
|
|
|
348
|
-
|
|
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
|
|
352
|
-
|
|
112
|
+
const perHost = cpu
|
|
113
|
+
.partitionBy('host')
|
|
114
|
+
.rolling('5m', { cpu: 'avg', cpu_sd: 'stdev' });
|
|
353
115
|
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
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
|
-
|
|
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
|
-
|
|
372
|
-
- optional prefixing:
|
|
125
|
+
## Quick start: bounded-memory sampling
|
|
373
126
|
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
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
|
-
|
|
387
|
-
|
|
388
|
-
host
|
|
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
|
-
|
|
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
|
|
144
|
+
const points = series.sample({ reservoir: { size: 500 } }).toRows();
|
|
145
|
+
// 500 uncorrelated points drawn uniformly from the source.
|
|
396
146
|
```
|
|
397
147
|
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
- `trailing`
|
|
401
|
-
- `centered`
|
|
402
|
-
- `leading`
|
|
148
|
+
## Performance
|
|
403
149
|
|
|
404
|
-
|
|
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
|
-
|
|
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
|
-
|
|
165
|
+
See the [full benchmark results](website/docs/reference/benchmarks.mdx)
|
|
166
|
+
for detailed numbers. Run locally:
|
|
409
167
|
|
|
410
|
-
```
|
|
411
|
-
|
|
168
|
+
```sh
|
|
169
|
+
npm run build && node packages/core/bench/vs-pondjs.cjs
|
|
412
170
|
```
|
|
413
171
|
|
|
414
|
-
|
|
172
|
+
## Documentation
|
|
415
173
|
|
|
416
|
-
|
|
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
|
-
|
|
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
|
-
|
|
427
|
-
- `movingAverage`
|
|
428
|
-
- `loess`
|
|
192
|
+
## Examples
|
|
429
193
|
|
|
430
|
-
|
|
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
|
-
##
|
|
201
|
+
## Develop
|
|
433
202
|
|
|
434
|
-
|
|
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
|
-
```
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
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
|
-
|
|
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.
|
|
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.
|
|
34
|
+
"pond-ts": "^0.18.0",
|
|
35
35
|
"react": "^18.0.0 || ^19.0.0"
|
|
36
36
|
},
|
|
37
37
|
"devDependencies": {
|