@pond-ts/react 0.11.4 → 0.11.5
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 +1092 -0
- package/LICENSE +21 -0
- package/README.md +460 -0
- package/package.json +4 -2
package/CHANGELOG.md
ADDED
|
@@ -0,0 +1,1092 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
All notable changes to this project are documented here.
|
|
4
|
+
|
|
5
|
+
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/).
|
|
6
|
+
`pond-ts` and `@pond-ts/react` release together under a single `v*` tag, so this
|
|
7
|
+
file covers both packages. Pre-1.0: minor bumps may include new features and
|
|
8
|
+
type-level changes; patch bumps are strictly additive.
|
|
9
|
+
|
|
10
|
+
[Unreleased]: https://github.com/pjm17971/pond-ts/compare/v0.11.5...HEAD
|
|
11
|
+
|
|
12
|
+
## [Unreleased]
|
|
13
|
+
|
|
14
|
+
## [0.11.5] — 2026-04-29
|
|
15
|
+
|
|
16
|
+
### Fixed
|
|
17
|
+
|
|
18
|
+
- Published tarballs for both `pond-ts` and `@pond-ts/react` now
|
|
19
|
+
include `README.md`, `LICENSE`, and `CHANGELOG.md`. Earlier
|
|
20
|
+
releases shipped only `dist/` + `package.json`, which left the
|
|
21
|
+
npm page rendering as "This package does not have a README"
|
|
22
|
+
despite the comprehensive root README. The repo-root files were
|
|
23
|
+
invisible to `npm pack` because npm publishes from the package
|
|
24
|
+
directory and only auto-includes README/LICENSE when those files
|
|
25
|
+
live in the package dir itself. Each package now has a `prepack`
|
|
26
|
+
step that copies them in from the repo root before build.
|
|
27
|
+
|
|
28
|
+
[0.11.5]: https://github.com/pjm17971/pond-ts/compare/v0.11.4...v0.11.5
|
|
29
|
+
|
|
30
|
+
## [0.11.4] — 2026-04-29
|
|
31
|
+
|
|
32
|
+
### Added
|
|
33
|
+
|
|
34
|
+
- **`LiveSeries` snapshot/append primitives** — closes the gap
|
|
35
|
+
where networked `LiveSeries` setups (gRPC, WebSocket fanout) had
|
|
36
|
+
to hand-roll the parallel APIs that already existed on
|
|
37
|
+
`TimeSeries`.
|
|
38
|
+
- **Codec-agnostic typed-tuple primitives:** `LiveSeries.toRows()`,
|
|
39
|
+
`LiveSeries.toObjects()`, `LiveSeries.pushMany(rows)`,
|
|
40
|
+
`Event.toRow(schema)`. Operate in `RowForSchema<S>` typed
|
|
41
|
+
tuples — JSON, MessagePack, protobuf, anything else applies at
|
|
42
|
+
the application boundary, not inside the library.
|
|
43
|
+
- **JSON sugar layered on top:** `LiveSeries.toJSON()`,
|
|
44
|
+
`LiveSeries.fromJSON(input, options?)`,
|
|
45
|
+
`LiveSeries.pushJson(rows)`, `Event.toJsonRow(schema)`. Closes
|
|
46
|
+
the wire→push safety hole — `pushJson` validates a
|
|
47
|
+
`JsonRowForSchema<S>` against the schema at compile time, so
|
|
48
|
+
schema evolution breaks the call site instead of swallowing
|
|
49
|
+
via `live.push(row as never)`.
|
|
50
|
+
- **`pushMany(rows)` is non-variadic.** Pair with the existing
|
|
51
|
+
variadic `push(...rows)` (now a one-line wrapper); reach for
|
|
52
|
+
`pushMany` when ingesting a snapshot or any large array —
|
|
53
|
+
variadic spread allocates a stack frame per element and can
|
|
54
|
+
blow on multi-thousand-row snapshots.
|
|
55
|
+
|
|
56
|
+
Surfaced by the gRPC experiment's M1 milestone
|
|
57
|
+
([pond-grpc-experiment#3](https://github.com/pjm17971/pond-grpc-experiment/pull/3)).
|
|
58
|
+
See PLAN.md Phase 4 for the deferred adaptor-extraction
|
|
59
|
+
framing (codec strategies parked until two real codecs exist
|
|
60
|
+
in working code).
|
|
61
|
+
|
|
62
|
+
### Changed
|
|
63
|
+
|
|
64
|
+
- `LiveSeries.push(...rows)` is now a wrapper around
|
|
65
|
+
`LiveSeries.pushMany(rows)`. Behavior is identical — same
|
|
66
|
+
validation, listener fires, and retention pass.
|
|
67
|
+
|
|
68
|
+
[0.11.4]: https://github.com/pjm17971/pond-ts/compare/v0.11.3...v0.11.4
|
|
69
|
+
|
|
70
|
+
## [0.11.3] — 2026-04-28
|
|
71
|
+
|
|
72
|
+
### Added
|
|
73
|
+
|
|
74
|
+
- **`pond-ts/types` subpath export** — type-only entry point that
|
|
75
|
+
exposes the schema-shape, row-shape, and JSON-shape types
|
|
76
|
+
(`SeriesSchema`, `ColumnDef`, `RowForSchema`,
|
|
77
|
+
`JsonRowForSchema`, etc.) without dragging in the runtime.
|
|
78
|
+
Schema-as-contract consumers — packages whose only job is to
|
|
79
|
+
declare the `as const` schema flowing through producer /
|
|
80
|
+
aggregator / web — can now constrain literals via
|
|
81
|
+
`satisfies SeriesSchema` without adding `pond-ts` as a runtime
|
|
82
|
+
dependency. Surfaced by the gRPC experiment's `packages/shared`,
|
|
83
|
+
where `import { SeriesSchema } from 'pond-ts'` would have
|
|
84
|
+
pulled in the whole library for one type.
|
|
85
|
+
|
|
86
|
+
```ts
|
|
87
|
+
import type { SeriesSchema } from 'pond-ts/types';
|
|
88
|
+
export const schema = [
|
|
89
|
+
{ name: 'time', kind: 'time' },
|
|
90
|
+
{ name: 'cpu', kind: 'number' },
|
|
91
|
+
] as const satisfies SeriesSchema;
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
Existing `import { SeriesSchema } from 'pond-ts'` calls keep
|
|
95
|
+
working unchanged.
|
|
96
|
+
|
|
97
|
+
[0.11.3]: https://github.com/pjm17971/pond-ts/compare/v0.11.2...v0.11.3
|
|
98
|
+
|
|
99
|
+
## [0.11.2] — 2026-04-28
|
|
100
|
+
|
|
101
|
+
### Added
|
|
102
|
+
|
|
103
|
+
- `minSamples` option on `TimeSeries.rolling`,
|
|
104
|
+
`PartitionedTimeSeries.rolling`, `LiveRollingAggregation`, and
|
|
105
|
+
the `LivePartitionedSeries` rolling sugar — suppresses output
|
|
106
|
+
rows whose window contains fewer than the configured number of
|
|
107
|
+
source events. Forwarded to `TimeSeries.baseline` and
|
|
108
|
+
`TimeSeries.outliers` (and their per-partition variants), which
|
|
109
|
+
pass it to their internal rolling pass. Defaults to `0` (no
|
|
110
|
+
gate) so existing call sites are unaffected. Use it on noisy
|
|
111
|
+
rolling stats (e.g. the rolling stdev that feeds
|
|
112
|
+
`baseline()`'s ±σ bands) to hide the warm-up region where a
|
|
113
|
+
tiny-sample stdev would collapse the band tight enough to
|
|
114
|
+
false-flag normal events.
|
|
115
|
+
|
|
116
|
+
[0.11.2]: https://github.com/pjm17971/pond-ts/compare/v0.11.1...v0.11.2
|
|
117
|
+
|
|
118
|
+
## [0.11.1] — 2026-04-27
|
|
119
|
+
|
|
120
|
+
Closes a packaging footgun the dashboard agent surfaced while
|
|
121
|
+
upgrading from `pond-ts@0.10.1` to `pond-ts@0.11.0`.
|
|
122
|
+
|
|
123
|
+
When users had `@pond-ts/react@0.10.1` (which declared
|
|
124
|
+
`dependencies: { "pond-ts": "^0.10.0" }`) and bumped only
|
|
125
|
+
`pond-ts` to `0.11.0`, npm satisfied the react package's `^0.10.0`
|
|
126
|
+
range by nesting a _second_ copy of `pond-ts@0.10.1` under
|
|
127
|
+
`@pond-ts/react/node_modules/`. Two pond-ts copies meant two
|
|
128
|
+
distinct `Sequence` / `Time` / etc. classes with non-shared JS
|
|
129
|
+
private (`#`) brands. TypeScript surfaced this as
|
|
130
|
+
`Property '#private' refers to a different member`, which is
|
|
131
|
+
opaque without the package context.
|
|
132
|
+
|
|
133
|
+
### Changed
|
|
134
|
+
|
|
135
|
+
- **`@pond-ts/react`**: moved `pond-ts` from `dependencies` to
|
|
136
|
+
`peerDependencies` (range unchanged: `^0.11.0`). With peer-dep
|
|
137
|
+
semantics, npm refuses to install a duplicate `pond-ts`; instead
|
|
138
|
+
it warns at install time about peer-version mismatch — concrete,
|
|
139
|
+
actionable feedback rather than a runtime brand-check failure.
|
|
140
|
+
|
|
141
|
+
This is the standard pattern for packages that wrap another
|
|
142
|
+
library's classes (`react-dom` peer-deps `react`, etc.):
|
|
143
|
+
`@pond-ts/react`'s hooks return and operate on `pond-ts`
|
|
144
|
+
instances, so they MUST share class identity with the consumer's
|
|
145
|
+
`pond-ts`.
|
|
146
|
+
|
|
147
|
+
**Mild break:** consumers who installed only `@pond-ts/react`
|
|
148
|
+
and relied on the transitive `pond-ts` will now get an npm
|
|
149
|
+
warning and need to add `pond-ts` to their direct dependencies.
|
|
150
|
+
In practice anyone using `@pond-ts/react` is already importing
|
|
151
|
+
`pond-ts` types/classes, so the typical setup already has it
|
|
152
|
+
declared explicitly.
|
|
153
|
+
|
|
154
|
+
### Notes
|
|
155
|
+
|
|
156
|
+
- **Why caret (`^0.11.0`) and not exact pin?** Pre-1.0 caret
|
|
157
|
+
semver only accepts patches within the same minor (so
|
|
158
|
+
`^0.11.0` matches 0.11.x but not 0.12.0). That already
|
|
159
|
+
enforces minor-level lockstep — exact pinning would force
|
|
160
|
+
consumers to bump both packages for every patch, even when one
|
|
161
|
+
package's bump is a lockstep no-op.
|
|
162
|
+
|
|
163
|
+
[0.11.1]: https://github.com/pjm17971/pond-ts/compare/v0.11.0...v0.11.1
|
|
164
|
+
|
|
165
|
+
## [0.11.0] — 2026-04-27
|
|
166
|
+
|
|
167
|
+
The "live partitioning" release. Closes the cross-entity
|
|
168
|
+
correctness story end-to-end — the per-partition primitives we
|
|
169
|
+
shipped in v0.9.0 / v0.10.0 for batch now have a live counterpart
|
|
170
|
+
that handles ingestion, retention, grace, and stateful pipelines
|
|
171
|
+
on multi-host streams.
|
|
172
|
+
|
|
173
|
+
Without this, every multi-host live pipeline (rolling avg, fill,
|
|
174
|
+
diff, rate, cumulative, pctChange) silently mixes data across
|
|
175
|
+
entities — the same hazard the partitionBy work resolved for
|
|
176
|
+
batch, but live-side. Dashboard agent's v0.9.0 round-2 feedback
|
|
177
|
+
explicitly named "LivePartitionedSeries would be the obvious next
|
|
178
|
+
step" as the missing piece.
|
|
179
|
+
|
|
180
|
+
### Added
|
|
181
|
+
|
|
182
|
+
- **`liveSeries.partitionBy(col, options?)`** — returns
|
|
183
|
+
`LivePartitionedSeries<S, K>`, the live counterpart to
|
|
184
|
+
`PartitionedTimeSeries`. Routes events from a source
|
|
185
|
+
`LiveSource<S>` into per-partition `LiveSeries<S>` sub-buffers,
|
|
186
|
+
each with its own retention, grace window, and stateful
|
|
187
|
+
operator pipeline.
|
|
188
|
+
|
|
189
|
+
Per-partition semantics (settled in design):
|
|
190
|
+
- Retention applies per partition (a chatty host can't squeeze
|
|
191
|
+
a quiet one out of the buffer)
|
|
192
|
+
- Grace windows apply per partition (late events touch only
|
|
193
|
+
their own partition)
|
|
194
|
+
- Aggregation timing is per partition (one host's rolling avg
|
|
195
|
+
fires when that host has enough data)
|
|
196
|
+
- Auto-spawn on new partition values; optional `groups` for
|
|
197
|
+
typed declared partitions (mirrors batch typed-groups)
|
|
198
|
+
|
|
199
|
+
Terminals:
|
|
200
|
+
- `.toMap()` → `Map<K, LiveSource<S>>` for direct per-partition
|
|
201
|
+
subscription
|
|
202
|
+
- `.collect()` → unified `LiveSeries<S>` (append-only fan-in)
|
|
203
|
+
- `.apply(factory)` → unified `LiveSeries<R>` with per-
|
|
204
|
+
partition operator chains
|
|
205
|
+
- `.dispose()` cleans up source subscription, all per-partition
|
|
206
|
+
pipeline subscribers, and `toMap`-created factory chains
|
|
207
|
+
|
|
208
|
+
- **Typed chainable sugar** — `partitioned.fill(...).rolling(...).collect()`
|
|
209
|
+
matches the batch chainable view. Sugar coverage on both
|
|
210
|
+
`LivePartitionedSeries` and the chained `LivePartitionedView`:
|
|
211
|
+
`fill`, `diff`, `rate`, `pctChange`, `cumulative`, `rolling`.
|
|
212
|
+
|
|
213
|
+
```ts
|
|
214
|
+
const cpuSmoothed = live
|
|
215
|
+
.partitionBy('host')
|
|
216
|
+
.fill({ cpu: 'hold' })
|
|
217
|
+
.rolling('1m', { cpu: 'avg', host: 'last' })
|
|
218
|
+
.collect();
|
|
219
|
+
```
|
|
220
|
+
|
|
221
|
+
`LivePartitionedView<SBase, R, K>` is a lazy chain step holding
|
|
222
|
+
a composed factory; terminals delegate to the root partitioned
|
|
223
|
+
series. Auto-spawn flows through the chain — a new partition
|
|
224
|
+
triggers a fresh factory invocation.
|
|
225
|
+
|
|
226
|
+
- **`LivePartitionedView`** exported from package root.
|
|
227
|
+
|
|
228
|
+
- **`ARCHITECTURE.md`** at repo root — first-pass document for
|
|
229
|
+
contributors (human or AI) reading the codebase cold. Covers
|
|
230
|
+
layered model, stateful primitives, recurring patterns
|
|
231
|
+
(typed-groups, trusted construction via `static #foo`,
|
|
232
|
+
factory-based per-partition state, append-only fan-in vs
|
|
233
|
+
mirrored materialization, per-method JSDoc warnings, perf-
|
|
234
|
+
check discipline), decision log, and conventions.
|
|
235
|
+
|
|
236
|
+
### Changed
|
|
237
|
+
|
|
238
|
+
- **CLAUDE.md** points to `ARCHITECTURE.md` so future sessions
|
|
239
|
+
discover it alongside `PLAN.md`.
|
|
240
|
+
|
|
241
|
+
### Notes
|
|
242
|
+
|
|
243
|
+
- **Append-only fan-in semantics** for `collect()` and `apply()`
|
|
244
|
+
on `LivePartitionedSeries` — per-partition retention/grace
|
|
245
|
+
evictions do NOT propagate to the unified buffer. Documented
|
|
246
|
+
via JSDoc; the unified buffer's own retention is independent.
|
|
247
|
+
Use `toMap()` for current per-partition state.
|
|
248
|
+
|
|
249
|
+
- **Post-commit error semantics for partition rejection** — when
|
|
250
|
+
the partition view throws inside the source's event listener
|
|
251
|
+
(rogue value, partition ordering rejection), the source has
|
|
252
|
+
already committed the event. Documented in
|
|
253
|
+
`LiveSeries.partitionBy` JSDoc; recommend upstream input
|
|
254
|
+
validation if source/partition atomicity matters.
|
|
255
|
+
|
|
256
|
+
- **Rolling drops partition column unless explicitly added.**
|
|
257
|
+
`LiveSeries.rolling` (and the partitioned chain via it) only
|
|
258
|
+
retains columns named in `mapping` — include `host: 'last'` (or
|
|
259
|
+
similar) to keep the partition tag visible in the unified
|
|
260
|
+
output. Documented in `rolling`'s JSDoc on both the
|
|
261
|
+
`LivePartitionedSeries` and `LivePartitionedView` surfaces.
|
|
262
|
+
|
|
263
|
+
### Performance
|
|
264
|
+
|
|
265
|
+
- Routing overhead measured at ~88ms for 100k events × 10 hosts
|
|
266
|
+
(50ms over bare push). Apples-to-apples vs equivalent un-
|
|
267
|
+
partitioned operator chains: ~1.8-2.6× cost. Constant per
|
|
268
|
+
event (~0.8 µs); cardinality scales flat (Map lookup is O(1)).
|
|
269
|
+
See `scripts/perf-live-partitioned.mjs`.
|
|
270
|
+
|
|
271
|
+
- An `_acceptEvent` private-method optimization to bypass row
|
|
272
|
+
re-validation in partition routing was scoped and rejected for
|
|
273
|
+
v0.11 — the benefit (~0.3-0.4 µs/event saved) is marginal for
|
|
274
|
+
typical telemetry workloads (1-10k events/sec) and the cost
|
|
275
|
+
(validation-bypass primitive on the public API surface) wasn't
|
|
276
|
+
justified. May revisit if a high-throughput user surfaces the
|
|
277
|
+
bottleneck with real workload data.
|
|
278
|
+
|
|
279
|
+
[0.11.0]: https://github.com/pjm17971/pond-ts/compare/v0.10.1...v0.11.0
|
|
280
|
+
|
|
281
|
+
## [0.10.1] — 2026-04-27
|
|
282
|
+
|
|
283
|
+
Strictly additive over v0.10.0. Closes the export gap surfaced by
|
|
284
|
+
the Codex CSV-cleaner v0.10 retest:
|
|
285
|
+
|
|
286
|
+
> `MaterializeSchema` exists in `dist/types.d.ts` but is not
|
|
287
|
+
> exported from the package root, so the script had to spell out
|
|
288
|
+
> the materialized schema locally for strict typing.
|
|
289
|
+
|
|
290
|
+
### Added
|
|
291
|
+
|
|
292
|
+
- **`MaterializeSchema<S>`** now exported from the package root.
|
|
293
|
+
Users typing `materialize` output (or composing it into wrapper
|
|
294
|
+
utilities) can import the type directly from `pond-ts` instead
|
|
295
|
+
of digging into the dist-types.
|
|
296
|
+
- **`DedupeKeep<S>`** also exported (was the same gap — the type
|
|
297
|
+
for the `dedupe({ keep })` resolver function shape). Closes the
|
|
298
|
+
same friction for callers writing custom dedupe resolvers in
|
|
299
|
+
isolation.
|
|
300
|
+
|
|
301
|
+
[0.10.1]: https://github.com/pjm17971/pond-ts/compare/v0.10.0...v0.10.1
|
|
302
|
+
|
|
303
|
+
## [0.10.0] — 2026-04-27
|
|
304
|
+
|
|
305
|
+
The "round-2 dashboard agent feedback" release. After v0.9.0
|
|
306
|
+
shipped the cross-entity correctness wave, three independent
|
|
307
|
+
agents (Codex CSV-cleaner, fresh CSV-cleaner eval, dashboard
|
|
308
|
+
agent) flagged refinements. v0.10 delivers all three:
|
|
309
|
+
|
|
310
|
+
- A grid-completion primitive that doesn't pre-pick a fill method
|
|
311
|
+
(Codex's "regularize without filling" friction)
|
|
312
|
+
- A terminal `toMap` that materializes the partition view directly
|
|
313
|
+
to a Map keyed by partition value (dashboard agent's
|
|
314
|
+
`.collect().groupBy(col, fn)` chain pain)
|
|
315
|
+
- Typed partition declaration via `groups` for narrowed Map keys
|
|
316
|
+
and declared-order iteration (dashboard agent's third
|
|
317
|
+
refinement; mirrors `pivotByGroup({ groups })`)
|
|
318
|
+
|
|
319
|
+
Strictly additive over v0.9.x — no behavior changes for existing
|
|
320
|
+
callers.
|
|
321
|
+
|
|
322
|
+
### Added
|
|
323
|
+
|
|
324
|
+
- **`series.materialize(sequence, options?)`** — emits one
|
|
325
|
+
time-keyed row per sequence bucket, populating value columns
|
|
326
|
+
from a chosen source event in the bucket (or `undefined` for
|
|
327
|
+
empty buckets). Does only the grid step; pairs naturally with
|
|
328
|
+
`fill()` for explicit fill-policy control:
|
|
329
|
+
|
|
330
|
+
```ts
|
|
331
|
+
series
|
|
332
|
+
.partitionBy('host')
|
|
333
|
+
.materialize(Sequence.every('1m'))
|
|
334
|
+
.fill({ cpu: 'linear' }, { maxGap: '3m' })
|
|
335
|
+
.collect();
|
|
336
|
+
```
|
|
337
|
+
|
|
338
|
+
Three `select` modes: `'first'` / `'last'` (default) /
|
|
339
|
+
`'nearest'` — all bucket-bounded; empty buckets emit
|
|
340
|
+
`undefined` regardless. Three `sample` anchors:
|
|
341
|
+
`'begin'` (default) / `'center'` / `'end'`. Output schema
|
|
342
|
+
widens value columns to optional (`MaterializeSchema<S>`).
|
|
343
|
+
|
|
344
|
+
The `PartitionedTimeSeries.materialize` sugar auto-populates
|
|
345
|
+
the partition column on every output row, including
|
|
346
|
+
empty-bucket rows — without this, downstream code would need a
|
|
347
|
+
`.fill({ host: 'hold' })` step that fails for partitions where
|
|
348
|
+
every event sits in a long-outage gap.
|
|
349
|
+
|
|
350
|
+
Distinct from `align()` (which mandates a `'hold'` or
|
|
351
|
+
`'linear'` fill method and returns interval-keyed) and
|
|
352
|
+
`aggregate()` (which applies a per-column reducer). See
|
|
353
|
+
`cleaning.mdx` for the full operator-comparison table.
|
|
354
|
+
|
|
355
|
+
- **`PartitionedTimeSeries.toMap(transform?)`** — terminal that
|
|
356
|
+
returns `Map<key, TimeSeries<S>>` (or `Map<key, R>` with a
|
|
357
|
+
transform) directly from the partition view. Replaces the
|
|
358
|
+
`.collect().groupBy(col, fn)` chain dashboard code was using.
|
|
359
|
+
|
|
360
|
+
Three overloads cover the common shapes: bare per-partition
|
|
361
|
+
`TimeSeries`, transform that returns `TimeSeries<R>`, and
|
|
362
|
+
transform that returns arbitrary `R`. Map iteration order
|
|
363
|
+
matches the order each partition was first encountered in the
|
|
364
|
+
source events (or declared order when `groups` is set).
|
|
365
|
+
|
|
366
|
+
Map keys are stringified partition values for single-column
|
|
367
|
+
partitions (preserving the natural string representation:
|
|
368
|
+
`'api-1'`, `'eu'`, etc.), or JSON arrays for composite
|
|
369
|
+
partitions (`'["api-1","eu"]'`). `undefined` partition values
|
|
370
|
+
use the leading-space sentinel `' undefined'` to avoid
|
|
371
|
+
collision with the literal string `'undefined'` — distinct
|
|
372
|
+
from `groupBy`'s bare `'undefined'` key, which silently
|
|
373
|
+
collapses the two cases. Documented as an intentional
|
|
374
|
+
improvement; migrators changing from `.get('undefined')` to
|
|
375
|
+
`.get(' undefined')`.
|
|
376
|
+
|
|
377
|
+
**3.3× faster than the `.collect().groupBy(col, fn)` chain it
|
|
378
|
+
replaces** at 100k events × 10 hosts (33 ms vs 108 ms,
|
|
379
|
+
measured by `scripts/perf-partitioned-toMap.mjs`).
|
|
380
|
+
|
|
381
|
+
- **`series.partitionBy(col, { groups })` typed declaration**
|
|
382
|
+
— pre-declares the expected partition values, narrowing the
|
|
383
|
+
partition view's `K` type from `string` to the literal union.
|
|
384
|
+
Propagates through every sugar method's return type and through
|
|
385
|
+
`toMap`'s `Map` key:
|
|
386
|
+
|
|
387
|
+
```ts
|
|
388
|
+
const HOSTS = ['api-1', 'api-2', 'api-3'] as const;
|
|
389
|
+
const byHost = series
|
|
390
|
+
.partitionBy('host', { groups: HOSTS })
|
|
391
|
+
.fill({ cpu: 'linear' })
|
|
392
|
+
.toMap();
|
|
393
|
+
// byHost: Map<'api-1' | 'api-2' | 'api-3', TimeSeries<S>>
|
|
394
|
+
```
|
|
395
|
+
|
|
396
|
+
Mirrors `pivotByGroup({ groups })` — same design vocabulary,
|
|
397
|
+
same discipline: declared-order iteration, empty declared
|
|
398
|
+
groups produce empty entries, partition values not in `groups`
|
|
399
|
+
throw at construction time, empty `groups: []` and duplicate
|
|
400
|
+
values throw upfront, single-column only (composite + groups
|
|
401
|
+
throws). Numeric and boolean partition columns are stringified
|
|
402
|
+
by the encoder, so declared groups must be the stringified
|
|
403
|
+
form (`groups: ['1', '2']` for a numeric column).
|
|
404
|
+
|
|
405
|
+
- **Per-method `**Multi-entity series:**` JSDoc warnings**
|
|
406
|
+
remain on every stateful operator (shipped in v0.9.0); the
|
|
407
|
+
v0.10 operators (`materialize`, `toMap`) inherit the same
|
|
408
|
+
discoverability.
|
|
409
|
+
|
|
410
|
+
### Changed
|
|
411
|
+
|
|
412
|
+
- **CLAUDE.md adds a perf-check policy.** New operators that walk
|
|
413
|
+
events, allocate per-event, or scale with input dimensions
|
|
414
|
+
must have an analytical complexity statement, a benchmark
|
|
415
|
+
script (`packages/core/scripts/perf-<operator>.mjs`), and
|
|
416
|
+
before/after numbers in the commit message. Surfaces in the
|
|
417
|
+
Layer 1 self-review checklist. Every v0.10 PR followed this:
|
|
418
|
+
`materialize` got `perf-materialize.mjs` (and two optimization
|
|
419
|
+
passes that landed –41% on the partitioned variant);
|
|
420
|
+
`toMap` got `perf-partitioned-toMap.mjs` (3.3× speedup
|
|
421
|
+
measurement); typed `groups` got `perf-partitionby-groups.mjs`
|
|
422
|
+
(zero chain-step regression via the class-private trusted
|
|
423
|
+
factory).
|
|
424
|
+
|
|
425
|
+
[0.10.0]: https://github.com/pjm17971/pond-ts/compare/v0.9.1...v0.10.0
|
|
426
|
+
|
|
427
|
+
## [0.9.1] — 2026-04-26
|
|
428
|
+
|
|
429
|
+
Strictly additive over v0.9.0. Closes a sugar-method type bug
|
|
430
|
+
identified independently by two agents (a fresh CSV-cleaner eval
|
|
431
|
+
against v0.9.0 and Codex on a v0.9.0 retest), plus folds in two
|
|
432
|
+
fresh-agent doc improvements.
|
|
433
|
+
|
|
434
|
+
### Fixed
|
|
435
|
+
|
|
436
|
+
- **`PartitionedTimeSeries.fill` now accepts `maxGap`.** PR #78
|
|
437
|
+
added `maxGap` to `TimeSeries.fill` for v0.9.0 but the partitioned
|
|
438
|
+
sugar's option type was not widened, so the headline v0.9.0 chain —
|
|
439
|
+
`partitionBy('host').fill('linear', { maxGap: '5m' })` — failed
|
|
440
|
+
type checking and forced callers into `.apply()`. The underlying
|
|
441
|
+
impl already passed options through, so this is a one-line type
|
|
442
|
+
widening: `{ limit?: number; maxGap?: DurationInput }`.
|
|
443
|
+
|
|
444
|
+
### Added
|
|
445
|
+
|
|
446
|
+
- **9 new tests** under `TimeSeries.partitionBy.test.ts`:
|
|
447
|
+
- 4 regression tests pinning the partitioned `fill(maxGap)` chain
|
|
448
|
+
works (bare `maxGap`, all-or-nothing per-partition span,
|
|
449
|
+
`limit + maxGap` composition, full `partitionBy + dedupe +
|
|
450
|
+
fill(maxGap)` chain).
|
|
451
|
+
- 5 composite-key round-trip tests addressing a refinement flagged
|
|
452
|
+
by the dashboard agent: `partitionBy(['host', 'region'])`
|
|
453
|
+
preserves both key columns in the schema, on every output event,
|
|
454
|
+
keeps `(host, region)` tuples distinct (no collapse on host
|
|
455
|
+
alone), and round-trips through `apply()` and the full chain.
|
|
456
|
+
- **`cleaning.mdx` "Schema first — `required: false`" section.**
|
|
457
|
+
Leads the page; documents why optional cells need the flag and
|
|
458
|
+
surfaces the `fromJSON`/`null` workaround for the known
|
|
459
|
+
`RowForSchema` variance limitation. Previously this prose only
|
|
460
|
+
lived in the 0.8.2 changelog (fresh-agent feedback).
|
|
461
|
+
- **`cleaning.mdx` "End-to-end multi-entity cleaning pipeline"
|
|
462
|
+
section.** The unified `partitionBy + dedupe + fill(maxGap)`
|
|
463
|
+
chain in one place plus a step-by-step hazard table.
|
|
464
|
+
Previously split across three sections (fresh-agent feedback).
|
|
465
|
+
|
|
466
|
+
[0.9.1]: https://github.com/pjm17971/pond-ts/compare/v0.9.0...v0.9.1
|
|
467
|
+
|
|
468
|
+
## [0.9.0] — 2026-04-26
|
|
469
|
+
|
|
470
|
+
The "cross-entity correctness + cleaning hygiene" release. Three
|
|
471
|
+
independent CSV-cleaner agent runs (Codex, Claude, Gemini) all hit
|
|
472
|
+
the same shape: stateful transforms (`fill('linear')`, `rolling`,
|
|
473
|
+
`diff`, etc.) silently mix data across entities on multi-host
|
|
474
|
+
series, and `fill('linear', { limit: 3 })` fabricates interpolated
|
|
475
|
+
data across long outages instead of leaving the unknown unknown.
|
|
476
|
+
|
|
477
|
+
v0.9.0 ships three operator-level fixes plus a discoverability pass
|
|
478
|
+
on every affected method's JSDoc.
|
|
479
|
+
|
|
480
|
+
### Added
|
|
481
|
+
|
|
482
|
+
- **`series.partitionBy(col).<op>(...).collect()`** — chainable
|
|
483
|
+
per-partition view over `TimeSeries`. Sugar methods for every
|
|
484
|
+
stateful operator (`fill`, `align`, `rolling`, `smooth`,
|
|
485
|
+
`baseline`, `outliers`, `diff`, `rate`, `pctChange`, `cumulative`,
|
|
486
|
+
`shift`, `aggregate`, `dedupe`) run the underlying transform per
|
|
487
|
+
partition. `.collect()` materializes back to `TimeSeries<S>`.
|
|
488
|
+
`.apply(g => /* arbitrary chain */)` is the terminal escape hatch.
|
|
489
|
+
One primitive covers the cross-entity hazard for every at-risk
|
|
490
|
+
method, instead of adding a `partitionBy` option to each.
|
|
491
|
+
- **`series.dedupe({ keep })`** — first-class deduplication with
|
|
492
|
+
policies: `'first' | 'last' | 'error' | 'drop' | { min: col } |
|
|
493
|
+
{ max: col } | (events) => Event`. Default key is the full event
|
|
494
|
+
key (`begin` for time-keyed, `begin+end` for time-range,
|
|
495
|
+
`begin+end+value` for interval-keyed); default resolution is
|
|
496
|
+
`'last'`. `partitionBy('host').dedupe()` is the multi-entity
|
|
497
|
+
pattern.
|
|
498
|
+
- **`fill(strategy, { maxGap })`** — duration-based gap cap,
|
|
499
|
+
complements the existing count-based `limit`. Both compose; most
|
|
500
|
+
restrictive wins.
|
|
501
|
+
|
|
502
|
+
### Changed
|
|
503
|
+
|
|
504
|
+
- **`fill` is now all-or-nothing.** A gap either fits both caps and
|
|
505
|
+
is filled entirely, or exceeds either cap and is left fully
|
|
506
|
+
unfilled. Previously `limit: 3` on a 5-cell gap filled 3 cells and
|
|
507
|
+
left 2 unfilled — propagating stale `'hold'` values past their
|
|
508
|
+
useful lifetime and inventing misleading `'linear'` slopes across
|
|
509
|
+
long outages. Existing `limit` callers see strictly more
|
|
510
|
+
conservative behavior; to opt back in to partial fill, set
|
|
511
|
+
`limit`/`maxGap` larger than any gap you want filled.
|
|
512
|
+
- **Every stateful TimeSeries method's JSDoc** now includes a
|
|
513
|
+
`**Multi-entity series:**` warning paragraph naming the operator's
|
|
514
|
+
specific cross-entity hazard and pointing at the
|
|
515
|
+
`partitionBy(col).<method>(...).collect()` pattern. Discoverable
|
|
516
|
+
in LSP hover, IDE quick-help, and any tool that reads type
|
|
517
|
+
definitions.
|
|
518
|
+
- **`PartitionedTimeSeries` view** preserves partition state across
|
|
519
|
+
every sugar call, so multi-step per-partition chains compose
|
|
520
|
+
cleanly without re-partitioning at each step.
|
|
521
|
+
|
|
522
|
+
### Fixed
|
|
523
|
+
|
|
524
|
+
- Pre-existing brand-check bug on `series.filter(...).diff(...)`
|
|
525
|
+
and similar chains: events constructed via
|
|
526
|
+
`#fromTrustedEvents` (which uses `Object.create` to bypass the
|
|
527
|
+
constructor) hit a JS-private brand check on `#diffOrRate` and
|
|
528
|
+
threw. Refactored to a class-static private (`static
|
|
529
|
+
#diffOrRate`) — runtime-private without the per-instance brand
|
|
530
|
+
failure.
|
|
531
|
+
|
|
532
|
+
[0.9.0]: https://github.com/pjm17971/pond-ts/compare/v0.8.2...v0.9.0
|
|
533
|
+
|
|
534
|
+
## [0.8.2] — 2026-04-26
|
|
535
|
+
|
|
536
|
+
Strictly additive over v0.8.1. Closes friction surfaced by two
|
|
537
|
+
independent agent runs against a realistic CSV-cleaning task —
|
|
538
|
+
specifically, the missing fan-in primitive that forces callers out
|
|
539
|
+
of the typed contract when reassembling per-host transformed
|
|
540
|
+
subseries.
|
|
541
|
+
|
|
542
|
+
### Added
|
|
543
|
+
|
|
544
|
+
- **`TimeSeries.concat([s1, s2, ...])`** — concatenates the events of
|
|
545
|
+
N same-schema `TimeSeries` instances, re-sorted by key. The
|
|
546
|
+
row-append / vertical-stack counterpart to `joinMany` (which
|
|
547
|
+
column-merges by key). Matches `Array.prototype.concat` /
|
|
548
|
+
`pandas.concat(axis=0)` / SQL `UNION ALL` semantics. Closes the
|
|
549
|
+
round-trip after `groupBy(col, fn)` + per-group transforms without
|
|
550
|
+
forcing callers to unwrap events back to row tuples.
|
|
551
|
+
|
|
552
|
+
```ts
|
|
553
|
+
const filledByHost = series.groupBy('host', (g) =>
|
|
554
|
+
g.fill({ cpu: 'linear' }, { limit: 2 }),
|
|
555
|
+
);
|
|
556
|
+
const combined = TimeSeries.concat([...filledByHost.values()]);
|
|
557
|
+
// back to one TimeSeries<S>; events from all hosts re-sorted.
|
|
558
|
+
```
|
|
559
|
+
|
|
560
|
+
Schemas must match column-by-column on `name` and `kind`; throws
|
|
561
|
+
upfront on mismatch. Same-key events from different inputs are
|
|
562
|
+
both kept (row-append, not key-dedupe).
|
|
563
|
+
|
|
564
|
+
Coming from pondjs: `timeSeriesListMerge`'s concatenation case
|
|
565
|
+
maps to `TimeSeries.concat([...])`; its column-union case maps to
|
|
566
|
+
`TimeSeries.joinMany([...])`.
|
|
567
|
+
|
|
568
|
+
- **`TimeSeries.fromEvents(events, { schema, name })`** — builds a
|
|
569
|
+
typed series from a flat `Event[]` array. Sorts by key. Companion
|
|
570
|
+
to `merge` for the case where you have raw events rather than a
|
|
571
|
+
list of series.
|
|
572
|
+
|
|
573
|
+
- **`TimeRange.toJSON()`** returns `{ start: number, end: number }`,
|
|
574
|
+
the same shape `JsonTimeRangeInput` accepts, so
|
|
575
|
+
`new TimeRange(range.toJSON())` round-trips. Implicitly invoked by
|
|
576
|
+
`JSON.stringify(range)`.
|
|
577
|
+
|
|
578
|
+
- **`TimeRange.toString()`** returns ISO-8601 `start/end` format
|
|
579
|
+
(e.g. `2025-01-15T09:00:00.000Z/2025-01-15T10:00:00.000Z`) for
|
|
580
|
+
debug logs and human-readable display.
|
|
581
|
+
|
|
582
|
+
### Known limitation
|
|
583
|
+
|
|
584
|
+
Two type-level fixes flagged by the agents are tracked but deferred
|
|
585
|
+
to a future variance refactor:
|
|
586
|
+
|
|
587
|
+
- `toJSON()` returns `TimeSeriesJsonInput<SeriesSchema>` (loose),
|
|
588
|
+
not `TimeSeriesJsonInput<S>`. Cast the result at the call site
|
|
589
|
+
if you need the narrow schema preserved.
|
|
590
|
+
- `RowForSchema` doesn't honor `required: false`. Use `fromJSON`
|
|
591
|
+
with `null` cells instead of the row-array constructor with
|
|
592
|
+
`undefined`.
|
|
593
|
+
|
|
594
|
+
Both are real but blocked by class-wide invariance through method
|
|
595
|
+
overloads. See PLAN.md "Known type-level limitation" for the full
|
|
596
|
+
story.
|
|
597
|
+
|
|
598
|
+
## [0.8.1] — 2026-04-26
|
|
599
|
+
|
|
600
|
+
Strictly additive over v0.8.0 — typed overload narrows result types when
|
|
601
|
+
opted in via `groups`; untyped form is unchanged. Plus a docs reorg.
|
|
602
|
+
|
|
603
|
+
### Added
|
|
604
|
+
|
|
605
|
+
- **`pivotByGroup` typed overload** — pass `{ groups: [...] as const }`
|
|
606
|
+
and the output schema becomes literal-typed, so downstream
|
|
607
|
+
`baseline` / `rolling` / `toPoints` calls narrow without `as never`
|
|
608
|
+
casts. Eliminates the dashboard friction reported on v0.8.0.
|
|
609
|
+
|
|
610
|
+
```ts
|
|
611
|
+
const HOSTS = ['api-1', 'api-2'] as const;
|
|
612
|
+
const wide = long.pivotByGroup('host', 'cpu', { groups: HOSTS });
|
|
613
|
+
// wide.schema is now literal-typed:
|
|
614
|
+
// [time, { name: 'api-1_cpu', kind: 'number', required: false },
|
|
615
|
+
// { name: 'api-2_cpu', kind: 'number', required: false }]
|
|
616
|
+
wide.baseline('api-1_cpu', { window: '1m', sigma: 2 }); // no cast
|
|
617
|
+
```
|
|
618
|
+
|
|
619
|
+
Behavior in the typed path: declaration order (not alphabetical),
|
|
620
|
+
declared-but-empty groups still emit columns, runtime values not
|
|
621
|
+
in the declared set throw upfront. Untyped form (no `groups`)
|
|
622
|
+
keeps existing alphabetical / dynamic-discovery / loose-output
|
|
623
|
+
behavior.
|
|
624
|
+
|
|
625
|
+
### Changed
|
|
626
|
+
|
|
627
|
+
- **Docs site reorganized.** `Transforms` → **TimeSeries**;
|
|
628
|
+
`Live` → **LiveSeries**; new **Advanced** section for charting and
|
|
629
|
+
array columns. Concepts moves to `Start here`. New **Reshaping**
|
|
630
|
+
page splits `pivotByGroup` / `groupBy` / `join` / `joinMany` from
|
|
631
|
+
Aggregation, plus a new **Queries** page covering `at` / `first` /
|
|
632
|
+
`timeRange` / `includesKey` / `intersection` / iterators / output
|
|
633
|
+
forms — everything that interrogates a series rather than
|
|
634
|
+
transforming it. JSON ingest renamed to **Ingest** and slotted as
|
|
635
|
+
the first page under TimeSeries.
|
|
636
|
+
|
|
637
|
+
## [0.8.0] — 2026-04-25
|
|
638
|
+
|
|
639
|
+
### Added
|
|
640
|
+
|
|
641
|
+
- **`TimeSeries.pivotByGroup(groupCol, valueCol, options?)`** — long-to-wide
|
|
642
|
+
reshape on a categorical column. Each distinct value of `groupCol` becomes
|
|
643
|
+
its own column in the output schema named `${group}_${value}`, holding the
|
|
644
|
+
value column at that timestamp. Rows sharing a timestamp collapse into one
|
|
645
|
+
output row; missing `(timestamp, group)` cells are `undefined`.
|
|
646
|
+
|
|
647
|
+
```ts
|
|
648
|
+
// Long: { ts, cpu, host } per row
|
|
649
|
+
// Wide: { ts, "api-1_cpu", "api-2_cpu", ... } per row
|
|
650
|
+
long.pivotByGroup('host', 'cpu').toPoints();
|
|
651
|
+
// Drops straight into <Line dataKey="api-1_cpu" /> etc.
|
|
652
|
+
```
|
|
653
|
+
|
|
654
|
+
Duplicate `(timestamp, group)` pairs throw by default; opt-in
|
|
655
|
+
`{ aggregate: 'avg' | 'sum' | 'first' | 'last' | 'min' | 'max' | 'median' | 'p95' | ... }`
|
|
656
|
+
to combine. The aggregator's output kind must match the value column's
|
|
657
|
+
kind — `count`, `unique`, `topN` and other kind-changing reducers are
|
|
658
|
+
rejected upfront with a clear error. Output schema is dynamic so the
|
|
659
|
+
return type is `TimeSeries<SeriesSchema>` (loosely typed). Time-keyed
|
|
660
|
+
input required.
|
|
661
|
+
|
|
662
|
+
Use `pivotByGroup` for the per-group dashboard case ("one source, many
|
|
663
|
+
producers, one chart line per producer"). Use `groupBy + joinMany` when
|
|
664
|
+
each group spawns multiple derived columns (e.g. per-host baseline →
|
|
665
|
+
cpu/avg/upper/lower per host). At 200k events × 100 groups, runs in
|
|
666
|
+
~43 ms — at parity with hand-rolled JS that skips `TimeSeries`
|
|
667
|
+
construction entirely.
|
|
668
|
+
|
|
669
|
+
### Changed
|
|
670
|
+
|
|
671
|
+
- Charting docs lead with `series.join(other, ...).toPoints()` for
|
|
672
|
+
cross-source overlays. The manual `mergeWideRows` recipe is demoted to
|
|
673
|
+
"non-`TimeSeries` inputs". A new "Per-group wide rows" section covers
|
|
674
|
+
`pivotByGroup` end-to-end with Recharts.
|
|
675
|
+
|
|
676
|
+
### Notes
|
|
677
|
+
|
|
678
|
+
- **Live counterpart deferred.** No `LiveSeries.pivotByGroup` /
|
|
679
|
+
`LiveSeries.merge` / `LiveSeries.join` yet — see PLAN.md "Known scope
|
|
680
|
+
gap: live merge / join". Snapshot-then-batch is the workaround:
|
|
681
|
+
`useSnapshot` per source + `useMemo` running a batch `pivotByGroup` or
|
|
682
|
+
`join`.
|
|
683
|
+
|
|
684
|
+
## [0.7.0] — 2026-04-25
|
|
685
|
+
|
|
686
|
+
### Changed (breaking)
|
|
687
|
+
|
|
688
|
+
- **`TimeSeries.toPoints()` returns wide rows** instead of single-column
|
|
689
|
+
`{ ts, value }[]`. Every event becomes one row with `ts` plus every
|
|
690
|
+
value column from the schema as a top-level key:
|
|
691
|
+
|
|
692
|
+
```ts
|
|
693
|
+
// Before: // After:
|
|
694
|
+
series.toPoints('cpu');
|
|
695
|
+
series.toPoints();
|
|
696
|
+
// [{ ts, value }, ...] // [{ ts, cpu, host, ... }, ...]
|
|
697
|
+
```
|
|
698
|
+
|
|
699
|
+
This aligns pond-ts's multi-column nature with what every chart
|
|
700
|
+
library actually wants (Recharts, Observable Plot, visx all consume
|
|
701
|
+
wide rows directly). Band charts, multi-series overlays, and
|
|
702
|
+
`<Area>` ranged-`dataKey` patterns become a single `toPoints()`
|
|
703
|
+
call instead of a manual merge.
|
|
704
|
+
|
|
705
|
+
**Migration:** for the common single-column case, compose with
|
|
706
|
+
`select`:
|
|
707
|
+
|
|
708
|
+
```ts
|
|
709
|
+
const cpuPoints = series.select('cpu').toPoints();
|
|
710
|
+
// [{ ts, cpu }, ...]
|
|
711
|
+
```
|
|
712
|
+
|
|
713
|
+
Then read the column by name (`row.cpu`) instead of the old
|
|
714
|
+
`.value`. Wide form keeps every event — the old narrow form
|
|
715
|
+
dropped events whose column was `undefined`; the new form preserves
|
|
716
|
+
them so chart libraries can render gaps via `connectNulls={false}`.
|
|
717
|
+
|
|
718
|
+
**Watch out for `value`-named columns.** If your schema has a value
|
|
719
|
+
column literally named `value`, the new wide rows will have a
|
|
720
|
+
`value` key that looks identical to the old narrow shape — but it's
|
|
721
|
+
the column-named-`value`, not the narrow-form `value`. Audit any
|
|
722
|
+
`row.value` reads after upgrading; the safe migration is
|
|
723
|
+
`row.<schema-column-name>`.
|
|
724
|
+
|
|
725
|
+
- **`TimeSeries.fromPoints()` accepts wide-row points** with a schema
|
|
726
|
+
of any number of value columns. Schema's first column must still be
|
|
727
|
+
`kind: 'time'`.
|
|
728
|
+
|
|
729
|
+
```ts
|
|
730
|
+
TimeSeries.fromPoints(
|
|
731
|
+
[{ ts: 0, cpu: 0.3, host: 'api-1' }, ...],
|
|
732
|
+
{
|
|
733
|
+
schema: [
|
|
734
|
+
{ name: 'time', kind: 'time' },
|
|
735
|
+
{ name: 'cpu', kind: 'number' },
|
|
736
|
+
{ name: 'host', kind: 'string' },
|
|
737
|
+
] as const,
|
|
738
|
+
},
|
|
739
|
+
);
|
|
740
|
+
```
|
|
741
|
+
|
|
742
|
+
Previously restricted to exactly two columns with `{ ts, value }`
|
|
743
|
+
rows; that form is gone.
|
|
744
|
+
|
|
745
|
+
## [0.6.0] — 2026-04-25
|
|
746
|
+
|
|
747
|
+
### Added
|
|
748
|
+
|
|
749
|
+
- **`'end'` sample option** for `align()` and `Sequence.bounded()`. Joins
|
|
750
|
+
`'begin'` and `'center'` as a third anchor inside each grid step.
|
|
751
|
+
Useful for end-of-period readings (close-of-day, last value before
|
|
752
|
+
bucket close). Inclusion semantics are left-exclusive
|
|
753
|
+
(`sample ∈ (range.begin, range.end]`) so an end-sample at exactly
|
|
754
|
+
`range.begin()` doesn't pull in an interval that sits entirely
|
|
755
|
+
before the range.
|
|
756
|
+
|
|
757
|
+
### Type-surface change
|
|
758
|
+
|
|
759
|
+
- `AlignSample` and `SequenceSample` literal unions widen from
|
|
760
|
+
`'begin' | 'center'` to `'begin' | 'center' | 'end'`. Pattern-matching
|
|
761
|
+
consumers that exhaustively `switch` on the old two-value union
|
|
762
|
+
silently miss the new arm — minor bump rather than a patch per this
|
|
763
|
+
project's "patch bumps are strictly additive" rule. Update any
|
|
764
|
+
`switch (sample)` blocks to handle `'end'` (or add a `default`).
|
|
765
|
+
|
|
766
|
+
## [0.5.11] — 2026-04-24
|
|
767
|
+
|
|
768
|
+
### Fixed
|
|
769
|
+
|
|
770
|
+
- **`LiveSeries` rejects `graceWindow > retention.maxAge` at construction.**
|
|
771
|
+
A late event accepted within grace but older than `maxAge` would be evicted
|
|
772
|
+
immediately by retention — the grace contract would be meaningless. The
|
|
773
|
+
guard only fires when both options are set explicitly; default behavior is
|
|
774
|
+
unchanged. `LiveAggregation` bucket closure (which inherits grace from the
|
|
775
|
+
source) still behaves as before.
|
|
776
|
+
|
|
777
|
+
### Changed
|
|
778
|
+
|
|
779
|
+
- Docs: clarified `graceWindow`'s scope in the `LiveSeriesOptions`
|
|
780
|
+
docstring. Enforced at ingest and honored by `LiveAggregation` bucket
|
|
781
|
+
closure; `rolling()` / `window()` live views do not re-flow late events
|
|
782
|
+
through historical windows. Matches the actual pipeline behavior; full
|
|
783
|
+
late-event propagation through live transforms is explicitly out of
|
|
784
|
+
scope (see Akidau's Streaming 102 for the larger story).
|
|
785
|
+
|
|
786
|
+
## [0.5.10] — 2026-04-24
|
|
787
|
+
|
|
788
|
+
### Fixed
|
|
789
|
+
|
|
790
|
+
- **`baseline()` emits `undefined` for `upper` / `lower` when the rolling
|
|
791
|
+
window is flat (`sd === 0`)** — matching `outliers()`'s behavior. Before,
|
|
792
|
+
a zero-width band would cause a naive `value > upper || value < lower`
|
|
793
|
+
filter to flag every non-equal point as anomalous inside a constant run.
|
|
794
|
+
The `avg` and `sd` columns still report their true values; only the band
|
|
795
|
+
edges collapse to `undefined`.
|
|
796
|
+
|
|
797
|
+
### Changed
|
|
798
|
+
|
|
799
|
+
- Internal: consolidated a duplicate `OptionalNumberCol` type alias into
|
|
800
|
+
the pre-existing `OptionalNumberColumn`. No surface change.
|
|
801
|
+
- Docs: walked back an over-claim in `outliers()`'s docstring. It was
|
|
802
|
+
documented as "sugar over `baseline().filter()`" but is implemented
|
|
803
|
+
independently. Now says the two are conceptually equivalent.
|
|
804
|
+
|
|
805
|
+
## [0.5.9] — 2026-04-23
|
|
806
|
+
|
|
807
|
+
### Added
|
|
808
|
+
|
|
809
|
+
- **`TimeSeries.baseline(col, opts)`** — rolling-stats primitive. Runs one
|
|
810
|
+
rolling pass and appends four optional number columns (`avg`, `sd`,
|
|
811
|
+
`upper = avg + σ·sd`, `lower = avg - σ·sd`) to the source schema. Band
|
|
812
|
+
charts read `toPoints('upper')` / `toPoints('lower')` directly; outlier
|
|
813
|
+
filters compare against `upper` / `lower`. Replaces the band-plus-outliers
|
|
814
|
+
two-pass pattern with one call. Custom column names via `{ names }` if the
|
|
815
|
+
defaults collide.
|
|
816
|
+
|
|
817
|
+
[0.8.2]: https://github.com/pjm17971/pond-ts/compare/v0.8.1...v0.8.2
|
|
818
|
+
[0.8.1]: https://github.com/pjm17971/pond-ts/compare/v0.8.0...v0.8.1
|
|
819
|
+
[0.8.0]: https://github.com/pjm17971/pond-ts/compare/v0.7.0...v0.8.0
|
|
820
|
+
[0.7.0]: https://github.com/pjm17971/pond-ts/compare/v0.6.0...v0.7.0
|
|
821
|
+
[0.6.0]: https://github.com/pjm17971/pond-ts/compare/v0.5.11...v0.6.0
|
|
822
|
+
[0.5.11]: https://github.com/pjm17971/pond-ts/compare/v0.5.10...v0.5.11
|
|
823
|
+
[0.5.10]: https://github.com/pjm17971/pond-ts/compare/v0.5.9...v0.5.10
|
|
824
|
+
[0.5.9]: https://github.com/pjm17971/pond-ts/compare/v0.5.8...v0.5.9
|
|
825
|
+
|
|
826
|
+
## [0.5.8] — 2026-04-23
|
|
827
|
+
|
|
828
|
+
### Added
|
|
829
|
+
|
|
830
|
+
- **`TimeSeries.outliers(col, { window, sigma, alignment? })`** —
|
|
831
|
+
rolling-baseline outlier detection. Returns `TimeSeries<S>` filtered to
|
|
832
|
+
events whose value deviates from the trailing rolling average by more than
|
|
833
|
+
`sigma · rolling_stdev`. Composes directly with aggregate, groupBy, etc.
|
|
834
|
+
- **`TimeSeries.prototype.toPoints(col)`** — flat `{ ts, value }[]` export
|
|
835
|
+
matching conventional chart-library shape (Recharts, Observable Plot, d3).
|
|
836
|
+
Filters `undefined` values; returns a frozen array.
|
|
837
|
+
- **`TimeSeries.fromPoints(points, { schema, name? })`** — inverse
|
|
838
|
+
constructor for round-tripping chart-style points back into pond-native
|
|
839
|
+
operations. Schema must have exactly two columns.
|
|
840
|
+
|
|
841
|
+
[0.5.8]: https://github.com/pjm17971/pond-ts/compare/v0.5.7...v0.5.8
|
|
842
|
+
|
|
843
|
+
## [0.5.7] — 2026-04-23
|
|
844
|
+
|
|
845
|
+
### Added
|
|
846
|
+
|
|
847
|
+
- **`smooth('ema', { warmup: N })`** — drops the first `N` output rows so
|
|
848
|
+
callers don't have to write `.slice(N)` after every EMA call. The smoother
|
|
849
|
+
still processes those events, so kept rows are computed against a warm EMA.
|
|
850
|
+
`warmup: 0` is a no-op; warmup ≥ series length returns an empty series.
|
|
851
|
+
|
|
852
|
+
[0.5.7]: https://github.com/pjm17971/pond-ts/compare/v0.5.6...v0.5.7
|
|
853
|
+
|
|
854
|
+
## [0.5.6] — 2026-04-23
|
|
855
|
+
|
|
856
|
+
### Added
|
|
857
|
+
|
|
858
|
+
- **`useCurrent` reference stability** — the returned record and each of its
|
|
859
|
+
fields are reference-stable across renders when structurally unchanged. A
|
|
860
|
+
no-op push (same aggregate values) hands back the previous references;
|
|
861
|
+
downstream `useMemo([current.host], ...)` only re-runs when that specific
|
|
862
|
+
field changes. Scalar fields compare via `===`; array fields compare length
|
|
863
|
+
then elementwise.
|
|
864
|
+
|
|
865
|
+
[0.5.6]: https://github.com/pjm17971/pond-ts/compare/v0.5.5...v0.5.6
|
|
866
|
+
|
|
867
|
+
## [0.5.5] — 2026-04-23
|
|
868
|
+
|
|
869
|
+
### Added
|
|
870
|
+
|
|
871
|
+
- **Narrow return types for `rolling` + `aggregate` output-map overloads.**
|
|
872
|
+
`rolling(w, { avg: { from: 'cpu', using: 'avg' }, ... })` now returns
|
|
873
|
+
`TimeSeries<RollingOutputMapSchema<S, M>>` — `e.get('avg')` narrows to
|
|
874
|
+
`number | undefined` instead of `ColumnValue | undefined`, and `e.key()`
|
|
875
|
+
preserves the source's first-column kind. Same fix on `aggregate`'s
|
|
876
|
+
output-map overload.
|
|
877
|
+
|
|
878
|
+
### Fixed
|
|
879
|
+
|
|
880
|
+
- `min` / `max` were missing from the numeric-reducer list in `ReduceResult`
|
|
881
|
+
(v0.5.2 regression). Both reducers have `outputKind: 'number'` at runtime;
|
|
882
|
+
the type now agrees. `reduce({ cpu: 'max' })` narrows to `number | undefined`.
|
|
883
|
+
|
|
884
|
+
[0.5.5]: https://github.com/pjm17971/pond-ts/compare/v0.5.4...v0.5.5
|
|
885
|
+
|
|
886
|
+
## [0.5.4] — 2026-04-23
|
|
887
|
+
|
|
888
|
+
### Added
|
|
889
|
+
|
|
890
|
+
- **`rolling` accepts `AggregateOutputMap`** — feature parity with
|
|
891
|
+
`aggregate`. Multi-reducer-per-column now works in one pass:
|
|
892
|
+
```ts
|
|
893
|
+
series.rolling('1m', {
|
|
894
|
+
avg: { from: 'cpu', using: 'avg' },
|
|
895
|
+
sd: { from: 'cpu', using: 'stdev' },
|
|
896
|
+
});
|
|
897
|
+
```
|
|
898
|
+
Two new overloads on both window-only and sequence-driven forms.
|
|
899
|
+
|
|
900
|
+
### Changed
|
|
901
|
+
|
|
902
|
+
- `rolling`'s internal column walker now routes through the shared
|
|
903
|
+
`normalizeAggregateColumns` helper. Schema-column order is preserved for
|
|
904
|
+
`AggregateMap` inputs so the runtime layout continues to match
|
|
905
|
+
`RollingSchema<S, M>`.
|
|
906
|
+
|
|
907
|
+
[0.5.4]: https://github.com/pjm17971/pond-ts/compare/v0.5.3...v0.5.4
|
|
908
|
+
|
|
909
|
+
## [0.5.3] — 2026-04-23
|
|
910
|
+
|
|
911
|
+
### Added
|
|
912
|
+
|
|
913
|
+
- **Source-kind narrowing on array-output reducers in `ReduceResult`.**
|
|
914
|
+
`unique` and `` `top${number}` `` now narrow their output to
|
|
915
|
+
`ReadonlyArray<T>` where `T` is the source column's element type:
|
|
916
|
+
```ts
|
|
917
|
+
series.reduce({ host: 'unique' }).host;
|
|
918
|
+
// ^ ReadonlyArray<string> | undefined (was ReadonlyArray<ScalarValue>)
|
|
919
|
+
```
|
|
920
|
+
Array-kind source columns fall back to the wide `ReadonlyArray<ScalarValue>`
|
|
921
|
+
union since element kind isn't schema-visible.
|
|
922
|
+
|
|
923
|
+
[0.5.3]: https://github.com/pjm17971/pond-ts/compare/v0.5.2...v0.5.3
|
|
924
|
+
|
|
925
|
+
## [0.5.2] — 2026-04-23
|
|
926
|
+
|
|
927
|
+
### Added
|
|
928
|
+
|
|
929
|
+
- **`TimeSeries.reduce` per-entry type narrowing.** Numeric reducers
|
|
930
|
+
(`sum`/`avg`/`count`/`median`/`stdev`/`difference`/`pNN`) narrow to
|
|
931
|
+
`number | undefined`; `unique`/`top${N}` narrow to `ReadonlyArray<…> |
|
|
932
|
+
undefined`; `first`/`last`/`keep` preserve the source column kind. Custom
|
|
933
|
+
reducer functions and `AggregateOutputSpec` entries keep the wide
|
|
934
|
+
`ColumnValue | undefined` fallback. Narrowing lives in the new
|
|
935
|
+
`types-reduce.ts` — same file-split pattern used later for the output-map
|
|
936
|
+
narrowing.
|
|
937
|
+
|
|
938
|
+
### Changed
|
|
939
|
+
|
|
940
|
+
- `useCurrent` now aliases `ReduceResult<S, Mapping>` directly; the hook's
|
|
941
|
+
duplicated narrowing logic is gone.
|
|
942
|
+
|
|
943
|
+
[0.5.2]: https://github.com/pjm17971/pond-ts/compare/v0.5.1...v0.5.2
|
|
944
|
+
|
|
945
|
+
## [0.5.1] — 2026-04-23
|
|
946
|
+
|
|
947
|
+
### Added
|
|
948
|
+
|
|
949
|
+
- **`TimeSeries.tail(duration?)`** — trailing temporal slice, the
|
|
950
|
+
counterpart to `Array.slice(-n)`. Called with no argument, returns the
|
|
951
|
+
whole series. Composes with every other `TimeSeries` method.
|
|
952
|
+
- **`useCurrent` hook (`@pond-ts/react`)** — subscribes to a live source and
|
|
953
|
+
returns the current value of a reducer mapping. Signature:
|
|
954
|
+
`useCurrent(source, mapping, { tail?, throttle? })`. Stable-shape record
|
|
955
|
+
even while the source is empty, so destructuring on first render is safe.
|
|
956
|
+
|
|
957
|
+
[0.5.1]: https://github.com/pjm17971/pond-ts/compare/v0.5.0...v0.5.1
|
|
958
|
+
|
|
959
|
+
## [0.5.0] — 2026-04-23
|
|
960
|
+
|
|
961
|
+
### Added
|
|
962
|
+
|
|
963
|
+
- **First-class `'array'` column kind.** New `ArrayValue = ReadonlyArray<ScalarValue>`
|
|
964
|
+
and `ColumnValue = ScalarValue | ArrayValue` types. Array columns are inert
|
|
965
|
+
with respect to numerical operators (`diff`, `rate`, `cumulative`,
|
|
966
|
+
`rolling`-over-numbers skip them automatically via `NumericColumnNameForSchema`).
|
|
967
|
+
- **`unique` reducer** — distinct sorted values; works in `reduce`,
|
|
968
|
+
`aggregate`, and `rolling`. Flattens array-kind sources one level (set union
|
|
969
|
+
across arrays in a bucket).
|
|
970
|
+
- **`top(n)` reducer** — top N values by frequency with deterministic
|
|
971
|
+
tie-break. String-pattern dispatch (`'top3'`, `'top10'`) parallel to `pNN`,
|
|
972
|
+
plus a `top(n)` helper that returns the typed string literal. Incremental
|
|
973
|
+
bucket + rolling state via a count map. Also flattens array-kind sources.
|
|
974
|
+
- **Five array-prefixed operators on `TimeSeries`**:
|
|
975
|
+
- `arrayContains(col, value)` — has this one
|
|
976
|
+
- `arrayContainsAll(col, values)` — has every one (AND / subset)
|
|
977
|
+
- `arrayContainsAny(col, values)` — has at least one (OR / intersection)
|
|
978
|
+
- `arrayAggregate(col, reducer, { as?, kind? })` — per-event reduction
|
|
979
|
+
reusing the full reducer registry (count, sum, avg, unique, custom, etc.).
|
|
980
|
+
Replace in place or append via `as`.
|
|
981
|
+
- `arrayExplode(col, { as?, kind? })` — fan each event out into one event
|
|
982
|
+
per array element. Replace the array column or keep it alongside a scalar
|
|
983
|
+
sibling.
|
|
984
|
+
- **LiveSeries accepts `kind: 'array'`** on its schema with array cells
|
|
985
|
+
frozen on push.
|
|
986
|
+
- **JSON round-trip** for array cells works unchanged (toJSON / fromJSON
|
|
987
|
+
pass arrays through naturally).
|
|
988
|
+
- **Docs**: new `guides/arrays.mdx` reference page;
|
|
989
|
+
`examples/error-rate-dashboard.mdx` scenario walkthrough backed 1:1 by an
|
|
990
|
+
E2E test; `reducer-reference.mdx` expanded with concrete input/output
|
|
991
|
+
examples for `unique` and `top(n)`.
|
|
992
|
+
|
|
993
|
+
### Changed
|
|
994
|
+
|
|
995
|
+
- **`reduce()` / `ReduceResult` / `CustomAggregateReducer` return types** widened
|
|
996
|
+
from `ScalarValue | undefined` to `ColumnValue | undefined`. Narrowed
|
|
997
|
+
annotations (`: number | undefined`) keep working; only callers with
|
|
998
|
+
explicit `: ScalarValue | undefined` annotations need to widen.
|
|
999
|
+
(v0.5.2 narrows these further per-entry.)
|
|
1000
|
+
|
|
1001
|
+
[0.5.0]: https://github.com/pjm17971/pond-ts/compare/v0.4.3...v0.5.0
|
|
1002
|
+
|
|
1003
|
+
## [0.4.3] — 2026-04-22
|
|
1004
|
+
|
|
1005
|
+
### Added
|
|
1006
|
+
|
|
1007
|
+
- `useLiveQuery` and `useLatest` hooks in `@pond-ts/react`.
|
|
1008
|
+
|
|
1009
|
+
### Fixed
|
|
1010
|
+
|
|
1011
|
+
- LiveView eviction mirroring (uses `EMITS_EVICT` symbol to safely detect
|
|
1012
|
+
evict-capable sources; avoids duck-typing that broke on `LiveAggregation`).
|
|
1013
|
+
- Type narrowing through `LiveAggregation` / `LiveRollingAggregation` via
|
|
1014
|
+
`Out` type parameter.
|
|
1015
|
+
- `Time.toDate()` convenience method.
|
|
1016
|
+
- `useWindow` under React StrictMode (view creation moved to `useEffect`).
|
|
1017
|
+
- `TimeSeries[Symbol.iterator]` and `toArray()` for ergonomic iteration.
|
|
1018
|
+
- `useSnapshot` accepts `SnapshotSource<S>` structural type (no casts for
|
|
1019
|
+
`LiveAggregation` input).
|
|
1020
|
+
|
|
1021
|
+
[0.4.3]: https://github.com/pjm17971/pond-ts/compare/v0.4.2...v0.4.3
|
|
1022
|
+
|
|
1023
|
+
## [0.4.2] — 2026-04-21
|
|
1024
|
+
|
|
1025
|
+
### Changed
|
|
1026
|
+
|
|
1027
|
+
- First release using npm OIDC Trusted Publisher (no stored tokens).
|
|
1028
|
+
|
|
1029
|
+
[0.4.2]: https://github.com/pjm17971/pond-ts/compare/v0.4.1...v0.4.2
|
|
1030
|
+
|
|
1031
|
+
## [0.4.1] — 2026-04-21
|
|
1032
|
+
|
|
1033
|
+
Administrative — no behavioral changes.
|
|
1034
|
+
|
|
1035
|
+
[0.4.1]: https://github.com/pjm17971/pond-ts/compare/v0.4.0...v0.4.1
|
|
1036
|
+
|
|
1037
|
+
## [0.4.0] — 2026-04-21
|
|
1038
|
+
|
|
1039
|
+
### Added
|
|
1040
|
+
|
|
1041
|
+
- **`@pond-ts/react` package** — React hooks for live series
|
|
1042
|
+
(`useLiveSeries`, `useTimeSeries`, `useSnapshot`, `useWindow`, `useDerived`,
|
|
1043
|
+
`takeSnapshot`). Monorepo restructure completed.
|
|
1044
|
+
- **LiveView + LiveSource composition** — `filter`, `map`, `select`,
|
|
1045
|
+
`window` views that compose with `LiveAggregation` / `LiveRollingAggregation`
|
|
1046
|
+
via a shared `LiveSource<S>` interface.
|
|
1047
|
+
- **Live per-event and carry-forward transforms** — `diff`, `rate`,
|
|
1048
|
+
`pctChange`, `fill`, `cumulative` available as LiveView variants.
|
|
1049
|
+
- **Grace period on `LiveAggregation`** — delays bucket closing so
|
|
1050
|
+
out-of-order events within a window accumulate into their correct bucket.
|
|
1051
|
+
Defaults from source `LiveSeries`'s `graceWindow`.
|
|
1052
|
+
- **Streaming dashboard example** with E2E tests.
|
|
1053
|
+
- **Benchmark suite** comparing `pond-ts` vs `pondjs`.
|
|
1054
|
+
|
|
1055
|
+
[0.4.0]: https://github.com/pjm17971/pond-ts/compare/v0.3.0...v0.4.0
|
|
1056
|
+
|
|
1057
|
+
## [0.3.0] — 2026-04-21
|
|
1058
|
+
|
|
1059
|
+
### Added
|
|
1060
|
+
|
|
1061
|
+
- **`LiveSeries`** — mutable, append-optimized streaming buffer sharing the
|
|
1062
|
+
same schema type as `TimeSeries`. Retention policies (`maxEvents`,
|
|
1063
|
+
`maxAge`, `maxBytes`). Synchronous subscriptions (`event`, `batch`,
|
|
1064
|
+
`evict`). Ordering modes (`strict`, `drop`, `reorder`).
|
|
1065
|
+
- **`LiveAggregation`** — incremental bucketed aggregation over a
|
|
1066
|
+
`LiveSource`.
|
|
1067
|
+
- **`LiveRollingAggregation`** — sliding-window reduction over a
|
|
1068
|
+
`LiveSource`.
|
|
1069
|
+
|
|
1070
|
+
[0.3.0]: https://github.com/pjm17971/pond-ts/compare/v0.2.0...v0.3.0
|
|
1071
|
+
|
|
1072
|
+
## [0.2.0] — 2026-04-16
|
|
1073
|
+
|
|
1074
|
+
### Added
|
|
1075
|
+
|
|
1076
|
+
- **Phase 2 batch expansion**: `reduce`, `groupBy`, `diff`, `rate`, `fill`.
|
|
1077
|
+
- **Phase 2.5 columnar primitives**: `pctChange`, `cumulative`, `shift`,
|
|
1078
|
+
`bfill` fill strategy.
|
|
1079
|
+
- **Aggregator parity with pondjs**: `median`, `stdev`, `percentile`
|
|
1080
|
+
(`pNN`), `difference`, `keep`.
|
|
1081
|
+
|
|
1082
|
+
[0.2.0]: https://github.com/pjm17971/pond-ts/compare/v0.1.4...v0.2.0
|
|
1083
|
+
|
|
1084
|
+
## [0.1.x] — 2026-04-16
|
|
1085
|
+
|
|
1086
|
+
Phase 0 (core performance) and Phase 1 (batch hardening) releases. Five
|
|
1087
|
+
critical O(N²) hot paths optimized (172× aggregate, 182× rolling, 15×
|
|
1088
|
+
movingAverage, 7.5× loess, 819× includesKey, 134× alignLinearAt).
|
|
1089
|
+
`toJSON`/`fromJSON` round-trip, custom aggregate reducers, edge-case
|
|
1090
|
+
coverage across every analytical primitive.
|
|
1091
|
+
|
|
1092
|
+
See [tag history](https://github.com/pjm17971/pond-ts/tags) for details.
|