@pond-ts/react 0.15.1 → 0.16.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 +230 -1
- package/package.json +2 -2
package/CHANGELOG.md
CHANGED
|
@@ -7,10 +7,239 @@ 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.16.0...HEAD
|
|
11
11
|
|
|
12
12
|
## [Unreleased]
|
|
13
13
|
|
|
14
|
+
## [0.16.0] — 2026-05-06
|
|
15
|
+
|
|
16
|
+
Live-API ergonomic wave. Four PRs:
|
|
17
|
+
[#122](https://github.com/pjm17971/pond-ts/pull/122) (buffer-as-window
|
|
18
|
+
Tier 1), [#123](https://github.com/pjm17971/pond-ts/pull/123)
|
|
19
|
+
(`stats()` accessor), [#124](https://github.com/pjm17971/pond-ts/pull/124)
|
|
20
|
+
(`history` option + compile-time fused uniqueness),
|
|
21
|
+
[#125](https://github.com/pjm17971/pond-ts/pull/125) (Tier 2 query
|
|
22
|
+
primitives). Strictly additive surface — no public-API removals or
|
|
23
|
+
narrowings.
|
|
24
|
+
|
|
25
|
+
### Added
|
|
26
|
+
|
|
27
|
+
- **`live.reduce(mapping, opts?)`** on `LiveSeries` and `LiveView`
|
|
28
|
+
— streaming reduce over the source's current buffer. Mirrors
|
|
29
|
+
`series.reduce(mapping)` from batch but reactive: per-event
|
|
30
|
+
`add`, per-eviction `remove`, microtask-deferred trigger
|
|
31
|
+
emission so retention has run before the snapshot. Closes the
|
|
32
|
+
buffer-as-window persona's biggest ergonomic gap.
|
|
33
|
+
- **`live.timeRange()`** on `LiveSeries` and `LiveView` — O(1)
|
|
34
|
+
temporal extent of the current buffer (`undefined` when empty).
|
|
35
|
+
- **`live.eventRate()`** on `LiveSeries` and `LiveView` — O(1)
|
|
36
|
+
events-per-second over the buffer's time span (zero when fewer
|
|
37
|
+
than two events). Convenience over the existing
|
|
38
|
+
`view.eventRate()` shape; no window argument required.
|
|
39
|
+
- **`live.count()`** on `LiveSeries` (alias for `length`) for
|
|
40
|
+
parity with `LiveView.count()` and chainable composition with
|
|
41
|
+
`eventRate()`.
|
|
42
|
+
- **`stats()` accessor on every live accumulator/series.** Per-class
|
|
43
|
+
shapes, all returning a plain record (cumulative integer counters
|
|
44
|
+
+ current-state fields):
|
|
45
|
+
|
|
46
|
+
| Class | Shape |
|
|
47
|
+
|---|---|
|
|
48
|
+
| LiveSeries | `{ ingested, evicted, rejected, length, earliestTs?, latestTs? }` |
|
|
49
|
+
| LiveRollingAggregation | `{ eventsObserved, evictions, emissions, windowSize }` |
|
|
50
|
+
| LiveFusedRolling | `{ eventsObserved, evictions, emissions, windowSize, windowsCount }` |
|
|
51
|
+
| LiveAggregation | `{ eventsObserved, bucketsClosed, openBuckets, openBucketStart? }` |
|
|
52
|
+
| LiveReduce | `{ eventsObserved, evictions, emissions, bufferSize }` |
|
|
53
|
+
| LivePartitionedSeries | `{ partitions, eventsRouted }` |
|
|
54
|
+
| LivePartitionedSyncRolling | `{ partitions, eventsObserved, emissions, windowSize }` |
|
|
55
|
+
| LivePartitionedFusedRolling | `{ partitions, eventsObserved, emissions, windowSize, windowsCount }` |
|
|
56
|
+
|
|
57
|
+
Per-event cost: ~1-3 integer increments in already-existing
|
|
58
|
+
handlers. `stats()` itself is O(1) — or O(partitions) for the
|
|
59
|
+
max-across-partitions `windowSize` on partitioned variants.
|
|
60
|
+
Polling-based by design — wall-clock timers inside pond would
|
|
61
|
+
break the data-is-the-clock invariant.
|
|
62
|
+
|
|
63
|
+
- **`history: false | RetentionPolicy` option on
|
|
64
|
+
`LiveRollingAggregation` and `LiveFusedRolling`** (and
|
|
65
|
+
partitioned variants — threaded through
|
|
66
|
+
`LivePartitionedSeries.rolling` end-to-end). Controls how much
|
|
67
|
+
of the rolling's emitted history the accumulator keeps in its
|
|
68
|
+
own output buffer (the one read by `length` / `at(i)`). Default
|
|
69
|
+
`true` preserves current behavior; `false` skips the push
|
|
70
|
+
entirely (`'event'` listeners and `value()` still work, but
|
|
71
|
+
`length` stays at 0); `RetentionPolicy` (`{ maxEvents?, maxAge? }`)
|
|
72
|
+
caps the buffer using the same shape as `LiveSeries.retention`.
|
|
73
|
+
Stricter validation: rejects 0, negative, or non-integer
|
|
74
|
+
`maxEvents`; `Infinity` is the documented "no cap" sentinel.
|
|
75
|
+
|
|
76
|
+
- **Compile-time uniqueness check on fused output columns**
|
|
77
|
+
(`FusedMappingValid<FM>`). Two windows declaring the same
|
|
78
|
+
output name now fail at the call site with a branded error
|
|
79
|
+
type naming the conflict. Wired into all four fused-rolling
|
|
80
|
+
overloads (LiveSeries, LiveView, root + view
|
|
81
|
+
LivePartitionedSeries). Runtime check still in place.
|
|
82
|
+
|
|
83
|
+
- **Tier 2 query primitives on `LiveSeries` and `LiveView`** —
|
|
84
|
+
pure parity additions mirroring `TimeSeries`:
|
|
85
|
+
- `find(pred)`, `some(pred)`, `every(pred)` — O(N) predicate query
|
|
86
|
+
- `includesKey(key)`, `bisect(key)`, `atOrBefore(key)`,
|
|
87
|
+
`atOrAfter(key)` — O(log N) binary search on the sorted buffer
|
|
88
|
+
|
|
89
|
+
Use cases: "is there already an event with key K?" / "what was
|
|
90
|
+
the most recent event before time T?" Both come up in dashboard
|
|
91
|
+
patterns where the live buffer IS the working set.
|
|
92
|
+
|
|
93
|
+
- **`KeyLike` type** exported from the package root (re-exported
|
|
94
|
+
from `TimeSeries`). Accepts `EventKey | TimestampInput |
|
|
95
|
+
TimeRangeInput | IntervalInput`; normalised by the new query
|
|
96
|
+
primitives.
|
|
97
|
+
|
|
98
|
+
- **`DurationLiteral` and `DurationUnit` types** extracted from
|
|
99
|
+
`utils/duration.ts` and exported. Same shape as before, just
|
|
100
|
+
named.
|
|
101
|
+
|
|
102
|
+
- **Concrete return types from partitioned rolling overloads.**
|
|
103
|
+
`LivePartitionedSeries.rolling` and `LivePartitionedView.rolling`
|
|
104
|
+
clock-trigger and fused-mapping overloads now return the concrete
|
|
105
|
+
`LivePartitionedSyncRolling` / `LivePartitionedFusedRolling`
|
|
106
|
+
classes (instead of bare `LiveSource<...>`), exposing `stats()`
|
|
107
|
+
to callers without a cast. Strictly additive — concrete classes
|
|
108
|
+
implement `LiveSource` plus `stats()`.
|
|
109
|
+
|
|
110
|
+
### Changed
|
|
111
|
+
|
|
112
|
+
- **`LiveSeries.clear()`** now increments the `evicted` counter
|
|
113
|
+
on `stats()` to match the existing `'evict'` listener fan-out.
|
|
114
|
+
Previously cleared the buffer and fired listeners but didn't
|
|
115
|
+
update the counter.
|
|
116
|
+
- **`LiveSeries` insertion comparator** delegates to
|
|
117
|
+
`EventKey.compare` (was previously `begin/end` only). Affects
|
|
118
|
+
interval-keyed series with same-span / different-value
|
|
119
|
+
intervals: previously stored in arrival order — and broke
|
|
120
|
+
`bisect`/`includesKey` queries — now stored in value-ascending
|
|
121
|
+
order. Time-keyed and timeRange-keyed series unaffected.
|
|
122
|
+
- **`LiveView.map(fn)` runtime check** rejects re-keying maps
|
|
123
|
+
that produce non-monotonic outputs. Throws `ValidationError`
|
|
124
|
+
at append time rather than silently breaking the view's
|
|
125
|
+
sorted-buffer invariant (which Tier 2 query primitives rely
|
|
126
|
+
on). Sane transforms (data-only maps, monotonic time-shifts)
|
|
127
|
+
unaffected.
|
|
128
|
+
- **`LiveAggregationOptions.grace`** type tightened from
|
|
129
|
+
`DurationInput | \`${number}${unit}\`` (redundant union) to
|
|
130
|
+
just `DurationInput`. No behavioral change.
|
|
131
|
+
|
|
132
|
+
### Notes
|
|
133
|
+
|
|
134
|
+
- React package (`@pond-ts/react`) version-bumped lock-step; no
|
|
135
|
+
hook surface changes in this release. New core hooks
|
|
136
|
+
(`useLiveReduce`, `useStats`, optional-window `useEventRate`)
|
|
137
|
+
are queued for a follow-up — see PLAN.md for the design.
|
|
138
|
+
- Codex caught real bugs on every Layer-2-reviewed PR in this
|
|
139
|
+
wave (1 HIGH + 1 MEDIUM on PR #123, 1 HIGH + 1 MEDIUM on
|
|
140
|
+
PR #124, 2 MEDIUM on PR #125). The Layer 2 + Codex two-pass
|
|
141
|
+
protocol earned its keep again.
|
|
142
|
+
|
|
143
|
+
## [0.15.2] — 2026-05-06
|
|
144
|
+
|
|
145
|
+
Performance fix for live rolling at firehose rates. The gRPC
|
|
146
|
+
experiment's step 6
|
|
147
|
+
([pond-grpc-experiment#26](https://github.com/pjm17971/pond-grpc-experiment/pull/26))
|
|
148
|
+
attempted to use the non-partitioned `live.rolling({...}, opts)`
|
|
149
|
+
overload for global counters and saw throughput collapse from 88k/s
|
|
150
|
+
to 21k/s — a 4× regression even worse than the V7→V6 gap that
|
|
151
|
+
motivated v0.15.0. The cliff is the same `Array.shift()` pattern
|
|
152
|
+
already flagged as queued tactical work in PLAN; the gRPC encounter
|
|
153
|
+
made it urgent.
|
|
154
|
+
|
|
155
|
+
### Fixed
|
|
156
|
+
|
|
157
|
+
- **Eviction is now O(1) per ingest in all live rolling classes.**
|
|
158
|
+
Replaced `entries.shift()` (worst-case O(N) on the deque length)
|
|
159
|
+
with a head-index pointer + periodic batched compaction:
|
|
160
|
+
- `LiveFusedRolling.#compactFront` — non-partitioned multi-window
|
|
161
|
+
- `LivePartitionedFusedRolling.#compactPartitionFront` —
|
|
162
|
+
per-partition fused
|
|
163
|
+
- `LiveRollingAggregation.#removeFirst` — single-window
|
|
164
|
+
non-partitioned
|
|
165
|
+
- `LivePartitionedSyncRolling.#evictPartition` — per-partition
|
|
166
|
+
single-window synced
|
|
167
|
+
|
|
168
|
+
The pattern: track a `frontIdx` field; "evicting" advances the
|
|
169
|
+
pointer instead of shifting. When the dead prefix grows past
|
|
170
|
+
half the array length, batch-splice it off and reset the
|
|
171
|
+
pointer. Per-event cost stays O(1) amortized at every live-
|
|
172
|
+
window size — each surviving entry is copied at most once
|
|
173
|
+
between two compactions, and compactions fire at most every
|
|
174
|
+
(live-size) events.
|
|
175
|
+
|
|
176
|
+
An earlier draft also compacted on a fixed 1024-entry threshold;
|
|
177
|
+
Codex's adversarial review on PR #119 caught that this would
|
|
178
|
+
reintroduce O(live_size / 1024) per-eviction cost on large
|
|
179
|
+
windows (100k+ live entries) — the threshold would fire
|
|
180
|
+
repeatedly and copy the entire live slice each time. The
|
|
181
|
+
proportional guard alone has the right amortization invariant.
|
|
182
|
+
|
|
183
|
+
### Performance
|
|
184
|
+
|
|
185
|
+
`packages/core/scripts/perf-fused-rolling.mjs` — new regression
|
|
186
|
+
scenario that reproduces the cliff (50k-event deque with continuous
|
|
187
|
+
eviction):
|
|
188
|
+
|
|
189
|
+
```
|
|
190
|
+
Worst-case shift pattern (50s window, 50k fill + 50k evict):
|
|
191
|
+
median (ms) min (ms) max (ms)
|
|
192
|
+
pre-fix 1123.12 1118.47 1149.95
|
|
193
|
+
v0.15.2 53.00 52.34 53.56
|
|
194
|
+
speedup 21.2×
|
|
195
|
+
|
|
196
|
+
Steady-state deque, no eviction (5m window, 200k events):
|
|
197
|
+
median (ms) min (ms) max (ms)
|
|
198
|
+
pre-fix 91.28 89.84 97.04
|
|
199
|
+
v0.15.2 99.28 96.80 103.94
|
|
200
|
+
delta +9% (within noise)
|
|
201
|
+
```
|
|
202
|
+
|
|
203
|
+
The fix targets the eviction-loop case specifically. Workloads with
|
|
204
|
+
no eviction (or rare eviction relative to ingest) see no change —
|
|
205
|
+
V8's internal hidden-offset optimization handles those well. The
|
|
206
|
+
cliff appears once eviction fires per-ingest at large deque size,
|
|
207
|
+
which is exactly the firehose-rolling shape.
|
|
208
|
+
|
|
209
|
+
### Why the cliff was hidden
|
|
210
|
+
|
|
211
|
+
V8's `Array.shift()` is amortized O(1) for shift-heavy workloads up
|
|
212
|
+
to ~10k-element arrays — it maintains a hidden offset and only
|
|
213
|
+
periodically compacts. Beyond that size or with mixed access
|
|
214
|
+
patterns, the optimization breaks down and shift falls back to true
|
|
215
|
+
O(N) memcpy. The bench scales from 1k to 50k deque sizes and the
|
|
216
|
+
cliff appears around 30k-40k. Pond's tests pin behavior at small
|
|
217
|
+
window sizes; the cliff was invisible to the test suite, only
|
|
218
|
+
showed up under the gRPC experiment's firehose load.
|
|
219
|
+
|
|
220
|
+
### What this unlocks
|
|
221
|
+
|
|
222
|
+
The agent's manual-counter workaround in `aggregator/src/aggregate.ts`
|
|
223
|
+
can now drop. The natural shape — a non-partitioned
|
|
224
|
+
`live.rolling({...}, { trigger })` over the firehose — is now
|
|
225
|
+
viable at the rates the experiment cares about. PLAN's
|
|
226
|
+
"`samples` reducer would exhibit a similar shape at firehose"
|
|
227
|
+
caveat also resolves: same fix in the same call sites covers
|
|
228
|
+
samples too.
|
|
229
|
+
|
|
230
|
+
### Note for downstream consumers
|
|
231
|
+
|
|
232
|
+
This is a **strict-additive perf fix.** All output behavior is
|
|
233
|
+
preserved — same eviction order, same emission timing, same
|
|
234
|
+
snapshot values. The deque's internal representation changed
|
|
235
|
+
(`#entries[0]` may now be a logically-evicted entry until periodic
|
|
236
|
+
compaction); any downstream code reading `#entries` directly would
|
|
237
|
+
break, but those fields are private. Public APIs and types are
|
|
238
|
+
unchanged.
|
|
239
|
+
|
|
240
|
+
[0.16.0]: https://github.com/pjm17971/pond-ts/compare/v0.15.2...v0.16.0
|
|
241
|
+
[0.15.2]: https://github.com/pjm17971/pond-ts/compare/v0.15.1...v0.15.2
|
|
242
|
+
|
|
14
243
|
## [0.15.1] — 2026-05-05
|
|
15
244
|
|
|
16
245
|
Type-narrowing follow-up to v0.15.0. The fused partitioned-rolling
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@pond-ts/react",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.16.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.16.0",
|
|
35
35
|
"react": "^18.0.0 || ^19.0.0"
|
|
36
36
|
},
|
|
37
37
|
"devDependencies": {
|