pond-ts 0.25.0 → 0.26.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.
package/CHANGELOG.md
CHANGED
|
@@ -7,7 +7,8 @@ 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.26.0...HEAD
|
|
11
|
+
[0.26.0]: https://github.com/pjm17971/pond-ts/compare/v0.25.0...v0.26.0
|
|
11
12
|
[0.25.0]: https://github.com/pjm17971/pond-ts/compare/v0.24.0...v0.25.0
|
|
12
13
|
[0.24.0]: https://github.com/pjm17971/pond-ts/compare/v0.23.0...v0.24.0
|
|
13
14
|
[0.23.0]: https://github.com/pjm17971/pond-ts/compare/v0.22.0...v0.23.0
|
|
@@ -19,6 +20,27 @@ type-level changes; patch bumps are strictly additive.
|
|
|
19
20
|
|
|
20
21
|
## [Unreleased]
|
|
21
22
|
|
|
23
|
+
## [0.26.0] — 2026-06-15
|
|
24
|
+
|
|
25
|
+
### Changed
|
|
26
|
+
|
|
27
|
+
- **`rolling(...)` now builds its output columns directly instead of
|
|
28
|
+
materializing events.** The rolling family was the last batch operator still
|
|
29
|
+
assembling a row per event and re-validating/re-packing it through the
|
|
30
|
+
constructor; it now reads the key axis and source values straight off the
|
|
31
|
+
columnar store and writes the result columns via trusted construction. The
|
|
32
|
+
result is unchanged for the common (scalar) cases. Measured: `rolling` with
|
|
33
|
+
`avg`/`sum` ~2.2–2.7× faster; rolling `stdev` on 100k events ~3.3–7.3×
|
|
34
|
+
(a 1-event window 45.7 ms → 6.3 ms); partitioned rolling ~1.8×.
|
|
35
|
+
`baseline` / `outliers` (which delegate to `rolling`) inherit the speedup.
|
|
36
|
+
- **Behavior note — `array` columns:** an identity-comparing reducer (`keep`,
|
|
37
|
+
or a custom reducer using `===` on the cell) on an `array`-kind source
|
|
38
|
+
column now compares the value stored in the column, not the original object
|
|
39
|
+
reference passed at construction. Two rows given the *same* array object
|
|
40
|
+
therefore read as distinct. Scalar columns (number / string / boolean) are
|
|
41
|
+
unaffected. A non-finite or wrong-kind reducer result is still rejected with
|
|
42
|
+
a `ValidationError`, exactly as the constructor's intake did.
|
|
43
|
+
|
|
22
44
|
## [0.25.0] — 2026-06-15
|
|
23
45
|
|
|
24
46
|
### Changed
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import { type Column } from '../../columnar/index.js';
|
|
2
|
+
/**
|
|
3
|
+
* Build a typed {@link Column} from a per-row value array, dispatching on the
|
|
4
|
+
* column's `kind`. An `undefined` cell becomes missing (its validity bit is
|
|
5
|
+
* left unset, so `read(i)` returns `undefined`). Numeric arrays reject
|
|
6
|
+
* non-finite values at construction (`float64ColumnFromArray`) — packed
|
|
7
|
+
* numeric columns stay NaN-free.
|
|
8
|
+
*
|
|
9
|
+
* This is the shared form of the kind→builder dispatch that `fill`
|
|
10
|
+
* (`buildFilledColumn`), `map` (its inline builder), and `collapse`
|
|
11
|
+
* (`buildScalarColumn`) each carry a local copy of — flagged there as a
|
|
12
|
+
* follow-up to converge. Columnar `rolling` is the fourth caller and uses
|
|
13
|
+
* this one directly; retrofitting the other three onto it is a separate
|
|
14
|
+
* (optional) cleanup.
|
|
15
|
+
*/
|
|
16
|
+
export declare function columnFromValuesByKind(kind: string, values: unknown[]): Column;
|
|
17
|
+
/**
|
|
18
|
+
* Throw if any *defined* value fails the column's `kind` contract, mirroring
|
|
19
|
+
* the constructor's strict intake (`validate.ts`): a `number` must be finite;
|
|
20
|
+
* `string` / `boolean` must match `typeof`; an `array` must be a real array
|
|
21
|
+
* whose elements are each a finite number, string, or boolean. `undefined`
|
|
22
|
+
* (missing) values are allowed.
|
|
23
|
+
*
|
|
24
|
+
* Computed-writer paths that assemble via trusted construction bypass intake,
|
|
25
|
+
* and {@link columnFromValuesByKind}'s `*FromArray` builders silently coerce a
|
|
26
|
+
* kind mismatch to a *missing* cell — so a caller that must preserve the intake
|
|
27
|
+
* rejection (e.g. columnar `rolling`, matching `mapColumns`) calls this first.
|
|
28
|
+
* `label` is prefixed to the thrown message.
|
|
29
|
+
*/
|
|
30
|
+
export declare function assertColumnValuesMatchKind(kind: string, values: ReadonlyArray<unknown>, label: string): void;
|
|
31
|
+
//# sourceMappingURL=column-builders.d.ts.map
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
import { arrayColumnFromArray, booleanColumnFromArray, float64ColumnFromArray, stringColumnFromArray, } from '../../columnar/index.js';
|
|
2
|
+
import { ValidationError } from '../../core/errors.js';
|
|
3
|
+
/**
|
|
4
|
+
* Build a typed {@link Column} from a per-row value array, dispatching on the
|
|
5
|
+
* column's `kind`. An `undefined` cell becomes missing (its validity bit is
|
|
6
|
+
* left unset, so `read(i)` returns `undefined`). Numeric arrays reject
|
|
7
|
+
* non-finite values at construction (`float64ColumnFromArray`) — packed
|
|
8
|
+
* numeric columns stay NaN-free.
|
|
9
|
+
*
|
|
10
|
+
* This is the shared form of the kind→builder dispatch that `fill`
|
|
11
|
+
* (`buildFilledColumn`), `map` (its inline builder), and `collapse`
|
|
12
|
+
* (`buildScalarColumn`) each carry a local copy of — flagged there as a
|
|
13
|
+
* follow-up to converge. Columnar `rolling` is the fourth caller and uses
|
|
14
|
+
* this one directly; retrofitting the other three onto it is a separate
|
|
15
|
+
* (optional) cleanup.
|
|
16
|
+
*/
|
|
17
|
+
export function columnFromValuesByKind(kind, values) {
|
|
18
|
+
switch (kind) {
|
|
19
|
+
case 'number':
|
|
20
|
+
return float64ColumnFromArray(values);
|
|
21
|
+
case 'string':
|
|
22
|
+
return stringColumnFromArray(values);
|
|
23
|
+
case 'boolean':
|
|
24
|
+
return booleanColumnFromArray(values);
|
|
25
|
+
case 'array':
|
|
26
|
+
// ArrayValue cells; the cast is the kind dispatch's trust point.
|
|
27
|
+
return arrayColumnFromArray(values);
|
|
28
|
+
default:
|
|
29
|
+
throw new TypeError(`columnFromValuesByKind: unsupported kind '${kind}'`);
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
/**
|
|
33
|
+
* Throw if any *defined* value fails the column's `kind` contract, mirroring
|
|
34
|
+
* the constructor's strict intake (`validate.ts`): a `number` must be finite;
|
|
35
|
+
* `string` / `boolean` must match `typeof`; an `array` must be a real array
|
|
36
|
+
* whose elements are each a finite number, string, or boolean. `undefined`
|
|
37
|
+
* (missing) values are allowed.
|
|
38
|
+
*
|
|
39
|
+
* Computed-writer paths that assemble via trusted construction bypass intake,
|
|
40
|
+
* and {@link columnFromValuesByKind}'s `*FromArray` builders silently coerce a
|
|
41
|
+
* kind mismatch to a *missing* cell — so a caller that must preserve the intake
|
|
42
|
+
* rejection (e.g. columnar `rolling`, matching `mapColumns`) calls this first.
|
|
43
|
+
* `label` is prefixed to the thrown message.
|
|
44
|
+
*/
|
|
45
|
+
export function assertColumnValuesMatchKind(kind, values, label) {
|
|
46
|
+
for (let i = 0; i < values.length; i += 1) {
|
|
47
|
+
const v = values[i];
|
|
48
|
+
if (v === undefined)
|
|
49
|
+
continue; // missing cell — allowed
|
|
50
|
+
let ok;
|
|
51
|
+
switch (kind) {
|
|
52
|
+
case 'number':
|
|
53
|
+
ok = typeof v === 'number' && Number.isFinite(v);
|
|
54
|
+
break;
|
|
55
|
+
case 'string':
|
|
56
|
+
ok = typeof v === 'string';
|
|
57
|
+
break;
|
|
58
|
+
case 'boolean':
|
|
59
|
+
ok = typeof v === 'boolean';
|
|
60
|
+
break;
|
|
61
|
+
case 'array': {
|
|
62
|
+
if (!Array.isArray(v)) {
|
|
63
|
+
ok = false;
|
|
64
|
+
break;
|
|
65
|
+
}
|
|
66
|
+
// Indexed loop, NOT `.every` (which skips holes): a sparse array's
|
|
67
|
+
// hole reads as `undefined`, an invalid element that intake's indexed
|
|
68
|
+
// scan rejects. Match it exactly so sparse arrays throw here rather
|
|
69
|
+
// than getting silently coerced to a missing cell by the builder.
|
|
70
|
+
ok = true;
|
|
71
|
+
for (let j = 0; j < v.length; j += 1) {
|
|
72
|
+
const el = v[j];
|
|
73
|
+
if (!((typeof el === 'number' && Number.isFinite(el)) ||
|
|
74
|
+
typeof el === 'string' ||
|
|
75
|
+
typeof el === 'boolean')) {
|
|
76
|
+
ok = false;
|
|
77
|
+
break;
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
break;
|
|
81
|
+
}
|
|
82
|
+
default:
|
|
83
|
+
ok = false;
|
|
84
|
+
}
|
|
85
|
+
if (!ok) {
|
|
86
|
+
// `ValidationError` (not `RangeError`) so the failure class matches the
|
|
87
|
+
// constructor's strict intake — a caller that catches `ValidationError`
|
|
88
|
+
// for bad user data sees columnar-rolling rejections identically. The
|
|
89
|
+
// value is stringified defensively: some invalid values (e.g. an array
|
|
90
|
+
// containing a `Symbol`) throw from `String(...)`, which would mask the
|
|
91
|
+
// `ValidationError` with a `TypeError`.
|
|
92
|
+
let shown;
|
|
93
|
+
try {
|
|
94
|
+
shown = String(v);
|
|
95
|
+
}
|
|
96
|
+
catch {
|
|
97
|
+
shown = `<unstringifiable ${typeof v}>`;
|
|
98
|
+
}
|
|
99
|
+
throw new ValidationError(`${label}: result ${shown} is not a valid '${kind}' value`);
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
//# sourceMappingURL=column-builders.js.map
|
|
@@ -900,6 +900,14 @@ export declare class TimeSeries<S extends SeriesSchema> {
|
|
|
900
900
|
* series carrying multiple entities (host, region, device id), use
|
|
901
901
|
* `series.partitionBy(col).rolling(...).collect()` to scope per
|
|
902
902
|
* entity. See {@link TimeSeries.partitionBy}.
|
|
903
|
+
*
|
|
904
|
+
* **`array`-column identity:** the window reads values from the
|
|
905
|
+
* columnar store, so on an `array`-kind source column an identity-
|
|
906
|
+
* comparing reducer (`keep`, or a custom reducer using `===` on the
|
|
907
|
+
* cell) compares the stored cell, not the original object reference
|
|
908
|
+
* from construction. Two rows that were given the *same* array object
|
|
909
|
+
* therefore read as distinct values here. Scalar columns
|
|
910
|
+
* (number / string / boolean) are unaffected (value semantics).
|
|
903
911
|
*/
|
|
904
912
|
rolling<const Mapping extends ValidatedAggregateMap<S, Mapping>>(window: DurationInput, mapping: Mapping, options?: {
|
|
905
913
|
alignment?: RollingAlignment;
|
|
@@ -5,6 +5,7 @@ import { fillOp } from './operators/fill.js';
|
|
|
5
5
|
import { mapOp } from './operators/map.js';
|
|
6
6
|
import { shiftOp } from './operators/shift.js';
|
|
7
7
|
import { collapseOp } from './operators/collapse.js';
|
|
8
|
+
import { assertColumnValuesMatchKind, columnFromValuesByKind, } from './operators/column-builders.js';
|
|
8
9
|
import { BoundedSequence } from '../sequence/bounded-sequence.js';
|
|
9
10
|
import { Interval } from '../core/interval.js';
|
|
10
11
|
import { Time } from '../core/time.js';
|
|
@@ -12,7 +13,7 @@ import { TimeRange } from '../core/time-range.js';
|
|
|
12
13
|
import { compareEventKeys } from '../core/temporal.js';
|
|
13
14
|
import { PartitionedTimeSeries } from './partitioned-time-series.js';
|
|
14
15
|
import { Sequence } from '../sequence/sequence.js';
|
|
15
|
-
import { IntervalKeyColumn, TimeKeyColumn, TimeRangeKeyColumn, float64ColumnFromArray, stringColumnFromArray, withColumnsRenamed, withColumnsSelected, withKeyColumn, withRowRange, withRowSelection, } from '../columnar/index.js';
|
|
16
|
+
import { ColumnarStore, IntervalKeyColumn, TimeKeyColumn, TimeRangeKeyColumn, float64ColumnFromArray, stringColumnFromArray, withColumnsRenamed, withColumnsSelected, withKeyColumn, withRowRange, withRowSelection, } from '../columnar/index.js';
|
|
16
17
|
import { SeriesStore } from '../live/series-store.js';
|
|
17
18
|
import { parseDuration } from '../core/duration.js';
|
|
18
19
|
import { resolveReducer, bucketStateFor, rollingStateFor, } from '../reducers/index.js';
|
|
@@ -1979,33 +1980,34 @@ export class TimeSeries {
|
|
|
1979
1980
|
this.schema[0],
|
|
1980
1981
|
...resultColumnDefs,
|
|
1981
1982
|
]);
|
|
1983
|
+
// Columnar output path (3C): read the key + source columns straight off
|
|
1984
|
+
// the store and build the result columns directly — no `this.events`
|
|
1985
|
+
// materialization, no per-row `Event`, no row re-validation/re-pack.
|
|
1986
|
+
const store = this.#store.store;
|
|
1987
|
+
const rowCount = store.length;
|
|
1988
|
+
const sourceCols = columnSpecs.map((spec) => store.columns.get(spec.source));
|
|
1982
1989
|
const reducerStates = columnSpecs.map((spec) => isBuiltInAggregateReducer(spec.reducer)
|
|
1983
1990
|
? createRollingReducerState(spec.reducer)
|
|
1984
1991
|
: null);
|
|
1985
|
-
|
|
1986
|
-
const
|
|
1992
|
+
// Begin axis from the key buffer (replaces `this.events.map(e => e.begin())`).
|
|
1993
|
+
const beginTimes = store.keys.begin;
|
|
1994
|
+
// One value accumulator per output column; the result columns are built
|
|
1995
|
+
// from these and assembled via trusted construction below.
|
|
1996
|
+
const outValues = columnSpecs.map(() => new Array(rowCount));
|
|
1987
1997
|
let windowStart = 0;
|
|
1988
1998
|
let windowEnd = 0;
|
|
1989
1999
|
const addEvent = (index) => {
|
|
1990
|
-
const event = this.events[index];
|
|
1991
|
-
const data = event.data();
|
|
1992
2000
|
for (let i = 0; i < reducerStates.length; i++) {
|
|
1993
2001
|
const state = reducerStates[i];
|
|
1994
|
-
if (state)
|
|
1995
|
-
|
|
1996
|
-
state.add(index, data[spec.source]);
|
|
1997
|
-
}
|
|
2002
|
+
if (state)
|
|
2003
|
+
state.add(index, sourceCols[i].read(index));
|
|
1998
2004
|
}
|
|
1999
2005
|
};
|
|
2000
2006
|
const removeEvent = (index) => {
|
|
2001
|
-
const event = this.events[index];
|
|
2002
|
-
const data = event.data();
|
|
2003
2007
|
for (let i = 0; i < reducerStates.length; i++) {
|
|
2004
2008
|
const state = reducerStates[i];
|
|
2005
|
-
if (state)
|
|
2006
|
-
|
|
2007
|
-
state.remove(index, data[spec.source]);
|
|
2008
|
-
}
|
|
2009
|
+
if (state)
|
|
2010
|
+
state.remove(index, sourceCols[i].read(index));
|
|
2009
2011
|
}
|
|
2010
2012
|
};
|
|
2011
2013
|
const snapshotWindow = () => {
|
|
@@ -2015,25 +2017,33 @@ export class TimeSeries {
|
|
|
2015
2017
|
const state = reducerStates[i];
|
|
2016
2018
|
if (state)
|
|
2017
2019
|
return state.snapshot();
|
|
2018
|
-
|
|
2019
|
-
|
|
2020
|
-
|
|
2021
|
-
|
|
2022
|
-
|
|
2023
|
-
|
|
2020
|
+
// Custom reducer: read the window's source values directly off the
|
|
2021
|
+
// column in arrival order — identical value list to the old
|
|
2022
|
+
// `this.events.slice(windowStart, windowEnd)` path.
|
|
2023
|
+
const col = sourceCols[i];
|
|
2024
|
+
const values = [];
|
|
2025
|
+
for (let j = windowStart; j < windowEnd; j++)
|
|
2026
|
+
values.push(col.read(j));
|
|
2024
2027
|
return applyAggregateReducer(spec.reducer, values);
|
|
2025
2028
|
});
|
|
2026
2029
|
};
|
|
2030
|
+
// Scatter one window's aggregated values across every row of an equal-key
|
|
2031
|
+
// group (replaces the per-row frozen `[key, ...aggregated]` tuple write).
|
|
2032
|
+
const writeGroup = (groupStart, groupEnd, aggregated) => {
|
|
2033
|
+
for (let index = groupStart; index < groupEnd; index++) {
|
|
2034
|
+
for (let c = 0; c < outValues.length; c++) {
|
|
2035
|
+
outValues[c][index] = aggregated[c];
|
|
2036
|
+
}
|
|
2037
|
+
}
|
|
2038
|
+
};
|
|
2027
2039
|
if (alignment === 'trailing') {
|
|
2028
|
-
for (let groupStart = 0; groupStart <
|
|
2040
|
+
for (let groupStart = 0; groupStart < rowCount;) {
|
|
2029
2041
|
const anchor = beginTimes[groupStart];
|
|
2030
2042
|
let groupEnd = groupStart + 1;
|
|
2031
|
-
while (groupEnd <
|
|
2032
|
-
beginTimes[groupEnd] === anchor) {
|
|
2043
|
+
while (groupEnd < rowCount && beginTimes[groupEnd] === anchor) {
|
|
2033
2044
|
groupEnd += 1;
|
|
2034
2045
|
}
|
|
2035
|
-
while (windowEnd <
|
|
2036
|
-
beginTimes[windowEnd] <= anchor) {
|
|
2046
|
+
while (windowEnd < rowCount && beginTimes[windowEnd] <= anchor) {
|
|
2037
2047
|
addEvent(windowEnd);
|
|
2038
2048
|
windowEnd += 1;
|
|
2039
2049
|
}
|
|
@@ -2043,22 +2053,15 @@ export class TimeSeries {
|
|
|
2043
2053
|
removeEvent(windowStart);
|
|
2044
2054
|
windowStart += 1;
|
|
2045
2055
|
}
|
|
2046
|
-
|
|
2047
|
-
for (let index = groupStart; index < groupEnd; index++) {
|
|
2048
|
-
resultRows[index] = Object.freeze([
|
|
2049
|
-
this.events[index].key(),
|
|
2050
|
-
...aggregated,
|
|
2051
|
-
]);
|
|
2052
|
-
}
|
|
2056
|
+
writeGroup(groupStart, groupEnd, snapshotWindow());
|
|
2053
2057
|
groupStart = groupEnd;
|
|
2054
2058
|
}
|
|
2055
2059
|
}
|
|
2056
2060
|
else if (alignment === 'leading') {
|
|
2057
|
-
for (let groupStart = 0; groupStart <
|
|
2061
|
+
for (let groupStart = 0; groupStart < rowCount;) {
|
|
2058
2062
|
const anchor = beginTimes[groupStart];
|
|
2059
2063
|
let groupEnd = groupStart + 1;
|
|
2060
|
-
while (groupEnd <
|
|
2061
|
-
beginTimes[groupEnd] === anchor) {
|
|
2064
|
+
while (groupEnd < rowCount && beginTimes[groupEnd] === anchor) {
|
|
2062
2065
|
groupEnd += 1;
|
|
2063
2066
|
}
|
|
2064
2067
|
const lowerBound = anchor;
|
|
@@ -2068,28 +2071,20 @@ export class TimeSeries {
|
|
|
2068
2071
|
windowStart += 1;
|
|
2069
2072
|
}
|
|
2070
2073
|
const upperBound = anchor + windowMs;
|
|
2071
|
-
while (windowEnd <
|
|
2072
|
-
beginTimes[windowEnd] < upperBound) {
|
|
2074
|
+
while (windowEnd < rowCount && beginTimes[windowEnd] < upperBound) {
|
|
2073
2075
|
addEvent(windowEnd);
|
|
2074
2076
|
windowEnd += 1;
|
|
2075
2077
|
}
|
|
2076
|
-
|
|
2077
|
-
for (let index = groupStart; index < groupEnd; index++) {
|
|
2078
|
-
resultRows[index] = Object.freeze([
|
|
2079
|
-
this.events[index].key(),
|
|
2080
|
-
...aggregated,
|
|
2081
|
-
]);
|
|
2082
|
-
}
|
|
2078
|
+
writeGroup(groupStart, groupEnd, snapshotWindow());
|
|
2083
2079
|
groupStart = groupEnd;
|
|
2084
2080
|
}
|
|
2085
2081
|
}
|
|
2086
2082
|
else {
|
|
2087
2083
|
const halfWindow = windowMs / 2;
|
|
2088
|
-
for (let groupStart = 0; groupStart <
|
|
2084
|
+
for (let groupStart = 0; groupStart < rowCount;) {
|
|
2089
2085
|
const anchor = beginTimes[groupStart];
|
|
2090
2086
|
let groupEnd = groupStart + 1;
|
|
2091
|
-
while (groupEnd <
|
|
2092
|
-
beginTimes[groupEnd] === anchor) {
|
|
2087
|
+
while (groupEnd < rowCount && beginTimes[groupEnd] === anchor) {
|
|
2093
2088
|
groupEnd += 1;
|
|
2094
2089
|
}
|
|
2095
2090
|
const lowerBound = anchor - halfWindow;
|
|
@@ -2099,26 +2094,36 @@ export class TimeSeries {
|
|
|
2099
2094
|
windowStart += 1;
|
|
2100
2095
|
}
|
|
2101
2096
|
const upperBound = anchor + halfWindow;
|
|
2102
|
-
while (windowEnd <
|
|
2103
|
-
beginTimes[windowEnd] < upperBound) {
|
|
2097
|
+
while (windowEnd < rowCount && beginTimes[windowEnd] < upperBound) {
|
|
2104
2098
|
addEvent(windowEnd);
|
|
2105
2099
|
windowEnd += 1;
|
|
2106
2100
|
}
|
|
2107
|
-
|
|
2108
|
-
for (let index = groupStart; index < groupEnd; index++) {
|
|
2109
|
-
resultRows[index] = Object.freeze([
|
|
2110
|
-
this.events[index].key(),
|
|
2111
|
-
...aggregated,
|
|
2112
|
-
]);
|
|
2113
|
-
}
|
|
2101
|
+
writeGroup(groupStart, groupEnd, snapshotWindow());
|
|
2114
2102
|
groupStart = groupEnd;
|
|
2115
2103
|
}
|
|
2116
2104
|
}
|
|
2117
|
-
|
|
2118
|
-
|
|
2119
|
-
|
|
2120
|
-
|
|
2121
|
-
|
|
2105
|
+
// Assemble the result columns from the per-column accumulators and wrap via
|
|
2106
|
+
// trusted construction — the key column passes through zero-copy, and there
|
|
2107
|
+
// is no row re-validation / re-pack (the win this path exists for).
|
|
2108
|
+
const outColumns = new Map();
|
|
2109
|
+
for (let c = 0; c < columnSpecs.length; c++) {
|
|
2110
|
+
const kind = resultColumnDefs[c].kind;
|
|
2111
|
+
const values = outValues[c];
|
|
2112
|
+
// The old event-based path re-packed via the constructor's strict intake,
|
|
2113
|
+
// which rejected any defined result that didn't match the declared output
|
|
2114
|
+
// kind — a non-finite number (a `sum` overflow), or a wrong-typed value
|
|
2115
|
+
// (a custom reducer / `kind` override producing a string for a number
|
|
2116
|
+
// column, etc.). Trusted construction skips intake, and the `*FromArray`
|
|
2117
|
+
// builders silently coerce a kind mismatch to a *missing* cell — so
|
|
2118
|
+
// re-assert the same contract here (same value rules AND the same
|
|
2119
|
+
// `ValidationError` class as intake), keeping packed columns clean.
|
|
2120
|
+
// Missing cells (`undefined`, e.g. the minSamples warm-up) are
|
|
2121
|
+
// unaffected. (#225 Codex finding.)
|
|
2122
|
+
assertColumnValuesMatchKind(kind, values, `rolling column '${columnSpecs[c].output}'`);
|
|
2123
|
+
outColumns.set(columnSpecs[c].output, columnFromValuesByKind(kind, values));
|
|
2124
|
+
}
|
|
2125
|
+
const outStore = ColumnarStore.fromTrustedStore(resultSchema, store.keys, outColumns);
|
|
2126
|
+
return TimeSeries.#fromTrustedStore(this.name, resultSchema, outStore);
|
|
2122
2127
|
}
|
|
2123
2128
|
/**
|
|
2124
2129
|
* Example: `series.smooth("value", "ema", { alpha: 0.2 })`.
|