pond-ts 0.65.0 → 0.67.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/API.md +211 -34
- package/CHANGELOG.md +1111 -1
- package/dist/live/listener-set.d.ts +18 -0
- package/dist/live/listener-set.js +34 -0
- package/dist/live/live-partitioned-series.d.ts +1 -1
- package/dist/live/live-partitioned-series.js +64 -0
- package/dist/live/live-reduce.d.ts +14 -14
- package/dist/live/live-reduce.js +22 -15
- package/dist/live/live-series.d.ts +18 -21
- package/dist/live/live-series.js +265 -104
- package/dist/live/live-view.d.ts +18 -9
- package/dist/live/live-view.js +72 -8
- package/dist/reducers/index.d.ts +10 -1
- package/dist/reducers/index.js +18 -24
- package/dist/reducers/rolling.d.ts +22 -0
- package/dist/reducers/rolling.js +65 -0
- package/package.json +1 -1
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* [PND-LIVFIX] A listener set whose iteration snapshot is cached: `add` /
|
|
3
|
+
* `delete` invalidate it, `snapshot()` rebuilds it lazily. Dispatch
|
|
4
|
+
* iterates the snapshot, so a listener that subscribes or unsubscribes
|
|
5
|
+
* mid-fan-out neither disturbs the others nor fires for the event it was
|
|
6
|
+
* added during — without allocating an array per event on the hot path
|
|
7
|
+
* (subscriptions change rarely; events arrive at kHz).
|
|
8
|
+
*/
|
|
9
|
+
export declare class ListenerSet<F extends (...args: never[]) => void> {
|
|
10
|
+
#private;
|
|
11
|
+
get size(): number;
|
|
12
|
+
add(fn: F): void;
|
|
13
|
+
delete(fn: F): void;
|
|
14
|
+
clear(): void;
|
|
15
|
+
snapshot(): readonly F[];
|
|
16
|
+
[Symbol.iterator](): Iterator<F>;
|
|
17
|
+
}
|
|
18
|
+
//# sourceMappingURL=listener-set.d.ts.map
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* [PND-LIVFIX] A listener set whose iteration snapshot is cached: `add` /
|
|
3
|
+
* `delete` invalidate it, `snapshot()` rebuilds it lazily. Dispatch
|
|
4
|
+
* iterates the snapshot, so a listener that subscribes or unsubscribes
|
|
5
|
+
* mid-fan-out neither disturbs the others nor fires for the event it was
|
|
6
|
+
* added during — without allocating an array per event on the hot path
|
|
7
|
+
* (subscriptions change rarely; events arrive at kHz).
|
|
8
|
+
*/
|
|
9
|
+
export class ListenerSet {
|
|
10
|
+
#set = new Set();
|
|
11
|
+
#snapshot = null;
|
|
12
|
+
get size() {
|
|
13
|
+
return this.#set.size;
|
|
14
|
+
}
|
|
15
|
+
add(fn) {
|
|
16
|
+
this.#set.add(fn);
|
|
17
|
+
this.#snapshot = null;
|
|
18
|
+
}
|
|
19
|
+
delete(fn) {
|
|
20
|
+
if (this.#set.delete(fn))
|
|
21
|
+
this.#snapshot = null;
|
|
22
|
+
}
|
|
23
|
+
clear() {
|
|
24
|
+
this.#set.clear();
|
|
25
|
+
this.#snapshot = null;
|
|
26
|
+
}
|
|
27
|
+
snapshot() {
|
|
28
|
+
return (this.#snapshot ??= Array.from(this.#set));
|
|
29
|
+
}
|
|
30
|
+
[Symbol.iterator]() {
|
|
31
|
+
return this.snapshot()[Symbol.iterator]();
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
//# sourceMappingURL=listener-set.js.map
|
|
@@ -6,7 +6,7 @@ import { type LiveFillMapping, type LiveFillStrategy } from './live-view.js';
|
|
|
6
6
|
import type { Trigger } from './triggers.js';
|
|
7
7
|
import { type DiffSchema, type EventDataForSchema, type LiveSource, type NumericColumnNameForSchema, type RollingSchema, type SeriesSchema } from '../schema/index.js';
|
|
8
8
|
import type { FusedMapping, FusedMappingValid, FusedPartitionedRollingSchema, ValidatedAggregateMap } from '../schema/index.js';
|
|
9
|
-
import type
|
|
9
|
+
import { type DurationInput } from '../core/duration.js';
|
|
10
10
|
import type { LiveRollingOptions, RollingWindow } from './live-rolling-aggregation.js';
|
|
11
11
|
/**
|
|
12
12
|
* Per-partition retention and grace settings for a partitioned live
|
|
@@ -3,6 +3,14 @@ import { LiveRollingAggregation } from './live-rolling-aggregation.js';
|
|
|
3
3
|
import { LivePartitionedSyncRolling } from './live-partitioned-sync-rolling.js';
|
|
4
4
|
import { LivePartitionedFusedRolling } from './live-partitioned-fused-rolling.js';
|
|
5
5
|
import { makeCumulativeView, makeDiffView, makeFillView, makeStrideSampleView, } from './live-view.js';
|
|
6
|
+
import { parseDuration } from '../core/duration.js';
|
|
7
|
+
/** [PND-LIVFIX] The option keys `partitionBy` accepts; anything else throws. */
|
|
8
|
+
const PARTITION_OPTION_KEYS = new Set([
|
|
9
|
+
'groups',
|
|
10
|
+
'retention',
|
|
11
|
+
'graceWindow',
|
|
12
|
+
'ordering',
|
|
13
|
+
]);
|
|
6
14
|
/** Encoder for partition values → keys. Mirrors the batch single-column case. */
|
|
7
15
|
function partitionKey(event, col) {
|
|
8
16
|
const v = event.data()[col];
|
|
@@ -92,10 +100,33 @@ export class LivePartitionedSeries {
|
|
|
92
100
|
// Pipeline counters for {@link LivePartitionedSeries.stats}.
|
|
93
101
|
// Cumulative since construction; never reset.
|
|
94
102
|
#statsEventsRouted = 0;
|
|
103
|
+
/**
|
|
104
|
+
* [PND-LIVFIX] Quiet-partition age sweep. Per-partition `maxAge` used
|
|
105
|
+
* to run only when a partition received a push, so a partition that
|
|
106
|
+
* went quiet never evicted (audit 2026-06 §4.3). The router now tracks
|
|
107
|
+
* the source watermark (newest key routed) and, when it has advanced
|
|
108
|
+
* by at least `#sweepEveryMs` since the last sweep, asks EVERY
|
|
109
|
+
* partition to age out against it (`LiveSeries._sweepAge`). The
|
|
110
|
+
* throttle keeps the hot path O(1) per event on high-cardinality
|
|
111
|
+
* sources — a sweep is O(partitions) — while bounding how long a quiet
|
|
112
|
+
* partition can overstay `maxAge` to `maxAge / 8`. `Infinity` when the
|
|
113
|
+
* partition retention has no `maxAge` (nothing to sweep).
|
|
114
|
+
*/
|
|
115
|
+
#sweepEveryMs;
|
|
116
|
+
#watermarkMs = -Infinity;
|
|
117
|
+
#lastSweepMs = -Infinity;
|
|
95
118
|
constructor(source, by, options = {}) {
|
|
96
119
|
this.name = source.name;
|
|
97
120
|
this.schema = source.schema;
|
|
98
121
|
this.by = by;
|
|
122
|
+
// [PND-LIVFIX] Unknown options used to be silently ignored, so a JS
|
|
123
|
+
// caller passing e.g. `maxPartitions` got no signal (audit §4.3).
|
|
124
|
+
for (const key of Object.keys(options)) {
|
|
125
|
+
if (!PARTITION_OPTION_KEYS.has(key)) {
|
|
126
|
+
throw new TypeError(`LivePartitionedSeries: unknown option "${key}". Known options: ` +
|
|
127
|
+
`${[...PARTITION_OPTION_KEYS].join(', ')}.`);
|
|
128
|
+
}
|
|
129
|
+
}
|
|
99
130
|
if (!source.schema.some((c) => c.name === by)) {
|
|
100
131
|
throw new TypeError(`LivePartitionedSeries: column "${String(by)}" not in schema`);
|
|
101
132
|
}
|
|
@@ -120,6 +151,11 @@ export class LivePartitionedSeries {
|
|
|
120
151
|
};
|
|
121
152
|
this.#onSpawn = new Set();
|
|
122
153
|
this.#disposers = new Set();
|
|
154
|
+
const maxAge = options.retention?.maxAge;
|
|
155
|
+
this.#sweepEveryMs =
|
|
156
|
+
maxAge === undefined
|
|
157
|
+
? Infinity
|
|
158
|
+
: Math.max(1, Math.floor(parseDuration(maxAge) / 8));
|
|
123
159
|
// Column-native routing path: when the source is a chunked-backed
|
|
124
160
|
// LiveSeries (top-level strict time-keyed), route its appended chunks
|
|
125
161
|
// through `scatterByPartition`-style bucketing into per-partition
|
|
@@ -698,6 +734,32 @@ export class LivePartitionedSeries {
|
|
|
698
734
|
// instead of round-tripping `Event → row → Event` (which would
|
|
699
735
|
// re-validate and re-allocate per event).
|
|
700
736
|
part._pushTrustedEvents([event]);
|
|
737
|
+
this.#advanceWatermark(event.begin());
|
|
738
|
+
}
|
|
739
|
+
/** [PND-LIVFIX] Track the newest routed key and sweep quiet partitions. */
|
|
740
|
+
#advanceWatermark(beginMs) {
|
|
741
|
+
if (this.#sweepEveryMs === Infinity)
|
|
742
|
+
return;
|
|
743
|
+
if (beginMs > this.#watermarkMs)
|
|
744
|
+
this.#watermarkMs = beginMs;
|
|
745
|
+
if (this.#watermarkMs - this.#lastSweepMs < this.#sweepEveryMs)
|
|
746
|
+
return;
|
|
747
|
+
this.#lastSweepMs = this.#watermarkMs;
|
|
748
|
+
// A partition's `'evict'` listener may throw; the sweep must still
|
|
749
|
+
// reach every other partition (else a quiet partition waits another
|
|
750
|
+
// throttle window on someone else's bug). Isolate per partition and
|
|
751
|
+
// surface the first error after the loop, like a push does.
|
|
752
|
+
let first;
|
|
753
|
+
for (const part of this.#partitions.values()) {
|
|
754
|
+
try {
|
|
755
|
+
part._sweepAge(this.#watermarkMs);
|
|
756
|
+
}
|
|
757
|
+
catch (error) {
|
|
758
|
+
first ??= { error };
|
|
759
|
+
}
|
|
760
|
+
}
|
|
761
|
+
if (first)
|
|
762
|
+
throw first.error;
|
|
701
763
|
}
|
|
702
764
|
/**
|
|
703
765
|
* Column-native routing: bucket a source chunk's rows by partition and
|
|
@@ -733,6 +795,8 @@ export class LivePartitionedSeries {
|
|
|
733
795
|
one[0] = i;
|
|
734
796
|
part._stageRows(store, one);
|
|
735
797
|
}
|
|
798
|
+
if (n > 0)
|
|
799
|
+
this.#advanceWatermark(store.beginAt(n - 1));
|
|
736
800
|
}
|
|
737
801
|
/**
|
|
738
802
|
* One-time replay of a chunked source's existing buffer at
|
|
@@ -65,20 +65,20 @@ type EventListener = (event: any) => void;
|
|
|
65
65
|
* snapshot to a `TimeSeries` first via
|
|
66
66
|
* `live.toTimeSeries().reduce(...)`.
|
|
67
67
|
*
|
|
68
|
-
*
|
|
69
|
-
*
|
|
70
|
-
*
|
|
71
|
-
*
|
|
72
|
-
*
|
|
73
|
-
*
|
|
74
|
-
*
|
|
75
|
-
*
|
|
76
|
-
*
|
|
77
|
-
*
|
|
78
|
-
* and
|
|
79
|
-
* the
|
|
80
|
-
*
|
|
81
|
-
*
|
|
68
|
+
* **`reorder` + retention** ([PND-LIVFIX]). The windowed reducers
|
|
69
|
+
* `min` / `max` / `first` / `last` normally keep forward-sliding-window
|
|
70
|
+
* state (a monotone deque, head-removal ordered entries) that assumes
|
|
71
|
+
* eviction removes the oldest-arrived event first — true for `strict` /
|
|
72
|
+
* `drop` and the chunked backing. A `reorder` source evicts its sorted
|
|
73
|
+
* prefix, which may be a later arrival, and those states used to go
|
|
74
|
+
* stale or `undefined` there. Over a `reorder` source this class now
|
|
75
|
+
* selects removal-by-any-index structures for those four reducers (a
|
|
76
|
+
* sorted array for the extrema, an index-keyed map for `first` / `last`)
|
|
77
|
+
* — exact in any eviction order, at O(n) per update instead of O(1),
|
|
78
|
+
* and only on that source shape. `first` / `last` still follow
|
|
79
|
+
* ARRIVAL order (the caveat above), never a value the source has
|
|
80
|
+
* evicted. `samples` and the value-based reducers were already
|
|
81
|
+
* order-independent.
|
|
82
82
|
*
|
|
83
83
|
* **Source contract — `EMITS_EVICT` is load-bearing.** This
|
|
84
84
|
* class's reducer state stays in sync with the source's current
|
package/dist/live/live-reduce.js
CHANGED
|
@@ -68,20 +68,20 @@ import { EMITS_EVICT, } from '../schema/index.js';
|
|
|
68
68
|
* snapshot to a `TimeSeries` first via
|
|
69
69
|
* `live.toTimeSeries().reduce(...)`.
|
|
70
70
|
*
|
|
71
|
-
*
|
|
72
|
-
*
|
|
73
|
-
*
|
|
74
|
-
*
|
|
75
|
-
*
|
|
76
|
-
*
|
|
77
|
-
*
|
|
78
|
-
*
|
|
79
|
-
*
|
|
80
|
-
*
|
|
81
|
-
* and
|
|
82
|
-
* the
|
|
83
|
-
*
|
|
84
|
-
*
|
|
71
|
+
* **`reorder` + retention** ([PND-LIVFIX]). The windowed reducers
|
|
72
|
+
* `min` / `max` / `first` / `last` normally keep forward-sliding-window
|
|
73
|
+
* state (a monotone deque, head-removal ordered entries) that assumes
|
|
74
|
+
* eviction removes the oldest-arrived event first — true for `strict` /
|
|
75
|
+
* `drop` and the chunked backing. A `reorder` source evicts its sorted
|
|
76
|
+
* prefix, which may be a later arrival, and those states used to go
|
|
77
|
+
* stale or `undefined` there. Over a `reorder` source this class now
|
|
78
|
+
* selects removal-by-any-index structures for those four reducers (a
|
|
79
|
+
* sorted array for the extrema, an index-keyed map for `first` / `last`)
|
|
80
|
+
* — exact in any eviction order, at O(n) per update instead of O(1),
|
|
81
|
+
* and only on that source shape. `first` / `last` still follow
|
|
82
|
+
* ARRIVAL order (the caveat above), never a value the source has
|
|
83
|
+
* evicted. `samples` and the value-based reducers were already
|
|
84
|
+
* order-independent.
|
|
85
85
|
*
|
|
86
86
|
* **Source contract — `EMITS_EVICT` is load-bearing.** This
|
|
87
87
|
* class's reducer state stays in sync with the source's current
|
|
@@ -182,7 +182,14 @@ export class LiveReduce {
|
|
|
182
182
|
// the live aggregation surface; keeps `LiveReduce`'s reducer
|
|
183
183
|
// semantics identical to `aggregate` / `rolling`.
|
|
184
184
|
this.#columns = normalizeAggregateColumns(source.schema, mapping);
|
|
185
|
-
|
|
185
|
+
// [PND-LIVFIX] A `reorder` source evicts its sorted prefix, which can
|
|
186
|
+
// be a later arrival than the FIFO frontier; the windowed states for
|
|
187
|
+
// `min` / `max` / `first` / `last` must then accept removal of any
|
|
188
|
+
// index. Ask the source (internal getter on `LiveSeries`); anything
|
|
189
|
+
// else is append-only.
|
|
190
|
+
const evictionOrder = source._evictionOrder ??
|
|
191
|
+
'arrival';
|
|
192
|
+
this.#states = this.#columns.map((c) => rollingStateFor(c.reducer, evictionOrder));
|
|
186
193
|
// Output schema: source's first (time/keyed) column + each
|
|
187
194
|
// reducer's output column. Matches LiveRollingAggregation's
|
|
188
195
|
// schema shape for consistency.
|
|
@@ -41,14 +41,6 @@ export type LiveSeriesOptions<S extends SeriesSchema> = {
|
|
|
41
41
|
type EventListener<S extends SeriesSchema> = (event: EventForSchema<S>) => void;
|
|
42
42
|
type BatchListener<S extends SeriesSchema> = (events: ReadonlyArray<EventForSchema<S>>) => void;
|
|
43
43
|
type EvictListener<S extends SeriesSchema> = (events: ReadonlyArray<EventForSchema<S>>) => void;
|
|
44
|
-
/**
|
|
45
|
-
* A mutable, append-optimized event buffer for live ingestion — the entry
|
|
46
|
-
* point to the live layer. `push` validates rows against the same schema
|
|
47
|
-
* as `TimeSeries`, retention (`maxEvents` / `maxAge`) bounds the buffer,
|
|
48
|
-
* and subscribers react on the `'event'`, `'batch'`, and `'evict'`
|
|
49
|
-
* channels; snapshot to the batch layer at any time with `toTimeSeries()`.
|
|
50
|
-
* Example: `new LiveSeries({ name, schema, retention: { maxEvents: 10_000 } })`.
|
|
51
|
-
*/
|
|
52
44
|
export declare class LiveSeries<S extends SeriesSchema> {
|
|
53
45
|
#private;
|
|
54
46
|
readonly name: string;
|
|
@@ -100,19 +92,24 @@ export declare class LiveSeries<S extends SeriesSchema> {
|
|
|
100
92
|
* **Commit granularity differs by backing.** On the `Event[]`
|
|
101
93
|
* backing each row is appended then its `'event'` fires, so a
|
|
102
94
|
* handler observes `length` grow row-by-row (`1, 2, …`) within one
|
|
103
|
-
* `pushMany
|
|
104
|
-
*
|
|
105
|
-
*
|
|
106
|
-
*
|
|
107
|
-
*
|
|
108
|
-
*
|
|
109
|
-
*
|
|
110
|
-
*
|
|
111
|
-
*
|
|
112
|
-
*
|
|
113
|
-
*
|
|
114
|
-
*
|
|
115
|
-
*
|
|
95
|
+
* `pushMany`. On the chunked columnar backing (top-level `strict`
|
|
96
|
+
* time-keyed series) the whole batch is appended as one chunk
|
|
97
|
+
* *before* any `'event'` fires, so a handler sees the full post-batch
|
|
98
|
+
* `length` for every event of the batch. This is intrinsic to
|
|
99
|
+
* all-or-nothing columnar append — per-row commit would reintroduce
|
|
100
|
+
* the per-row `Event` cost the chunked backing exists to avoid. The
|
|
101
|
+
* cross-backing contract callers can rely on: every
|
|
102
|
+
* successfully-ingested row fires exactly one `'event'`, in order,
|
|
103
|
+
* before `'batch'`/`'evict'`.
|
|
104
|
+
*
|
|
105
|
+
* **Listener errors are isolated** ([PND-LIVFIX]). A listener that
|
|
106
|
+
* throws does not stop the push: every row is still committed, the
|
|
107
|
+
* remaining listeners still run, retention still runs, and `'batch'` /
|
|
108
|
+
* `'evict'` still fire. The first error is rethrown to the caller once
|
|
109
|
+
* the push has completed, so `length`, `ingested` and every subscriber
|
|
110
|
+
* (a derived `filter()` view included) agree afterwards. A push made
|
|
111
|
+
* from inside a listener is queued and runs after the current push,
|
|
112
|
+
* in arrival order, so emission stays monotonic.
|
|
116
113
|
*/
|
|
117
114
|
pushMany(rows: ReadonlyArray<RowForSchema<S>>): void;
|
|
118
115
|
/**
|