@pond-ts/react 0.15.0 → 0.15.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (2) hide show
  1. package/CHANGELOG.md +170 -1
  2. package/package.json +1 -1
package/CHANGELOG.md CHANGED
@@ -7,10 +7,179 @@ 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.15.0...HEAD
10
+ [Unreleased]: https://github.com/pjm17971/pond-ts/compare/v0.15.2...HEAD
11
11
 
12
12
  ## [Unreleased]
13
13
 
14
+ ## [0.15.2] — 2026-05-06
15
+
16
+ Performance fix for live rolling at firehose rates. The gRPC
17
+ experiment's step 6
18
+ ([pond-grpc-experiment#26](https://github.com/pjm17971/pond-grpc-experiment/pull/26))
19
+ attempted to use the non-partitioned `live.rolling({...}, opts)`
20
+ overload for global counters and saw throughput collapse from 88k/s
21
+ to 21k/s — a 4× regression even worse than the V7→V6 gap that
22
+ motivated v0.15.0. The cliff is the same `Array.shift()` pattern
23
+ already flagged as queued tactical work in PLAN; the gRPC encounter
24
+ made it urgent.
25
+
26
+ ### Fixed
27
+
28
+ - **Eviction is now O(1) per ingest in all live rolling classes.**
29
+ Replaced `entries.shift()` (worst-case O(N) on the deque length)
30
+ with a head-index pointer + periodic batched compaction:
31
+ - `LiveFusedRolling.#compactFront` — non-partitioned multi-window
32
+ - `LivePartitionedFusedRolling.#compactPartitionFront` —
33
+ per-partition fused
34
+ - `LiveRollingAggregation.#removeFirst` — single-window
35
+ non-partitioned
36
+ - `LivePartitionedSyncRolling.#evictPartition` — per-partition
37
+ single-window synced
38
+
39
+ The pattern: track a `frontIdx` field; "evicting" advances the
40
+ pointer instead of shifting. When the dead prefix grows past
41
+ half the array length, batch-splice it off and reset the
42
+ pointer. Per-event cost stays O(1) amortized at every live-
43
+ window size — each surviving entry is copied at most once
44
+ between two compactions, and compactions fire at most every
45
+ (live-size) events.
46
+
47
+ An earlier draft also compacted on a fixed 1024-entry threshold;
48
+ Codex's adversarial review on PR #119 caught that this would
49
+ reintroduce O(live_size / 1024) per-eviction cost on large
50
+ windows (100k+ live entries) — the threshold would fire
51
+ repeatedly and copy the entire live slice each time. The
52
+ proportional guard alone has the right amortization invariant.
53
+
54
+ ### Performance
55
+
56
+ `packages/core/scripts/perf-fused-rolling.mjs` — new regression
57
+ scenario that reproduces the cliff (50k-event deque with continuous
58
+ eviction):
59
+
60
+ ```
61
+ Worst-case shift pattern (50s window, 50k fill + 50k evict):
62
+ median (ms) min (ms) max (ms)
63
+ pre-fix 1123.12 1118.47 1149.95
64
+ v0.15.2 53.00 52.34 53.56
65
+ speedup 21.2×
66
+
67
+ Steady-state deque, no eviction (5m window, 200k events):
68
+ median (ms) min (ms) max (ms)
69
+ pre-fix 91.28 89.84 97.04
70
+ v0.15.2 99.28 96.80 103.94
71
+ delta +9% (within noise)
72
+ ```
73
+
74
+ The fix targets the eviction-loop case specifically. Workloads with
75
+ no eviction (or rare eviction relative to ingest) see no change —
76
+ V8's internal hidden-offset optimization handles those well. The
77
+ cliff appears once eviction fires per-ingest at large deque size,
78
+ which is exactly the firehose-rolling shape.
79
+
80
+ ### Why the cliff was hidden
81
+
82
+ V8's `Array.shift()` is amortized O(1) for shift-heavy workloads up
83
+ to ~10k-element arrays — it maintains a hidden offset and only
84
+ periodically compacts. Beyond that size or with mixed access
85
+ patterns, the optimization breaks down and shift falls back to true
86
+ O(N) memcpy. The bench scales from 1k to 50k deque sizes and the
87
+ cliff appears around 30k-40k. Pond's tests pin behavior at small
88
+ window sizes; the cliff was invisible to the test suite, only
89
+ showed up under the gRPC experiment's firehose load.
90
+
91
+ ### What this unlocks
92
+
93
+ The agent's manual-counter workaround in `aggregator/src/aggregate.ts`
94
+ can now drop. The natural shape — a non-partitioned
95
+ `live.rolling({...}, { trigger })` over the firehose — is now
96
+ viable at the rates the experiment cares about. PLAN's
97
+ "`samples` reducer would exhibit a similar shape at firehose"
98
+ caveat also resolves: same fix in the same call sites covers
99
+ samples too.
100
+
101
+ ### Note for downstream consumers
102
+
103
+ This is a **strict-additive perf fix.** All output behavior is
104
+ preserved — same eviction order, same emission timing, same
105
+ snapshot values. The deque's internal representation changed
106
+ (`#entries[0]` may now be a logically-evicted entry until periodic
107
+ compaction); any downstream code reading `#entries` directly would
108
+ break, but those fields are private. Public APIs and types are
109
+ unchanged.
110
+
111
+ [0.15.2]: https://github.com/pjm17971/pond-ts/compare/v0.15.1...v0.15.2
112
+
113
+ ## [0.15.1] — 2026-05-05
114
+
115
+ Type-narrowing follow-up to v0.15.0. The fused partitioned-rolling
116
+ typing chain exposed a pre-existing pond limitation where
117
+ `partitionBy('host')` widened the partition-column type instead of
118
+ narrowing it to the literal `'host'`. The gRPC experiment's V8
119
+ migration ([pond-grpc-experiment#22](https://github.com/pjm17971/pond-grpc-experiment/pull/22))
120
+ worked around it as `partitionBy<'host'>('host')` — clobbering the
121
+ value-type parameter `K` to fill the column-name slot. v0.15.1
122
+ captures the column literal directly so the workaround can drop.
123
+
124
+ ### Fixed
125
+
126
+ - **`partitionBy` narrows the partition column literal.** The
127
+ `by` argument's literal type now flows into a new `ByCol`
128
+ generic on `LivePartitionedSeries<S, K, ByCol>` and
129
+ `LivePartitionedView<SBase, R, K, ByCol>`. Threaded through every
130
+ per-partition method (`fill`, `diff`, `rate`, `pctChange`,
131
+ `cumulative`, `apply`, the rolling overloads). The fused
132
+ partitioned-rolling overload's
133
+ `FusedPartitionedRollingSchema<S, ByCol, FM>` now resolves
134
+ correctly without the `<'host'>` workaround:
135
+
136
+ ```ts
137
+ // Before v0.15.1: needed the explicit type arg to narrow
138
+ // host through the fused-rolling schema chain.
139
+ live.partitionBy<'host'>('host').rolling({ ... }, { trigger });
140
+
141
+ // v0.15.1+: the literal 'host' is captured automatically.
142
+ live.partitionBy('host').rolling({ ... }, { trigger });
143
+ // Output schema includes `host` narrowed to its column kind;
144
+ // event.get('host') resolves correctly.
145
+ ```
146
+
147
+ Existing V8 callers using the `partitionBy<'host'>('host')`
148
+ workaround continue to narrow correctly. Type-parameter order
149
+ on `partitionBy` is `<ByCol, K>` (column name first, value type
150
+ second) so the explicit `<'host'>` binds the literal to `ByCol`
151
+ — exactly what the workaround intended pre-v0.15.1. The
152
+ workaround can now drop because automatic inference does the
153
+ same job, but it doesn't have to.
154
+
155
+ ### Type system
156
+
157
+ - `LivePartitionedSeries<S, K, ByCol>` — third generic added with
158
+ default `keyof EventDataForSchema<S> & string`. Backwards-
159
+ compatible: existing references to `LivePartitionedSeries<S, K>`
160
+ and `LivePartitionedSeries<S>` resolve to the upper-bound default.
161
+ - `LivePartitionedView<SBase, R, K, ByCol>` — same shape; `ByCol`
162
+ threaded through every chain hop so partition-column literals
163
+ survive `partitionBy('host').fill(...).rolling({...}, opts)`.
164
+
165
+ ### Test surface
166
+
167
+ `test-d/fused-rolling.test-d.ts` extended to pin the narrowing at
168
+ both the root and chained levels:
169
+
170
+ ```ts
171
+ const fC = live.partitionBy('host').rolling({ ... }, { trigger });
172
+ sampleEvent.get('host'); // narrows to string | undefined
173
+
174
+ const chained = live.partitionBy('host').fill({ cpu: 'hold' })
175
+ .rolling({ '1m': { cpu_avg: ... } }, { trigger });
176
+ chainedSample.get('host'); // narrows correctly through the chain
177
+ ```
178
+
179
+ All 1115 + 55 runtime tests still pass; type-d clean.
180
+
181
+ [0.15.1]: https://github.com/pjm17971/pond-ts/compare/v0.15.0...v0.15.1
182
+
14
183
  ## [0.15.0] — 2026-05-05
15
184
 
16
185
  The "fused multi-window rolling" release. Shipping the primitive
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pond-ts/react",
3
- "version": "0.15.0",
3
+ "version": "0.15.2",
4
4
  "description": "React hooks for pond-ts live time series",
5
5
  "license": "MIT",
6
6
  "repository": {