@pond-ts/process 0.54.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 +5914 -0
- package/LICENSE +21 -0
- package/README.md +345 -0
- package/dist/cjs-fallback.cjs +15 -0
- package/dist/column.d.ts +197 -0
- package/dist/column.js +306 -0
- package/dist/errors.d.ts +22 -0
- package/dist/errors.js +25 -0
- package/dist/graph.d.ts +89 -0
- package/dist/graph.js +133 -0
- package/dist/index.d.ts +59 -0
- package/dist/index.js +45 -0
- package/dist/node.d.ts +151 -0
- package/dist/node.js +268 -0
- package/dist/plan/builder.d.ts +138 -0
- package/dist/plan/builder.js +166 -0
- package/dist/plan/fluent.d.ts +93 -0
- package/dist/plan/fluent.js +140 -0
- package/dist/plan/folds.d.ts +25 -0
- package/dist/plan/folds.js +190 -0
- package/dist/plan/graph.d.ts +171 -0
- package/dist/plan/graph.js +658 -0
- package/dist/plan/history.d.ts +61 -0
- package/dist/plan/history.js +82 -0
- package/dist/plan/host.d.ts +173 -0
- package/dist/plan/host.js +234 -0
- package/dist/plan/identity.d.ts +81 -0
- package/dist/plan/identity.js +158 -0
- package/dist/plan/params.d.ts +15 -0
- package/dist/plan/params.js +26 -0
- package/dist/plan/registry.d.ts +162 -0
- package/dist/plan/registry.js +422 -0
- package/dist/plan/run.d.ts +211 -0
- package/dist/plan/run.js +360 -0
- package/dist/plan/slots.d.ts +65 -0
- package/dist/plan/slots.js +114 -0
- package/dist/plan/source.d.ts +49 -0
- package/dist/plan/source.js +54 -0
- package/dist/plan/types.d.ts +376 -0
- package/dist/plan/types.js +20 -0
- package/dist/pool/index.d.ts +15 -0
- package/dist/pool/index.js +12 -0
- package/dist/pool/pool.d.ts +92 -0
- package/dist/pool/pool.js +237 -0
- package/dist/pool/protocol.d.ts +48 -0
- package/dist/pool/protocol.js +9 -0
- package/dist/pool/wire.d.ts +52 -0
- package/dist/pool/wire.js +95 -0
- package/dist/pool/worker.d.ts +22 -0
- package/dist/pool/worker.js +80 -0
- package/dist/port.d.ts +79 -0
- package/dist/port.js +222 -0
- package/dist/source.d.ts +161 -0
- package/dist/source.js +182 -0
- package/dist/types.d.ts +77 -0
- package/dist/types.js +26 -0
- package/package.json +50 -0
package/dist/port.js
ADDED
|
@@ -0,0 +1,222 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Typed ports and the wiring between them.
|
|
3
|
+
*
|
|
4
|
+
* An `Outlet<T>` is a node's typed output; an `Inlet<T>` is a typed
|
|
5
|
+
* input. Because both are generic in the value type and the connect
|
|
6
|
+
* signatures require a matching `T`, wiring a `string` output into a
|
|
7
|
+
* `number` input is a compile error rather than a runtime surprise.
|
|
8
|
+
*
|
|
9
|
+
* Ports also carry the evaluation state. Each outlet holds the cached
|
|
10
|
+
* value plus a **version stamp** that increments only when a recomputed
|
|
11
|
+
* value actually differs (per the port's `equals`). Nodes record the
|
|
12
|
+
* input versions they last computed against, which is what lets a node
|
|
13
|
+
* that was marked dirty skip recomputation when nothing upstream really
|
|
14
|
+
* changed. See `node.ts` for the pull algorithm that uses this.
|
|
15
|
+
*/
|
|
16
|
+
import { CycleError, MissingOutputError, UnconnectedInputError, } from './errors.js';
|
|
17
|
+
/** A node's typed output port. */
|
|
18
|
+
export class Outlet {
|
|
19
|
+
name;
|
|
20
|
+
/** The node this output belongs to. */
|
|
21
|
+
node;
|
|
22
|
+
#value;
|
|
23
|
+
#hasValue = false;
|
|
24
|
+
#version = 0;
|
|
25
|
+
#equals;
|
|
26
|
+
#downstream = new Set();
|
|
27
|
+
/** @internal Constructed by `Node`; not part of the public surface. */
|
|
28
|
+
constructor(node, name, equals) {
|
|
29
|
+
this.node = node;
|
|
30
|
+
this.name = name;
|
|
31
|
+
this.#equals = equals ?? Object.is;
|
|
32
|
+
}
|
|
33
|
+
/**
|
|
34
|
+
* Evaluates this outlet and returns its value.
|
|
35
|
+
*
|
|
36
|
+
* Pull-based: the owning node recomputes only if it is dirty *and* an
|
|
37
|
+
* upstream version actually changed. A clean node returns the cached
|
|
38
|
+
* value without touching the rest of the graph.
|
|
39
|
+
*/
|
|
40
|
+
get() {
|
|
41
|
+
this.node.ensureFresh();
|
|
42
|
+
if (!this.#hasValue) {
|
|
43
|
+
throw new MissingOutputError(`Node '${this.node.kind}' produced no value for output '${this.name}'`);
|
|
44
|
+
}
|
|
45
|
+
return this.#value;
|
|
46
|
+
}
|
|
47
|
+
/**
|
|
48
|
+
* Returns the cached value without evaluating anything, or `undefined`
|
|
49
|
+
* if this outlet has never produced one. Use it to inspect graph state
|
|
50
|
+
* (a debug view, a node inspector) without forcing computation.
|
|
51
|
+
*/
|
|
52
|
+
peek() {
|
|
53
|
+
return this.#value;
|
|
54
|
+
}
|
|
55
|
+
/**
|
|
56
|
+
* How many times this outlet's value has actually changed. Downstream
|
|
57
|
+
* nodes compare against this to decide whether a dirty mark was a real
|
|
58
|
+
* change or a false alarm. `0` means nothing has been produced yet.
|
|
59
|
+
*/
|
|
60
|
+
get version() {
|
|
61
|
+
return this.#version;
|
|
62
|
+
}
|
|
63
|
+
/** The inlets this outlet feeds. */
|
|
64
|
+
get connections() {
|
|
65
|
+
return [...this.#downstream];
|
|
66
|
+
}
|
|
67
|
+
/**
|
|
68
|
+
* Wires this output into `inlet`.
|
|
69
|
+
*
|
|
70
|
+
* @throws {CycleError} if the edge would close a cycle.
|
|
71
|
+
*/
|
|
72
|
+
connect(inlet) {
|
|
73
|
+
link(this, inlet);
|
|
74
|
+
return this;
|
|
75
|
+
}
|
|
76
|
+
/** Removes the edge to `inlet`, if present. */
|
|
77
|
+
disconnect(inlet) {
|
|
78
|
+
if (this.#downstream.has(inlet))
|
|
79
|
+
unlink(inlet);
|
|
80
|
+
return this;
|
|
81
|
+
}
|
|
82
|
+
/** @internal Stores a computed value, bumping the version iff it changed. */
|
|
83
|
+
produce(value) {
|
|
84
|
+
if (this.#hasValue && this.#equals(this.#value, value))
|
|
85
|
+
return;
|
|
86
|
+
this.#value = value;
|
|
87
|
+
this.#hasValue = true;
|
|
88
|
+
this.#version += 1;
|
|
89
|
+
}
|
|
90
|
+
/** @internal */
|
|
91
|
+
invalidateDownstream() {
|
|
92
|
+
for (const inlet of this.#downstream)
|
|
93
|
+
inlet.node.markDirty();
|
|
94
|
+
}
|
|
95
|
+
/** @internal */
|
|
96
|
+
addDownstream(inlet) {
|
|
97
|
+
this.#downstream.add(inlet);
|
|
98
|
+
}
|
|
99
|
+
/** @internal */
|
|
100
|
+
removeDownstream(inlet) {
|
|
101
|
+
this.#downstream.delete(inlet);
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
/** A node's typed input port. */
|
|
105
|
+
export class Inlet {
|
|
106
|
+
name;
|
|
107
|
+
/** The node this input belongs to. */
|
|
108
|
+
node;
|
|
109
|
+
#source;
|
|
110
|
+
#defaultValue;
|
|
111
|
+
#hasDefault;
|
|
112
|
+
/** @internal Constructed by `Node`; not part of the public surface. */
|
|
113
|
+
constructor(node, name, defaultValue) {
|
|
114
|
+
this.node = node;
|
|
115
|
+
this.name = name;
|
|
116
|
+
this.#defaultValue = defaultValue;
|
|
117
|
+
this.#hasDefault = defaultValue !== undefined;
|
|
118
|
+
}
|
|
119
|
+
/**
|
|
120
|
+
* Wires `outlet` into this input, replacing any existing connection.
|
|
121
|
+
*
|
|
122
|
+
* @throws {CycleError} if the edge would close a cycle.
|
|
123
|
+
*/
|
|
124
|
+
connect(outlet) {
|
|
125
|
+
link(outlet, this);
|
|
126
|
+
return this;
|
|
127
|
+
}
|
|
128
|
+
/** Removes the incoming edge, if any. Falls back to the default value. */
|
|
129
|
+
disconnect() {
|
|
130
|
+
unlink(this);
|
|
131
|
+
return this;
|
|
132
|
+
}
|
|
133
|
+
/** Whether an outlet is wired into this input. */
|
|
134
|
+
get connected() {
|
|
135
|
+
return this.#source !== undefined;
|
|
136
|
+
}
|
|
137
|
+
/** The outlet feeding this input, or `undefined` if unconnected. */
|
|
138
|
+
get source() {
|
|
139
|
+
return this.#source;
|
|
140
|
+
}
|
|
141
|
+
/**
|
|
142
|
+
* Pulls the upstream value, evaluating it if stale.
|
|
143
|
+
*
|
|
144
|
+
* @throws {UnconnectedInputError} if unconnected and no default was declared.
|
|
145
|
+
*/
|
|
146
|
+
get() {
|
|
147
|
+
if (this.#source !== undefined)
|
|
148
|
+
return this.#source.get();
|
|
149
|
+
if (this.#hasDefault)
|
|
150
|
+
return this.#defaultValue;
|
|
151
|
+
throw new UnconnectedInputError(`Input '${this.name}' of node '${this.node.kind}' is not connected and has no default value`);
|
|
152
|
+
}
|
|
153
|
+
/**
|
|
154
|
+
* The version of the upstream outlet, or `0` when unconnected. Nodes
|
|
155
|
+
* record this to detect real changes.
|
|
156
|
+
*
|
|
157
|
+
* @internal
|
|
158
|
+
*/
|
|
159
|
+
sourceVersion() {
|
|
160
|
+
return this.#source?.version ?? 0;
|
|
161
|
+
}
|
|
162
|
+
/** @internal */
|
|
163
|
+
setSource(outlet) {
|
|
164
|
+
this.#source = outlet;
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
/**
|
|
168
|
+
* Creates the edge `outlet -> inlet`, replacing whatever the inlet was
|
|
169
|
+
* connected to. Both `Outlet.connect` and `Inlet.connect` route here, so
|
|
170
|
+
* the two directions can't drift apart.
|
|
171
|
+
*/
|
|
172
|
+
function link(outlet, inlet) {
|
|
173
|
+
if (inlet.source === outlet)
|
|
174
|
+
return;
|
|
175
|
+
if (wouldCycle(outlet.node, inlet.node)) {
|
|
176
|
+
throw new CycleError(`Connecting '${outlet.node.kind}.${outlet.name}' to '${inlet.node.kind}.${inlet.name}' would create a cycle`);
|
|
177
|
+
}
|
|
178
|
+
unlink(inlet);
|
|
179
|
+
inlet.setSource(outlet);
|
|
180
|
+
outlet.addDownstream(inlet);
|
|
181
|
+
// `invalidate()`, not `markDirty()`: version stamps are per-outlet but
|
|
182
|
+
// recorded per-inlet-name, so rewiring an inlet to a *different*
|
|
183
|
+
// outlet that happens to sit at the same version number would look
|
|
184
|
+
// like "nothing changed" and serve a stale cached value. A structural
|
|
185
|
+
// change forces the recompute; the equality check in `produce()` still
|
|
186
|
+
// stops the cascade there if the new value matches the old.
|
|
187
|
+
inlet.node.invalidate();
|
|
188
|
+
}
|
|
189
|
+
/** Removes the edge feeding `inlet`, if any. */
|
|
190
|
+
function unlink(inlet) {
|
|
191
|
+
const current = inlet.source;
|
|
192
|
+
if (current === undefined)
|
|
193
|
+
return;
|
|
194
|
+
current.removeDownstream(inlet);
|
|
195
|
+
inlet.setSource(undefined);
|
|
196
|
+
inlet.node.invalidate();
|
|
197
|
+
}
|
|
198
|
+
/**
|
|
199
|
+
* Whether `consumer` is reachable by walking upstream from `producer` —
|
|
200
|
+
* i.e. whether feeding `producer`'s output into `consumer` would close a
|
|
201
|
+
* loop. Runs at connect time, so the graph is acyclic by construction
|
|
202
|
+
* and evaluation never has to guard against infinite recursion.
|
|
203
|
+
*/
|
|
204
|
+
function wouldCycle(producer, consumer) {
|
|
205
|
+
const seen = new Set();
|
|
206
|
+
const stack = [producer];
|
|
207
|
+
while (stack.length > 0) {
|
|
208
|
+
const current = stack.pop();
|
|
209
|
+
if (current === consumer)
|
|
210
|
+
return true;
|
|
211
|
+
if (seen.has(current))
|
|
212
|
+
continue;
|
|
213
|
+
seen.add(current);
|
|
214
|
+
for (const inlet of current.inletList()) {
|
|
215
|
+
const upstream = inlet.source;
|
|
216
|
+
if (upstream !== undefined)
|
|
217
|
+
stack.push(upstream.node);
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
return false;
|
|
221
|
+
}
|
|
222
|
+
//# sourceMappingURL=port.js.map
|
package/dist/source.d.ts
ADDED
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Entry points into a graph: nodes with no inputs whose value comes from
|
|
3
|
+
* outside.
|
|
4
|
+
*/
|
|
5
|
+
import { TimeSeries } from 'pond-ts';
|
|
6
|
+
import type { EventForSchema, SeriesSchema } from 'pond-ts';
|
|
7
|
+
import { ProcessError } from './errors.js';
|
|
8
|
+
import { Node } from './node.js';
|
|
9
|
+
import type { PortSpec } from './types.js';
|
|
10
|
+
/**
|
|
11
|
+
* Spec map for a node that declares no inputs.
|
|
12
|
+
*
|
|
13
|
+
* `never` rather than `PortSpec<never>`: `PortSpec` is invariant in its
|
|
14
|
+
* value type, so `PortSpec<never>` does not satisfy the `PortSpec<any>`
|
|
15
|
+
* element constraint, while bare `never` is assignable to anything.
|
|
16
|
+
*/
|
|
17
|
+
export type NoInputs = Readonly<Record<string, never>>;
|
|
18
|
+
/** Thrown when a source is pulled before a value has been set. */
|
|
19
|
+
export declare class UnsetSourceError extends ProcessError {
|
|
20
|
+
}
|
|
21
|
+
/**
|
|
22
|
+
* A graph input whose value is pushed in from outside via {@link set}.
|
|
23
|
+
*
|
|
24
|
+
* Setting an equal value (per the port's `equals`) does not bump the
|
|
25
|
+
* output version, so downstream nodes revalidate and then skip their own
|
|
26
|
+
* work — no cascade.
|
|
27
|
+
*/
|
|
28
|
+
export declare class SourceNode<T> extends Node<NoInputs, {
|
|
29
|
+
readonly value: PortSpec<T>;
|
|
30
|
+
}> {
|
|
31
|
+
#private;
|
|
32
|
+
constructor(options: {
|
|
33
|
+
readonly kind?: string;
|
|
34
|
+
readonly initial?: T;
|
|
35
|
+
readonly equals?: (a: T, b: T) => boolean;
|
|
36
|
+
});
|
|
37
|
+
/** The current value, or `undefined` if none has been set. */
|
|
38
|
+
get value(): T | undefined;
|
|
39
|
+
/** Replaces the value and invalidates everything downstream. */
|
|
40
|
+
set(value: T): this;
|
|
41
|
+
}
|
|
42
|
+
/**
|
|
43
|
+
* Creates a settable graph input.
|
|
44
|
+
*
|
|
45
|
+
* ```ts
|
|
46
|
+
* const raw = source<TimeSeries<Schema>>();
|
|
47
|
+
* raw.set(series);
|
|
48
|
+
* ```
|
|
49
|
+
*/
|
|
50
|
+
export declare function source<T>(options?: {
|
|
51
|
+
readonly kind?: string;
|
|
52
|
+
readonly initial?: T;
|
|
53
|
+
readonly equals?: (a: T, b: T) => boolean;
|
|
54
|
+
}): SourceNode<T>;
|
|
55
|
+
/**
|
|
56
|
+
* What {@link fromLive} needs from a pond live source: enough to
|
|
57
|
+
* materialize a snapshot, and a way to hear that one is due.
|
|
58
|
+
*
|
|
59
|
+
* Looser than core's `LiveSource<S>` in exactly one place — the
|
|
60
|
+
* `'event'` listener's parameter is `any`. That is not laziness: this
|
|
61
|
+
* package never reads the event, it only needs the notification, and
|
|
62
|
+
* core's incremental operators type that listener differently.
|
|
63
|
+
* `LiveAggregation.on('event', …)` hands back a widened `ClosedEvent`
|
|
64
|
+
* rather than a schema-narrowed `EventForSchema<Out>`, so it does not
|
|
65
|
+
* satisfy `LiveSource<Out>` structurally. Requiring the exact interface
|
|
66
|
+
* would reject precisely the sources worth binding (see {@link fromLive}).
|
|
67
|
+
*
|
|
68
|
+
* `LiveSeries`, `LiveView`, `LiveAggregation`, `LiveRollingAggregation`,
|
|
69
|
+
* and `LiveFusedRolling` all match this.
|
|
70
|
+
*/
|
|
71
|
+
export interface GraphSource<S extends SeriesSchema> {
|
|
72
|
+
readonly name: string;
|
|
73
|
+
readonly schema: S;
|
|
74
|
+
readonly length: number;
|
|
75
|
+
at(index: number): EventForSchema<S> | undefined;
|
|
76
|
+
on(type: 'event', fn: (event: any) => void): () => void;
|
|
77
|
+
}
|
|
78
|
+
/**
|
|
79
|
+
* A live source that can snapshot itself to the batch layer in one call.
|
|
80
|
+
* `LiveSeries` and `LiveView` match; the incremental operators do not.
|
|
81
|
+
*/
|
|
82
|
+
export interface SnapshotSource<S extends SeriesSchema> extends GraphSource<S> {
|
|
83
|
+
toTimeSeries(name?: string): TimeSeries<S>;
|
|
84
|
+
}
|
|
85
|
+
/**
|
|
86
|
+
* A graph input backed by a pond live source. Snapshots lazily; call
|
|
87
|
+
* {@link dispose} to unsubscribe.
|
|
88
|
+
*/
|
|
89
|
+
export declare class LiveSourceNode<S extends SeriesSchema> extends Node<NoInputs, {
|
|
90
|
+
readonly value: PortSpec<TimeSeries<S>>;
|
|
91
|
+
}> {
|
|
92
|
+
#private;
|
|
93
|
+
constructor(live: GraphSource<S>, options: {
|
|
94
|
+
readonly kind?: string;
|
|
95
|
+
});
|
|
96
|
+
/** Stops listening. The node keeps its last snapshot. */
|
|
97
|
+
dispose(): void;
|
|
98
|
+
/** Whether this node is still subscribed. */
|
|
99
|
+
get subscribed(): boolean;
|
|
100
|
+
}
|
|
101
|
+
/**
|
|
102
|
+
* Binds a pond live source into a graph.
|
|
103
|
+
*
|
|
104
|
+
* Incoming events **invalidate** the node; they do not snapshot. The
|
|
105
|
+
* `toTimeSeries()` call happens when something pulls, so a burst of
|
|
106
|
+
* events costs one dirty mark each (O(1) after the first, since dirty
|
|
107
|
+
* propagation cuts off at already-dirty nodes) and exactly one snapshot
|
|
108
|
+
* at the next pull — not one snapshot per event.
|
|
109
|
+
*
|
|
110
|
+
* That keeps the graph on the right side of pond's split: incremental
|
|
111
|
+
* per-event computation stays in the live layer, and the graph composes
|
|
112
|
+
* whole-value batch transforms over snapshots.
|
|
113
|
+
*
|
|
114
|
+
* ```ts
|
|
115
|
+
* const feed = fromLive(liveSeries);
|
|
116
|
+
* const hourly = derive({ s: feed.out.value }, ({ s }) =>
|
|
117
|
+
* s.aggregate(Sequence.every('1h'), { cpu: 'avg' }),
|
|
118
|
+
* );
|
|
119
|
+
* // ... events arrive ...
|
|
120
|
+
* hourly.out.value.get(); // one snapshot, one aggregate
|
|
121
|
+
* feed.dispose();
|
|
122
|
+
* ```
|
|
123
|
+
*
|
|
124
|
+
* ## Bind the aggregation, not the buffer
|
|
125
|
+
*
|
|
126
|
+
* The graph has no partial invalidation: a dirty node recomputes from a
|
|
127
|
+
* whole snapshot, so the pipeline above re-aggregates every retained
|
|
128
|
+
* event on every pull even though only the tail moved. Push the windowed
|
|
129
|
+
* work down into the live layer instead, and bind *its* output:
|
|
130
|
+
*
|
|
131
|
+
* ```ts
|
|
132
|
+
* const feed = fromLive(liveSeries.aggregate(Sequence.every('1h'), { cpu: 'avg' }));
|
|
133
|
+
* const peak = derive({ s: feed.out.value }, ({ s }) => s.column('cpu').max());
|
|
134
|
+
* ```
|
|
135
|
+
*
|
|
136
|
+
* `LiveAggregation` keeps its buckets current per event, so a pull
|
|
137
|
+
* materializes bucket count rather than event count. Measured at 200k
|
|
138
|
+
* events through a 50k-event buffer, pulling every 1k events: **9.05 ms
|
|
139
|
+
* per pull re-aggregating the buffer, 0.04 ms per pull off the live
|
|
140
|
+
* aggregation — 235x.** The gap widens with buffer size, because the
|
|
141
|
+
* first is O(retained events) and the second is O(buckets).
|
|
142
|
+
*
|
|
143
|
+
* **This is a semantic change, not just a faster path.** A live
|
|
144
|
+
* aggregation exposes *closed* buckets. Data is the clock, so the
|
|
145
|
+
* newest bucket stays invisible until an event crosses its end, while
|
|
146
|
+
* re-aggregating the raw buffer includes that partial tail bucket
|
|
147
|
+
* immediately. Two hours of minute data ending at 1h59m reads as one
|
|
148
|
+
* row through the aggregation and two through the buffer. If the
|
|
149
|
+
* current, still-filling bucket has to be on screen, keep
|
|
150
|
+
* re-aggregating the buffer and pay for it — or drive emission with a
|
|
151
|
+
* `Trigger` so buckets close on a schedule you control.
|
|
152
|
+
*
|
|
153
|
+
* This is why `fromLive` takes a {@link GraphSource} rather than a
|
|
154
|
+
* {@link SnapshotSource}: the incremental operators are precisely the
|
|
155
|
+
* ones without a `toTimeSeries()` method, and excluding them would rule
|
|
156
|
+
* out the only answer to the cost above.
|
|
157
|
+
*/
|
|
158
|
+
export declare function fromLive<S extends SeriesSchema>(live: GraphSource<S>, options?: {
|
|
159
|
+
readonly kind?: string;
|
|
160
|
+
}): LiveSourceNode<S>;
|
|
161
|
+
//# sourceMappingURL=source.d.ts.map
|
package/dist/source.js
ADDED
|
@@ -0,0 +1,182 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Entry points into a graph: nodes with no inputs whose value comes from
|
|
3
|
+
* outside.
|
|
4
|
+
*/
|
|
5
|
+
import { TimeSeries } from 'pond-ts';
|
|
6
|
+
import { ProcessError } from './errors.js';
|
|
7
|
+
import { Node } from './node.js';
|
|
8
|
+
/** Thrown when a source is pulled before a value has been set. */
|
|
9
|
+
export class UnsetSourceError extends ProcessError {
|
|
10
|
+
}
|
|
11
|
+
/**
|
|
12
|
+
* A graph input whose value is pushed in from outside via {@link set}.
|
|
13
|
+
*
|
|
14
|
+
* Setting an equal value (per the port's `equals`) does not bump the
|
|
15
|
+
* output version, so downstream nodes revalidate and then skip their own
|
|
16
|
+
* work — no cascade.
|
|
17
|
+
*/
|
|
18
|
+
export class SourceNode extends Node {
|
|
19
|
+
#state;
|
|
20
|
+
constructor(options) {
|
|
21
|
+
// The state cell is created before `super()` so `compute` can close
|
|
22
|
+
// over it — a derived constructor cannot touch `this` until the
|
|
23
|
+
// base constructor returns.
|
|
24
|
+
const state = {
|
|
25
|
+
value: options.initial,
|
|
26
|
+
hasValue: options.initial !== undefined,
|
|
27
|
+
};
|
|
28
|
+
const valuePort = options.equals
|
|
29
|
+
? { equals: options.equals }
|
|
30
|
+
: {};
|
|
31
|
+
super({
|
|
32
|
+
kind: options.kind ?? 'source',
|
|
33
|
+
inputs: {},
|
|
34
|
+
outputs: { value: valuePort },
|
|
35
|
+
compute: () => {
|
|
36
|
+
if (!state.hasValue) {
|
|
37
|
+
throw new UnsetSourceError('Source has no value — call set() before pulling from it');
|
|
38
|
+
}
|
|
39
|
+
return { value: state.value };
|
|
40
|
+
},
|
|
41
|
+
});
|
|
42
|
+
this.#state = state;
|
|
43
|
+
}
|
|
44
|
+
/** The current value, or `undefined` if none has been set. */
|
|
45
|
+
get value() {
|
|
46
|
+
return this.#state.value;
|
|
47
|
+
}
|
|
48
|
+
/** Replaces the value and invalidates everything downstream. */
|
|
49
|
+
set(value) {
|
|
50
|
+
this.#state.value = value;
|
|
51
|
+
this.#state.hasValue = true;
|
|
52
|
+
this.invalidate();
|
|
53
|
+
return this;
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
/**
|
|
57
|
+
* Creates a settable graph input.
|
|
58
|
+
*
|
|
59
|
+
* ```ts
|
|
60
|
+
* const raw = source<TimeSeries<Schema>>();
|
|
61
|
+
* raw.set(series);
|
|
62
|
+
* ```
|
|
63
|
+
*/
|
|
64
|
+
export function source(options = {}) {
|
|
65
|
+
return new SourceNode(options);
|
|
66
|
+
}
|
|
67
|
+
/** Whether a live source can snapshot itself. */
|
|
68
|
+
function canSnapshot(live) {
|
|
69
|
+
return typeof live.toTimeSeries === 'function';
|
|
70
|
+
}
|
|
71
|
+
/**
|
|
72
|
+
* Snapshots any live source to the batch layer.
|
|
73
|
+
*
|
|
74
|
+
* Prefers the source's own `toTimeSeries()`, which reads columns. The
|
|
75
|
+
* `at()` fallback walks events, which pond's design notes call a bug in
|
|
76
|
+
* a bulk path — and it would be, against a raw buffer. It only runs for
|
|
77
|
+
* sources that have no snapshot method, and those are the *aggregation*
|
|
78
|
+
* outputs, whose length is bucket count rather than event count. Walking
|
|
79
|
+
* 24 hourly buckets is not the same act as walking 200k events.
|
|
80
|
+
*/
|
|
81
|
+
function snapshot(live) {
|
|
82
|
+
if (canSnapshot(live))
|
|
83
|
+
return live.toTimeSeries();
|
|
84
|
+
const events = [];
|
|
85
|
+
for (let index = 0; index < live.length; index += 1) {
|
|
86
|
+
const event = live.at(index);
|
|
87
|
+
if (event !== undefined)
|
|
88
|
+
events.push(event);
|
|
89
|
+
}
|
|
90
|
+
return TimeSeries.fromEvents(events, {
|
|
91
|
+
name: live.name,
|
|
92
|
+
schema: live.schema,
|
|
93
|
+
});
|
|
94
|
+
}
|
|
95
|
+
/**
|
|
96
|
+
* A graph input backed by a pond live source. Snapshots lazily; call
|
|
97
|
+
* {@link dispose} to unsubscribe.
|
|
98
|
+
*/
|
|
99
|
+
export class LiveSourceNode extends Node {
|
|
100
|
+
#unsubscribe;
|
|
101
|
+
constructor(live, options) {
|
|
102
|
+
super({
|
|
103
|
+
kind: options.kind ?? 'liveSource',
|
|
104
|
+
inputs: {},
|
|
105
|
+
outputs: { value: {} },
|
|
106
|
+
compute: () => ({ value: snapshot(live) }),
|
|
107
|
+
});
|
|
108
|
+
this.#unsubscribe = live.on('event', () => {
|
|
109
|
+
this.invalidate();
|
|
110
|
+
});
|
|
111
|
+
}
|
|
112
|
+
/** Stops listening. The node keeps its last snapshot. */
|
|
113
|
+
dispose() {
|
|
114
|
+
this.#unsubscribe?.();
|
|
115
|
+
this.#unsubscribe = undefined;
|
|
116
|
+
}
|
|
117
|
+
/** Whether this node is still subscribed. */
|
|
118
|
+
get subscribed() {
|
|
119
|
+
return this.#unsubscribe !== undefined;
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
/**
|
|
123
|
+
* Binds a pond live source into a graph.
|
|
124
|
+
*
|
|
125
|
+
* Incoming events **invalidate** the node; they do not snapshot. The
|
|
126
|
+
* `toTimeSeries()` call happens when something pulls, so a burst of
|
|
127
|
+
* events costs one dirty mark each (O(1) after the first, since dirty
|
|
128
|
+
* propagation cuts off at already-dirty nodes) and exactly one snapshot
|
|
129
|
+
* at the next pull — not one snapshot per event.
|
|
130
|
+
*
|
|
131
|
+
* That keeps the graph on the right side of pond's split: incremental
|
|
132
|
+
* per-event computation stays in the live layer, and the graph composes
|
|
133
|
+
* whole-value batch transforms over snapshots.
|
|
134
|
+
*
|
|
135
|
+
* ```ts
|
|
136
|
+
* const feed = fromLive(liveSeries);
|
|
137
|
+
* const hourly = derive({ s: feed.out.value }, ({ s }) =>
|
|
138
|
+
* s.aggregate(Sequence.every('1h'), { cpu: 'avg' }),
|
|
139
|
+
* );
|
|
140
|
+
* // ... events arrive ...
|
|
141
|
+
* hourly.out.value.get(); // one snapshot, one aggregate
|
|
142
|
+
* feed.dispose();
|
|
143
|
+
* ```
|
|
144
|
+
*
|
|
145
|
+
* ## Bind the aggregation, not the buffer
|
|
146
|
+
*
|
|
147
|
+
* The graph has no partial invalidation: a dirty node recomputes from a
|
|
148
|
+
* whole snapshot, so the pipeline above re-aggregates every retained
|
|
149
|
+
* event on every pull even though only the tail moved. Push the windowed
|
|
150
|
+
* work down into the live layer instead, and bind *its* output:
|
|
151
|
+
*
|
|
152
|
+
* ```ts
|
|
153
|
+
* const feed = fromLive(liveSeries.aggregate(Sequence.every('1h'), { cpu: 'avg' }));
|
|
154
|
+
* const peak = derive({ s: feed.out.value }, ({ s }) => s.column('cpu').max());
|
|
155
|
+
* ```
|
|
156
|
+
*
|
|
157
|
+
* `LiveAggregation` keeps its buckets current per event, so a pull
|
|
158
|
+
* materializes bucket count rather than event count. Measured at 200k
|
|
159
|
+
* events through a 50k-event buffer, pulling every 1k events: **9.05 ms
|
|
160
|
+
* per pull re-aggregating the buffer, 0.04 ms per pull off the live
|
|
161
|
+
* aggregation — 235x.** The gap widens with buffer size, because the
|
|
162
|
+
* first is O(retained events) and the second is O(buckets).
|
|
163
|
+
*
|
|
164
|
+
* **This is a semantic change, not just a faster path.** A live
|
|
165
|
+
* aggregation exposes *closed* buckets. Data is the clock, so the
|
|
166
|
+
* newest bucket stays invisible until an event crosses its end, while
|
|
167
|
+
* re-aggregating the raw buffer includes that partial tail bucket
|
|
168
|
+
* immediately. Two hours of minute data ending at 1h59m reads as one
|
|
169
|
+
* row through the aggregation and two through the buffer. If the
|
|
170
|
+
* current, still-filling bucket has to be on screen, keep
|
|
171
|
+
* re-aggregating the buffer and pay for it — or drive emission with a
|
|
172
|
+
* `Trigger` so buckets close on a schedule you control.
|
|
173
|
+
*
|
|
174
|
+
* This is why `fromLive` takes a {@link GraphSource} rather than a
|
|
175
|
+
* {@link SnapshotSource}: the incremental operators are precisely the
|
|
176
|
+
* ones without a `toTimeSeries()` method, and excluding them would rule
|
|
177
|
+
* out the only answer to the cost above.
|
|
178
|
+
*/
|
|
179
|
+
export function fromLive(live, options = {}) {
|
|
180
|
+
return new LiveSourceNode(live, options);
|
|
181
|
+
}
|
|
182
|
+
//# sourceMappingURL=source.js.map
|
package/dist/types.d.ts
ADDED
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Port declarations and the type-level plumbing that turns them into
|
|
3
|
+
* typed inlet / outlet maps on a node.
|
|
4
|
+
*
|
|
5
|
+
* A `PortSpec<T>` is a compile-time declaration: it carries the port's
|
|
6
|
+
* value type and, optionally, the equality used to decide whether a
|
|
7
|
+
* recomputed value counts as *changed* (see `Outlet` version stamping
|
|
8
|
+
* in `port.ts`). At runtime a spec is a plain object — usually empty.
|
|
9
|
+
*/
|
|
10
|
+
/**
|
|
11
|
+
* Declares one port's value type.
|
|
12
|
+
*
|
|
13
|
+
* `valueType` is a phantom marker: never set at runtime, present only so
|
|
14
|
+
* `T` is inferable from a spec (`PortSpec<infer T>`). Together with the
|
|
15
|
+
* `equals` parameter position it also makes `PortSpec` invariant in `T`,
|
|
16
|
+
* so a `PortSpec<string>` is not silently accepted where a
|
|
17
|
+
* `PortSpec<number>` is required.
|
|
18
|
+
*/
|
|
19
|
+
export interface PortSpec<T> {
|
|
20
|
+
/**
|
|
21
|
+
* Decides whether a newly computed value differs from the cached one.
|
|
22
|
+
* Returning `true` suppresses the version bump, so every downstream
|
|
23
|
+
* node skips recomputation even though it was marked dirty.
|
|
24
|
+
*
|
|
25
|
+
* It also **keeps the previously cached value** — the newly computed
|
|
26
|
+
* one is discarded. That is what makes the cutoff work (downstream
|
|
27
|
+
* `Object.is` checks still see the same reference), but it means a
|
|
28
|
+
* loose `equals` loses data: an `equals` comparing only an `id` field
|
|
29
|
+
* will keep serving the old object after the rest of it changed.
|
|
30
|
+
* Compare everything a consumer can observe.
|
|
31
|
+
*
|
|
32
|
+
* Defaults to `Object.is`. For immutable pond values (`TimeSeries`,
|
|
33
|
+
* `Event`) identity is the right test — a transform that genuinely
|
|
34
|
+
* changed something returns a new instance. Supply a structural
|
|
35
|
+
* comparison when a node produces scalars or small records, where
|
|
36
|
+
* "same number as last time" is common and downstream work is not
|
|
37
|
+
* free.
|
|
38
|
+
*/
|
|
39
|
+
readonly equals?: (a: T, b: T) => boolean;
|
|
40
|
+
/**
|
|
41
|
+
* Value used when the inlet is left unconnected. Without it, pulling
|
|
42
|
+
* through an unconnected inlet throws `UnconnectedInputError`.
|
|
43
|
+
*
|
|
44
|
+
* Outputs ignore this field.
|
|
45
|
+
*/
|
|
46
|
+
readonly defaultValue?: T;
|
|
47
|
+
/** Phantom type marker. Never present at runtime. */
|
|
48
|
+
readonly valueType?: T;
|
|
49
|
+
}
|
|
50
|
+
/**
|
|
51
|
+
* A named set of port declarations, as passed to {@link defineNode}.
|
|
52
|
+
*
|
|
53
|
+
* `PortSpec<any>` is deliberate: `PortSpec` is invariant in its value
|
|
54
|
+
* type, so a bounded element type (`unknown`, `never`) would reject
|
|
55
|
+
* every concrete spec. The constraint exists to pin the *shape*; the
|
|
56
|
+
* per-port types are recovered by inference through {@link PortValues}.
|
|
57
|
+
*/
|
|
58
|
+
export type PortSpecMap = Readonly<Record<string, PortSpec<any>>>;
|
|
59
|
+
/** Extracts the value type carried by a {@link PortSpec}. */
|
|
60
|
+
export type PortValue<P> = P extends PortSpec<infer T> ? T : never;
|
|
61
|
+
/** Maps a spec map to the plain value record `compute` receives / returns. */
|
|
62
|
+
export type PortValues<M> = {
|
|
63
|
+
[K in keyof M]: PortValue<M[K]>;
|
|
64
|
+
};
|
|
65
|
+
/**
|
|
66
|
+
* Declares a port of type `T`.
|
|
67
|
+
*
|
|
68
|
+
* ```ts
|
|
69
|
+
* inputs: { series: port<TimeSeries<Schema>>() },
|
|
70
|
+
* outputs: { mean: port<number>({ equals: (a, b) => a === b }) },
|
|
71
|
+
* ```
|
|
72
|
+
*/
|
|
73
|
+
export declare function port<T>(options?: {
|
|
74
|
+
readonly equals?: (a: T, b: T) => boolean;
|
|
75
|
+
readonly defaultValue?: T;
|
|
76
|
+
}): PortSpec<T>;
|
|
77
|
+
//# sourceMappingURL=types.d.ts.map
|
package/dist/types.js
ADDED
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Port declarations and the type-level plumbing that turns them into
|
|
3
|
+
* typed inlet / outlet maps on a node.
|
|
4
|
+
*
|
|
5
|
+
* A `PortSpec<T>` is a compile-time declaration: it carries the port's
|
|
6
|
+
* value type and, optionally, the equality used to decide whether a
|
|
7
|
+
* recomputed value counts as *changed* (see `Outlet` version stamping
|
|
8
|
+
* in `port.ts`). At runtime a spec is a plain object — usually empty.
|
|
9
|
+
*/
|
|
10
|
+
/**
|
|
11
|
+
* Declares a port of type `T`.
|
|
12
|
+
*
|
|
13
|
+
* ```ts
|
|
14
|
+
* inputs: { series: port<TimeSeries<Schema>>() },
|
|
15
|
+
* outputs: { mean: port<number>({ equals: (a, b) => a === b }) },
|
|
16
|
+
* ```
|
|
17
|
+
*/
|
|
18
|
+
export function port(options = {}) {
|
|
19
|
+
const spec = {};
|
|
20
|
+
if (options.equals !== undefined)
|
|
21
|
+
spec.equals = options.equals;
|
|
22
|
+
if (options.defaultValue !== undefined)
|
|
23
|
+
spec.defaultValue = options.defaultValue;
|
|
24
|
+
return spec;
|
|
25
|
+
}
|
|
26
|
+
//# sourceMappingURL=types.js.map
|