@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/column.js ADDED
@@ -0,0 +1,306 @@
1
+ /**
2
+ * Column-valued node support — [PND-PROCCOL].
3
+ *
4
+ * An op computes a study by calling into the corpus, which returns a
5
+ * `TimeSeries` whose new column is already **packed** (a `Float64Array`
6
+ * plus a validity bitmap). The obvious adapter then unpacks that into a
7
+ * boxed `Array<number | undefined>` to use as the node's value. Keeping
8
+ * the `Column` instead is worth doing for **memory and sizeability**: 20
9
+ * SMAs over 500k rows cost 271 MB of GC-managed heap boxed versus 42 MB
10
+ * as columns (rss 466 MB vs 353 MB — the bytes move to `arrayBuffers`
11
+ * rather than vanish).
12
+ *
13
+ * It is **not** worth doing for read throughput, which measurement
14
+ * contradicted: folding a max over 500k cells took 0.91 ms over the boxed
15
+ * array, 0.96 ms walking the buffer plus validity bits, and 4.27 ms via
16
+ * `Column.scan()` — whose per-cell callback costs more than either. A
17
+ * reduction on a hot path should walk `toFloat64Array()` and the bits
18
+ * directly rather than call `scan`.
19
+ *
20
+ * What that needs, and pond does not expose, is here:
21
+ *
22
+ * - {@link columnBytes} — a value's retained size, so a cache budget can
23
+ * be expressed in bytes rather than entries ([PND-PROCCACHE]).
24
+ * - {@link packColumn} — build a packed column from loose values, for an
25
+ * op whose kernel hands back an array.
26
+ * - {@link appendColumn} — put a column back onto a series for the
27
+ * renderer path, avoiding the round trip where the column is gapless.
28
+ * - {@link columnBuffers} / {@link columnFromBuffers} — the buffer pair a
29
+ * column *is*, for moving one across an isolate boundary
30
+ * ([PND-PROCPAR]).
31
+ */
32
+ import { Float64Column } from 'pond-ts';
33
+ /** Bytes a validity bitmap needs for `length` cells. */
34
+ function bitmapByteCount(length) {
35
+ return Math.ceil(length / 8);
36
+ }
37
+ /**
38
+ * A `ValidityBitmap` built outside core.
39
+ *
40
+ * The interface is structural — `bits` / `length` / `definedCount` /
41
+ * `isDefined` / `countInRange`, all public — so a producer can implement
42
+ * it without reaching into `pond-ts`'s internals. Core's own factory
43
+ * (`createValidityBitmap`) is deliberately not exported; this is the
44
+ * supported way to construct one from outside.
45
+ */
46
+ class PackedValidity {
47
+ bits;
48
+ length;
49
+ definedCount;
50
+ constructor(bits, length, definedCount) {
51
+ this.bits = bits;
52
+ this.length = length;
53
+ this.definedCount = definedCount;
54
+ }
55
+ isDefined(i) {
56
+ if (i < 0 || i >= this.length)
57
+ return false;
58
+ return (this.bits[i >> 3] & (1 << (i & 7))) !== 0;
59
+ }
60
+ countInRange(start, end) {
61
+ const lo = Math.max(0, start);
62
+ const hi = Math.min(this.length, end);
63
+ let n = 0;
64
+ for (let i = lo; i < hi; i += 1)
65
+ if (this.isDefined(i))
66
+ n += 1;
67
+ return n;
68
+ }
69
+ }
70
+ /**
71
+ * Retained size of a column value, in bytes.
72
+ *
73
+ * Approximate by construction — it counts the backing buffers a packed
74
+ * column owns, which is what a cache budget is actually trying to bound,
75
+ * and ignores per-object overhead. A chunked column reports the sum of
76
+ * its chunks. A column whose kind this cannot size returns `0` rather
77
+ * than guessing, so a budget treats it as free instead of evicting on a
78
+ * fabricated number.
79
+ *
80
+ * Bytes rather than entries is the point: a 500k-row result and a scalar
81
+ * both count as one entry, and only one of them matters.
82
+ */
83
+ export function columnBytes(column) {
84
+ const anyColumn = column;
85
+ // The aggregate bitmap's REAL bytes, not the minimum its cell count
86
+ // implies: a bitmap over a larger buffer retains that buffer.
87
+ const validity = anyColumn.validity;
88
+ const validityBytes = validity === undefined
89
+ ? 0
90
+ : (validity.bits?.length ?? bitmapByteCount(validity.length));
91
+ // A chunked column owns no value buffer of its own — its bytes are its
92
+ // chunks' (each a packed column, sized recursively) plus the aggregate
93
+ // validity bitmap and the chunk-offset index built at construction.
94
+ // Reading `_values` here returned 0 for every chunked column, while
95
+ // the doc above promised the sum — an undercount a byte budget would
96
+ // treat as free.
97
+ const chunks = anyColumn.chunks;
98
+ if (Array.isArray(chunks)) {
99
+ const offsets = anyColumn.chunkOffsets;
100
+ let total = validityBytes +
101
+ (offsets === undefined
102
+ ? 0
103
+ : offsets.length * (offsets.BYTES_PER_ELEMENT ?? 4));
104
+ for (const chunk of chunks)
105
+ total += columnBytes(chunk);
106
+ return total;
107
+ }
108
+ const values = anyColumn._values;
109
+ if (values === undefined)
110
+ return 0;
111
+ const perElement = values.BYTES_PER_ELEMENT ?? 8;
112
+ // The BACKING buffer's capacity, not the column's logical length — a
113
+ // one-row column viewing an 8 MB buffer retains 8 MB, and sizing it
114
+ // at 8 bytes is exactly the undercount that defeats a byte budget.
115
+ return values.length * perElement + validityBytes;
116
+ }
117
+ /**
118
+ * Packs loose values into a `Float64Column`.
119
+ *
120
+ * For an op whose kernel returns an array — the financial kernels are
121
+ * array-out (`rollingValues`, `emaValues`) — this confines the boxing to
122
+ * one pass, instead of the boxed array being retained as the node value.
123
+ *
124
+ * `NaN` is treated as **missing**, matching what a study's warm-up
125
+ * means, and never packed as a defined cell: core's reducer non-finite
126
+ * policy requires that a column flagged `allFinite` contain no `NaN`,
127
+ * and a wrongly-set flag makes reducers take an unguarded path and
128
+ * silently return a wrong result. `allFinite` is therefore computed by
129
+ * inspecting every defined cell, not assumed.
130
+ */
131
+ export function packColumn(values) {
132
+ const length = values.length;
133
+ const buffer = new Float64Array(length);
134
+ const bits = new Uint8Array(bitmapByteCount(length));
135
+ let defined = 0;
136
+ let allFinite = true;
137
+ for (let i = 0; i < length; i += 1) {
138
+ const v = values[i];
139
+ if (v === undefined || v === null || Number.isNaN(v))
140
+ continue;
141
+ buffer[i] = v;
142
+ bits[i >> 3] |= 1 << (i & 7);
143
+ defined += 1;
144
+ if (!Number.isFinite(v))
145
+ allFinite = false;
146
+ }
147
+ // No gaps: omit the bitmap entirely rather than carry an all-ones one.
148
+ // A gapless column is also the case `appendColumn` can round-trip
149
+ // without boxing, so this is worth detecting.
150
+ if (defined === length)
151
+ return new Float64Column(buffer, length, undefined, allFinite);
152
+ return new Float64Column(buffer, length, new PackedValidity(bits, length, defined), allFinite);
153
+ }
154
+ /**
155
+ * Appends a column to a series under `name` — the renderer path, where a
156
+ * caller genuinely wants one `TimeSeries` carrying several studies.
157
+ *
158
+ * **A gapless column round-trips without boxing**, via the column's own
159
+ * `Float64Array`. A column *with* gaps cannot: core's `withColumn` takes
160
+ * values, not a column, and rejects a non-finite cell, so a warm-up has
161
+ * to be expressed as `undefined` in a boxed array. Core appends columns
162
+ * directly internally (`withColumnAppended`) but does not expose it;
163
+ * until it does, a gapped column pays one boxing pass here.
164
+ *
165
+ * Since most studies have a warm-up, that fallback is the common case —
166
+ * which is exactly why assembly should be requested rather than assumed
167
+ * ([PND-PROCTERM]). A facts-only request never calls this.
168
+ */
169
+ export function appendColumn(series, name, column) {
170
+ if (column.length !== series.length) {
171
+ throw new RangeError(`appendColumn '${name}': column length ${column.length} does not match series length ${series.length}`);
172
+ }
173
+ const wide = series;
174
+ const packed = column;
175
+ if (packed.validity === undefined &&
176
+ typeof packed.toFloat64Array === 'function') {
177
+ return wide.withColumn(name, packed.toFloat64Array());
178
+ }
179
+ const boxed = new Array(column.length);
180
+ for (let i = 0; i < column.length; i += 1) {
181
+ const v = column.at(i);
182
+ boxed[i] = v === undefined || Number.isNaN(v) ? undefined : v;
183
+ }
184
+ return wide.withColumn(name, boxed);
185
+ }
186
+ export function columnBuffers(column) {
187
+ const packed = column;
188
+ if (packed.kind !== 'number' || packed.storage !== 'packed')
189
+ return undefined;
190
+ const values = packed._values;
191
+ if (!(values instanceof Float64Array))
192
+ return undefined;
193
+ const length = packed.length;
194
+ const validity = packed.validity;
195
+ return {
196
+ length,
197
+ // `slice`, not `subarray`: the result is transferred, and transferring
198
+ // a view detaches the buffer it borrows from — here, the live memo.
199
+ values: values.slice(0, length),
200
+ ...(validity === undefined
201
+ ? {}
202
+ : { bits: validity.bits.slice(0, bitmapByteCount(length)) }),
203
+ definedCount: validity === undefined ? length : validity.definedCount,
204
+ allFinite: packed.allFinite ?? false,
205
+ };
206
+ }
207
+ /** Rebuilds a column from {@link columnBuffers}, adopting both buffers. */
208
+ export function columnFromBuffers(wire) {
209
+ return new Float64Column(wire.values, wire.length, wire.bits === undefined
210
+ ? undefined
211
+ : new PackedValidity(wire.bits, wire.length, wire.definedCount), wire.allFinite);
212
+ }
213
+ export function columnView(column) {
214
+ const packed = column;
215
+ if (packed.kind !== 'number' || packed.storage !== 'packed')
216
+ return undefined;
217
+ const values = packed._values;
218
+ if (!(values instanceof Float64Array))
219
+ return undefined;
220
+ const length = packed.length;
221
+ const validity = packed.validity;
222
+ const view = values.subarray(0, length);
223
+ if (validity === undefined) {
224
+ return {
225
+ length,
226
+ values: view,
227
+ definedCount: length,
228
+ defined: () => true,
229
+ at: (i) => (i >= 0 && i < length ? view[i] : undefined),
230
+ };
231
+ }
232
+ const bits = validity.bits;
233
+ // LSB-first, one bit per cell — pond's layout, and Arrow's.
234
+ const defined = (i) => i >= 0 && i < length && (bits[i >> 3] & (1 << (i & 7))) !== 0;
235
+ return {
236
+ length,
237
+ values: view,
238
+ bits,
239
+ definedCount: validity.definedCount,
240
+ defined,
241
+ at: (i) => (defined(i) ? view[i] : undefined),
242
+ };
243
+ }
244
+ /** Bytes needed for `length` validity bits. */
245
+ export function validityByteCount(length) {
246
+ return (length + 7) >> 3;
247
+ }
248
+ /**
249
+ * Prepares a {@link RangeOutput} of `length`, carrying `[0, keep)` from
250
+ * `prior` — a `Float64Array.set` for the values and a byte-wise copy for
251
+ * the bitmap, with the straddling byte masked.
252
+ */
253
+ export function prepareRange(length, keep, prior) {
254
+ const values = new Float64Array(length);
255
+ const bits = new Uint8Array(validityByteCount(length));
256
+ // Clamped to `length` as well as to the prior: a series that SHRANK
257
+ // gives `keep > length`, and `values.set` then throws
258
+ // `RangeError: offset is out of bounds` before the op ever runs.
259
+ const copy = prior === undefined ? 0 : Math.min(keep, prior.length, length);
260
+ if (prior !== undefined && copy > 0) {
261
+ values.set(prior.values.subarray(0, copy));
262
+ if (prior.bits === undefined) {
263
+ // No bitmap ⇒ every prior cell was defined.
264
+ for (let i = 0; i < copy; i += 1)
265
+ bits[i >> 3] |= 1 << (i & 7);
266
+ }
267
+ else {
268
+ const whole = copy >> 3;
269
+ bits.set(prior.bits.subarray(0, whole));
270
+ // The byte straddling `copy` carries bits past it that belong to
271
+ // rows this range is about to rewrite — mask them off.
272
+ const rest = copy & 7;
273
+ if (rest !== 0)
274
+ bits[whole] = prior.bits[whole] & ((1 << rest) - 1);
275
+ }
276
+ }
277
+ return {
278
+ values,
279
+ bits,
280
+ set(i, v) {
281
+ values[i] = v;
282
+ bits[i >> 3] |= 1 << (i & 7);
283
+ },
284
+ clear(i) {
285
+ values[i] = 0;
286
+ bits[i >> 3] &= ~(1 << (i & 7));
287
+ },
288
+ };
289
+ }
290
+ /** Seals a {@link RangeOutput} into a column, counting validity once. */
291
+ export function sealRange(out, length) {
292
+ const bits = out.bits;
293
+ let defined = 0;
294
+ let allFinite = true;
295
+ for (let i = 0; i < length; i += 1) {
296
+ if ((bits[i >> 3] & (1 << (i & 7))) === 0)
297
+ continue;
298
+ defined += 1;
299
+ if (!Number.isFinite(out.values[i]))
300
+ allFinite = false;
301
+ }
302
+ if (defined === length)
303
+ return new Float64Column(out.values, length, undefined, allFinite);
304
+ return new Float64Column(out.values, length, new PackedValidity(bits, length, defined), allFinite);
305
+ }
306
+ //# sourceMappingURL=column.js.map
@@ -0,0 +1,22 @@
1
+ /** Errors thrown by the graph engine. */
2
+ /** Base class for every error this package throws. */
3
+ export declare class ProcessError extends Error {
4
+ constructor(message: string);
5
+ }
6
+ /**
7
+ * Thrown by `connect()` when the edge would close a cycle. The graph is
8
+ * kept acyclic by construction rather than detected during evaluation,
9
+ * so a cycle surfaces at the line that wires it, not at some later pull.
10
+ */
11
+ export declare class CycleError extends ProcessError {
12
+ }
13
+ /** Thrown when pulling through an inlet with no connection and no default. */
14
+ export declare class UnconnectedInputError extends ProcessError {
15
+ }
16
+ /**
17
+ * Thrown when a node's `compute` omits a declared output, or when an
18
+ * outlet is read before anything has produced a value for it.
19
+ */
20
+ export declare class MissingOutputError extends ProcessError {
21
+ }
22
+ //# sourceMappingURL=errors.d.ts.map
package/dist/errors.js ADDED
@@ -0,0 +1,25 @@
1
+ /** Errors thrown by the graph engine. */
2
+ /** Base class for every error this package throws. */
3
+ export class ProcessError extends Error {
4
+ constructor(message) {
5
+ super(message);
6
+ this.name = new.target.name;
7
+ }
8
+ }
9
+ /**
10
+ * Thrown by `connect()` when the edge would close a cycle. The graph is
11
+ * kept acyclic by construction rather than detected during evaluation,
12
+ * so a cycle surfaces at the line that wires it, not at some later pull.
13
+ */
14
+ export class CycleError extends ProcessError {
15
+ }
16
+ /** Thrown when pulling through an inlet with no connection and no default. */
17
+ export class UnconnectedInputError extends ProcessError {
18
+ }
19
+ /**
20
+ * Thrown when a node's `compute` omits a declared output, or when an
21
+ * outlet is read before anything has produced a value for it.
22
+ */
23
+ export class MissingOutputError extends ProcessError {
24
+ }
25
+ //# sourceMappingURL=errors.js.map
@@ -0,0 +1,89 @@
1
+ /**
2
+ * Graph-level inspection: enumerate what's wired to what, get a
3
+ * topological order, dump the structure.
4
+ *
5
+ * Nodes work standalone — connections live on the ports, and evaluation
6
+ * never consults a container. `Graph` is a read-only view over an
7
+ * already-wired set of nodes, for the cases where you need the whole
8
+ * picture: rendering a node editor, logging evaluation order, asserting
9
+ * on structure in tests.
10
+ */
11
+ import type { Inlet, Outlet } from './port.js';
12
+ import type { Node } from './node.js';
13
+ /** One connection between two nodes. */
14
+ export interface GraphEdge {
15
+ readonly from: Outlet<any>;
16
+ readonly to: Inlet<any>;
17
+ }
18
+ /** Serialized form of one node. Structure only — no values. */
19
+ export interface GraphNodeJson {
20
+ readonly id: string;
21
+ readonly kind: string;
22
+ readonly inputs: readonly string[];
23
+ readonly outputs: readonly string[];
24
+ }
25
+ /** Serialized form of one edge, by node id and port name. */
26
+ export interface GraphEdgeJson {
27
+ readonly from: {
28
+ readonly node: string;
29
+ readonly port: string;
30
+ };
31
+ readonly to: {
32
+ readonly node: string;
33
+ readonly port: string;
34
+ };
35
+ }
36
+ /** Structural dump of a graph. See {@link Graph.toJSON}. */
37
+ export interface GraphJson {
38
+ readonly nodes: readonly GraphNodeJson[];
39
+ readonly edges: readonly GraphEdgeJson[];
40
+ }
41
+ /** A read-only view over a wired set of nodes. */
42
+ export declare class Graph {
43
+ #private;
44
+ constructor(nodes: Iterable<Node<any, any>>);
45
+ /**
46
+ * Discovers every node reachable from `roots`, following connections
47
+ * in both directions.
48
+ *
49
+ * ```ts
50
+ * const graph = Graph.from(sink);
51
+ * graph.order().map((node) => node.kind);
52
+ * ```
53
+ */
54
+ static from(...roots: readonly Node<any, any>[]): Graph;
55
+ /** Every node in the graph, in discovery order. */
56
+ get nodes(): readonly Node<any, any>[];
57
+ /**
58
+ * Every connection, grouped by producing node in {@link order}. Like
59
+ * `order()`, the result is independent of how the graph was
60
+ * discovered.
61
+ */
62
+ edges(): readonly GraphEdge[];
63
+ /**
64
+ * Nodes in dependency order — every node appears after the nodes
65
+ * feeding it. Evaluation doesn't need this (pulling is recursive and
66
+ * finds its own order); it's for display and for reasoning about a
67
+ * graph you didn't build.
68
+ *
69
+ * The DFS is seeded in node-id order rather than discovery order, so
70
+ * the result depends only on which nodes are in the graph — not on
71
+ * which node `Graph.from` happened to start from. Several topological
72
+ * orders are usually valid; this picks the same one every time.
73
+ */
74
+ order(): readonly Node<any, any>[];
75
+ /**
76
+ * Structural dump: node ids, kinds, port names, and edges.
77
+ *
78
+ * Nodes come out in {@link order}, not discovery order, so the dump of
79
+ * a given graph is the same whichever node `Graph.from` started at —
80
+ * which is what makes it safe to diff two dumps against each other.
81
+ *
82
+ * This is a **description, not a serialization** — there is no
83
+ * `fromJSON`. Rebuilding a graph from JSON needs a registry mapping
84
+ * `kind` to a factory, plus per-node config in the dump; neither
85
+ * exists yet. Use this for inspection, diffing, and rendering.
86
+ */
87
+ toJSON(): GraphJson;
88
+ }
89
+ //# sourceMappingURL=graph.d.ts.map
package/dist/graph.js ADDED
@@ -0,0 +1,133 @@
1
+ /**
2
+ * Graph-level inspection: enumerate what's wired to what, get a
3
+ * topological order, dump the structure.
4
+ *
5
+ * Nodes work standalone — connections live on the ports, and evaluation
6
+ * never consults a container. `Graph` is a read-only view over an
7
+ * already-wired set of nodes, for the cases where you need the whole
8
+ * picture: rendering a node editor, logging evaluation order, asserting
9
+ * on structure in tests.
10
+ */
11
+ /** A read-only view over a wired set of nodes. */
12
+ export class Graph {
13
+ #nodes;
14
+ constructor(nodes) {
15
+ this.#nodes = [...new Set(nodes)];
16
+ }
17
+ /**
18
+ * Discovers every node reachable from `roots`, following connections
19
+ * in both directions.
20
+ *
21
+ * ```ts
22
+ * const graph = Graph.from(sink);
23
+ * graph.order().map((node) => node.kind);
24
+ * ```
25
+ */
26
+ static from(...roots) {
27
+ const seen = new Set();
28
+ const queue = [...roots];
29
+ while (queue.length > 0) {
30
+ const node = queue.shift();
31
+ if (seen.has(node))
32
+ continue;
33
+ seen.add(node);
34
+ for (const inlet of node.inletList()) {
35
+ const upstream = inlet.source;
36
+ if (upstream !== undefined)
37
+ queue.push(upstream.node);
38
+ }
39
+ for (const outlet of node.outletList()) {
40
+ for (const inlet of outlet.connections)
41
+ queue.push(inlet.node);
42
+ }
43
+ }
44
+ return new Graph(seen);
45
+ }
46
+ /** Every node in the graph, in discovery order. */
47
+ get nodes() {
48
+ return this.#nodes;
49
+ }
50
+ /**
51
+ * Every connection, grouped by producing node in {@link order}. Like
52
+ * `order()`, the result is independent of how the graph was
53
+ * discovered.
54
+ */
55
+ edges() {
56
+ const edges = [];
57
+ for (const node of this.order()) {
58
+ for (const outlet of node.outletList()) {
59
+ const consumers = [...outlet.connections].sort((a, b) => byId(a.node, b.node));
60
+ for (const inlet of consumers)
61
+ edges.push({ from: outlet, to: inlet });
62
+ }
63
+ }
64
+ return edges;
65
+ }
66
+ /**
67
+ * Nodes in dependency order — every node appears after the nodes
68
+ * feeding it. Evaluation doesn't need this (pulling is recursive and
69
+ * finds its own order); it's for display and for reasoning about a
70
+ * graph you didn't build.
71
+ *
72
+ * The DFS is seeded in node-id order rather than discovery order, so
73
+ * the result depends only on which nodes are in the graph — not on
74
+ * which node `Graph.from` happened to start from. Several topological
75
+ * orders are usually valid; this picks the same one every time.
76
+ */
77
+ order() {
78
+ const known = new Set(this.#nodes);
79
+ const visited = new Set();
80
+ const ordered = [];
81
+ const seeds = [...this.#nodes].sort(byId);
82
+ const visit = (node) => {
83
+ if (visited.has(node))
84
+ return;
85
+ visited.add(node);
86
+ for (const inlet of node.inletList()) {
87
+ const upstream = inlet.source?.node;
88
+ // Skip dependencies outside this graph — `order()` sorts the
89
+ // nodes it was given, it doesn't silently widen the set.
90
+ if (upstream !== undefined && known.has(upstream))
91
+ visit(upstream);
92
+ }
93
+ ordered.push(node);
94
+ };
95
+ for (const node of seeds)
96
+ visit(node);
97
+ return ordered;
98
+ }
99
+ /**
100
+ * Structural dump: node ids, kinds, port names, and edges.
101
+ *
102
+ * Nodes come out in {@link order}, not discovery order, so the dump of
103
+ * a given graph is the same whichever node `Graph.from` started at —
104
+ * which is what makes it safe to diff two dumps against each other.
105
+ *
106
+ * This is a **description, not a serialization** — there is no
107
+ * `fromJSON`. Rebuilding a graph from JSON needs a registry mapping
108
+ * `kind` to a factory, plus per-node config in the dump; neither
109
+ * exists yet. Use this for inspection, diffing, and rendering.
110
+ */
111
+ toJSON() {
112
+ return {
113
+ nodes: this.order().map((node) => ({
114
+ id: node.id,
115
+ kind: node.kind,
116
+ inputs: node.inletList().map((inlet) => inlet.name),
117
+ outputs: node.outletList().map((outlet) => outlet.name),
118
+ })),
119
+ edges: this.edges().map((edge) => ({
120
+ from: { node: edge.from.node.id, port: edge.from.name },
121
+ to: { node: edge.to.node.id, port: edge.to.name },
122
+ })),
123
+ };
124
+ }
125
+ }
126
+ /**
127
+ * Orders nodes by creation sequence. Node ids are `n1`, `n2`, … so a
128
+ * plain string sort would put `n10` before `n2`; compare numerically.
129
+ */
130
+ function byId(a, b) {
131
+ return a.id.localeCompare(b.id, undefined, { numeric: true });
132
+ }
133
+ //# sourceMappingURL=graph.js.map
@@ -0,0 +1,59 @@
1
+ /**
2
+ * `@pond-ts/process` — a typed dataflow graph over pond values.
3
+ *
4
+ * Where the rest of pond is chain-first (`series.rolling(...).aggregate(...)`),
5
+ * this package is for the case chaining can't express: when the pipeline
6
+ * itself is **data** — assembled at runtime, reshaped by a user, rendered
7
+ * as a node editor, or shared as one computation with several outputs.
8
+ *
9
+ * The chain API remains the primary way to use pond. Reach for a graph
10
+ * only when the topology isn't known at authoring time.
11
+ *
12
+ * ```ts
13
+ * import { source, derive } from '@pond-ts/process';
14
+ *
15
+ * const raw = source<TimeSeries<Schema>>();
16
+ * const hourly = derive({ s: raw.out.value }, ({ s }) =>
17
+ * s.aggregate(Sequence.every('1h'), { cpu: 'avg' }),
18
+ * );
19
+ *
20
+ * raw.set(series);
21
+ * hourly.out.value.get();
22
+ * ```
23
+ */
24
+ export { Inlet, Outlet } from './port.js';
25
+ export { Node, defineNode, derive } from './node.js';
26
+ export type { NodeSpec, NodeFactory, InletsFor, OutletsFor, OutletValue, SpecsForOutlets, DerivedOutput, } from './node.js';
27
+ export { port } from './types.js';
28
+ export type { PortSpec, PortSpecMap, PortValue, PortValues } from './types.js';
29
+ export { source, fromLive, SourceNode, LiveSourceNode, UnsetSourceError, } from './source.js';
30
+ export type { NoInputs, GraphSource, SnapshotSource } from './source.js';
31
+ export { columnBytes, packColumn, appendColumn, columnBuffers, columnFromBuffers, columnView, prepareRange, sealRange, validityByteCount, } from './column.js';
32
+ export type { ColumnBuffers, ColumnView, RangeOutput } from './column.js';
33
+ export { requiredHistory } from './plan/history.js';
34
+ export type { HistoryResult } from './plan/history.js';
35
+ export { Graph } from './graph.js';
36
+ export type { GraphEdge, GraphJson, GraphNodeJson, GraphEdgeJson, } from './graph.js';
37
+ export { ProcessError, CycleError, UnconnectedInputError, MissingOutputError, } from './errors.js';
38
+ export { createRegistry, Registry, int, num, choice, flag, } from './plan/registry.js';
39
+ export { UnknownOpError, ParamError } from './plan/registry.js';
40
+ export type { DefMap } from './plan/registry.js';
41
+ export { isFold } from './plan/types.js';
42
+ export { STANDARD_FOLDS, last, extremes, percentileRank, shape, } from './plan/folds.js';
43
+ export type { OpDescriptor } from './plan/registry.js';
44
+ export { specId, refToId, explain, unitOf, columnsOf, dependsOn, outputKey, } from './plan/identity.js';
45
+ export { bind, BoundGraph, UnitError } from './plan/graph.js';
46
+ export { expandSlots, SlotError } from './plan/slots.js';
47
+ export { plan, PlanBuilder, BuilderError } from './plan/builder.js';
48
+ export type { NodeHandle, OutputHandle, InputRef, BuiltRequest, } from './plan/builder.js';
49
+ export { process, ProcessBuilder } from './plan/fluent.js';
50
+ export type { BuildOptions, FluentColumnRef, SelectedColumnRef, SingleColumnNode, MultiColumnNode, ColumnSelection, FactRef, FluentRequest, } from './plan/fluent.js';
51
+ export { createSourceRegistry, defineSource, sourceId, SourceRegistry, UnknownSourceError, } from './plan/source.js';
52
+ export type { SourceParams, SourceRef, LoadedSource, SourceLoadContext, SourceDef, } from './plan/source.js';
53
+ export type { SlotDef, Slots } from './plan/slots.js';
54
+ export { createHost, Host, toWire, UnknownDatasetError } from './plan/host.js';
55
+ export type { Envelope, AsyncEnvelope, PlanEnvelope, SlotEnvelope, AsyncPlanEnvelope, AsyncSlotEnvelope, DatasetInfo, WireResult, } from './plan/host.js';
56
+ export { run } from './plan/run.js';
57
+ export type { ErrorPolicy, RunOptions, PlanRequest, SlotRequest, Select, RunRequest, RunResult, NodeTiming, OutputInfo, Fact, Skipped, } from './plan/run.js';
58
+ export type { Spec, Plan, Input, SpecRef, ParamValue, ParamDef, NumberParam, EnumParam, BooleanParam, Params, Units, UnitSpec, InputDef, OutputDef, OpContext, OpResult, OpDef, Def, FoldDef, FoldContext, FactBody, } from './plan/types.js';
59
+ //# sourceMappingURL=index.d.ts.map
package/dist/index.js ADDED
@@ -0,0 +1,45 @@
1
+ /**
2
+ * `@pond-ts/process` — a typed dataflow graph over pond values.
3
+ *
4
+ * Where the rest of pond is chain-first (`series.rolling(...).aggregate(...)`),
5
+ * this package is for the case chaining can't express: when the pipeline
6
+ * itself is **data** — assembled at runtime, reshaped by a user, rendered
7
+ * as a node editor, or shared as one computation with several outputs.
8
+ *
9
+ * The chain API remains the primary way to use pond. Reach for a graph
10
+ * only when the topology isn't known at authoring time.
11
+ *
12
+ * ```ts
13
+ * import { source, derive } from '@pond-ts/process';
14
+ *
15
+ * const raw = source<TimeSeries<Schema>>();
16
+ * const hourly = derive({ s: raw.out.value }, ({ s }) =>
17
+ * s.aggregate(Sequence.every('1h'), { cpu: 'avg' }),
18
+ * );
19
+ *
20
+ * raw.set(series);
21
+ * hourly.out.value.get();
22
+ * ```
23
+ */
24
+ export { Inlet, Outlet } from './port.js';
25
+ export { Node, defineNode, derive } from './node.js';
26
+ export { port } from './types.js';
27
+ export { source, fromLive, SourceNode, LiveSourceNode, UnsetSourceError, } from './source.js';
28
+ export { columnBytes, packColumn, appendColumn, columnBuffers, columnFromBuffers, columnView, prepareRange, sealRange, validityByteCount, } from './column.js';
29
+ export { requiredHistory } from './plan/history.js';
30
+ export { Graph } from './graph.js';
31
+ export { ProcessError, CycleError, UnconnectedInputError, MissingOutputError, } from './errors.js';
32
+ // ─── Plan layer ([PND-DEMOM0]) ──────────────────────────────────
33
+ export { createRegistry, Registry, int, num, choice, flag, } from './plan/registry.js';
34
+ export { UnknownOpError, ParamError } from './plan/registry.js';
35
+ export { isFold } from './plan/types.js';
36
+ export { STANDARD_FOLDS, last, extremes, percentileRank, shape, } from './plan/folds.js';
37
+ export { specId, refToId, explain, unitOf, columnsOf, dependsOn, outputKey, } from './plan/identity.js';
38
+ export { bind, BoundGraph, UnitError } from './plan/graph.js';
39
+ export { expandSlots, SlotError } from './plan/slots.js';
40
+ export { plan, PlanBuilder, BuilderError } from './plan/builder.js';
41
+ export { process, ProcessBuilder } from './plan/fluent.js';
42
+ export { createSourceRegistry, defineSource, sourceId, SourceRegistry, UnknownSourceError, } from './plan/source.js';
43
+ export { createHost, Host, toWire, UnknownDatasetError } from './plan/host.js';
44
+ export { run } from './plan/run.js';
45
+ //# sourceMappingURL=index.js.map