@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
|
@@ -0,0 +1,376 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Plan-layer types — [PND-DEMOM0].
|
|
3
|
+
*
|
|
4
|
+
* A plan is **data**: a DAG of `{ op, params, inputs }` specs that can
|
|
5
|
+
* arrive as JSON from a saved view or an agent. This file defines that
|
|
6
|
+
* shape and the op declarations it resolves against.
|
|
7
|
+
*/
|
|
8
|
+
import type { ColumnView, RangeOutput } from '../column.js';
|
|
9
|
+
import type { Column, TimeSeries, SeriesSchema } from 'pond-ts';
|
|
10
|
+
/** A JSON-safe param value. Params arrive off a wire, not from code. */
|
|
11
|
+
export type ParamValue = string | number | boolean;
|
|
12
|
+
/** One node in a plan: an op, its params, and where its inputs come from. */
|
|
13
|
+
export interface Spec {
|
|
14
|
+
readonly op: string;
|
|
15
|
+
readonly params?: Readonly<Record<string, ParamValue>>;
|
|
16
|
+
/** A raw source column name, or a nested spec. Plural from the start. */
|
|
17
|
+
readonly inputs: readonly Input[];
|
|
18
|
+
}
|
|
19
|
+
/**
|
|
20
|
+
* One named output of an upstream spec.
|
|
21
|
+
*
|
|
22
|
+
* A nested input used to read output 0 and nothing else, so `sma` of a
|
|
23
|
+
* Bollinger band could only ever smooth `Upper` — a limitation nobody
|
|
24
|
+
* hit while `select.output` existed to pick one at the end. Folds made
|
|
25
|
+
* it visible: a fold *is* the end, so with no way to say which output it
|
|
26
|
+
* reads, the middle and lower bands became unreachable.
|
|
27
|
+
*/
|
|
28
|
+
export interface PickedOutput {
|
|
29
|
+
readonly from: Spec;
|
|
30
|
+
/** The output's declared suffix, e.g. `'Lower'`. */
|
|
31
|
+
readonly output: string;
|
|
32
|
+
}
|
|
33
|
+
/** An input is a raw column name, another spec, or one output of one. */
|
|
34
|
+
export type Input = string | Spec | PickedOutput;
|
|
35
|
+
/** Narrows an input to the picked-output form. */
|
|
36
|
+
export declare function isPicked(input: Input): input is PickedOutput;
|
|
37
|
+
/** The spec an input refers to, ignoring which output it picks. */
|
|
38
|
+
export declare function specOf(input: Spec | PickedOutput): Spec;
|
|
39
|
+
/** A DAG. Declaration order is free; nesting is inline. */
|
|
40
|
+
export type Plan = readonly Spec[];
|
|
41
|
+
/** How a request refers to a resolved spec: inline, or by its id. */
|
|
42
|
+
export type SpecRef = string | Spec;
|
|
43
|
+
export interface NumberParam {
|
|
44
|
+
readonly kind: 'number' | 'integer';
|
|
45
|
+
readonly default: number;
|
|
46
|
+
/** The legal range. Outside it is an error. */
|
|
47
|
+
readonly min?: number;
|
|
48
|
+
readonly max?: number;
|
|
49
|
+
/**
|
|
50
|
+
* The **useful** range — `[lo, hi]`, within `[min, max]`.
|
|
51
|
+
*
|
|
52
|
+
* `min`/`max` answer "would this be rejected?", which is a different
|
|
53
|
+
* question from "what would anyone actually pick?", and the gap between
|
|
54
|
+
* them is usually enormous: a `period` legal to 5000 is interesting
|
|
55
|
+
* below about 200, so a slider drawn on the legal range spends 96% of
|
|
56
|
+
* its travel where nobody goes. Declaring the useful range is what lets
|
|
57
|
+
* a control be drawn on it and a model be told it.
|
|
58
|
+
*
|
|
59
|
+
* Purely advisory — nothing rejects a value outside it, since a bound
|
|
60
|
+
* that rejects is what `min`/`max` already are.
|
|
61
|
+
*/
|
|
62
|
+
readonly suggest?: readonly [number, number];
|
|
63
|
+
readonly label?: string;
|
|
64
|
+
}
|
|
65
|
+
export interface EnumParam {
|
|
66
|
+
readonly kind: 'enum';
|
|
67
|
+
readonly default: string;
|
|
68
|
+
readonly of: readonly string[];
|
|
69
|
+
readonly label?: string;
|
|
70
|
+
}
|
|
71
|
+
export interface BooleanParam {
|
|
72
|
+
readonly kind: 'boolean';
|
|
73
|
+
readonly default: boolean;
|
|
74
|
+
readonly label?: string;
|
|
75
|
+
}
|
|
76
|
+
export type ParamDef = NumberParam | EnumParam | BooleanParam;
|
|
77
|
+
/** The resolved param record an op's `run` receives, post-defaults. */
|
|
78
|
+
export type Params = Readonly<Record<string, ParamValue>>;
|
|
79
|
+
/**
|
|
80
|
+
* A unit is either declared outright or inherited from input 0.
|
|
81
|
+
*
|
|
82
|
+
* Units are an **input to resolution**, not a property of a series: pond
|
|
83
|
+
* series do not carry units, consumers do. Passing them in is what lets
|
|
84
|
+
* an op demand a typed input (an annualiser wanting `variance`) and what
|
|
85
|
+
* lets a response report concrete units.
|
|
86
|
+
*/
|
|
87
|
+
export type UnitSpec = 'inherit' | string;
|
|
88
|
+
/** Consumer-supplied units for the bound source's raw columns. */
|
|
89
|
+
export type Units = Readonly<Record<string, string>>;
|
|
90
|
+
export interface InputDef {
|
|
91
|
+
/** Name the op's `run` reads this input by. */
|
|
92
|
+
readonly role: string;
|
|
93
|
+
/** A unit this input must already carry, if the op demands one. */
|
|
94
|
+
readonly unit?: string;
|
|
95
|
+
}
|
|
96
|
+
export interface OutputDef {
|
|
97
|
+
/**
|
|
98
|
+
* Suffix appended to the spec's id to name this output's column.
|
|
99
|
+
*
|
|
100
|
+
* `''` for a single-output op, so the column *is* the id. A band
|
|
101
|
+
* declares `'Upper' | 'Middle' | 'Lower'`, matching the corpus's own
|
|
102
|
+
* `prefix` convention — one spec, three columns, moved and deleted as
|
|
103
|
+
* a unit.
|
|
104
|
+
*/
|
|
105
|
+
readonly id: string;
|
|
106
|
+
readonly unit: UnitSpec;
|
|
107
|
+
/**
|
|
108
|
+
* Params this output depends on. When declared, a change to a param
|
|
109
|
+
* *not* listed here leaves this output's version untouched, so
|
|
110
|
+
* everything downstream of it skips ([PND-PROCSEL]). Omit to mean
|
|
111
|
+
* "depends on everything", which is correct but never skips.
|
|
112
|
+
*/
|
|
113
|
+
readonly dependsOn?: readonly string[];
|
|
114
|
+
}
|
|
115
|
+
/** What an op's `run` is handed. */
|
|
116
|
+
export interface OpContext {
|
|
117
|
+
/**
|
|
118
|
+
* The bound source, widened to carry this op's input columns under the
|
|
119
|
+
* names in {@link inputs}. Prepared by the plan layer so an op can call
|
|
120
|
+
* the corpus normally — the studies take `(series, { column })`.
|
|
121
|
+
*/
|
|
122
|
+
readonly series: TimeSeries<SeriesSchema>;
|
|
123
|
+
/** Column name per input role. */
|
|
124
|
+
readonly inputs: Readonly<Record<string, string>>;
|
|
125
|
+
readonly params: Params;
|
|
126
|
+
/** This spec's id — the output column name, or a band's prefix. */
|
|
127
|
+
readonly id: string;
|
|
128
|
+
}
|
|
129
|
+
/**
|
|
130
|
+
* What a ranged recompute is given — [PND-PROCRANGE].
|
|
131
|
+
*
|
|
132
|
+
* ## The previous output is an argument, not state
|
|
133
|
+
*
|
|
134
|
+
* The obvious way to make recompute incremental is to let a node reach
|
|
135
|
+
* for its own last output and patch it. That makes `compute` a function
|
|
136
|
+
* of its inputs *and* of history, which is a real loss: two callers with
|
|
137
|
+
* the same data but different edit sequences can disagree, and `explain`
|
|
138
|
+
* stops describing what a value actually depends on.
|
|
139
|
+
*
|
|
140
|
+
* Passing {@link previous} in as an argument keeps the op a pure
|
|
141
|
+
* function — of more things than before, but declared things. The
|
|
142
|
+
* mutable part stays in the graph, which is a cache and was already
|
|
143
|
+
* impure.
|
|
144
|
+
*
|
|
145
|
+
* ## Why this is safe now and was not before
|
|
146
|
+
*
|
|
147
|
+
* Incremental recompute is only honest if a patched result equals a
|
|
148
|
+
* from-scratch one. Until [PND-PROCKERN] it did not: a ranged fill over
|
|
149
|
+
* the rolling kernel differed on **every cell**, because the accumulator
|
|
150
|
+
* carried a different rounding history. That is fixed for `avg`/`stdev`,
|
|
151
|
+
* which is why an op **opts in** rather than getting this by default —
|
|
152
|
+
* an op whose kernel is not range-exact must not declare `runRange`, or
|
|
153
|
+
* its answers become dependent on the sequence of edits that produced
|
|
154
|
+
* them. `median`, percentiles, `min` and `max` are in that category
|
|
155
|
+
* today.
|
|
156
|
+
*/
|
|
157
|
+
export interface RangeContext extends OpContext {
|
|
158
|
+
/** First row that must be recomputed, already widened by the lookback. */
|
|
159
|
+
readonly from: number;
|
|
160
|
+
/** One past the last row — the series length. */
|
|
161
|
+
readonly to: number;
|
|
162
|
+
/**
|
|
163
|
+
* This node's previous output, one entry per declared output.
|
|
164
|
+
*
|
|
165
|
+
* Shorter than the current series when rows were appended. An op
|
|
166
|
+
* copies what it keeps and fills `[from, to)`.
|
|
167
|
+
*/
|
|
168
|
+
readonly previous: readonly Column[];
|
|
169
|
+
/**
|
|
170
|
+
* The same outputs as **zero-copy views**, positionally aligned with
|
|
171
|
+
* {@link previous} — `undefined` for any that is not packed numeric.
|
|
172
|
+
*
|
|
173
|
+
* This is what makes ranged recompute worth doing, and reading
|
|
174
|
+
* {@link previous} cell by cell instead is the difference between a
|
|
175
|
+
* `memcpy` and a walk. An op carries `[0, from)` forward unchanged, so
|
|
176
|
+
* that prefix should move as a block:
|
|
177
|
+
*
|
|
178
|
+
* ```ts
|
|
179
|
+
* const prior = ctx.previousView[0];
|
|
180
|
+
* const out = new Float64Array(ctx.to);
|
|
181
|
+
* if (prior) out.set(prior.values.subarray(0, ctx.from));
|
|
182
|
+
* for (let i = ctx.from; i < ctx.to; i += 1) out[i] = …;
|
|
183
|
+
* ```
|
|
184
|
+
*
|
|
185
|
+
* Measured over 500k rows × 5 studies: the boxed form — a `.at(i)`
|
|
186
|
+
* per cell into an `Array` — left ranged recompute at **4×** against a
|
|
187
|
+
* full pass. The same op reading this view runs at **31×**. The graph
|
|
188
|
+
* was never the bottleneck; the prefix walk was.
|
|
189
|
+
*
|
|
190
|
+
* Borrowed, like any {@link ColumnView}: read within the call, never
|
|
191
|
+
* retain.
|
|
192
|
+
*/
|
|
193
|
+
readonly previousView: readonly (ColumnView | undefined)[];
|
|
194
|
+
/**
|
|
195
|
+
* Prepared output buffers, one per declared output, each of length
|
|
196
|
+
* {@link to} and already carrying `[0, from)` from the previous
|
|
197
|
+
* result — values **and** validity, copied as blocks.
|
|
198
|
+
*
|
|
199
|
+
* **This is the fast path, and also the correct one.** Writing here
|
|
200
|
+
* and returning nothing lets the graph seal the buffers directly.
|
|
201
|
+
* Rebuilding the whole output instead means carrying the prefix by
|
|
202
|
+
* hand, and the obvious way to do that is silently wrong: packed
|
|
203
|
+
* storage holds `0` at a missing cell rather than `NaN`, so copying
|
|
204
|
+
* only the values turns every warm-up gap into a defined zero — 1,875
|
|
205
|
+
* wrong cells on a 5-study pass, caught by comparing against a
|
|
206
|
+
* from-scratch run rather than by any type.
|
|
207
|
+
*
|
|
208
|
+
* ```ts
|
|
209
|
+
* runRange: (ctx) => {
|
|
210
|
+
* const out = ctx.out[0]!;
|
|
211
|
+
* for (let i = ctx.from; i < ctx.to; i += 1) {
|
|
212
|
+
* const v = compute(i);
|
|
213
|
+
* if (v === undefined) out.clear(i);
|
|
214
|
+
* else out.set(i, v);
|
|
215
|
+
* }
|
|
216
|
+
* // no return
|
|
217
|
+
* }
|
|
218
|
+
* ```
|
|
219
|
+
*/
|
|
220
|
+
readonly out: readonly RangeOutput[];
|
|
221
|
+
}
|
|
222
|
+
/**
|
|
223
|
+
* An op's result: one entry per declared output, in declaration order.
|
|
224
|
+
*
|
|
225
|
+
* A `Column` is returned as-is (already packed); loose values are packed
|
|
226
|
+
* once by the plan layer rather than retained boxed ([PND-PROCCOL]).
|
|
227
|
+
*/
|
|
228
|
+
export type OpResult = Column | ArrayLike<number | undefined> | readonly (Column | ArrayLike<number | undefined>)[];
|
|
229
|
+
export interface OpDef {
|
|
230
|
+
/** Discriminates against {@link FoldDef}. Optional, because an op is the default. */
|
|
231
|
+
readonly kind?: 'op';
|
|
232
|
+
readonly name: string;
|
|
233
|
+
readonly family: string;
|
|
234
|
+
readonly summary: string;
|
|
235
|
+
readonly params: Readonly<Record<string, ParamDef>>;
|
|
236
|
+
readonly inputs: readonly InputDef[];
|
|
237
|
+
readonly outputs: readonly OutputDef[];
|
|
238
|
+
/**
|
|
239
|
+
* Human lineage fragment, e.g. `SMA(20) of iv21`. Optional — a generic
|
|
240
|
+
* `op(params) of inputs` is derived when absent, but a label reads far
|
|
241
|
+
* better in a legend chip or a graph node.
|
|
242
|
+
*/
|
|
243
|
+
readonly label?: (params: Params, inputs: string) => string;
|
|
244
|
+
/**
|
|
245
|
+
* Rows of history this op needs **before** a requested range for its
|
|
246
|
+
* output over that range to be fully defined — [PND-PROCHIST].
|
|
247
|
+
*
|
|
248
|
+
* A count-window study of `period` bars needs `period - 1`. Declaring
|
|
249
|
+
* it lets {@link requiredHistory} derive the minimum safe tail for a
|
|
250
|
+
* whole plan, so a consumer slicing a hot leading edge stops guessing:
|
|
251
|
+
* an 8-study stack over 500k rows costs 765 ms/tick, and the same stack
|
|
252
|
+
* over a 5,000-row tail 5.4 ms/tick. The window is the difference
|
|
253
|
+
* between 1.3 ticks/sec and interactive.
|
|
254
|
+
*
|
|
255
|
+
* **An IIR op has no exact finite warm-up** — an EMA depends on every
|
|
256
|
+
* row before it, decaying but never reaching zero. `4 * period` is the
|
|
257
|
+
* usual engineering answer and each such op should declare it rather
|
|
258
|
+
* than have the folder assume one, because the multiplier is a claim
|
|
259
|
+
* about acceptable error and only the op knows what it is.
|
|
260
|
+
*
|
|
261
|
+
* Omitted means **unknown, not zero**, and {@link requiredHistory}
|
|
262
|
+
* reports that rather than returning a number a caller would slice
|
|
263
|
+
* against. An op that genuinely needs no history — anything
|
|
264
|
+
* element-wise — should say `() => 0`.
|
|
265
|
+
*/
|
|
266
|
+
readonly lookback?: (params: Params) => number;
|
|
267
|
+
readonly run: (ctx: OpContext) => OpResult;
|
|
268
|
+
/**
|
|
269
|
+
* Recompute only `[from, to)`, given the previous output —
|
|
270
|
+
* [PND-PROCRANGE]. Optional, and **opt-in for a reason**.
|
|
271
|
+
*
|
|
272
|
+
* The graph calls this instead of {@link run} when it knows which rows
|
|
273
|
+
* changed and this node has a previous output to patch; otherwise it
|
|
274
|
+
* falls back to a full {@link run}, so declaring nothing is always
|
|
275
|
+
* correct and merely slower.
|
|
276
|
+
*
|
|
277
|
+
* **Only declare it if a patched result is bit-identical to a
|
|
278
|
+
* from-scratch one.** That holds for the range-exact rolling kernel
|
|
279
|
+
* ([PND-PROCKERN]) and does not hold for `median`, percentiles, `min`
|
|
280
|
+
* or `max`, which still sweep whole-series. An op that declares this
|
|
281
|
+
* without that property makes its answers depend on the sequence of
|
|
282
|
+
* edits that produced them — which is invisible in a test that only
|
|
283
|
+
* ever computes from scratch.
|
|
284
|
+
*
|
|
285
|
+
* Requires {@link lookback}, since that is what widens an upstream
|
|
286
|
+
* dirty range into this node's. Without it the graph cannot know how
|
|
287
|
+
* far back a change reaches and will not range.
|
|
288
|
+
*/
|
|
289
|
+
readonly runRange?: (ctx: RangeContext) => OpResult | void;
|
|
290
|
+
}
|
|
291
|
+
/**
|
|
292
|
+
* What a fold returns: the body of a fact.
|
|
293
|
+
*
|
|
294
|
+
* Deliberately loose. `last` answers with a value and a timestamp,
|
|
295
|
+
* `extremes` with two of each, `percentileRank` with a fraction and a
|
|
296
|
+
* sentence explaining it. Forcing those into one shape would mean
|
|
297
|
+
* inventing a lowest common denominator that suits none of them.
|
|
298
|
+
*/
|
|
299
|
+
export type FactBody = Readonly<Record<string, unknown>>;
|
|
300
|
+
export interface FoldContext {
|
|
301
|
+
/**
|
|
302
|
+
* Dense values per input role, gaps as `undefined`.
|
|
303
|
+
*
|
|
304
|
+
* Prepared by the graph rather than by each fold, and — the point of
|
|
305
|
+
* the original exercise — prepared **inside the memo**, so densifying
|
|
306
|
+
* a 150,000-row column happens once per version rather than once per
|
|
307
|
+
* request.
|
|
308
|
+
*
|
|
309
|
+
* **Prefer {@link FoldContext.numeric}.** This is the boxed form, and
|
|
310
|
+
* it is now **lazy**: touching a role allocates an `Array` of that
|
|
311
|
+
* column's length and fills it, which is the single largest heap cost
|
|
312
|
+
* in the graph ([PND-PROCCOL]). `latest` reads one cell and used to
|
|
313
|
+
* pay for 500,000 of them. Untouched roles cost nothing, so a fold
|
|
314
|
+
* that never reads this never allocates.
|
|
315
|
+
*/
|
|
316
|
+
readonly values: Readonly<Record<string, readonly (number | undefined)[]>>;
|
|
317
|
+
/**
|
|
318
|
+
* A **zero-copy** columnar view of one input role — no allocation, at
|
|
319
|
+
* any length.
|
|
320
|
+
*
|
|
321
|
+
* `values` is the column's own storage and a cell is meaningful only
|
|
322
|
+
* where `defined(i)`; both are borrowed and must not be retained past
|
|
323
|
+
* the fold. `undefined` when the role's column is not packed numeric
|
|
324
|
+
* (a string column, or a value an op returned boxed), which is the
|
|
325
|
+
* caller's cue to fall back to {@link FoldContext.values}.
|
|
326
|
+
*
|
|
327
|
+
* Reading is **not faster** this way — a buffer walk reaches parity
|
|
328
|
+
* with the boxed array and `Column.scan()` is 4.7× slower than either.
|
|
329
|
+
* What changes is that nothing is allocated to do it.
|
|
330
|
+
*/
|
|
331
|
+
numeric(role: string): ColumnView | undefined;
|
|
332
|
+
/**
|
|
333
|
+
* Timestamp at a row index.
|
|
334
|
+
*
|
|
335
|
+
* A function rather than an array because a fold reports two or three
|
|
336
|
+
* rows out of 150,000, and materializing the key column to answer that
|
|
337
|
+
* was most of what a reduction used to cost.
|
|
338
|
+
*/
|
|
339
|
+
readonly at: (index: number) => number;
|
|
340
|
+
readonly params: Params;
|
|
341
|
+
/** This fold's id — the fact's own cache key and citation. */
|
|
342
|
+
readonly id: string;
|
|
343
|
+
}
|
|
344
|
+
/**
|
|
345
|
+
* A **fold** — a node that ends in a fact rather than a column.
|
|
346
|
+
*
|
|
347
|
+
* Reductions used to be a fixed enum on the selector, computed after the
|
|
348
|
+
* graph had finished: `last` rescanned 150,000 values on a total cache
|
|
349
|
+
* hit, and `percentileRank` densified and filtered twice, every request,
|
|
350
|
+
* forever. Measured at 10.85 ms of an 11.6 ms warm run — the graph
|
|
351
|
+
* memoized every intermediate and then recomputed the only part anyone
|
|
352
|
+
* actually read.
|
|
353
|
+
*
|
|
354
|
+
* A fold is an ordinary registry entry with an ordinary content-addressed
|
|
355
|
+
* id, so it caches, it carries provenance, and a consumer adds one by
|
|
356
|
+
* calling `define` rather than by editing this library. What it cannot do
|
|
357
|
+
* is feed anything: a fold produces a fact, so it is always a leaf, and
|
|
358
|
+
* naming one as an op's input is rejected at compile time.
|
|
359
|
+
*/
|
|
360
|
+
export interface FoldDef {
|
|
361
|
+
readonly kind: 'fold';
|
|
362
|
+
readonly name: string;
|
|
363
|
+
readonly family: string;
|
|
364
|
+
readonly summary: string;
|
|
365
|
+
readonly params: Readonly<Record<string, ParamDef>>;
|
|
366
|
+
readonly inputs: readonly InputDef[];
|
|
367
|
+
/** The unit the fact carries. `'inherit'` takes input 0's. */
|
|
368
|
+
readonly unit: UnitSpec;
|
|
369
|
+
readonly label?: (params: Params, inputs: string) => string;
|
|
370
|
+
readonly fold: (ctx: FoldContext) => FactBody;
|
|
371
|
+
}
|
|
372
|
+
/** Anything the registry holds. */
|
|
373
|
+
export type Def = OpDef | FoldDef;
|
|
374
|
+
/** Narrows a registry entry to the terminal kind. */
|
|
375
|
+
export declare function isFold(def: Def): def is FoldDef;
|
|
376
|
+
//# sourceMappingURL=types.d.ts.map
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Plan-layer types — [PND-DEMOM0].
|
|
3
|
+
*
|
|
4
|
+
* A plan is **data**: a DAG of `{ op, params, inputs }` specs that can
|
|
5
|
+
* arrive as JSON from a saved view or an agent. This file defines that
|
|
6
|
+
* shape and the op declarations it resolves against.
|
|
7
|
+
*/
|
|
8
|
+
/** Narrows an input to the picked-output form. */
|
|
9
|
+
export function isPicked(input) {
|
|
10
|
+
return typeof input !== 'string' && 'from' in input;
|
|
11
|
+
}
|
|
12
|
+
/** The spec an input refers to, ignoring which output it picks. */
|
|
13
|
+
export function specOf(input) {
|
|
14
|
+
return 'from' in input ? input.from : input;
|
|
15
|
+
}
|
|
16
|
+
/** Narrows a registry entry to the terminal kind. */
|
|
17
|
+
export function isFold(def) {
|
|
18
|
+
return def.kind === 'fold';
|
|
19
|
+
}
|
|
20
|
+
//# sourceMappingURL=types.js.map
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `@pond-ts/process/pool` — whole requests across resident workers.
|
|
3
|
+
*
|
|
4
|
+
* A separate entry point because it is **Node-only** (it imports
|
|
5
|
+
* `node:worker_threads`); the package index stays runtime-neutral.
|
|
6
|
+
*
|
|
7
|
+
* See {@link HostPool} for which of the two parallelism shapes this is,
|
|
8
|
+
* and why this one comes first.
|
|
9
|
+
*/
|
|
10
|
+
export { HostPool } from './pool.js';
|
|
11
|
+
export type { HostPoolOptions } from './pool.js';
|
|
12
|
+
export type { PoolSetup, PoolSetupConfig } from './protocol.js';
|
|
13
|
+
export { toWire, fromWire } from './wire.js';
|
|
14
|
+
export type { WireResult, WireColumn } from './wire.js';
|
|
15
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `@pond-ts/process/pool` — whole requests across resident workers.
|
|
3
|
+
*
|
|
4
|
+
* A separate entry point because it is **Node-only** (it imports
|
|
5
|
+
* `node:worker_threads`); the package index stays runtime-neutral.
|
|
6
|
+
*
|
|
7
|
+
* See {@link HostPool} for which of the two parallelism shapes this is,
|
|
8
|
+
* and why this one comes first.
|
|
9
|
+
*/
|
|
10
|
+
export { HostPool } from './pool.js';
|
|
11
|
+
export { toWire, fromWire } from './wire.js';
|
|
12
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `HostPool` — whole requests across resident workers ([PND-PROCPAR]).
|
|
3
|
+
*
|
|
4
|
+
* ## Which parallelism this is
|
|
5
|
+
*
|
|
6
|
+
* The worker-threads assessment
|
|
7
|
+
* (`docs/notes/worker-threads-assessment-2026-07.md`) found two distinct
|
|
8
|
+
* wins and warned against conflating them:
|
|
9
|
+
*
|
|
10
|
+
* - **Latency** of one composite query — split its nodes across workers.
|
|
11
|
+
* Measured 2.42× on a 5-study stack, and it needs an engine change: a
|
|
12
|
+
* node's value can only be produced by its own `compute`, so a result
|
|
13
|
+
* computed elsewhere has nowhere to land. Still ahead of us.
|
|
14
|
+
* - **Throughput** under concurrent queries — run *whole* requests, each
|
|
15
|
+
* single-threaded, on a pool of resident hosts. Near-linear, no
|
|
16
|
+
* decomposition, no numeric-semantics questions, **and no engine
|
|
17
|
+
* change at all**.
|
|
18
|
+
*
|
|
19
|
+
* This is the second. It is deliberately first: the agent workload is
|
|
20
|
+
* many overlapping questions, so throughput is what it feels, and this
|
|
21
|
+
* shape cannot get an answer wrong — each request runs the same
|
|
22
|
+
* single-threaded code it runs today, in a different isolate.
|
|
23
|
+
*
|
|
24
|
+
* ## What makes it cheap
|
|
25
|
+
*
|
|
26
|
+
* Each worker holds a **long-lived `Host`**, so compiled nodes and
|
|
27
|
+
* cached columns survive between requests exactly as they do in-process.
|
|
28
|
+
* The pool is only a router. What crosses the boundary is a plan
|
|
29
|
+
* (JSON by construction) and a result whose columns travel as
|
|
30
|
+
* transferable buffers rather than boxed values.
|
|
31
|
+
*
|
|
32
|
+
* ## What it is not
|
|
33
|
+
*
|
|
34
|
+
* Not a cache-sharing scheme. Each worker warms its own graph, so N
|
|
35
|
+
* workers hold up to N copies of a hot column and a question already
|
|
36
|
+
* answered by worker 2 is cold on worker 3. That is the honest cost of
|
|
37
|
+
* the simple shape: it buys throughput, not deduplication. Route related
|
|
38
|
+
* requests to the same worker (see {@link HostPool.run}'s `affinity`) if
|
|
39
|
+
* repeat-hit rate matters more than even spread.
|
|
40
|
+
*/
|
|
41
|
+
import type { AsyncEnvelope } from '../plan/host.js';
|
|
42
|
+
import type { RunResult } from '../plan/run.js';
|
|
43
|
+
export interface HostPoolOptions {
|
|
44
|
+
/**
|
|
45
|
+
* Module specifier for the worker's setup module — a `URL`, or an
|
|
46
|
+
* absolute path/specifier a worker can `import()`.
|
|
47
|
+
*
|
|
48
|
+
* It must export a setup function (as `default` or `setup`) returning
|
|
49
|
+
* `{ registry, units?, sources?, datasets? }`. A module, not a value,
|
|
50
|
+
* because a registry is functions and functions do not survive
|
|
51
|
+
* structured clone.
|
|
52
|
+
*/
|
|
53
|
+
readonly setup: URL | string;
|
|
54
|
+
/** Workers to start. Default: `availableParallelism() - 1`, min 1. */
|
|
55
|
+
readonly size?: number;
|
|
56
|
+
/** Passed to the setup function, so one module can serve several pools. */
|
|
57
|
+
readonly setupOptions?: unknown;
|
|
58
|
+
}
|
|
59
|
+
export declare class HostPool {
|
|
60
|
+
#private;
|
|
61
|
+
private constructor();
|
|
62
|
+
/**
|
|
63
|
+
* Starts the pool and waits for every worker to be listening.
|
|
64
|
+
*
|
|
65
|
+
* Setup modules are imported lazily *inside* each worker on its first
|
|
66
|
+
* request, so `start` resolving does not mean the registry loaded — a
|
|
67
|
+
* broken setup module surfaces as that first request's error, where a
|
|
68
|
+
* caller can read it, rather than as a start-up crash with no
|
|
69
|
+
* request to attach it to.
|
|
70
|
+
*/
|
|
71
|
+
static start(options: HostPoolOptions): Promise<HostPool>;
|
|
72
|
+
/** Workers in the pool. */
|
|
73
|
+
get size(): number;
|
|
74
|
+
/** Requests dispatched and not yet answered. */
|
|
75
|
+
get inFlight(): number;
|
|
76
|
+
/**
|
|
77
|
+
* Runs one envelope on the least-busy worker.
|
|
78
|
+
*
|
|
79
|
+
* `assemble` is forced off — the pool answers `columns`, and the caller
|
|
80
|
+
* assembles a `TimeSeries` from them if it wants one. Assembling
|
|
81
|
+
* worker-side would build an object that cannot cross the boundary.
|
|
82
|
+
*
|
|
83
|
+
* Pass `affinity` to pin related requests to one worker. Requests
|
|
84
|
+
* sharing an affinity key land on the same host, so its warm nodes are
|
|
85
|
+
* reused — worth it when a caller re-asks overlapping questions about
|
|
86
|
+
* one dataset, and pointless when every request is unrelated.
|
|
87
|
+
*/
|
|
88
|
+
run(envelope: AsyncEnvelope, affinity?: string): Promise<RunResult>;
|
|
89
|
+
/** Terminates every worker. Outstanding requests reject. */
|
|
90
|
+
close(): Promise<void>;
|
|
91
|
+
}
|
|
92
|
+
//# sourceMappingURL=pool.d.ts.map
|