@pond-ts/react 0.14.3 → 0.15.1
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 +238 -1
- package/package.json +2 -2
package/CHANGELOG.md
CHANGED
|
@@ -7,10 +7,247 @@ 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.15.1...HEAD
|
|
11
11
|
|
|
12
12
|
## [Unreleased]
|
|
13
13
|
|
|
14
|
+
## [0.15.1] — 2026-05-05
|
|
15
|
+
|
|
16
|
+
Type-narrowing follow-up to v0.15.0. The fused partitioned-rolling
|
|
17
|
+
typing chain exposed a pre-existing pond limitation where
|
|
18
|
+
`partitionBy('host')` widened the partition-column type instead of
|
|
19
|
+
narrowing it to the literal `'host'`. The gRPC experiment's V8
|
|
20
|
+
migration ([pond-grpc-experiment#22](https://github.com/pjm17971/pond-grpc-experiment/pull/22))
|
|
21
|
+
worked around it as `partitionBy<'host'>('host')` — clobbering the
|
|
22
|
+
value-type parameter `K` to fill the column-name slot. v0.15.1
|
|
23
|
+
captures the column literal directly so the workaround can drop.
|
|
24
|
+
|
|
25
|
+
### Fixed
|
|
26
|
+
|
|
27
|
+
- **`partitionBy` narrows the partition column literal.** The
|
|
28
|
+
`by` argument's literal type now flows into a new `ByCol`
|
|
29
|
+
generic on `LivePartitionedSeries<S, K, ByCol>` and
|
|
30
|
+
`LivePartitionedView<SBase, R, K, ByCol>`. Threaded through every
|
|
31
|
+
per-partition method (`fill`, `diff`, `rate`, `pctChange`,
|
|
32
|
+
`cumulative`, `apply`, the rolling overloads). The fused
|
|
33
|
+
partitioned-rolling overload's
|
|
34
|
+
`FusedPartitionedRollingSchema<S, ByCol, FM>` now resolves
|
|
35
|
+
correctly without the `<'host'>` workaround:
|
|
36
|
+
|
|
37
|
+
```ts
|
|
38
|
+
// Before v0.15.1: needed the explicit type arg to narrow
|
|
39
|
+
// host through the fused-rolling schema chain.
|
|
40
|
+
live.partitionBy<'host'>('host').rolling({ ... }, { trigger });
|
|
41
|
+
|
|
42
|
+
// v0.15.1+: the literal 'host' is captured automatically.
|
|
43
|
+
live.partitionBy('host').rolling({ ... }, { trigger });
|
|
44
|
+
// Output schema includes `host` narrowed to its column kind;
|
|
45
|
+
// event.get('host') resolves correctly.
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
Existing V8 callers using the `partitionBy<'host'>('host')`
|
|
49
|
+
workaround continue to narrow correctly. Type-parameter order
|
|
50
|
+
on `partitionBy` is `<ByCol, K>` (column name first, value type
|
|
51
|
+
second) so the explicit `<'host'>` binds the literal to `ByCol`
|
|
52
|
+
— exactly what the workaround intended pre-v0.15.1. The
|
|
53
|
+
workaround can now drop because automatic inference does the
|
|
54
|
+
same job, but it doesn't have to.
|
|
55
|
+
|
|
56
|
+
### Type system
|
|
57
|
+
|
|
58
|
+
- `LivePartitionedSeries<S, K, ByCol>` — third generic added with
|
|
59
|
+
default `keyof EventDataForSchema<S> & string`. Backwards-
|
|
60
|
+
compatible: existing references to `LivePartitionedSeries<S, K>`
|
|
61
|
+
and `LivePartitionedSeries<S>` resolve to the upper-bound default.
|
|
62
|
+
- `LivePartitionedView<SBase, R, K, ByCol>` — same shape; `ByCol`
|
|
63
|
+
threaded through every chain hop so partition-column literals
|
|
64
|
+
survive `partitionBy('host').fill(...).rolling({...}, opts)`.
|
|
65
|
+
|
|
66
|
+
### Test surface
|
|
67
|
+
|
|
68
|
+
`test-d/fused-rolling.test-d.ts` extended to pin the narrowing at
|
|
69
|
+
both the root and chained levels:
|
|
70
|
+
|
|
71
|
+
```ts
|
|
72
|
+
const fC = live.partitionBy('host').rolling({ ... }, { trigger });
|
|
73
|
+
sampleEvent.get('host'); // narrows to string | undefined
|
|
74
|
+
|
|
75
|
+
const chained = live.partitionBy('host').fill({ cpu: 'hold' })
|
|
76
|
+
.rolling({ '1m': { cpu_avg: ... } }, { trigger });
|
|
77
|
+
chainedSample.get('host'); // narrows correctly through the chain
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
All 1115 + 55 runtime tests still pass; type-d clean.
|
|
81
|
+
|
|
82
|
+
[0.15.1]: https://github.com/pjm17971/pond-ts/compare/v0.15.0...v0.15.1
|
|
83
|
+
|
|
84
|
+
## [0.15.0] — 2026-05-05
|
|
85
|
+
|
|
86
|
+
The "fused multi-window rolling" release. Shipping the primitive
|
|
87
|
+
that closes the gRPC experiment's V6→V7 architectural cliff: a
|
|
88
|
+
keyed-form overload on `live.rolling()` that maintains N windows
|
|
89
|
+
in one ingest pass over a single shared deque, emits one merged
|
|
90
|
+
event per trigger boundary, and (on the partitioned variant) eats
|
|
91
|
+
the doubled `#routeEvent` / `#evictPartition` / `_pushTrustedEvents`
|
|
92
|
+
hops V7 surfaced.
|
|
93
|
+
|
|
94
|
+
Two independent signals motivated this: the gRPC profile-diff
|
|
95
|
+
(PR #19 in `pond-grpc-experiment`) and the buffer-as-window
|
|
96
|
+
persona's metric-agent call site
|
|
97
|
+
(`series.rolling(RETENTION, mapping, ...)` as workaround). Both
|
|
98
|
+
point at one primitive; both shipped together. RFC #20 in
|
|
99
|
+
`pond-grpc-experiment` is the design record.
|
|
100
|
+
|
|
101
|
+
### Added
|
|
102
|
+
|
|
103
|
+
- **Keyed-form fused rolling on `LiveSeries.rolling`,
|
|
104
|
+
`LiveView.rolling`, and `LivePartitionedSeries.rolling`.** Pass
|
|
105
|
+
a record of `{ duration: mapping }` instead of `(window, mapping)`
|
|
106
|
+
to declare multiple windows; the rolling maintains them all in
|
|
107
|
+
one ingest pass:
|
|
108
|
+
|
|
109
|
+
```ts
|
|
110
|
+
const fused = byHost.rolling(
|
|
111
|
+
{
|
|
112
|
+
'1m': {
|
|
113
|
+
cpu_avg: { from: 'cpu', using: 'avg' },
|
|
114
|
+
cpu_sd: { from: 'cpu', using: 'stdev' },
|
|
115
|
+
},
|
|
116
|
+
'200ms': { cpu_samples: { from: 'cpu', using: 'samples' } },
|
|
117
|
+
},
|
|
118
|
+
{ trigger: Trigger.every('200ms') },
|
|
119
|
+
);
|
|
120
|
+
// fused emits one merged event per boundary with all four
|
|
121
|
+
// columns; one ingest pass per source event.
|
|
122
|
+
```
|
|
123
|
+
|
|
124
|
+
- **Output: one merged stream.** All declared windows' columns
|
|
125
|
+
concatenated into one record per trigger fire — not N
|
|
126
|
+
accumulators or N streams. User code collapses to one event
|
|
127
|
+
handler (the V7 → V8 migration in the gRPC experiment drops
|
|
128
|
+
~30 lines of `pendingByTs` / `partsFor` / `tryEmit` join
|
|
129
|
+
machinery).
|
|
130
|
+
- **Constraints.** Time-based windows only (object keys are
|
|
131
|
+
duration strings); single trigger across all windows by
|
|
132
|
+
design (per-window cadence falls back to two `rolling()`
|
|
133
|
+
calls, paying the V7 cost). On partitioned series, clock
|
|
134
|
+
trigger is required.
|
|
135
|
+
- **Per-window options.** Use the elaborated value form
|
|
136
|
+
(`{ mapping, minSamples }`) when one window needs different
|
|
137
|
+
options from the rest; bare-mapping value stays clean for
|
|
138
|
+
the common case.
|
|
139
|
+
- **Duplicate output column names** across windows are rejected
|
|
140
|
+
at construction with a clear error. Partition column auto-
|
|
141
|
+
injection is unified across all windows.
|
|
142
|
+
- **Single-window equivalence pin.**
|
|
143
|
+
`live.rolling('1m', mapping, opts)` and
|
|
144
|
+
`live.rolling({ '1m': mapping }, opts)` produce identical
|
|
145
|
+
output (locked down by tests).
|
|
146
|
+
|
|
147
|
+
- **`LiveFusedRolling<S, Out>`** — non-partitioned class, exposed
|
|
148
|
+
on the public surface via `live.rolling({...}, opts)`.
|
|
149
|
+
- **`LivePartitionedFusedRolling<S, K, Out>`** — synchronised-cross-
|
|
150
|
+
partition class, exposed via `byHost.rolling({...}, { trigger })`.
|
|
151
|
+
- **Type-level surface:** `FusedMapping<S>`, `FusedMappingValue<S>`,
|
|
152
|
+
`FusedMappingElaborated<S>`, `FusedRollingSchema<S, FM>`,
|
|
153
|
+
`FusedPartitionedRollingSchema<S, ByCol, FM>`, and
|
|
154
|
+
`DurationString` — all exported from `pond-ts`. Output column
|
|
155
|
+
kinds narrow correctly through `event.get('cpu_avg')` to
|
|
156
|
+
`number | undefined`.
|
|
157
|
+
|
|
158
|
+
### Performance
|
|
159
|
+
|
|
160
|
+
`packages/core/scripts/perf-fused-rolling.mjs` — bench against
|
|
161
|
+
gRPC RFC #20 acceptance criteria. Headline numbers (median of 3
|
|
162
|
+
runs, `node --expose-gc`):
|
|
163
|
+
|
|
164
|
+
```
|
|
165
|
+
Partitioned, 100k events × 100 hosts (the gRPC use case):
|
|
166
|
+
wall (ms) heap (MB)
|
|
167
|
+
single rolling baseline 95.20 74.33
|
|
168
|
+
two separate rollings (V7 shape) 141.12 101.71
|
|
169
|
+
fused two-window (V8 shape) 112.36 68.46
|
|
170
|
+
|
|
171
|
+
Fused vs V7 shape: -20.4% wall, -32.7% heap
|
|
172
|
+
Fused vs baseline: +18.0% wall, -7.9% heap
|
|
173
|
+
|
|
174
|
+
Partitioned, 100k events × 1000 hosts (saturation):
|
|
175
|
+
wall (ms) heap (MB)
|
|
176
|
+
two separate rollings (V7 shape) 700.35 556.56
|
|
177
|
+
fused two-window (V8 shape) 446.21 309.25
|
|
178
|
+
|
|
179
|
+
Fused vs V7 shape: -36.3% wall, -44.4% heap
|
|
180
|
+
```
|
|
181
|
+
|
|
182
|
+
**Scaling beyond two windows — the architectural argument
|
|
183
|
+
verified.** Every per-event pond hop runs ONCE in fused vs N times
|
|
184
|
+
in N separate rollings. The bench scales N from 2 to 5 windows
|
|
185
|
+
over the same 100k-events × 100-hosts source:
|
|
186
|
+
|
|
187
|
+
```
|
|
188
|
+
Separate (ms) Fused (ms) Wall delta
|
|
189
|
+
N = 2 152.91 102.91 -32.7%
|
|
190
|
+
N = 3 186.63 79.89 -57.2%
|
|
191
|
+
N = 4 245.42 107.51 -56.2%
|
|
192
|
+
N = 5 279.79 118.90 -57.5%
|
|
193
|
+
|
|
194
|
+
Separate (MB) Fused (MB) Heap delta
|
|
195
|
+
N = 2 108.13 72.20 -33.2%
|
|
196
|
+
N = 3 93.30 43.08 -53.8%
|
|
197
|
+
N = 4 113.69 47.19 -58.5%
|
|
198
|
+
N = 5 137.17 47.12 -65.6%
|
|
199
|
+
```
|
|
200
|
+
|
|
201
|
+
Fused stays roughly constant (~100ms) across N=2..5; separate
|
|
202
|
+
scales linearly. At N=5: **2.4× faster wall, 34% of the heap.**
|
|
203
|
+
|
|
204
|
+
The architectural cliff is closed and the win compounds with N.
|
|
205
|
+
Fused rolling's per-event cost is O(1) in the number of windows
|
|
206
|
+
for pipeline overhead — only O(N) for the unavoidable per-window
|
|
207
|
+
reducer-state updates (which separate also pays). Heap is
|
|
208
|
+
dominated by the saved per-rolling deque + per-partition state.
|
|
209
|
+
|
|
210
|
+
### Notes on what this does NOT include
|
|
211
|
+
|
|
212
|
+
- **`live.reduce(mapping)` sugar.** Designed in PLAN as
|
|
213
|
+
`live.rolling({ buffer: mapping }, { history: false })`; the
|
|
214
|
+
`'buffer'` sentinel is reserved at the type level but throws at
|
|
215
|
+
runtime for now. Lands with the buffer-as-window Tier 1 PR.
|
|
216
|
+
- **`TimeSeries.rolling` snapshot-side parity.** The keyed-form
|
|
217
|
+
overload is live-side only in v0.15.0; batch-side comes in a
|
|
218
|
+
follow-up.
|
|
219
|
+
- **Path A (share `LiveSeries` buffer).** Currently Path B (own
|
|
220
|
+
deque) — fused rolling subscribes via `'event'` and maintains
|
|
221
|
+
its own per-partition deque. Path A is a transparent perf
|
|
222
|
+
follow-up; same API.
|
|
223
|
+
- **Compile-time uniqueness check on output columns.** Runtime
|
|
224
|
+
check is in place; the type-level `CheckUniqueOutputs` helper
|
|
225
|
+
is parked as a follow-up. Same with tightening `DurationString`
|
|
226
|
+
to reject `'1min'`-style typos at the type level (today's
|
|
227
|
+
template-literal type is permissive; runtime `parseDuration`
|
|
228
|
+
catches malformed durations).
|
|
229
|
+
|
|
230
|
+
### Migration
|
|
231
|
+
|
|
232
|
+
Existing `live.rolling(window, mapping, opts)` calls are
|
|
233
|
+
unchanged. The keyed form is opt-in and additive. Two-rolling
|
|
234
|
+
patterns can migrate by collapsing to one fused call:
|
|
235
|
+
|
|
236
|
+
```ts
|
|
237
|
+
// Before:
|
|
238
|
+
const baseline = byHost.rolling('1m', m1, { trigger });
|
|
239
|
+
const slice = byHost.rolling('200ms', m2, { trigger });
|
|
240
|
+
// Then a per-(ts, host) join over both event streams …
|
|
241
|
+
|
|
242
|
+
// After:
|
|
243
|
+
const fused = byHost.rolling({ '1m': m1, '200ms': m2 }, { trigger });
|
|
244
|
+
fused.on('event', (e) => {
|
|
245
|
+
// All columns from both windows on one event.
|
|
246
|
+
});
|
|
247
|
+
```
|
|
248
|
+
|
|
249
|
+
[0.15.0]: https://github.com/pjm17971/pond-ts/compare/v0.14.3...v0.15.0
|
|
250
|
+
|
|
14
251
|
## [0.14.3] — 2026-05-04
|
|
15
252
|
|
|
16
253
|
A targeted allocation fix in the `'samples'` reducer's rolling-state
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@pond-ts/react",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.15.1",
|
|
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.15.0",
|
|
35
35
|
"react": "^18.0.0 || ^19.0.0"
|
|
36
36
|
},
|
|
37
37
|
"devDependencies": {
|