@pond-ts/react 0.14.3 → 0.15.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.
Files changed (2) hide show
  1. package/CHANGELOG.md +168 -1
  2. package/package.json +2 -2
package/CHANGELOG.md CHANGED
@@ -7,10 +7,177 @@ 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.14.3...HEAD
10
+ [Unreleased]: https://github.com/pjm17971/pond-ts/compare/v0.15.0...HEAD
11
11
 
12
12
  ## [Unreleased]
13
13
 
14
+ ## [0.15.0] — 2026-05-05
15
+
16
+ The "fused multi-window rolling" release. Shipping the primitive
17
+ that closes the gRPC experiment's V6→V7 architectural cliff: a
18
+ keyed-form overload on `live.rolling()` that maintains N windows
19
+ in one ingest pass over a single shared deque, emits one merged
20
+ event per trigger boundary, and (on the partitioned variant) eats
21
+ the doubled `#routeEvent` / `#evictPartition` / `_pushTrustedEvents`
22
+ hops V7 surfaced.
23
+
24
+ Two independent signals motivated this: the gRPC profile-diff
25
+ (PR #19 in `pond-grpc-experiment`) and the buffer-as-window
26
+ persona's metric-agent call site
27
+ (`series.rolling(RETENTION, mapping, ...)` as workaround). Both
28
+ point at one primitive; both shipped together. RFC #20 in
29
+ `pond-grpc-experiment` is the design record.
30
+
31
+ ### Added
32
+
33
+ - **Keyed-form fused rolling on `LiveSeries.rolling`,
34
+ `LiveView.rolling`, and `LivePartitionedSeries.rolling`.** Pass
35
+ a record of `{ duration: mapping }` instead of `(window, mapping)`
36
+ to declare multiple windows; the rolling maintains them all in
37
+ one ingest pass:
38
+
39
+ ```ts
40
+ const fused = byHost.rolling(
41
+ {
42
+ '1m': {
43
+ cpu_avg: { from: 'cpu', using: 'avg' },
44
+ cpu_sd: { from: 'cpu', using: 'stdev' },
45
+ },
46
+ '200ms': { cpu_samples: { from: 'cpu', using: 'samples' } },
47
+ },
48
+ { trigger: Trigger.every('200ms') },
49
+ );
50
+ // fused emits one merged event per boundary with all four
51
+ // columns; one ingest pass per source event.
52
+ ```
53
+
54
+ - **Output: one merged stream.** All declared windows' columns
55
+ concatenated into one record per trigger fire — not N
56
+ accumulators or N streams. User code collapses to one event
57
+ handler (the V7 → V8 migration in the gRPC experiment drops
58
+ ~30 lines of `pendingByTs` / `partsFor` / `tryEmit` join
59
+ machinery).
60
+ - **Constraints.** Time-based windows only (object keys are
61
+ duration strings); single trigger across all windows by
62
+ design (per-window cadence falls back to two `rolling()`
63
+ calls, paying the V7 cost). On partitioned series, clock
64
+ trigger is required.
65
+ - **Per-window options.** Use the elaborated value form
66
+ (`{ mapping, minSamples }`) when one window needs different
67
+ options from the rest; bare-mapping value stays clean for
68
+ the common case.
69
+ - **Duplicate output column names** across windows are rejected
70
+ at construction with a clear error. Partition column auto-
71
+ injection is unified across all windows.
72
+ - **Single-window equivalence pin.**
73
+ `live.rolling('1m', mapping, opts)` and
74
+ `live.rolling({ '1m': mapping }, opts)` produce identical
75
+ output (locked down by tests).
76
+
77
+ - **`LiveFusedRolling<S, Out>`** — non-partitioned class, exposed
78
+ on the public surface via `live.rolling({...}, opts)`.
79
+ - **`LivePartitionedFusedRolling<S, K, Out>`** — synchronised-cross-
80
+ partition class, exposed via `byHost.rolling({...}, { trigger })`.
81
+ - **Type-level surface:** `FusedMapping<S>`, `FusedMappingValue<S>`,
82
+ `FusedMappingElaborated<S>`, `FusedRollingSchema<S, FM>`,
83
+ `FusedPartitionedRollingSchema<S, ByCol, FM>`, and
84
+ `DurationString` — all exported from `pond-ts`. Output column
85
+ kinds narrow correctly through `event.get('cpu_avg')` to
86
+ `number | undefined`.
87
+
88
+ ### Performance
89
+
90
+ `packages/core/scripts/perf-fused-rolling.mjs` — bench against
91
+ gRPC RFC #20 acceptance criteria. Headline numbers (median of 3
92
+ runs, `node --expose-gc`):
93
+
94
+ ```
95
+ Partitioned, 100k events × 100 hosts (the gRPC use case):
96
+ wall (ms) heap (MB)
97
+ single rolling baseline 95.20 74.33
98
+ two separate rollings (V7 shape) 141.12 101.71
99
+ fused two-window (V8 shape) 112.36 68.46
100
+
101
+ Fused vs V7 shape: -20.4% wall, -32.7% heap
102
+ Fused vs baseline: +18.0% wall, -7.9% heap
103
+
104
+ Partitioned, 100k events × 1000 hosts (saturation):
105
+ wall (ms) heap (MB)
106
+ two separate rollings (V7 shape) 700.35 556.56
107
+ fused two-window (V8 shape) 446.21 309.25
108
+
109
+ Fused vs V7 shape: -36.3% wall, -44.4% heap
110
+ ```
111
+
112
+ **Scaling beyond two windows — the architectural argument
113
+ verified.** Every per-event pond hop runs ONCE in fused vs N times
114
+ in N separate rollings. The bench scales N from 2 to 5 windows
115
+ over the same 100k-events × 100-hosts source:
116
+
117
+ ```
118
+ Separate (ms) Fused (ms) Wall delta
119
+ N = 2 152.91 102.91 -32.7%
120
+ N = 3 186.63 79.89 -57.2%
121
+ N = 4 245.42 107.51 -56.2%
122
+ N = 5 279.79 118.90 -57.5%
123
+
124
+ Separate (MB) Fused (MB) Heap delta
125
+ N = 2 108.13 72.20 -33.2%
126
+ N = 3 93.30 43.08 -53.8%
127
+ N = 4 113.69 47.19 -58.5%
128
+ N = 5 137.17 47.12 -65.6%
129
+ ```
130
+
131
+ Fused stays roughly constant (~100ms) across N=2..5; separate
132
+ scales linearly. At N=5: **2.4× faster wall, 34% of the heap.**
133
+
134
+ The architectural cliff is closed and the win compounds with N.
135
+ Fused rolling's per-event cost is O(1) in the number of windows
136
+ for pipeline overhead — only O(N) for the unavoidable per-window
137
+ reducer-state updates (which separate also pays). Heap is
138
+ dominated by the saved per-rolling deque + per-partition state.
139
+
140
+ ### Notes on what this does NOT include
141
+
142
+ - **`live.reduce(mapping)` sugar.** Designed in PLAN as
143
+ `live.rolling({ buffer: mapping }, { history: false })`; the
144
+ `'buffer'` sentinel is reserved at the type level but throws at
145
+ runtime for now. Lands with the buffer-as-window Tier 1 PR.
146
+ - **`TimeSeries.rolling` snapshot-side parity.** The keyed-form
147
+ overload is live-side only in v0.15.0; batch-side comes in a
148
+ follow-up.
149
+ - **Path A (share `LiveSeries` buffer).** Currently Path B (own
150
+ deque) — fused rolling subscribes via `'event'` and maintains
151
+ its own per-partition deque. Path A is a transparent perf
152
+ follow-up; same API.
153
+ - **Compile-time uniqueness check on output columns.** Runtime
154
+ check is in place; the type-level `CheckUniqueOutputs` helper
155
+ is parked as a follow-up. Same with tightening `DurationString`
156
+ to reject `'1min'`-style typos at the type level (today's
157
+ template-literal type is permissive; runtime `parseDuration`
158
+ catches malformed durations).
159
+
160
+ ### Migration
161
+
162
+ Existing `live.rolling(window, mapping, opts)` calls are
163
+ unchanged. The keyed form is opt-in and additive. Two-rolling
164
+ patterns can migrate by collapsing to one fused call:
165
+
166
+ ```ts
167
+ // Before:
168
+ const baseline = byHost.rolling('1m', m1, { trigger });
169
+ const slice = byHost.rolling('200ms', m2, { trigger });
170
+ // Then a per-(ts, host) join over both event streams …
171
+
172
+ // After:
173
+ const fused = byHost.rolling({ '1m': m1, '200ms': m2 }, { trigger });
174
+ fused.on('event', (e) => {
175
+ // All columns from both windows on one event.
176
+ });
177
+ ```
178
+
179
+ [0.15.0]: https://github.com/pjm17971/pond-ts/compare/v0.14.3...v0.15.0
180
+
14
181
  ## [0.14.3] — 2026-05-04
15
182
 
16
183
  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.14.3",
3
+ "version": "0.15.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.14.0",
34
+ "pond-ts": "^0.15.0",
35
35
  "react": "^18.0.0 || ^19.0.0"
36
36
  },
37
37
  "devDependencies": {