pond-ts 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 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.14.3...HEAD
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
@@ -0,0 +1,55 @@
1
+ import type { ColumnValue, EventForSchema, LiveSource, SeriesSchema } from './types.js';
2
+ import type { FusedMapping } from './types-fused-rolling.js';
3
+ import type { LiveRollingOptions } from './LiveRollingAggregation.js';
4
+ type EventListener = (event: any) => void;
5
+ /**
6
+ * Multi-window rolling that maintains N windows in one ingest pass
7
+ * over a single shared deque. Replaces the workaround of multiple
8
+ * separate `LiveRollingAggregation`s sharing the same source — the
9
+ * gRPC experiment's V6→V7 profile-diff (PR #19) showed every per-
10
+ * event pond hop roughly doubled when running two rollings.
11
+ *
12
+ * Each declared window has:
13
+ * - Its own resolved duration (clipped to retention; see PLAN.md)
14
+ * - Its own column-spec list and reducer states
15
+ * - Its own head cursor into the shared deque
16
+ *
17
+ * Output is ONE merged stream: one event per trigger boundary, with
18
+ * all windows' columns concatenated into one record. Duplicate
19
+ * output column names across windows are rejected at construction
20
+ * with a clear error (compile-time detection is queued as a
21
+ * follow-up).
22
+ *
23
+ * **Single trigger.** All windows share the configured trigger.
24
+ * Per-window cadence is explicitly NOT supported — that's what
25
+ * fusion saves. Users who need per-window cadence fall back to two
26
+ * separate `rolling()` calls and pay the V7 cost.
27
+ *
28
+ * **Time-based windows only.** Object keys are duration strings.
29
+ * Count-based windows stay on the existing single-window
30
+ * `LiveRollingAggregation`. This constraint keeps the
31
+ * window-clip-to-retention rule and boundary-detection logic clean.
32
+ *
33
+ * Public API: constructed via the `live.rolling(fusedMapping, opts)`
34
+ * keyed-form overload on `LiveSeries` / `LiveView`. User code
35
+ * doesn't import this class directly.
36
+ */
37
+ export declare class LiveFusedRolling<S extends SeriesSchema, Out extends SeriesSchema = SeriesSchema> implements LiveSource<Out> {
38
+ #private;
39
+ readonly name: string;
40
+ readonly schema: Out;
41
+ constructor(source: LiveSource<S>, fusedMapping: FusedMapping<S>, options?: LiveRollingOptions);
42
+ get length(): number;
43
+ at(index: number): EventForSchema<Out> | undefined;
44
+ /**
45
+ * Read the current merged snapshot — every window's reducer
46
+ * outputs concatenated into one record. Useful for live-display
47
+ * patterns where the consumer wants the latest values without
48
+ * waiting for the next trigger fire.
49
+ */
50
+ value(): Record<string, ColumnValue | undefined>;
51
+ on(type: 'event', fn: EventListener): () => void;
52
+ dispose(): void;
53
+ }
54
+ export {};
55
+ //# sourceMappingURL=LiveFusedRolling.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"LiveFusedRolling.d.ts","sourceRoot":"","sources":["../src/LiveFusedRolling.ts"],"names":[],"mappings":"AAcA,OAAO,KAAK,EAGV,WAAW,EACX,cAAc,EACd,UAAU,EACV,YAAY,EACb,MAAM,YAAY,CAAC;AACpB,OAAO,KAAK,EAAE,YAAY,EAAqB,MAAM,0BAA0B,CAAC;AAChF,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,6BAA6B,CAAC;AAwCtE,KAAK,aAAa,GAAG,CAAC,KAAK,EAAE,GAAG,KAAK,IAAI,CAAC;AAE1C;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA+BG;AACH,qBAAa,gBAAgB,CAC3B,CAAC,SAAS,YAAY,EACtB,GAAG,SAAS,YAAY,GAAG,YAAY,CACvC,YAAW,UAAU,CAAC,GAAG,CAAC;;IAC1B,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,QAAQ,CAAC,MAAM,EAAE,GAAG,CAAC;gBAoCnB,MAAM,EAAE,UAAU,CAAC,CAAC,CAAC,EACrB,YAAY,EAAE,YAAY,CAAC,CAAC,CAAC,EAC7B,OAAO,GAAE,kBAAuB;IAyGlC,IAAI,MAAM,IAAI,MAAM,CAEnB;IAED,EAAE,CAAC,KAAK,EAAE,MAAM,GAAG,cAAc,CAAC,GAAG,CAAC,GAAG,SAAS;IAKlD;;;;;OAKG;IACH,KAAK,IAAI,MAAM,CAAC,MAAM,EAAE,WAAW,GAAG,SAAS,CAAC;IAahD,EAAE,CAAC,IAAI,EAAE,OAAO,EAAE,EAAE,EAAE,aAAa,GAAG,MAAM,IAAI;IAYhD,OAAO,IAAI,IAAI;CA0IhB"}