@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/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
|
package/dist/errors.d.ts
ADDED
|
@@ -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
|
package/dist/graph.d.ts
ADDED
|
@@ -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
|
package/dist/index.d.ts
ADDED
|
@@ -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
|