@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.
Files changed (57) hide show
  1. package/CHANGELOG.md +5914 -0
  2. package/LICENSE +21 -0
  3. package/README.md +345 -0
  4. package/dist/cjs-fallback.cjs +15 -0
  5. package/dist/column.d.ts +197 -0
  6. package/dist/column.js +306 -0
  7. package/dist/errors.d.ts +22 -0
  8. package/dist/errors.js +25 -0
  9. package/dist/graph.d.ts +89 -0
  10. package/dist/graph.js +133 -0
  11. package/dist/index.d.ts +59 -0
  12. package/dist/index.js +45 -0
  13. package/dist/node.d.ts +151 -0
  14. package/dist/node.js +268 -0
  15. package/dist/plan/builder.d.ts +138 -0
  16. package/dist/plan/builder.js +166 -0
  17. package/dist/plan/fluent.d.ts +93 -0
  18. package/dist/plan/fluent.js +140 -0
  19. package/dist/plan/folds.d.ts +25 -0
  20. package/dist/plan/folds.js +190 -0
  21. package/dist/plan/graph.d.ts +171 -0
  22. package/dist/plan/graph.js +658 -0
  23. package/dist/plan/history.d.ts +61 -0
  24. package/dist/plan/history.js +82 -0
  25. package/dist/plan/host.d.ts +173 -0
  26. package/dist/plan/host.js +234 -0
  27. package/dist/plan/identity.d.ts +81 -0
  28. package/dist/plan/identity.js +158 -0
  29. package/dist/plan/params.d.ts +15 -0
  30. package/dist/plan/params.js +26 -0
  31. package/dist/plan/registry.d.ts +162 -0
  32. package/dist/plan/registry.js +422 -0
  33. package/dist/plan/run.d.ts +211 -0
  34. package/dist/plan/run.js +360 -0
  35. package/dist/plan/slots.d.ts +65 -0
  36. package/dist/plan/slots.js +114 -0
  37. package/dist/plan/source.d.ts +49 -0
  38. package/dist/plan/source.js +54 -0
  39. package/dist/plan/types.d.ts +376 -0
  40. package/dist/plan/types.js +20 -0
  41. package/dist/pool/index.d.ts +15 -0
  42. package/dist/pool/index.js +12 -0
  43. package/dist/pool/pool.d.ts +92 -0
  44. package/dist/pool/pool.js +237 -0
  45. package/dist/pool/protocol.d.ts +48 -0
  46. package/dist/pool/protocol.js +9 -0
  47. package/dist/pool/wire.d.ts +52 -0
  48. package/dist/pool/wire.js +95 -0
  49. package/dist/pool/worker.d.ts +22 -0
  50. package/dist/pool/worker.js +80 -0
  51. package/dist/port.d.ts +79 -0
  52. package/dist/port.js +222 -0
  53. package/dist/source.d.ts +161 -0
  54. package/dist/source.js +182 -0
  55. package/dist/types.d.ts +77 -0
  56. package/dist/types.js +26 -0
  57. 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
@@ -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
@@ -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