pond-ts 0.1.4 → 0.3.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 (70) hide show
  1. package/README.md +18 -0
  2. package/dist/TimeSeries.d.ts +132 -2
  3. package/dist/TimeSeries.d.ts.map +1 -1
  4. package/dist/TimeSeries.js +1044 -239
  5. package/dist/TimeSeries.js.map +1 -1
  6. package/dist/index.d.ts +1 -1
  7. package/dist/index.d.ts.map +1 -1
  8. package/dist/reducers/avg.d.ts +3 -0
  9. package/dist/reducers/avg.d.ts.map +1 -0
  10. package/dist/reducers/avg.js +45 -0
  11. package/dist/reducers/avg.js.map +1 -0
  12. package/dist/reducers/count.d.ts +3 -0
  13. package/dist/reducers/count.d.ts.map +1 -0
  14. package/dist/reducers/count.js +35 -0
  15. package/dist/reducers/count.js.map +1 -0
  16. package/dist/reducers/difference.d.ts +3 -0
  17. package/dist/reducers/difference.d.ts.map +1 -0
  18. package/dist/reducers/difference.js +48 -0
  19. package/dist/reducers/difference.js.map +1 -0
  20. package/dist/reducers/first.d.ts +3 -0
  21. package/dist/reducers/first.d.ts.map +1 -0
  22. package/dist/reducers/first.js +23 -0
  23. package/dist/reducers/first.js.map +1 -0
  24. package/dist/reducers/index.d.ts +4 -0
  25. package/dist/reducers/index.d.ts.map +1 -0
  26. package/dist/reducers/index.js +35 -0
  27. package/dist/reducers/index.js.map +1 -0
  28. package/dist/reducers/keep.d.ts +3 -0
  29. package/dist/reducers/keep.d.ts.map +1 -0
  30. package/dist/reducers/keep.js +56 -0
  31. package/dist/reducers/keep.js.map +1 -0
  32. package/dist/reducers/last.d.ts +3 -0
  33. package/dist/reducers/last.d.ts.map +1 -0
  34. package/dist/reducers/last.js +23 -0
  35. package/dist/reducers/last.js.map +1 -0
  36. package/dist/reducers/max.d.ts +3 -0
  37. package/dist/reducers/max.d.ts.map +1 -0
  38. package/dist/reducers/max.js +25 -0
  39. package/dist/reducers/max.js.map +1 -0
  40. package/dist/reducers/median.d.ts +3 -0
  41. package/dist/reducers/median.d.ts.map +1 -0
  42. package/dist/reducers/median.js +39 -0
  43. package/dist/reducers/median.js.map +1 -0
  44. package/dist/reducers/min.d.ts +3 -0
  45. package/dist/reducers/min.d.ts.map +1 -0
  46. package/dist/reducers/min.js +25 -0
  47. package/dist/reducers/min.js.map +1 -0
  48. package/dist/reducers/percentile.d.ts +5 -0
  49. package/dist/reducers/percentile.d.ts.map +1 -0
  50. package/dist/reducers/percentile.js +56 -0
  51. package/dist/reducers/percentile.js.map +1 -0
  52. package/dist/reducers/rolling.d.ts +15 -0
  53. package/dist/reducers/rolling.d.ts.map +1 -0
  54. package/dist/reducers/rolling.js +84 -0
  55. package/dist/reducers/rolling.js.map +1 -0
  56. package/dist/reducers/stdev.d.ts +3 -0
  57. package/dist/reducers/stdev.d.ts.map +1 -0
  58. package/dist/reducers/stdev.js +58 -0
  59. package/dist/reducers/stdev.js.map +1 -0
  60. package/dist/reducers/sum.d.ts +3 -0
  61. package/dist/reducers/sum.d.ts.map +1 -0
  62. package/dist/reducers/sum.js +35 -0
  63. package/dist/reducers/sum.js.map +1 -0
  64. package/dist/reducers/types.d.ts +58 -0
  65. package/dist/reducers/types.d.ts.map +1 -0
  66. package/dist/reducers/types.js +2 -0
  67. package/dist/reducers/types.js.map +1 -0
  68. package/dist/types.d.ts +28 -4
  69. package/dist/types.d.ts.map +1 -1
  70. package/package.json +1 -1
@@ -1,10 +1,13 @@
1
+ var _a;
1
2
  import { BoundedSequence } from './BoundedSequence.js';
2
3
  import { parseTimestampString } from './calendar.js';
3
4
  import { Interval } from './Interval.js';
4
5
  import { Time } from './Time.js';
5
6
  import { TimeRange } from './TimeRange.js';
7
+ import { Event } from './Event.js';
6
8
  import { Sequence } from './Sequence.js';
7
9
  import { validateAndNormalize } from './validate.js';
10
+ import { resolveReducer, } from './reducers/index.js';
8
11
  function isObjectRow(value) {
9
12
  return typeof value === 'object' && value !== null && !Array.isArray(value);
10
13
  }
@@ -89,6 +92,23 @@ function parseJsonRows(schema, rows, options = {}) {
89
92
  }));
90
93
  });
91
94
  }
95
+ function serializeJsonKey(kind, key, rowFormat) {
96
+ if (kind === 'time') {
97
+ return key.begin();
98
+ }
99
+ if (kind === 'timeRange') {
100
+ return rowFormat === 'object'
101
+ ? { start: key.begin(), end: key.end() }
102
+ : [key.begin(), key.end()];
103
+ }
104
+ const interval = key;
105
+ return rowFormat === 'object'
106
+ ? { value: interval.value, start: interval.begin(), end: interval.end() }
107
+ : [interval.value, interval.begin(), interval.end()];
108
+ }
109
+ function serializeJsonValue(value) {
110
+ return value === undefined ? null : value;
111
+ }
92
112
  function toRows(schema, events) {
93
113
  return events.map((event) => {
94
114
  const data = event.data();
@@ -100,6 +120,20 @@ function toRows(schema, events) {
100
120
  ]);
101
121
  });
102
122
  }
123
+ function toObjects(schema, events) {
124
+ const keyColumn = schema[0];
125
+ const dataColumns = schema.slice(1);
126
+ return events.map((event) => {
127
+ const row = {
128
+ [keyColumn.name]: event.key(),
129
+ };
130
+ const data = event.data();
131
+ for (const column of dataColumns) {
132
+ row[column.name] = data[column.name];
133
+ }
134
+ return Object.freeze(row);
135
+ });
136
+ }
103
137
  function isEventKey(value) {
104
138
  return (typeof value === 'object' &&
105
139
  value !== null &&
@@ -180,26 +214,38 @@ function eventAnchorTime(key) {
180
214
  return key instanceof Time ? key.begin() : key.timeRange().midpoint();
181
215
  }
182
216
  function loessAt(x, anchors, values, span) {
183
- const points = anchors.flatMap((anchor, index) => {
184
- const value = values[index];
185
- return value === undefined ? [] : [{ x: anchor, y: value }];
186
- });
187
- if (points.length === 0) {
217
+ if (anchors.length === 0) {
188
218
  return undefined;
189
219
  }
190
- if (points.length === 1) {
191
- return points[0].y;
220
+ if (anchors.length === 1) {
221
+ return values[0];
192
222
  }
193
- const neighborCount = Math.max(2, Math.min(points.length, Math.ceil(span * points.length)));
194
- const sortedDistances = points
195
- .map((point) => ({ point, distance: Math.abs(point.x - x) }))
196
- .sort((left, right) => left.distance - right.distance);
197
- const bandwidth = sortedDistances[neighborCount - 1].distance;
223
+ const neighborCount = Math.max(2, Math.min(anchors.length, Math.ceil(span * anchors.length)));
224
+ let start = 0;
225
+ if (neighborCount < anchors.length) {
226
+ let low = 0;
227
+ let high = anchors.length - neighborCount;
228
+ while (low < high) {
229
+ const mid = Math.floor((low + high) / 2);
230
+ if (x - anchors[mid] > anchors[mid + neighborCount] - x) {
231
+ low = mid + 1;
232
+ }
233
+ else {
234
+ high = mid;
235
+ }
236
+ }
237
+ start = low;
238
+ }
239
+ const end = start + neighborCount;
240
+ const bandwidth = Math.max(Math.abs(x - anchors[start]), Math.abs(anchors[end - 1] - x));
198
241
  if (bandwidth === 0) {
199
- const coincident = sortedDistances
200
- .filter((entry) => entry.distance === 0)
201
- .map((entry) => entry.point.y);
202
- return (coincident.reduce((sum, value) => sum + value, 0) / coincident.length);
242
+ const coincidentStart = lowerBound(anchors, x);
243
+ const coincidentEnd = upperBound(anchors, x);
244
+ let coincidentSum = 0;
245
+ for (let index = coincidentStart; index < coincidentEnd; index++) {
246
+ coincidentSum += values[index];
247
+ }
248
+ return coincidentSum / (coincidentEnd - coincidentStart);
203
249
  }
204
250
  let weightedCount = 0;
205
251
  let sumW = 0;
@@ -207,7 +253,10 @@ function loessAt(x, anchors, values, span) {
207
253
  let sumWY = 0;
208
254
  let sumWXX = 0;
209
255
  let sumWXY = 0;
210
- for (const { point, distance } of sortedDistances.slice(0, neighborCount)) {
256
+ for (let index = start; index < end; index++) {
257
+ const pointX = anchors[index];
258
+ const pointY = values[index];
259
+ const distance = Math.abs(pointX - x);
211
260
  const ratio = distance / bandwidth;
212
261
  const weight = ratio >= 1 ? 0 : (1 - ratio ** 3) ** 3;
213
262
  if (weight === 0) {
@@ -215,10 +264,10 @@ function loessAt(x, anchors, values, span) {
215
264
  }
216
265
  weightedCount += 1;
217
266
  sumW += weight;
218
- sumWX += weight * point.x;
219
- sumWY += weight * point.y;
220
- sumWXX += weight * point.x * point.x;
221
- sumWXY += weight * point.x * point.y;
267
+ sumWX += weight * pointX;
268
+ sumWY += weight * pointY;
269
+ sumWXX += weight * pointX * pointX;
270
+ sumWXY += weight * pointX * pointY;
222
271
  }
223
272
  if (weightedCount === 0 || sumW === 0) {
224
273
  return undefined;
@@ -231,6 +280,34 @@ function loessAt(x, anchors, values, span) {
231
280
  const slope = (sumW * sumWXY - sumWX * sumWY) / denominator;
232
281
  return intercept + slope * x;
233
282
  }
283
+ function lowerBound(values, target) {
284
+ let low = 0;
285
+ let high = values.length;
286
+ while (low < high) {
287
+ const mid = Math.floor((low + high) / 2);
288
+ if (values[mid] < target) {
289
+ low = mid + 1;
290
+ }
291
+ else {
292
+ high = mid;
293
+ }
294
+ }
295
+ return low;
296
+ }
297
+ function upperBound(values, target) {
298
+ let low = 0;
299
+ let high = values.length;
300
+ while (low < high) {
301
+ const mid = Math.floor((low + high) / 2);
302
+ if (values[mid] <= target) {
303
+ low = mid + 1;
304
+ }
305
+ else {
306
+ high = mid;
307
+ }
308
+ }
309
+ return low;
310
+ }
234
311
  function makeSmoothSchema(schema, target, output) {
235
312
  if (output === undefined || output === target) {
236
313
  return Object.freeze([
@@ -273,28 +350,60 @@ function bucketOverlapsHalfOpen(bucket, event) {
273
350
  function aggregateValues(operation, values) {
274
351
  const defined = values.filter((value) => value !== undefined);
275
352
  const numeric = defined.filter((value) => typeof value === 'number');
276
- switch (operation) {
277
- case 'count':
278
- return defined.length;
279
- case 'sum':
280
- return numeric.reduce((sum, value) => sum + value, 0);
281
- case 'avg':
282
- return numeric.length === 0
283
- ? undefined
284
- : numeric.reduce((sum, value) => sum + value, 0) / numeric.length;
285
- case 'min':
286
- return numeric.length === 0
287
- ? undefined
288
- : numeric.reduce((left, right) => (left <= right ? left : right));
289
- case 'max':
290
- return numeric.length === 0
291
- ? undefined
292
- : numeric.reduce((left, right) => (left >= right ? left : right));
293
- case 'first':
294
- return defined[0];
295
- case 'last':
296
- return defined[defined.length - 1];
353
+ return resolveReducer(operation).reduce(defined, numeric);
354
+ }
355
+ function isBuiltInAggregateReducer(reducer) {
356
+ return typeof reducer === 'string';
357
+ }
358
+ function applyAggregateReducer(reducer, values) {
359
+ return isBuiltInAggregateReducer(reducer)
360
+ ? aggregateValues(reducer, values)
361
+ : reducer(values);
362
+ }
363
+ function isAggregateOutputSpec(value) {
364
+ return (typeof value === 'object' &&
365
+ value !== null &&
366
+ 'from' in value &&
367
+ 'using' in value);
368
+ }
369
+ function normalizeAggregateColumns(schema, mapping) {
370
+ const columnsByName = new Map(schema.slice(1).map((column) => [column.name, column]));
371
+ const normalized = [];
372
+ for (const [outputName, raw] of Object.entries(mapping)) {
373
+ const sourceName = isAggregateOutputSpec(raw) ? raw.from : outputName;
374
+ const sourceColumn = columnsByName.get(sourceName);
375
+ if (!sourceColumn) {
376
+ throw new TypeError(`aggregate mapping references unknown source column '${sourceName}'`);
377
+ }
378
+ if (sourceColumn.kind !== 'number' &&
379
+ sourceColumn.kind !== 'string' &&
380
+ sourceColumn.kind !== 'boolean') {
381
+ throw new TypeError(`aggregate source column '${sourceName}' must be a scalar value column`);
382
+ }
383
+ const reducer = isAggregateOutputSpec(raw) ? raw.using : raw;
384
+ if (typeof reducer !== 'string' && typeof reducer !== 'function') {
385
+ throw new TypeError(`aggregate reducer for '${outputName}' must be a built-in name or function`);
386
+ }
387
+ const explicitKind = isAggregateOutputSpec(raw) ? raw.kind : undefined;
388
+ const resolvedKind = explicitKind ??
389
+ (typeof reducer === 'string' &&
390
+ resolveReducer(reducer).outputKind === 'number'
391
+ ? 'number'
392
+ : sourceColumn.kind);
393
+ normalized.push({
394
+ output: outputName,
395
+ source: sourceName,
396
+ reducer,
397
+ kind: resolvedKind,
398
+ });
297
399
  }
400
+ return normalized;
401
+ }
402
+ function createAggregateBucketState(operation) {
403
+ return resolveReducer(operation).bucketState();
404
+ }
405
+ function createRollingReducerState(operation) {
406
+ return resolveReducer(operation).rollingState();
298
407
  }
299
408
  function parseDurationInput(value) {
300
409
  if (typeof value === 'number') {
@@ -428,7 +537,7 @@ export class TimeSeries {
428
537
  * the supplied `parse.timeZone`, which defaults to `UTC`.
429
538
  */
430
539
  static fromJSON(input) {
431
- return new TimeSeries({
540
+ return new _a({
432
541
  name: input.name,
433
542
  schema: input.schema,
434
543
  rows: parseJsonRows(input.schema, input.rows, input.parse),
@@ -441,6 +550,61 @@ export class TimeSeries {
441
550
  this.events = validateAndNormalize(input);
442
551
  Object.freeze(this);
443
552
  }
553
+ /**
554
+ * Example: `series.toJSON({ rowFormat: "object" })`.
555
+ * Serializes the series into the JSON-friendly shape accepted by `TimeSeries.fromJSON(...)`.
556
+ *
557
+ * Timestamps are emitted as numbers to avoid time zone ambiguity. Missing payload values are
558
+ * emitted as `null`. By default rows are emitted as arrays; use `rowFormat: "object"` for rows
559
+ * keyed by schema column names.
560
+ */
561
+ toJSON(options = {}) {
562
+ const rowFormat = options.rowFormat ?? 'array';
563
+ const dataColumns = this.schema.slice(1);
564
+ if (rowFormat === 'object') {
565
+ const keyColumn = this.schema[0];
566
+ const rows = this.events.map((event) => {
567
+ const row = {
568
+ [keyColumn.name]: serializeJsonKey(keyColumn.kind, event.key(), rowFormat),
569
+ };
570
+ const data = event.data();
571
+ for (const column of dataColumns) {
572
+ row[column.name] = serializeJsonValue(data[column.name]);
573
+ }
574
+ return Object.freeze(row);
575
+ });
576
+ return {
577
+ name: this.name,
578
+ schema: this.schema,
579
+ rows,
580
+ };
581
+ }
582
+ const rows = this.events.map((event) => {
583
+ const data = event.data();
584
+ return Object.freeze([
585
+ serializeJsonKey(this.schema[0].kind, event.key(), rowFormat),
586
+ ...dataColumns.map((column) => serializeJsonValue(data[column.name])),
587
+ ]);
588
+ });
589
+ return {
590
+ name: this.name,
591
+ schema: this.schema,
592
+ rows,
593
+ };
594
+ }
595
+ /**
596
+ * Builds a series from event data that has already been validated and ordered by the caller.
597
+ *
598
+ * This is intentionally private and only used by transforms that preserve the existing event
599
+ * order and normalized key invariants.
600
+ */
601
+ static #fromTrustedEvents(name, schema, events) {
602
+ const series = Object.create(_a.prototype);
603
+ series.name = name;
604
+ series.schema = Object.freeze(schema.slice());
605
+ series.events = Object.freeze(events.slice());
606
+ return Object.freeze(series);
607
+ }
444
608
  /** Example: `series.firstColumnKind`. Returns the first-column kind from the series schema. */
445
609
  get firstColumnKind() {
446
610
  return this.schema[0].kind;
@@ -449,6 +613,14 @@ export class TimeSeries {
449
613
  get rows() {
450
614
  return toRows(this.schema, this.events);
451
615
  }
616
+ /** Example: `series.toRows()`. Returns normalized row arrays using `Time`/`TimeRange`/`Interval` keys and `undefined` for missing payload values. */
617
+ toRows() {
618
+ return this.rows;
619
+ }
620
+ /** Example: `series.toObjects()`. Returns normalized schema-keyed object rows using temporal key objects and `undefined` for missing payload values. */
621
+ toObjects() {
622
+ return toObjects(this.schema, this.events);
623
+ }
452
624
  /** Example: `series.at(0)`. Returns the event at the supplied zero-based position, if present. */
453
625
  at(index) {
454
626
  return this.events[index];
@@ -466,7 +638,7 @@ export class TimeSeries {
466
638
  /** Example: `series.map(nextSchema, event => event)`. Maps each event into a new typed schema and returns a new series. */
467
639
  map(schema, mapper) {
468
640
  const mappedEvents = this.events.map((event, index) => mapper(event, index));
469
- return new TimeSeries({
641
+ return new _a({
470
642
  name: this.name,
471
643
  schema,
472
644
  rows: toRows(schema, mappedEvents),
@@ -478,10 +650,14 @@ export class TimeSeries {
478
650
  { name: 'time', kind: 'time' },
479
651
  ...this.schema.slice(1),
480
652
  ]);
481
- return new TimeSeries({
653
+ const resultEvents = this.events.map((event) => event.asTime(options));
654
+ if ((options.at ?? 'begin') === 'begin') {
655
+ return _a.#fromTrustedEvents(this.name, schema, resultEvents);
656
+ }
657
+ return new _a({
482
658
  name: this.name,
483
659
  schema,
484
- rows: toRows(schema, this.events.map((event) => event.asTime(options))),
660
+ rows: toRows(schema, resultEvents),
485
661
  });
486
662
  }
487
663
  /** Example: `series.asTimeRange()`. Converts the series key type to `"timeRange"` while preserving each event extent. */
@@ -490,11 +666,8 @@ export class TimeSeries {
490
666
  { name: 'timeRange', kind: 'timeRange' },
491
667
  ...this.schema.slice(1),
492
668
  ]);
493
- return new TimeSeries({
494
- name: this.name,
495
- schema,
496
- rows: toRows(schema, this.events.map((event) => event.asTimeRange())),
497
- });
669
+ const resultEvents = this.events.map((event) => event.asTimeRange());
670
+ return _a.#fromTrustedEvents(this.name, schema, resultEvents);
498
671
  }
499
672
  asInterval(value) {
500
673
  const schema = Object.freeze([
@@ -506,14 +679,13 @@ export class TimeSeries {
506
679
  ? event.asInterval(() => value(event, index))
507
680
  : event.asInterval(value);
508
681
  });
509
- return new TimeSeries({
510
- name: this.name,
511
- schema,
512
- rows: toRows(schema, nextEvents),
513
- });
682
+ return _a.#fromTrustedEvents(this.name, schema, nextEvents);
514
683
  }
515
684
  join(other, options = {}) {
516
- const [left, right] = prepareSeriesForJoin([this, other], options);
685
+ const [left, right] = prepareSeriesForJoin([
686
+ this,
687
+ other,
688
+ ], options);
517
689
  const joinType = options.type ?? 'outer';
518
690
  if (left.firstColumnKind !== right.firstColumnKind) {
519
691
  throw new TypeError('cannot join series with different key kinds');
@@ -566,11 +738,7 @@ export class TimeSeries {
566
738
  rightIndex += 1;
567
739
  }
568
740
  }
569
- return new TimeSeries({
570
- name: left.name,
571
- schema: resultSchema,
572
- rows: toRows(resultSchema, joinedEvents),
573
- });
741
+ return _a.#fromTrustedEvents(left.name, resultSchema, joinedEvents);
574
742
  }
575
743
  /**
576
744
  * Example: `series.align(Sequence.every("1m"))`.
@@ -602,7 +770,7 @@ export class TimeSeries {
602
770
  const range = options.range ?? this.timeRange();
603
771
  const resultSchema = makeAlignedSchema(this.schema);
604
772
  if (!range) {
605
- return new TimeSeries({
773
+ return new _a({
606
774
  name: this.name,
607
775
  schema: resultSchema,
608
776
  rows: [],
@@ -613,103 +781,481 @@ export class TimeSeries {
613
781
  }
614
782
  const intervals = toBoundedSequence(sequence, range, sample).intervals();
615
783
  const valueColumns = this.schema.slice(1);
616
- const alignedRows = intervals.map((interval) => {
617
- const t = sampleTime(interval, sample);
618
- const data = method === 'linear'
619
- ? this.#alignLinearAt(t, valueColumns)
620
- : this.#alignHoldAt(t);
621
- return Object.freeze([
622
- interval,
623
- ...resultSchema
624
- .slice(1)
625
- .map((column) => data[column.name]),
626
- ]);
627
- });
628
- return new TimeSeries({
784
+ const resultColumns = resultSchema.slice(1);
785
+ const alignedRows = method === 'linear'
786
+ ? (() => {
787
+ const cursor = { index: 0 };
788
+ const rows = new Array(intervals.length);
789
+ for (let i = 0; i < intervals.length; i += 1) {
790
+ const interval = intervals[i];
791
+ const t = sampleTime(interval, sample);
792
+ const data = alignLinearAt(this, t, valueColumns, cursor);
793
+ const row = new Array(resultColumns.length + 1);
794
+ row[0] = interval;
795
+ for (let j = 0; j < resultColumns.length; j += 1) {
796
+ const column = resultColumns[j];
797
+ row[j + 1] = data[column.name];
798
+ }
799
+ rows[i] = Object.freeze(row);
800
+ }
801
+ return rows;
802
+ })()
803
+ : intervals.map((interval) => {
804
+ const t = sampleTime(interval, sample);
805
+ const data = alignHoldAt(this, t);
806
+ return Object.freeze([
807
+ interval,
808
+ ...resultSchema
809
+ .slice(1)
810
+ .map((column) => data[column.name]),
811
+ ]);
812
+ });
813
+ return new _a({
629
814
  name: this.name,
630
815
  schema: resultSchema,
631
816
  rows: alignedRows,
632
817
  });
633
818
  }
819
+ aggregate(sequence, mapping, options = {}) {
820
+ return aggregateInternal(this, sequence, mapping, options);
821
+ }
822
+ reduce(columnOrMapping, reducer) {
823
+ if (typeof columnOrMapping === 'string') {
824
+ const values = this.events.map((event) => {
825
+ const data = event.data();
826
+ return data[columnOrMapping];
827
+ });
828
+ return applyAggregateReducer(reducer, values);
829
+ }
830
+ const columns = normalizeAggregateColumns(this.schema, columnOrMapping);
831
+ const result = {};
832
+ for (const col of columns) {
833
+ const values = this.events.map((event) => {
834
+ const data = event.data();
835
+ return data[col.source];
836
+ });
837
+ result[col.output] = applyAggregateReducer(col.reducer, values);
838
+ }
839
+ return result;
840
+ }
841
+ groupBy(column, transform) {
842
+ const buckets = new Map();
843
+ for (const event of this.events) {
844
+ const raw = event.data()[column];
845
+ const key = raw === undefined ? 'undefined' : String(raw);
846
+ let bucket = buckets.get(key);
847
+ if (!bucket) {
848
+ bucket = [];
849
+ buckets.set(key, bucket);
850
+ }
851
+ bucket.push(event);
852
+ }
853
+ const buildGroup = (events) => new _a({
854
+ name: this.name,
855
+ schema: this.schema,
856
+ rows: toRows(this.schema, events),
857
+ });
858
+ if (transform) {
859
+ const result = new Map();
860
+ for (const [key, events] of buckets) {
861
+ result.set(key, transform(buildGroup(events), key));
862
+ }
863
+ return result;
864
+ }
865
+ const result = new Map();
866
+ for (const [key, events] of buckets) {
867
+ result.set(key, buildGroup(events));
868
+ }
869
+ return result;
870
+ }
634
871
  /**
635
- * Example: `series.aggregate(Sequence.every("1m"), { value: "avg" })`.
636
- * Aggregates events into sequence buckets using built-in reducer names.
872
+ * Example: `series.diff("requests")`.
873
+ * Computes per-event differences for the specified numeric columns.
874
+ * Non-specified columns pass through unchanged. The first event gets
875
+ * `undefined` in affected columns unless `{ drop: true }` is passed,
876
+ * which removes the first event entirely.
637
877
  *
638
- * Buckets use half-open membership semantics: `[begin, end)`. Point events contribute to the
639
- * bucket containing their timestamp. Interval-like events contribute to every bucket they
640
- * overlap under half-open overlap rules.
878
+ * Example: `series.diff(["requests", "cpu"])`.
879
+ * Multiple columns can be diffed in a single call.
641
880
  *
642
- * Defaults:
643
- * - `range`: `series.timeRange()`
881
+ * Example: `series.diff("requests", { drop: true })`.
882
+ * Drops the first event instead of keeping it with undefined values.
883
+ */
884
+ diff(columns, options) {
885
+ return this.#diffOrRate('diff', columns, options);
886
+ }
887
+ /**
888
+ * Example: `series.rate("requests")`.
889
+ * Computes the per-second rate of change for the specified numeric columns.
890
+ * Non-specified columns pass through unchanged. The first event gets
891
+ * `undefined` in affected columns unless `{ drop: true }` is passed,
892
+ * which removes the first event entirely.
644
893
  *
645
- * As with `align(...)`, `Sequence` defines the underlying grid and `range` selects which portion
646
- * of that grid is bounded. With `Sequence.every(...)`, the default grid anchor is Unix epoch `0`,
647
- * but the default aggregation range is always the source series extent. When a
648
- * `BoundedSequence` is supplied, its intervals are used directly.
894
+ * Example: `series.rate(["requests", "cpu"])`.
895
+ * Multiple columns can be rated in a single call.
896
+ *
897
+ * Example: `series.rate("requests", { drop: true })`.
898
+ * Drops the first event instead of keeping it with undefined values.
899
+ */
900
+ rate(columns, options) {
901
+ return this.#diffOrRate('rate', columns, options);
902
+ }
903
+ /**
904
+ * Example: `series.pctChange("requests")`.
905
+ * Computes the percentage change `(curr - prev) / prev` for the specified
906
+ * numeric columns. Non-specified columns pass through unchanged. The first
907
+ * event gets `undefined` in affected columns unless `{ drop: true }` is
908
+ * passed.
909
+ */
910
+ pctChange(columns, options) {
911
+ return this.#diffOrRate('pctChange', columns, options);
912
+ }
913
+ #diffOrRate(mode, columns, options) {
914
+ const cols = typeof columns === 'string' ? [columns] : columns;
915
+ const drop = options?.drop === true;
916
+ if (cols.length === 0) {
917
+ throw new Error(`${mode}() requires at least one column name`);
918
+ }
919
+ const targetSet = new Set(cols);
920
+ const outSchema = Object.freeze(this.schema.map((col, i) => {
921
+ if (i === 0)
922
+ return col;
923
+ if (targetSet.has(col.name)) {
924
+ return { ...col, kind: 'number', required: false };
925
+ }
926
+ return col;
927
+ }));
928
+ const events = this.events;
929
+ if (events.length === 0) {
930
+ return _a.#fromTrustedEvents(this.name, outSchema, []);
931
+ }
932
+ const resultEvents = [];
933
+ if (!drop) {
934
+ const firstData = { ...events[0].data() };
935
+ for (const col of cols) {
936
+ firstData[col] = undefined;
937
+ }
938
+ resultEvents.push(new Event(events[0].key(), firstData));
939
+ }
940
+ for (let i = 1; i < events.length; i++) {
941
+ const prev = events[i - 1];
942
+ const curr = events[i];
943
+ const data = { ...curr.data() };
944
+ const dt = mode === 'rate' ? (curr.begin() - prev.begin()) / 1000 : undefined;
945
+ for (const col of cols) {
946
+ const prevVal = prev.data()[col];
947
+ const currVal = data[col];
948
+ if (typeof currVal === 'number' && typeof prevVal === 'number') {
949
+ const delta = currVal - prevVal;
950
+ if (mode === 'pctChange') {
951
+ data[col] = prevVal !== 0 ? delta / prevVal : undefined;
952
+ }
953
+ else if (mode === 'rate') {
954
+ data[col] = dt !== 0 ? delta / dt : undefined;
955
+ }
956
+ else {
957
+ data[col] = delta;
958
+ }
959
+ }
960
+ else {
961
+ data[col] = undefined;
962
+ }
963
+ }
964
+ resultEvents.push(new Event(curr.key(), data));
965
+ }
966
+ return _a.#fromTrustedEvents(this.name, outSchema, resultEvents);
967
+ }
968
+ /**
969
+ * Example: `series.cumulative({ requests: "sum" })`.
970
+ * Computes running accumulations for the specified numeric columns.
971
+ * Non-accumulated columns pass through unchanged.
649
972
  *
650
- * Override `range` when you need multiple series aggregated over the same reporting window,
651
- * including leading or trailing empty buckets outside an individual series extent.
973
+ * Built-in accumulators: `"sum"`, `"max"`, `"min"`, `"count"`.
974
+ * Custom accumulators: `(acc: number, value: number) => number`.
975
+ */
976
+ cumulative(spec) {
977
+ const entries = Object.entries(spec);
978
+ if (entries.length === 0) {
979
+ throw new Error('cumulative() requires at least one column');
980
+ }
981
+ const targetSet = new Set(entries.map(([name]) => name));
982
+ const outSchema = Object.freeze(this.schema.map((col, i) => {
983
+ if (i === 0)
984
+ return col;
985
+ if (targetSet.has(col.name)) {
986
+ return { ...col, kind: 'number', required: false };
987
+ }
988
+ return col;
989
+ }));
990
+ const events = this.events;
991
+ if (events.length === 0) {
992
+ return _a.#fromTrustedEvents(this.name, outSchema, []);
993
+ }
994
+ const state = new Map();
995
+ for (const [name, reducer] of entries) {
996
+ if (typeof reducer === 'function') {
997
+ const fn = reducer;
998
+ state.set(name, {
999
+ acc: undefined,
1000
+ apply: (acc, v) => (acc === undefined ? v : fn(acc, v)),
1001
+ });
1002
+ }
1003
+ else {
1004
+ switch (reducer) {
1005
+ case 'sum':
1006
+ state.set(name, {
1007
+ acc: undefined,
1008
+ apply: (acc, v) => (acc ?? 0) + v,
1009
+ });
1010
+ break;
1011
+ case 'count':
1012
+ state.set(name, { acc: undefined, apply: (acc) => (acc ?? 0) + 1 });
1013
+ break;
1014
+ case 'max':
1015
+ state.set(name, {
1016
+ acc: undefined,
1017
+ apply: (acc, v) => (acc === undefined || v > acc ? v : acc),
1018
+ });
1019
+ break;
1020
+ case 'min':
1021
+ state.set(name, {
1022
+ acc: undefined,
1023
+ apply: (acc, v) => (acc === undefined || v < acc ? v : acc),
1024
+ });
1025
+ break;
1026
+ }
1027
+ }
1028
+ }
1029
+ const resultEvents = [];
1030
+ for (const event of events) {
1031
+ const data = { ...event.data() };
1032
+ for (const [name, s] of state) {
1033
+ const raw = data[name];
1034
+ if (typeof raw === 'number') {
1035
+ s.acc = s.apply(s.acc, raw);
1036
+ data[name] = s.acc;
1037
+ }
1038
+ else {
1039
+ data[name] = s.acc;
1040
+ }
1041
+ }
1042
+ resultEvents.push(new Event(event.key(), data));
1043
+ }
1044
+ return _a.#fromTrustedEvents(this.name, outSchema, resultEvents);
1045
+ }
1046
+ /**
1047
+ * Example: `series.shift("value", 1)`.
1048
+ * Lags column values by N events (positive N) or leads them (negative N).
1049
+ * Vacated positions get `undefined`.
1050
+ */
1051
+ shift(columns, n) {
1052
+ const cols = typeof columns === 'string' ? [columns] : columns;
1053
+ if (cols.length === 0) {
1054
+ throw new Error('shift() requires at least one column name');
1055
+ }
1056
+ if (!Number.isInteger(n)) {
1057
+ throw new Error('shift() requires an integer offset');
1058
+ }
1059
+ const targetSet = new Set(cols);
1060
+ const outSchema = Object.freeze(this.schema.map((col, i) => {
1061
+ if (i === 0)
1062
+ return col;
1063
+ if (targetSet.has(col.name)) {
1064
+ return { ...col, kind: 'number', required: false };
1065
+ }
1066
+ return col;
1067
+ }));
1068
+ const events = this.events;
1069
+ if (events.length === 0) {
1070
+ return _a.#fromTrustedEvents(this.name, outSchema, []);
1071
+ }
1072
+ const resultEvents = [];
1073
+ for (let i = 0; i < events.length; i++) {
1074
+ const data = { ...events[i].data() };
1075
+ const srcIdx = i - n;
1076
+ for (const col of cols) {
1077
+ if (srcIdx >= 0 && srcIdx < events.length) {
1078
+ data[col] = events[srcIdx].data()[col];
1079
+ }
1080
+ else {
1081
+ data[col] = undefined;
1082
+ }
1083
+ }
1084
+ resultEvents.push(new Event(events[i].key(), data));
1085
+ }
1086
+ return _a.#fromTrustedEvents(this.name, outSchema, resultEvents);
1087
+ }
1088
+ /**
1089
+ * Example: `series.fill("hold")`.
1090
+ * Fills `undefined` values using the given strategy for all payload columns.
652
1091
  *
653
- * To align buckets to the beginning of the current series instead of epoch boundaries, override
654
- * the sequence anchor rather than the aggregation range:
1092
+ * Example: `series.fill({ cpu: "linear", host: "hold" })`.
1093
+ * Per-column fill strategies. Unmentioned columns are left as-is.
1094
+ * Strategy names: `"hold"` (forward fill), `"linear"` (time-interpolated),
1095
+ * `"zero"` (fill with 0). A non-string value is used as a literal fill value.
655
1096
  *
656
- * ```ts
657
- * const range = series.timeRange();
658
- * if (!range) {
659
- * throw new Error("empty series");
660
- * }
1097
+ * Example: `series.fill("hold", { limit: 3 })`.
1098
+ * Caps consecutive fills per column. After `limit` consecutive fills, further
1099
+ * `undefined` values are left as-is until a real value resets the counter.
661
1100
  *
662
- * const aggregated = series.aggregate(
663
- * Sequence.every("1m", { anchor: range.begin() }),
664
- * { value: "avg" },
665
- * );
666
- * ```
1101
+ * `"linear"` requires known values on both sides of a gap to interpolate.
1102
+ * Leading and trailing `undefined` runs are left unfilled.
667
1103
  */
668
- aggregate(sequence, mapping, options = {}) {
669
- const range = options.range ?? this.timeRange();
670
- const resultSchema = Object.freeze([
671
- { name: 'interval', kind: 'interval' },
672
- ...this.schema
673
- .slice(1)
674
- .filter((column) => column.name in mapping)
675
- .map((column) => {
676
- const operation = mapping[column.name];
677
- return {
678
- name: column.name,
679
- kind: operation === 'sum' ||
680
- operation === 'avg' ||
681
- operation === 'count'
682
- ? 'number'
683
- : column.kind,
684
- required: false,
685
- };
686
- }),
687
- ]);
688
- if (!range) {
689
- return new TimeSeries({
690
- name: this.name,
691
- schema: resultSchema,
692
- rows: [],
693
- });
1104
+ fill(strategy, options) {
1105
+ if (this.events.length === 0) {
1106
+ return this;
694
1107
  }
695
- const buckets = toBoundedSequence(sequence, range, 'begin').intervals();
696
- const resultRows = buckets.map((bucket) => {
697
- const contributors = this.events.filter((event) => bucketOverlapsHalfOpen(bucket, event.key()));
698
- const aggregated = resultSchema.slice(1).map((column) => {
699
- const operation = mapping[column.name];
700
- const values = contributors.map((event) => {
701
- const data = event.data();
702
- return data[column.name];
703
- });
704
- return aggregateValues(operation, values);
705
- });
706
- return Object.freeze([bucket, ...aggregated]);
707
- });
708
- return new TimeSeries({
709
- name: this.name,
710
- schema: resultSchema,
711
- rows: resultRows,
712
- });
1108
+ const colNames = this.schema.slice(1).map((c) => c.name);
1109
+ const specs = new Map();
1110
+ if (typeof strategy === 'string') {
1111
+ for (const name of colNames) {
1112
+ specs.set(name, { mode: strategy });
1113
+ }
1114
+ }
1115
+ else {
1116
+ const strategies = new Set([
1117
+ 'hold',
1118
+ 'bfill',
1119
+ 'linear',
1120
+ 'zero',
1121
+ ]);
1122
+ for (const [name, spec] of Object.entries(strategy)) {
1123
+ if (typeof spec === 'string' && strategies.has(spec)) {
1124
+ specs.set(name, { mode: spec });
1125
+ }
1126
+ else {
1127
+ specs.set(name, { mode: 'literal', value: spec });
1128
+ }
1129
+ }
1130
+ }
1131
+ const limit = options?.limit;
1132
+ const n = this.events.length;
1133
+ const columns = {};
1134
+ for (const name of colNames) {
1135
+ columns[name] = new Array(n);
1136
+ }
1137
+ for (let i = 0; i < n; i++) {
1138
+ const data = this.events[i].data();
1139
+ for (const name of colNames) {
1140
+ columns[name][i] = data[name];
1141
+ }
1142
+ }
1143
+ const times = new Array(n);
1144
+ for (let i = 0; i < n; i++) {
1145
+ times[i] = this.events[i].begin();
1146
+ }
1147
+ for (const [name, spec] of specs) {
1148
+ const col = columns[name];
1149
+ if (!col)
1150
+ continue;
1151
+ switch (spec.mode) {
1152
+ case 'hold': {
1153
+ let last;
1154
+ let consecutive = 0;
1155
+ for (let i = 0; i < n; i++) {
1156
+ if (col[i] !== undefined) {
1157
+ last = col[i];
1158
+ consecutive = 0;
1159
+ }
1160
+ else if (last !== undefined) {
1161
+ consecutive++;
1162
+ if (limit === undefined || consecutive <= limit) {
1163
+ col[i] = last;
1164
+ }
1165
+ }
1166
+ }
1167
+ break;
1168
+ }
1169
+ case 'bfill': {
1170
+ let next;
1171
+ let consecutive = 0;
1172
+ for (let i = n - 1; i >= 0; i--) {
1173
+ if (col[i] !== undefined) {
1174
+ next = col[i];
1175
+ consecutive = 0;
1176
+ }
1177
+ else if (next !== undefined) {
1178
+ consecutive++;
1179
+ if (limit === undefined || consecutive <= limit) {
1180
+ col[i] = next;
1181
+ }
1182
+ }
1183
+ }
1184
+ break;
1185
+ }
1186
+ case 'zero': {
1187
+ let consecutive = 0;
1188
+ for (let i = 0; i < n; i++) {
1189
+ if (col[i] !== undefined) {
1190
+ consecutive = 0;
1191
+ }
1192
+ else {
1193
+ consecutive++;
1194
+ if (limit === undefined || consecutive <= limit) {
1195
+ col[i] = 0;
1196
+ }
1197
+ }
1198
+ }
1199
+ break;
1200
+ }
1201
+ case 'literal': {
1202
+ let consecutive = 0;
1203
+ for (let i = 0; i < n; i++) {
1204
+ if (col[i] !== undefined) {
1205
+ consecutive = 0;
1206
+ }
1207
+ else {
1208
+ consecutive++;
1209
+ if (limit === undefined || consecutive <= limit) {
1210
+ col[i] = spec.value;
1211
+ }
1212
+ }
1213
+ }
1214
+ break;
1215
+ }
1216
+ case 'linear': {
1217
+ let gapStart = -1;
1218
+ for (let i = 0; i < n; i++) {
1219
+ if (col[i] !== undefined) {
1220
+ if (gapStart >= 0 && gapStart > 0) {
1221
+ const before = col[gapStart - 1];
1222
+ const after = col[i];
1223
+ const t0 = times[gapStart - 1];
1224
+ const t1 = times[i];
1225
+ const span = t1 - t0;
1226
+ const gapLen = i - gapStart;
1227
+ for (let j = gapStart; j < i; j++) {
1228
+ const fillIndex = j - gapStart + 1;
1229
+ if (limit !== undefined && fillIndex > limit)
1230
+ break;
1231
+ if (span === 0) {
1232
+ col[j] = before;
1233
+ }
1234
+ else {
1235
+ const ratio = (times[j] - t0) / span;
1236
+ col[j] = before + (after - before) * ratio;
1237
+ }
1238
+ }
1239
+ }
1240
+ gapStart = -1;
1241
+ }
1242
+ else if (gapStart < 0) {
1243
+ gapStart = i;
1244
+ }
1245
+ }
1246
+ break;
1247
+ }
1248
+ }
1249
+ }
1250
+ const resultEvents = [];
1251
+ for (let i = 0; i < n; i++) {
1252
+ const data = {};
1253
+ for (const name of colNames) {
1254
+ data[name] = columns[name][i];
1255
+ }
1256
+ resultEvents.push(new Event(this.events[i].key(), data));
1257
+ }
1258
+ return _a.#fromTrustedEvents(this.name, this.schema, resultEvents);
713
1259
  }
714
1260
  rolling(sequenceOrWindow, windowOrMapping, mappingOrOptions, maybeOptions = {}) {
715
1261
  const buildResultColumns = () => this.schema
@@ -765,7 +1311,7 @@ export class TimeSeries {
765
1311
  ...buildResultColumns(),
766
1312
  ]);
767
1313
  if (!range) {
768
- return new TimeSeries({
1314
+ return new _a({
769
1315
  name: this.name,
770
1316
  schema: resultSchema,
771
1317
  rows: [],
@@ -776,39 +1322,162 @@ export class TimeSeries {
776
1322
  const anchor = sampleTime(bucket, sample);
777
1323
  const contributors = this.events.filter((candidate) => anchorInWindow(candidate.begin(), anchor));
778
1324
  const aggregated = resultSchema.slice(1).map((column) => {
779
- const operation = mapping[column.name];
1325
+ const reducer = mapping[column.name];
780
1326
  const values = contributors.map((candidate) => {
781
1327
  const data = candidate.data();
782
1328
  return data[column.name];
783
1329
  });
784
- return aggregateValues(operation, values);
1330
+ return applyAggregateReducer(reducer, values);
785
1331
  });
786
1332
  return Object.freeze([bucket, ...aggregated]);
787
1333
  });
788
- return new TimeSeries({
1334
+ return new _a({
789
1335
  name: this.name,
790
1336
  schema: resultSchema,
791
1337
  rows: resultRows,
792
1338
  });
793
1339
  }
1340
+ const resultColumns = buildResultColumns();
794
1341
  const resultSchema = Object.freeze([
795
1342
  this.schema[0],
796
- ...buildResultColumns(),
1343
+ ...resultColumns,
797
1344
  ]);
798
- const resultRows = this.events.map((event) => {
799
- const anchor = event.begin();
800
- const contributors = this.events.filter((candidate) => anchorInWindow(candidate.begin(), anchor));
801
- const aggregated = resultSchema.slice(1).map((column) => {
802
- const operation = mapping[column.name];
803
- const values = contributors.map((candidate) => {
804
- const data = candidate.data();
805
- return data[column.name];
806
- });
807
- return aggregateValues(operation, values);
1345
+ const reducerStates = resultColumns.map((column) => {
1346
+ const reducer = mapping[column.name];
1347
+ return isBuiltInAggregateReducer(reducer)
1348
+ ? createRollingReducerState(reducer)
1349
+ : null;
1350
+ });
1351
+ const beginTimes = this.events.map((event) => event.begin());
1352
+ const resultRows = new Array(this.events.length);
1353
+ let windowStart = 0;
1354
+ let windowEnd = 0;
1355
+ const addEvent = (index) => {
1356
+ const event = this.events[index];
1357
+ const data = event.data();
1358
+ for (let i = 0; i < reducerStates.length; i++) {
1359
+ const state = reducerStates[i];
1360
+ if (state) {
1361
+ const column = resultColumns[i];
1362
+ state.add(index, data[column.name]);
1363
+ }
1364
+ }
1365
+ };
1366
+ const removeEvent = (index) => {
1367
+ const event = this.events[index];
1368
+ const data = event.data();
1369
+ for (let i = 0; i < reducerStates.length; i++) {
1370
+ const state = reducerStates[i];
1371
+ if (state) {
1372
+ const column = resultColumns[i];
1373
+ state.remove(index, data[column.name]);
1374
+ }
1375
+ }
1376
+ };
1377
+ const snapshotWindow = () => resultColumns.map((column, i) => {
1378
+ const state = reducerStates[i];
1379
+ if (state)
1380
+ return state.snapshot();
1381
+ const reducer = mapping[column.name];
1382
+ const values = this.events
1383
+ .slice(windowStart, windowEnd)
1384
+ .map((event) => {
1385
+ const data = event.data();
1386
+ return data[column.name];
808
1387
  });
809
- return Object.freeze([event.key(), ...aggregated]);
1388
+ return applyAggregateReducer(reducer, values);
810
1389
  });
811
- return new TimeSeries({
1390
+ if (alignment === 'trailing') {
1391
+ for (let groupStart = 0; groupStart < this.events.length;) {
1392
+ const anchor = beginTimes[groupStart];
1393
+ let groupEnd = groupStart + 1;
1394
+ while (groupEnd < this.events.length &&
1395
+ beginTimes[groupEnd] === anchor) {
1396
+ groupEnd += 1;
1397
+ }
1398
+ while (windowEnd < this.events.length &&
1399
+ beginTimes[windowEnd] <= anchor) {
1400
+ addEvent(windowEnd);
1401
+ windowEnd += 1;
1402
+ }
1403
+ const lowerBound = anchor - windowMs;
1404
+ while (windowStart < windowEnd &&
1405
+ beginTimes[windowStart] <= lowerBound) {
1406
+ removeEvent(windowStart);
1407
+ windowStart += 1;
1408
+ }
1409
+ const aggregated = snapshotWindow();
1410
+ for (let index = groupStart; index < groupEnd; index++) {
1411
+ resultRows[index] = Object.freeze([
1412
+ this.events[index].key(),
1413
+ ...aggregated,
1414
+ ]);
1415
+ }
1416
+ groupStart = groupEnd;
1417
+ }
1418
+ }
1419
+ else if (alignment === 'leading') {
1420
+ for (let groupStart = 0; groupStart < this.events.length;) {
1421
+ const anchor = beginTimes[groupStart];
1422
+ let groupEnd = groupStart + 1;
1423
+ while (groupEnd < this.events.length &&
1424
+ beginTimes[groupEnd] === anchor) {
1425
+ groupEnd += 1;
1426
+ }
1427
+ const lowerBound = anchor;
1428
+ while (windowStart < windowEnd &&
1429
+ beginTimes[windowStart] < lowerBound) {
1430
+ removeEvent(windowStart);
1431
+ windowStart += 1;
1432
+ }
1433
+ const upperBound = anchor + windowMs;
1434
+ while (windowEnd < this.events.length &&
1435
+ beginTimes[windowEnd] < upperBound) {
1436
+ addEvent(windowEnd);
1437
+ windowEnd += 1;
1438
+ }
1439
+ const aggregated = snapshotWindow();
1440
+ for (let index = groupStart; index < groupEnd; index++) {
1441
+ resultRows[index] = Object.freeze([
1442
+ this.events[index].key(),
1443
+ ...aggregated,
1444
+ ]);
1445
+ }
1446
+ groupStart = groupEnd;
1447
+ }
1448
+ }
1449
+ else {
1450
+ const halfWindow = windowMs / 2;
1451
+ for (let groupStart = 0; groupStart < this.events.length;) {
1452
+ const anchor = beginTimes[groupStart];
1453
+ let groupEnd = groupStart + 1;
1454
+ while (groupEnd < this.events.length &&
1455
+ beginTimes[groupEnd] === anchor) {
1456
+ groupEnd += 1;
1457
+ }
1458
+ const lowerBound = anchor - halfWindow;
1459
+ while (windowStart < windowEnd &&
1460
+ beginTimes[windowStart] < lowerBound) {
1461
+ removeEvent(windowStart);
1462
+ windowStart += 1;
1463
+ }
1464
+ const upperBound = anchor + halfWindow;
1465
+ while (windowEnd < this.events.length &&
1466
+ beginTimes[windowEnd] < upperBound) {
1467
+ addEvent(windowEnd);
1468
+ windowEnd += 1;
1469
+ }
1470
+ const aggregated = snapshotWindow();
1471
+ for (let index = groupStart; index < groupEnd; index++) {
1472
+ resultRows[index] = Object.freeze([
1473
+ this.events[index].key(),
1474
+ ...aggregated,
1475
+ ]);
1476
+ }
1477
+ groupStart = groupEnd;
1478
+ }
1479
+ }
1480
+ return new _a({
812
1481
  name: this.name,
813
1482
  schema: resultSchema,
814
1483
  rows: resultRows,
@@ -873,7 +1542,7 @@ export class TimeSeries {
873
1542
  .map((nextColumn) => nextEvent.data()[nextColumn.name]),
874
1543
  ]);
875
1544
  });
876
- return new TimeSeries({
1545
+ return new _a({
877
1546
  name: this.name,
878
1547
  schema: resultSchema,
879
1548
  rows: resultRows,
@@ -890,8 +1559,17 @@ export class TimeSeries {
890
1559
  span > 1) {
891
1560
  throw new TypeError('loess smoothing requires span to be a finite number in the range (0, 1]');
892
1561
  }
1562
+ const loessAnchors = [];
1563
+ const loessValues = [];
1564
+ for (let index = 0; index < anchors.length; index++) {
1565
+ const value = sourceValues[index];
1566
+ if (typeof value === 'number') {
1567
+ loessAnchors.push(anchors[index]);
1568
+ loessValues.push(value);
1569
+ }
1570
+ }
893
1571
  const resultRows = this.events.map((event, index) => {
894
- const smoothed = loessAt(anchors[index], anchors, sourceValues, span);
1572
+ const smoothed = loessAt(anchors[index], loessAnchors, loessValues, span);
895
1573
  const nextEvent = output === undefined
896
1574
  ? event.set(column, smoothed)
897
1575
  : event.merge({ [output]: smoothed });
@@ -902,7 +1580,7 @@ export class TimeSeries {
902
1580
  .map((nextColumn) => nextEvent.data()[nextColumn.name]),
903
1581
  ]);
904
1582
  });
905
- return new TimeSeries({
1583
+ return new _a({
906
1584
  name: this.name,
907
1585
  schema: resultSchema,
908
1586
  rows: resultRows,
@@ -914,24 +1592,78 @@ export class TimeSeries {
914
1592
  const window = options.window;
915
1593
  const windowMs = parseDurationInput(window);
916
1594
  const alignment = options.alignment ?? 'trailing';
917
- const anchorInWindow = (candidate, anchor) => {
918
- if (alignment === 'trailing') {
919
- return candidate > anchor - windowMs && candidate <= anchor;
1595
+ const resultValues = new Array(this.events.length);
1596
+ let windowStart = 0;
1597
+ let windowEnd = 0;
1598
+ let numericSum = 0;
1599
+ let numericCount = 0;
1600
+ const addEvent = (index) => {
1601
+ const value = sourceValues[index];
1602
+ if (typeof value === 'number') {
1603
+ numericSum += value;
1604
+ numericCount += 1;
920
1605
  }
921
- if (alignment === 'leading') {
922
- return candidate >= anchor && candidate < anchor + windowMs;
1606
+ };
1607
+ const removeEvent = (index) => {
1608
+ const value = sourceValues[index];
1609
+ if (typeof value === 'number') {
1610
+ numericSum -= value;
1611
+ numericCount -= 1;
923
1612
  }
924
- const halfWindow = windowMs / 2;
925
- return (candidate >= anchor - halfWindow && candidate < anchor + halfWindow);
926
1613
  };
1614
+ const snapshot = () => numericCount === 0 ? undefined : numericSum / numericCount;
1615
+ for (let groupStart = 0; groupStart < this.events.length;) {
1616
+ const anchor = anchors[groupStart];
1617
+ let groupEnd = groupStart + 1;
1618
+ while (groupEnd < this.events.length && anchors[groupEnd] === anchor) {
1619
+ groupEnd += 1;
1620
+ }
1621
+ if (alignment === 'trailing') {
1622
+ while (windowEnd < this.events.length &&
1623
+ anchors[windowEnd] <= anchor) {
1624
+ addEvent(windowEnd);
1625
+ windowEnd += 1;
1626
+ }
1627
+ const lowerBound = anchor - windowMs;
1628
+ while (windowStart < windowEnd && anchors[windowStart] <= lowerBound) {
1629
+ removeEvent(windowStart);
1630
+ windowStart += 1;
1631
+ }
1632
+ }
1633
+ else if (alignment === 'leading') {
1634
+ while (windowStart < windowEnd && anchors[windowStart] < anchor) {
1635
+ removeEvent(windowStart);
1636
+ windowStart += 1;
1637
+ }
1638
+ const upperBound = anchor + windowMs;
1639
+ while (windowEnd < this.events.length &&
1640
+ anchors[windowEnd] < upperBound) {
1641
+ addEvent(windowEnd);
1642
+ windowEnd += 1;
1643
+ }
1644
+ }
1645
+ else {
1646
+ const halfWindow = windowMs / 2;
1647
+ while (windowStart < windowEnd &&
1648
+ anchors[windowStart] < anchor - halfWindow) {
1649
+ removeEvent(windowStart);
1650
+ windowStart += 1;
1651
+ }
1652
+ const upperBound = anchor + halfWindow;
1653
+ while (windowEnd < this.events.length &&
1654
+ anchors[windowEnd] < upperBound) {
1655
+ addEvent(windowEnd);
1656
+ windowEnd += 1;
1657
+ }
1658
+ }
1659
+ const smoothed = snapshot();
1660
+ for (let index = groupStart; index < groupEnd; index++) {
1661
+ resultValues[index] = smoothed;
1662
+ }
1663
+ groupStart = groupEnd;
1664
+ }
927
1665
  const resultRows = this.events.map((event, index) => {
928
- const anchor = anchors[index];
929
- const values = sourceValues
930
- .filter((_, candidateIndex) => anchorInWindow(anchors[candidateIndex], anchor))
931
- .flatMap((value) => (value === undefined ? [] : [value]));
932
- const smoothed = values.length === 0
933
- ? undefined
934
- : values.reduce((sum, value) => sum + value, 0) / values.length;
1666
+ const smoothed = resultValues[index];
935
1667
  const nextEvent = output === undefined
936
1668
  ? event.set(column, smoothed)
937
1669
  : event.merge({ [output]: smoothed });
@@ -942,7 +1674,7 @@ export class TimeSeries {
942
1674
  .map((nextColumn) => nextEvent.data()[nextColumn.name]),
943
1675
  ]);
944
1676
  });
945
- return new TimeSeries({
1677
+ return new _a({
946
1678
  name: this.name,
947
1679
  schema: resultSchema,
948
1680
  rows: resultRows,
@@ -950,19 +1682,11 @@ export class TimeSeries {
950
1682
  }
951
1683
  /** Example: `series.slice(0, 10)`. Returns a positional half-open slice of the series. */
952
1684
  slice(beginIndex, endIndex) {
953
- return new TimeSeries({
954
- name: this.name,
955
- schema: this.schema,
956
- rows: toRows(this.schema, this.events.slice(beginIndex, endIndex)),
957
- });
1685
+ return _a.#fromTrustedEvents(this.name, this.schema, this.events.slice(beginIndex, endIndex));
958
1686
  }
959
1687
  /** Example: `series.filter(event => event.get("active"))`. Returns a new series containing only events that match the predicate. */
960
1688
  filter(predicate) {
961
- return new TimeSeries({
962
- name: this.name,
963
- schema: this.schema,
964
- rows: toRows(this.schema, this.events.filter((event, index) => predicate(event, index))),
965
- });
1689
+ return _a.#fromTrustedEvents(this.name, this.schema, this.events.filter((event, index) => predicate(event, index)));
966
1690
  }
967
1691
  /** Example: `series.find(event => event.get("value") > 0)`. Returns the first event that matches the predicate, if any. */
968
1692
  find(predicate) {
@@ -979,7 +1703,9 @@ export class TimeSeries {
979
1703
  /** Example: `series.includesKey(new Time(Date.now()))`. Returns `true` when the series contains an event with an exactly matching key. */
980
1704
  includesKey(key) {
981
1705
  const normalizedKey = toKey(key);
982
- return this.events.some((event) => event.key().equals(normalizedKey));
1706
+ const index = this.bisect(normalizedKey);
1707
+ return (index < this.events.length &&
1708
+ this.events[index].key().equals(normalizedKey));
983
1709
  }
984
1710
  /** Example: `series.bisect(new Time(Date.now()))`. Returns the insertion index for the supplied key in the ordered event sequence. */
985
1711
  bisect(key) {
@@ -1081,11 +1807,7 @@ export class TimeSeries {
1081
1807
  const trimmedEvents = this.events
1082
1808
  .map((event) => event.trim(range))
1083
1809
  .filter((event) => event !== undefined);
1084
- return new TimeSeries({
1085
- name: this.name,
1086
- schema: this.schema,
1087
- rows: toRows(this.schema, trimmedEvents),
1088
- });
1810
+ return _a.#fromTrustedEvents(this.name, this.schema, trimmedEvents);
1089
1811
  }
1090
1812
  /** Example: `series.before(Date.now())`. Returns the events ending strictly before the supplied temporal boundary. */
1091
1813
  before(boundary) {
@@ -1117,11 +1839,7 @@ export class TimeSeries {
1117
1839
  const selectedEvent = event.select(...keys);
1118
1840
  return selectedEvent;
1119
1841
  });
1120
- return new TimeSeries({
1121
- name: this.name,
1122
- schema: resultSchema,
1123
- rows: toRows(resultSchema, resultEvents),
1124
- });
1842
+ return _a.#fromTrustedEvents(this.name, resultSchema, resultEvents);
1125
1843
  }
1126
1844
  /** Example: `series.rename({ cpu: "usage" })`. Returns a new series with payload field names renamed according to the supplied mapping. */
1127
1845
  rename(mapping) {
@@ -1139,11 +1857,7 @@ export class TimeSeries {
1139
1857
  const renamedEvent = event.rename(mapping);
1140
1858
  return renamedEvent;
1141
1859
  });
1142
- return new TimeSeries({
1143
- name: this.name,
1144
- schema: resultSchema,
1145
- rows: toRows(resultSchema, resultEvents),
1146
- });
1860
+ return _a.#fromTrustedEvents(this.name, resultSchema, resultEvents);
1147
1861
  }
1148
1862
  collapse(keys, output, reducer, options) {
1149
1863
  const nextEvents = this.events.map((event) => {
@@ -1171,47 +1885,138 @@ export class TimeSeries {
1171
1885
  : 'string',
1172
1886
  },
1173
1887
  ]);
1174
- return new TimeSeries({
1175
- name: this.name,
1176
- schema: resultSchema,
1177
- rows: toRows(resultSchema, nextEvents),
1178
- });
1888
+ return _a.#fromTrustedEvents(this.name, resultSchema, nextEvents);
1179
1889
  }
1180
1890
  /** Example: `series.length`. Returns the number of events in the series. */
1181
1891
  get length() {
1182
1892
  return this.events.length;
1183
1893
  }
1184
- #alignHoldAt(t) {
1185
- const event = this.atOrBefore(new Time(t));
1186
- return (event?.data() ?? {});
1894
+ }
1895
+ _a = TimeSeries;
1896
+ function aggregateInternal(series, sequence, mapping, options = {}) {
1897
+ const range = options.range ?? series.timeRange();
1898
+ const aggregateColumns = normalizeAggregateColumns(series.schema, mapping);
1899
+ const resultSchema = Object.freeze([
1900
+ { name: 'interval', kind: 'interval' },
1901
+ ...aggregateColumns.map((column) => ({
1902
+ name: column.output,
1903
+ kind: column.kind,
1904
+ required: false,
1905
+ })),
1906
+ ]);
1907
+ if (!range) {
1908
+ return new TimeSeries({
1909
+ name: series.name,
1910
+ schema: resultSchema,
1911
+ rows: [],
1912
+ });
1187
1913
  }
1188
- #alignLinearAt(t, valueColumns) {
1189
- const exact = this.find((event) => event.begin() === t);
1190
- if (exact) {
1191
- return exact.data();
1192
- }
1193
- const previous = this.atOrBefore(new Time(t));
1194
- const next = this.atOrAfter(new Time(t));
1195
- if (!previous || !next || previous.begin() === next.begin()) {
1196
- return (previous?.data() ?? {});
1197
- }
1198
- const ratio = (t - previous.begin()) / (next.begin() - previous.begin());
1199
- const result = {};
1200
- const previousData = previous.data();
1201
- const nextData = next.data();
1202
- for (const column of valueColumns) {
1203
- const previousValue = previousData[column.name];
1204
- const nextValue = nextData[column.name];
1205
- if (column.kind === 'number' &&
1206
- typeof previousValue === 'number' &&
1207
- typeof nextValue === 'number') {
1208
- result[column.name] =
1209
- previousValue + (nextValue - previousValue) * ratio;
1210
- continue;
1914
+ const buckets = toBoundedSequence(sequence, range, 'begin').intervals();
1915
+ const columns = aggregateColumns;
1916
+ if (isTimeKeyed(series)) {
1917
+ const builtInOnly = columns.every((column) => isBuiltInAggregateReducer(column.reducer));
1918
+ let eventIndex = 0;
1919
+ const resultRows = buckets.map((bucket) => {
1920
+ const states = builtInOnly
1921
+ ? columns.map((column) => createAggregateBucketState(column.reducer))
1922
+ : undefined;
1923
+ while (eventIndex < series.events.length &&
1924
+ series.events[eventIndex].begin() < bucket.begin()) {
1925
+ eventIndex += 1;
1926
+ }
1927
+ const bucketStart = eventIndex;
1928
+ let scanIndex = bucketStart;
1929
+ while (scanIndex < series.events.length &&
1930
+ series.events[scanIndex].begin() < bucket.end()) {
1931
+ if (states) {
1932
+ const data = series.events[scanIndex].data();
1933
+ for (let index = 0; index < columns.length; index += 1) {
1934
+ const column = columns[index];
1935
+ states[index].add(data[column.source]);
1936
+ }
1937
+ }
1938
+ scanIndex += 1;
1939
+ }
1940
+ eventIndex = scanIndex;
1941
+ if (states) {
1942
+ return Object.freeze([
1943
+ bucket,
1944
+ ...states.map((state) => state.snapshot()),
1945
+ ]);
1211
1946
  }
1212
- result[column.name] = previousValue;
1947
+ const contributors = series.events.slice(bucketStart, scanIndex);
1948
+ const aggregated = columns.map((column) => {
1949
+ const values = contributors.map((event) => {
1950
+ const data = event.data();
1951
+ return data[column.source];
1952
+ });
1953
+ return applyAggregateReducer(column.reducer, values);
1954
+ });
1955
+ return Object.freeze([bucket, ...aggregated]);
1956
+ });
1957
+ return new TimeSeries({
1958
+ name: series.name,
1959
+ schema: resultSchema,
1960
+ rows: resultRows,
1961
+ });
1962
+ }
1963
+ const resultRows = buckets.map((bucket) => {
1964
+ const contributors = series.events.filter((event) => bucketOverlapsHalfOpen(bucket, event.key()));
1965
+ const aggregated = columns.map((column) => {
1966
+ const values = contributors.map((event) => {
1967
+ const data = event.data();
1968
+ return data[column.source];
1969
+ });
1970
+ return applyAggregateReducer(column.reducer, values);
1971
+ });
1972
+ return Object.freeze([bucket, ...aggregated]);
1973
+ });
1974
+ return new TimeSeries({
1975
+ name: series.name,
1976
+ schema: resultSchema,
1977
+ rows: resultRows,
1978
+ });
1979
+ }
1980
+ function alignHoldAt(series, t) {
1981
+ const event = series.atOrBefore(new Time(t));
1982
+ return (event?.data() ?? {});
1983
+ }
1984
+ function alignLinearAt(series, t, valueColumns, cursor) {
1985
+ const events = series.events;
1986
+ const hasCursor = cursor !== undefined;
1987
+ let index = hasCursor ? cursor.index : series.bisect(t);
1988
+ if (hasCursor) {
1989
+ while (index < events.length && events[index].begin() < t) {
1990
+ index += 1;
1213
1991
  }
1214
- return result;
1992
+ cursor.index = index;
1993
+ }
1994
+ if (index < events.length && events[index].begin() === t) {
1995
+ return events[index].data();
1996
+ }
1997
+ if (index === 0) {
1998
+ return {};
1999
+ }
2000
+ const previous = events[index - 1];
2001
+ const next = events[index];
2002
+ if (!next || previous.begin() === next.begin()) {
2003
+ return previous.data();
2004
+ }
2005
+ const ratio = (t - previous.begin()) / (next.begin() - previous.begin());
2006
+ const result = {};
2007
+ const previousData = previous.data();
2008
+ const nextData = next.data();
2009
+ for (const column of valueColumns) {
2010
+ const previousValue = previousData[column.name];
2011
+ const nextValue = nextData[column.name];
2012
+ if (column.kind === 'number' &&
2013
+ typeof previousValue === 'number' &&
2014
+ typeof nextValue === 'number') {
2015
+ result[column.name] = previousValue + (nextValue - previousValue) * ratio;
2016
+ continue;
2017
+ }
2018
+ result[column.name] = previousValue;
1215
2019
  }
2020
+ return result;
1216
2021
  }
1217
2022
  //# sourceMappingURL=TimeSeries.js.map