@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/node.d.ts
ADDED
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Nodes: units of computation with typed ports, and the pull-based
|
|
3
|
+
* evaluation that drives them.
|
|
4
|
+
*
|
|
5
|
+
* ## Evaluation model
|
|
6
|
+
*
|
|
7
|
+
* Two mechanisms, doing two different jobs:
|
|
8
|
+
*
|
|
9
|
+
* - **Dirty marking (push)** tells a node "something upstream moved,
|
|
10
|
+
* revalidate before answering." It propagates from the changed node
|
|
11
|
+
* through every downstream edge, cutting off at nodes already marked,
|
|
12
|
+
* so a change costs O(affected nodes) regardless of graph size. A
|
|
13
|
+
* clean node answers `get()` from cache without walking anything.
|
|
14
|
+
*
|
|
15
|
+
* - **Version stamps (pull)** tell a node whether the change was real.
|
|
16
|
+
* Each outlet's version increments only when a recomputed value
|
|
17
|
+
* actually differs, and each node records the input versions it last
|
|
18
|
+
* computed against. A dirty node whose input versions all match skips
|
|
19
|
+
* `compute` entirely and stays cached.
|
|
20
|
+
*
|
|
21
|
+
* The second is what makes the layer worth having over plain function
|
|
22
|
+
* calls: setting a source that happens to produce an identical
|
|
23
|
+
* downstream value stops the cascade there, so expensive transforms
|
|
24
|
+
* further down never run. Dirty marking alone (as in a naive dataflow
|
|
25
|
+
* graph) would recompute the whole subtree on every touch.
|
|
26
|
+
*/
|
|
27
|
+
import { Inlet, Outlet } from './port.js';
|
|
28
|
+
import type { PortSpec, PortSpecMap, PortValue, PortValues } from './types.js';
|
|
29
|
+
/** The inlet map exposed as `node.in`. */
|
|
30
|
+
export type InletsFor<In extends PortSpecMap> = {
|
|
31
|
+
readonly [K in keyof In]: Inlet<PortValue<In[K]>>;
|
|
32
|
+
};
|
|
33
|
+
/** The outlet map exposed as `node.out`. */
|
|
34
|
+
export type OutletsFor<Out extends PortSpecMap> = {
|
|
35
|
+
readonly [K in keyof Out]: Outlet<PortValue<Out[K]>>;
|
|
36
|
+
};
|
|
37
|
+
/** Declares a reusable node type. Passed to {@link defineNode}. */
|
|
38
|
+
export interface NodeSpec<In extends PortSpecMap, Out extends PortSpecMap> {
|
|
39
|
+
/** Stable identifier for this node type, used in errors and `toJSON()`. */
|
|
40
|
+
readonly kind: string;
|
|
41
|
+
readonly inputs: In;
|
|
42
|
+
readonly outputs: Out;
|
|
43
|
+
/**
|
|
44
|
+
* Produces every declared output from the current input values.
|
|
45
|
+
*
|
|
46
|
+
* Called only when an input version actually changed. Must be a pure
|
|
47
|
+
* function of its inputs — the engine caches the result and will not
|
|
48
|
+
* call it again until something upstream moves.
|
|
49
|
+
*/
|
|
50
|
+
compute(inputs: PortValues<In>): PortValues<Out>;
|
|
51
|
+
}
|
|
52
|
+
/**
|
|
53
|
+
* A unit of computation with typed input and output ports.
|
|
54
|
+
*
|
|
55
|
+
* Construct via {@link defineNode} or {@link derive} rather than
|
|
56
|
+
* directly — both wire the port maps up from the spec.
|
|
57
|
+
*/
|
|
58
|
+
export declare class Node<In extends PortSpecMap = PortSpecMap, Out extends PortSpecMap = PortSpecMap> {
|
|
59
|
+
#private;
|
|
60
|
+
/** Unique within the process. Stable across a graph's lifetime. */
|
|
61
|
+
readonly id: string;
|
|
62
|
+
/** The node type's name, from its spec. */
|
|
63
|
+
readonly kind: string;
|
|
64
|
+
/** Typed input ports, keyed as declared. */
|
|
65
|
+
readonly in: InletsFor<In>;
|
|
66
|
+
/** Typed output ports, keyed as declared. */
|
|
67
|
+
readonly out: OutletsFor<Out>;
|
|
68
|
+
constructor(spec: NodeSpec<In, Out>);
|
|
69
|
+
/** Whether this node will revalidate on the next pull. */
|
|
70
|
+
get dirty(): boolean;
|
|
71
|
+
/**
|
|
72
|
+
* The error thrown by the most recent `compute`, or `undefined` if the
|
|
73
|
+
* last evaluation succeeded (or none has run). Cached alongside the
|
|
74
|
+
* value: an errored node rethrows without re-running `compute` until
|
|
75
|
+
* an input changes, so a broken node stays cheap to poll — useful when
|
|
76
|
+
* rendering a graph where one node is misconfigured.
|
|
77
|
+
*/
|
|
78
|
+
get error(): unknown;
|
|
79
|
+
/**
|
|
80
|
+
* Forces this node to recompute on the next pull, even if no input
|
|
81
|
+
* version changed, and marks everything downstream dirty.
|
|
82
|
+
*
|
|
83
|
+
* Needed when a node's output depends on something the graph can't
|
|
84
|
+
* see — a settable source value, an external mutable resource.
|
|
85
|
+
*/
|
|
86
|
+
invalidate(): void;
|
|
87
|
+
}
|
|
88
|
+
/** Creates instances of a declared node type. */
|
|
89
|
+
export interface NodeFactory<In extends PortSpecMap, Out extends PortSpecMap> {
|
|
90
|
+
(): Node<In, Out>;
|
|
91
|
+
/** The `kind` of the nodes this factory produces. */
|
|
92
|
+
readonly kind: string;
|
|
93
|
+
}
|
|
94
|
+
/**
|
|
95
|
+
* Declares a reusable node type and returns a factory for it.
|
|
96
|
+
*
|
|
97
|
+
* ```ts
|
|
98
|
+
* const Stats = defineNode({
|
|
99
|
+
* kind: 'stats',
|
|
100
|
+
* inputs: { series: port<TimeSeries<Schema>>() },
|
|
101
|
+
* outputs: { mean: port<number>(), max: port<number>() },
|
|
102
|
+
* compute: ({ series }) => ({
|
|
103
|
+
* mean: series.column('cpu').mean(),
|
|
104
|
+
* max: series.column('cpu').max(),
|
|
105
|
+
* }),
|
|
106
|
+
* });
|
|
107
|
+
*
|
|
108
|
+
* const stats = Stats();
|
|
109
|
+
* raw.out.value.connect(stats.in.series);
|
|
110
|
+
* stats.out.mean.get();
|
|
111
|
+
* ```
|
|
112
|
+
*
|
|
113
|
+
* For a node type that needs per-instance configuration, close over it:
|
|
114
|
+
* `const smoother = (span: number) => defineNode({ ... })();`
|
|
115
|
+
*/
|
|
116
|
+
export declare function defineNode<In extends PortSpecMap, Out extends PortSpecMap>(spec: NodeSpec<In, Out>): NodeFactory<In, Out>;
|
|
117
|
+
/** Extracts the value type carried by an {@link Outlet}. */
|
|
118
|
+
export type OutletValue<O> = O extends Outlet<infer T> ? T : never;
|
|
119
|
+
/** The spec map implied by a record of source outlets. */
|
|
120
|
+
export type SpecsForOutlets<M> = {
|
|
121
|
+
[K in keyof M]: PortSpec<OutletValue<M[K]>>;
|
|
122
|
+
};
|
|
123
|
+
/** The single-output spec map produced by {@link derive}. */
|
|
124
|
+
export type DerivedOutput<R> = {
|
|
125
|
+
readonly value: PortSpec<R>;
|
|
126
|
+
};
|
|
127
|
+
/**
|
|
128
|
+
* Builds a single-output node and wires it to its sources in one step —
|
|
129
|
+
* the shape most graph edges take.
|
|
130
|
+
*
|
|
131
|
+
* ```ts
|
|
132
|
+
* const smoothed = derive({ series: raw.out.value }, ({ series }) =>
|
|
133
|
+
* series.smooth('cpu', { method: 'ema', alpha: 0.3 }),
|
|
134
|
+
* );
|
|
135
|
+
* smoothed.out.value.get();
|
|
136
|
+
* ```
|
|
137
|
+
*
|
|
138
|
+
* Equivalent to {@link defineNode} with one output named `value`, an
|
|
139
|
+
* input per source key, and the connections already made. Reach for
|
|
140
|
+
* `defineNode` when a node has several outputs, or when the node type is
|
|
141
|
+
* reused across graphs and deserves a name.
|
|
142
|
+
*/
|
|
143
|
+
export declare function derive<Sources extends Readonly<Record<string, Outlet<any>>>, R>(sources: Sources, compute: (inputs: {
|
|
144
|
+
[K in keyof Sources]: OutletValue<Sources[K]>;
|
|
145
|
+
}) => R, options?: {
|
|
146
|
+
/** Node type name for errors and `toJSON()`. Defaults to `'derive'`. */
|
|
147
|
+
readonly kind?: string;
|
|
148
|
+
/** Change test for the output. Defaults to `Object.is`. */
|
|
149
|
+
readonly equals?: (a: R, b: R) => boolean;
|
|
150
|
+
}): Node<SpecsForOutlets<Sources>, DerivedOutput<R>>;
|
|
151
|
+
//# sourceMappingURL=node.d.ts.map
|
package/dist/node.js
ADDED
|
@@ -0,0 +1,268 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Nodes: units of computation with typed ports, and the pull-based
|
|
3
|
+
* evaluation that drives them.
|
|
4
|
+
*
|
|
5
|
+
* ## Evaluation model
|
|
6
|
+
*
|
|
7
|
+
* Two mechanisms, doing two different jobs:
|
|
8
|
+
*
|
|
9
|
+
* - **Dirty marking (push)** tells a node "something upstream moved,
|
|
10
|
+
* revalidate before answering." It propagates from the changed node
|
|
11
|
+
* through every downstream edge, cutting off at nodes already marked,
|
|
12
|
+
* so a change costs O(affected nodes) regardless of graph size. A
|
|
13
|
+
* clean node answers `get()` from cache without walking anything.
|
|
14
|
+
*
|
|
15
|
+
* - **Version stamps (pull)** tell a node whether the change was real.
|
|
16
|
+
* Each outlet's version increments only when a recomputed value
|
|
17
|
+
* actually differs, and each node records the input versions it last
|
|
18
|
+
* computed against. A dirty node whose input versions all match skips
|
|
19
|
+
* `compute` entirely and stays cached.
|
|
20
|
+
*
|
|
21
|
+
* The second is what makes the layer worth having over plain function
|
|
22
|
+
* calls: setting a source that happens to produce an identical
|
|
23
|
+
* downstream value stops the cascade there, so expensive transforms
|
|
24
|
+
* further down never run. Dirty marking alone (as in a naive dataflow
|
|
25
|
+
* graph) would recompute the whole subtree on every touch.
|
|
26
|
+
*/
|
|
27
|
+
import { CycleError, MissingOutputError, ProcessError } from './errors.js';
|
|
28
|
+
import { Inlet, Outlet } from './port.js';
|
|
29
|
+
let nextNodeId = 1;
|
|
30
|
+
/**
|
|
31
|
+
* A unit of computation with typed input and output ports.
|
|
32
|
+
*
|
|
33
|
+
* Construct via {@link defineNode} or {@link derive} rather than
|
|
34
|
+
* directly — both wire the port maps up from the spec.
|
|
35
|
+
*/
|
|
36
|
+
export class Node {
|
|
37
|
+
/** Unique within the process. Stable across a graph's lifetime. */
|
|
38
|
+
id;
|
|
39
|
+
/** The node type's name, from its spec. */
|
|
40
|
+
kind;
|
|
41
|
+
/** Typed input ports, keyed as declared. */
|
|
42
|
+
in;
|
|
43
|
+
/** Typed output ports, keyed as declared. */
|
|
44
|
+
out;
|
|
45
|
+
#compute;
|
|
46
|
+
#inlets;
|
|
47
|
+
#outlets;
|
|
48
|
+
#dirty = true;
|
|
49
|
+
#computed = false;
|
|
50
|
+
#evaluating = false;
|
|
51
|
+
#inputVersions = new Map();
|
|
52
|
+
#error = undefined;
|
|
53
|
+
#hasError = false;
|
|
54
|
+
constructor(spec) {
|
|
55
|
+
this.id = `n${nextNodeId++}`;
|
|
56
|
+
this.kind = spec.kind;
|
|
57
|
+
this.#compute = (inputs) => spec.compute(inputs);
|
|
58
|
+
// Null-prototype maps: a port legitimately named `__proto__` would
|
|
59
|
+
// otherwise hit the prototype setter instead of defining a key,
|
|
60
|
+
// leaving the node with no ports at all and a hijacked `node.in`.
|
|
61
|
+
const inlets = Object.create(null);
|
|
62
|
+
for (const [name, portSpec] of Object.entries(spec.inputs)) {
|
|
63
|
+
inlets[name] = new Inlet(this, name, portSpec.defaultValue);
|
|
64
|
+
}
|
|
65
|
+
const outlets = Object.create(null);
|
|
66
|
+
for (const [name, portSpec] of Object.entries(spec.outputs)) {
|
|
67
|
+
outlets[name] = new Outlet(this, name, portSpec.equals);
|
|
68
|
+
}
|
|
69
|
+
this.in = Object.freeze(inlets);
|
|
70
|
+
this.out = Object.freeze(outlets);
|
|
71
|
+
this.#inlets = Object.values(inlets);
|
|
72
|
+
this.#outlets = Object.values(outlets);
|
|
73
|
+
}
|
|
74
|
+
/** Whether this node will revalidate on the next pull. */
|
|
75
|
+
get dirty() {
|
|
76
|
+
return this.#dirty;
|
|
77
|
+
}
|
|
78
|
+
/**
|
|
79
|
+
* The error thrown by the most recent `compute`, or `undefined` if the
|
|
80
|
+
* last evaluation succeeded (or none has run). Cached alongside the
|
|
81
|
+
* value: an errored node rethrows without re-running `compute` until
|
|
82
|
+
* an input changes, so a broken node stays cheap to poll — useful when
|
|
83
|
+
* rendering a graph where one node is misconfigured.
|
|
84
|
+
*/
|
|
85
|
+
get error() {
|
|
86
|
+
return this.#error;
|
|
87
|
+
}
|
|
88
|
+
/**
|
|
89
|
+
* Forces this node to recompute on the next pull, even if no input
|
|
90
|
+
* version changed, and marks everything downstream dirty.
|
|
91
|
+
*
|
|
92
|
+
* Needed when a node's output depends on something the graph can't
|
|
93
|
+
* see — a settable source value, an external mutable resource.
|
|
94
|
+
*/
|
|
95
|
+
invalidate() {
|
|
96
|
+
this.#computed = false;
|
|
97
|
+
this.markDirty();
|
|
98
|
+
}
|
|
99
|
+
/** @internal Marks stale and propagates downstream, cutting off at nodes already dirty. */
|
|
100
|
+
markDirty() {
|
|
101
|
+
if (this.#dirty)
|
|
102
|
+
return;
|
|
103
|
+
this.#dirty = true;
|
|
104
|
+
for (const outlet of this.#outlets)
|
|
105
|
+
outlet.invalidateDownstream();
|
|
106
|
+
}
|
|
107
|
+
/** @internal */
|
|
108
|
+
inletList() {
|
|
109
|
+
return this.#inlets;
|
|
110
|
+
}
|
|
111
|
+
/** @internal */
|
|
112
|
+
outletList() {
|
|
113
|
+
return this.#outlets;
|
|
114
|
+
}
|
|
115
|
+
/** @internal Brings this node's outlets up to date. See the module docstring. */
|
|
116
|
+
ensureFresh() {
|
|
117
|
+
// The re-entrancy check must precede the clean-node early return.
|
|
118
|
+
// `#dirty` is cleared before `compute` runs, so a compute that pulls
|
|
119
|
+
// its own node would otherwise take the early return and be handed
|
|
120
|
+
// the *previous* cached value — a silently stale result instead of
|
|
121
|
+
// the cycle it actually is.
|
|
122
|
+
if (this.#evaluating) {
|
|
123
|
+
throw new CycleError(`Node '${this.kind}' was pulled while it was still evaluating — its compute re-entered the graph`);
|
|
124
|
+
}
|
|
125
|
+
if (!this.#dirty) {
|
|
126
|
+
if (this.#hasError)
|
|
127
|
+
throw this.#error;
|
|
128
|
+
return;
|
|
129
|
+
}
|
|
130
|
+
this.#evaluating = true;
|
|
131
|
+
try {
|
|
132
|
+
// Refresh upstream first, then compare versions: a dirty mark says
|
|
133
|
+
// "maybe stale", the versions say whether anything really moved.
|
|
134
|
+
const versions = new Map();
|
|
135
|
+
let changed = !this.#computed;
|
|
136
|
+
for (const inlet of this.#inlets) {
|
|
137
|
+
try {
|
|
138
|
+
inlet.source?.node.ensureFresh();
|
|
139
|
+
}
|
|
140
|
+
catch (error) {
|
|
141
|
+
// Upstream failed. Drop any error cached from this node's own
|
|
142
|
+
// last compute: it is no longer why this node can't produce a
|
|
143
|
+
// value, and leaving it would point a graph inspector at the
|
|
144
|
+
// wrong node. The failing upstream keeps its own `error`.
|
|
145
|
+
this.#error = undefined;
|
|
146
|
+
this.#hasError = false;
|
|
147
|
+
throw error;
|
|
148
|
+
}
|
|
149
|
+
const version = inlet.sourceVersion();
|
|
150
|
+
versions.set(inlet.name, version);
|
|
151
|
+
if (this.#inputVersions.get(inlet.name) !== version)
|
|
152
|
+
changed = true;
|
|
153
|
+
}
|
|
154
|
+
this.#dirty = false;
|
|
155
|
+
if (!changed) {
|
|
156
|
+
if (this.#hasError)
|
|
157
|
+
throw this.#error;
|
|
158
|
+
return;
|
|
159
|
+
}
|
|
160
|
+
this.#inputVersions = versions;
|
|
161
|
+
this.#computed = true;
|
|
162
|
+
// Reading inputs, computing, and storing outputs share one catch:
|
|
163
|
+
// all three can fail, and a node that fails any of them must end
|
|
164
|
+
// up in the same state — error cached, rethrown on every pull
|
|
165
|
+
// until an input changes. Catching only around `compute` left a
|
|
166
|
+
// node that threw while reading an unconnected input marked clean
|
|
167
|
+
// with no value and no error, so the second pull reported a
|
|
168
|
+
// misleading "produced no value" instead of the real cause.
|
|
169
|
+
try {
|
|
170
|
+
const inputs = {};
|
|
171
|
+
for (const inlet of this.#inlets)
|
|
172
|
+
inputs[inlet.name] = inlet.get();
|
|
173
|
+
const produced = this.#compute(inputs);
|
|
174
|
+
if (produced === null || typeof produced !== 'object') {
|
|
175
|
+
throw new MissingOutputError(`Node '${this.kind}' compute must return an object of output values`);
|
|
176
|
+
}
|
|
177
|
+
const values = produced;
|
|
178
|
+
for (const outlet of this.#outlets) {
|
|
179
|
+
// `hasOwn`, not `in`: `in` walks the prototype chain, so an
|
|
180
|
+
// output named `toString` or `valueOf` would silently pass the
|
|
181
|
+
// guard and publish an `Object.prototype` member as its value.
|
|
182
|
+
if (!Object.hasOwn(values, outlet.name)) {
|
|
183
|
+
throw new MissingOutputError(`Node '${this.kind}' compute did not return output '${outlet.name}'`);
|
|
184
|
+
}
|
|
185
|
+
outlet.produce(values[outlet.name]);
|
|
186
|
+
}
|
|
187
|
+
this.#error = undefined;
|
|
188
|
+
this.#hasError = false;
|
|
189
|
+
}
|
|
190
|
+
catch (error) {
|
|
191
|
+
this.#error = error;
|
|
192
|
+
this.#hasError = true;
|
|
193
|
+
throw error;
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
finally {
|
|
197
|
+
this.#evaluating = false;
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
/**
|
|
202
|
+
* Declares a reusable node type and returns a factory for it.
|
|
203
|
+
*
|
|
204
|
+
* ```ts
|
|
205
|
+
* const Stats = defineNode({
|
|
206
|
+
* kind: 'stats',
|
|
207
|
+
* inputs: { series: port<TimeSeries<Schema>>() },
|
|
208
|
+
* outputs: { mean: port<number>(), max: port<number>() },
|
|
209
|
+
* compute: ({ series }) => ({
|
|
210
|
+
* mean: series.column('cpu').mean(),
|
|
211
|
+
* max: series.column('cpu').max(),
|
|
212
|
+
* }),
|
|
213
|
+
* });
|
|
214
|
+
*
|
|
215
|
+
* const stats = Stats();
|
|
216
|
+
* raw.out.value.connect(stats.in.series);
|
|
217
|
+
* stats.out.mean.get();
|
|
218
|
+
* ```
|
|
219
|
+
*
|
|
220
|
+
* For a node type that needs per-instance configuration, close over it:
|
|
221
|
+
* `const smoother = (span: number) => defineNode({ ... })();`
|
|
222
|
+
*/
|
|
223
|
+
export function defineNode(spec) {
|
|
224
|
+
const factory = () => new Node(spec);
|
|
225
|
+
return Object.assign(factory, { kind: spec.kind });
|
|
226
|
+
}
|
|
227
|
+
/**
|
|
228
|
+
* Builds a single-output node and wires it to its sources in one step —
|
|
229
|
+
* the shape most graph edges take.
|
|
230
|
+
*
|
|
231
|
+
* ```ts
|
|
232
|
+
* const smoothed = derive({ series: raw.out.value }, ({ series }) =>
|
|
233
|
+
* series.smooth('cpu', { method: 'ema', alpha: 0.3 }),
|
|
234
|
+
* );
|
|
235
|
+
* smoothed.out.value.get();
|
|
236
|
+
* ```
|
|
237
|
+
*
|
|
238
|
+
* Equivalent to {@link defineNode} with one output named `value`, an
|
|
239
|
+
* input per source key, and the connections already made. Reach for
|
|
240
|
+
* `defineNode` when a node has several outputs, or when the node type is
|
|
241
|
+
* reused across graphs and deserves a name.
|
|
242
|
+
*/
|
|
243
|
+
export function derive(sources, compute, options = {}) {
|
|
244
|
+
const inputs = {};
|
|
245
|
+
for (const name of Object.keys(sources))
|
|
246
|
+
inputs[name] = {};
|
|
247
|
+
const outputValue = options.equals
|
|
248
|
+
? { equals: options.equals }
|
|
249
|
+
: {};
|
|
250
|
+
const node = new Node({
|
|
251
|
+
kind: options.kind ?? 'derive',
|
|
252
|
+
inputs: inputs,
|
|
253
|
+
outputs: { value: outputValue },
|
|
254
|
+
compute: (values) => ({
|
|
255
|
+
value: compute(values),
|
|
256
|
+
}),
|
|
257
|
+
});
|
|
258
|
+
for (const [name, outlet] of Object.entries(sources)) {
|
|
259
|
+
const inlet = node.in[name];
|
|
260
|
+
if (inlet === undefined) {
|
|
261
|
+
// Unreachable: the inputs above are built from these same keys.
|
|
262
|
+
throw new ProcessError(`Node '${node.kind}' has no input named '${name}'`);
|
|
263
|
+
}
|
|
264
|
+
outlet.connect(inlet);
|
|
265
|
+
}
|
|
266
|
+
return node;
|
|
267
|
+
}
|
|
268
|
+
//# sourceMappingURL=node.js.map
|
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A programmable API that **emits** a plan — [PND-PROCBUILD].
|
|
3
|
+
*
|
|
4
|
+
* Plans-as-data is right for a wire format and required for a cache key,
|
|
5
|
+
* but it is not how an application wants to *author* a graph. A consumer
|
|
6
|
+
* building studies over its own metrics should not be assembling nested
|
|
7
|
+
* JSON by hand.
|
|
8
|
+
*
|
|
9
|
+
* ```ts
|
|
10
|
+
* const g = plan('ACME_5m').as('bands_and_stretch');
|
|
11
|
+
* const bb = g.add('bb', 'bollinger', { period: 20 }, ['px']);
|
|
12
|
+
* const z = g.add('z', 'zscore', { period: 20 }, ['px']);
|
|
13
|
+
*
|
|
14
|
+
* g.expose('upper_band', bb.last({ output: 'Upper' }));
|
|
15
|
+
* g.expose('stretch', z.percentileRank());
|
|
16
|
+
* g.expose('band_series', bb.columns());
|
|
17
|
+
*
|
|
18
|
+
* host.run(g.toJSON());
|
|
19
|
+
* ```
|
|
20
|
+
*
|
|
21
|
+
* **Slots are what make this possible**, which is why this depends on
|
|
22
|
+
* [PND-PROCSLOT] rather than standing alone: a builder needs a stable
|
|
23
|
+
* handle to refer back to, and a content-addressed id cannot be one — it
|
|
24
|
+
* changes the moment a param does. A slot is exactly a handle, and
|
|
25
|
+
* passing `bb` rather than the string `'bb'` is what turns a typo into a
|
|
26
|
+
* compile error.
|
|
27
|
+
*
|
|
28
|
+
* **The builder emits a plan; it does not replace one.** `toJSON()`
|
|
29
|
+
* produces the same envelope a model would compose, so there is one
|
|
30
|
+
* resolution path, one cache, and one thing to test — a graph built in
|
|
31
|
+
* code and one composed by a model land on the same nodes.
|
|
32
|
+
*
|
|
33
|
+
* It deliberately knows nothing about the registry: no op-name checking,
|
|
34
|
+
* no param typing. That keeps it a pure authoring layer with no
|
|
35
|
+
* dependency on the op corpus, and leaves the resolver as the single
|
|
36
|
+
* place a bad plan is diagnosed. Typing params off `ParamDef` is the
|
|
37
|
+
* open question in the ticket, not a gap this file is hiding.
|
|
38
|
+
*/
|
|
39
|
+
import { ProcessError } from '../errors.js';
|
|
40
|
+
import type { Select } from './run.js';
|
|
41
|
+
import type { SourceRef } from './source.js';
|
|
42
|
+
import type { Slots } from './slots.js';
|
|
43
|
+
import type { ParamValue } from './types.js';
|
|
44
|
+
/** Thrown when a graph is built wrong — before it is ever sent. */
|
|
45
|
+
export declare class BuilderError extends ProcessError {
|
|
46
|
+
}
|
|
47
|
+
/**
|
|
48
|
+
* The derived slot for a fold over `on` — a function of the node, the
|
|
49
|
+
* fold, **and its params**. Params used to be omitted, so
|
|
50
|
+
* `shape({points: 20})` followed by `shape({points: 100})` landed on one
|
|
51
|
+
* slot and the second call silently kept 20. Sorted by key so two
|
|
52
|
+
* spellings of one param set collide deliberately, the same rule
|
|
53
|
+
* `specId` applies one layer down.
|
|
54
|
+
*
|
|
55
|
+
* One asymmetry is inherent: this builder holds no registry, so it
|
|
56
|
+
* cannot resolve defaults — `shape()` and `shape({points: 40})` derive
|
|
57
|
+
* two slots here even though `specId` resolves them to one computation.
|
|
58
|
+
* That is safe: resolution collapses them to one node, and the response
|
|
59
|
+
* labels it with the first slot that named it. The registry-bound
|
|
60
|
+
* fluent layer canonicalizes params before calling this, so it does not
|
|
61
|
+
* split.
|
|
62
|
+
*/
|
|
63
|
+
export declare function foldSlot(on: string, op: string, params?: Readonly<Record<string, ParamValue>>): string;
|
|
64
|
+
/** One named output of a multi-output node. */
|
|
65
|
+
export interface OutputHandle {
|
|
66
|
+
readonly slot: string;
|
|
67
|
+
readonly output: string;
|
|
68
|
+
}
|
|
69
|
+
/** What a node's inputs may be: a source column, a node, or one named output. */
|
|
70
|
+
export type InputRef = string | NodeHandle | OutputHandle;
|
|
71
|
+
/**
|
|
72
|
+
* A reference to a node in the graph under construction.
|
|
73
|
+
*
|
|
74
|
+
* The fold methods **add a node** and hand back a handle to it, rather
|
|
75
|
+
* than producing a selector as they used to. That is the whole shape of
|
|
76
|
+
* [PND-PROCFOLD] at the authoring layer: `z.percentileRank()` always
|
|
77
|
+
* read like an op call, and now it is one.
|
|
78
|
+
*
|
|
79
|
+
* The derived slot is `<slot>:<fold>` — with the fold's params folded in
|
|
80
|
+
* when it has any, e.g. `<slot>:shape(points=100)`. Deterministic in all
|
|
81
|
+
* three, so calling `.last()` twice on the same node returns the same
|
|
82
|
+
* node rather than colliding, while `shape({points: 20})` and
|
|
83
|
+
* `shape({points: 100})` are two nodes rather than the second silently
|
|
84
|
+
* answering with the first's 20. Safe against a source column name,
|
|
85
|
+
* which cannot contain a colon.
|
|
86
|
+
*/
|
|
87
|
+
export interface NodeHandle {
|
|
88
|
+
readonly slot: string;
|
|
89
|
+
/** Latest defined value, with when. */
|
|
90
|
+
last(): NodeHandle;
|
|
91
|
+
/** Minimum and maximum, each with when. */
|
|
92
|
+
extremes(): NodeHandle;
|
|
93
|
+
/** Where the latest value sits in its own history. */
|
|
94
|
+
percentileRank(): NodeHandle;
|
|
95
|
+
/** A bounded sample of the whole series. */
|
|
96
|
+
shape(options?: {
|
|
97
|
+
points?: number;
|
|
98
|
+
}): NodeHandle;
|
|
99
|
+
}
|
|
100
|
+
/** The envelope a builder produces — what `Host.run` takes. */
|
|
101
|
+
export interface BuiltRequest<From extends string | SourceRef = string> {
|
|
102
|
+
readonly from: From;
|
|
103
|
+
readonly as?: string;
|
|
104
|
+
readonly nodes: Slots;
|
|
105
|
+
readonly outputs: Readonly<Record<string, Select>>;
|
|
106
|
+
}
|
|
107
|
+
export declare class PlanBuilder<From extends string | SourceRef = string> {
|
|
108
|
+
#private;
|
|
109
|
+
constructor(from: From);
|
|
110
|
+
/** Names the result, so a later request can refer back to it. */
|
|
111
|
+
as(name: string): this;
|
|
112
|
+
/**
|
|
113
|
+
* Adds a node under a caller-chosen slot name.
|
|
114
|
+
*
|
|
115
|
+
* The name is required rather than derived, because it *is* the stable
|
|
116
|
+
* identity — deriving `sma2` from a counter would renumber the moment
|
|
117
|
+
* a node is inserted above it, which is the property slots exist to
|
|
118
|
+
* provide.
|
|
119
|
+
*
|
|
120
|
+
* @throws {BuilderError} if the slot is already taken.
|
|
121
|
+
*/
|
|
122
|
+
add(slot: string, op: string, params: Readonly<Record<string, ParamValue>> | undefined, inputs: readonly InputRef[]): NodeHandle;
|
|
123
|
+
/**
|
|
124
|
+
* Surfaces a node under the caller's own name.
|
|
125
|
+
*
|
|
126
|
+
* Takes a handle, because a selector's only remaining job is to point
|
|
127
|
+
* at a node — what comes back is whatever that node produces.
|
|
128
|
+
*
|
|
129
|
+
* @throws {BuilderError} if the name is already used.
|
|
130
|
+
*/
|
|
131
|
+
expose(name: string, node: NodeHandle, options?: {
|
|
132
|
+
output?: string;
|
|
133
|
+
}): this;
|
|
134
|
+
/** The envelope. Plain JSON — nothing here survives into the request. */
|
|
135
|
+
toJSON(): BuiltRequest<From>;
|
|
136
|
+
}
|
|
137
|
+
export declare function plan<const From extends string | SourceRef>(from: From): PlanBuilder<From>;
|
|
138
|
+
//# sourceMappingURL=builder.d.ts.map
|