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 +238 -1
- package/dist/LiveFusedRolling.d.ts +55 -0
- package/dist/LiveFusedRolling.d.ts.map +1 -0
- package/dist/LiveFusedRolling.js +393 -0
- package/dist/LiveFusedRolling.js.map +1 -0
- package/dist/LivePartitionedFusedRolling.d.ts +65 -0
- package/dist/LivePartitionedFusedRolling.d.ts.map +1 -0
- package/dist/LivePartitionedFusedRolling.js +380 -0
- package/dist/LivePartitionedFusedRolling.js.map +1 -0
- package/dist/LivePartitionedSeries.d.ts +83 -28
- package/dist/LivePartitionedSeries.d.ts.map +1 -1
- package/dist/LivePartitionedSeries.js +78 -5
- package/dist/LivePartitionedSeries.js.map +1 -1
- package/dist/LiveSeries.d.ts +37 -1
- package/dist/LiveSeries.d.ts.map +1 -1
- package/dist/LiveSeries.js +25 -2
- package/dist/LiveSeries.js.map +1 -1
- package/dist/LiveView.d.ts +9 -0
- package/dist/LiveView.d.ts.map +1 -1
- package/dist/LiveView.js +6 -2
- package/dist/LiveView.js.map +1 -1
- package/dist/index.d.ts +3 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2 -0
- package/dist/index.js.map +1 -1
- package/dist/types-fused-rolling.d.ts +143 -0
- package/dist/types-fused-rolling.d.ts.map +1 -0
- package/dist/types-fused-rolling.js +2 -0
- package/dist/types-fused-rolling.js.map +1 -0
- package/package.json +1 -1
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
|
|
@@ -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"}
|