@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/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Peter Murphy
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,345 @@
|
|
|
1
|
+
# @pond-ts/process
|
|
2
|
+
|
|
3
|
+
> **Experimental.** Pre-1.0, and the API is expected to move as friction
|
|
4
|
+
> reports land — pin an exact version. The design iterates in the open
|
|
5
|
+
> against
|
|
6
|
+
> [RFC #543](https://github.com/pond-ts/pond/blob/main/docs/rfcs/process.md);
|
|
7
|
+
> the roadmap and the measurements behind every design call are in
|
|
8
|
+
> [PND_PROCESS_PLAN.md](https://github.com/pond-ts/pond/blob/main/docs/plans/PND_PROCESS_PLAN.md),
|
|
9
|
+
> reproducible from
|
|
10
|
+
> [`scripts/`](https://github.com/pond-ts/pond/tree/main/packages/process/scripts).
|
|
11
|
+
|
|
12
|
+
**Computations as data over
|
|
13
|
+
[pond-ts](https://www.npmjs.com/package/pond-ts).** A processing graph
|
|
14
|
+
authored fluently in application code — or composed as JSON by a saved
|
|
15
|
+
view or a tool-calling model — resolves against a declared op vocabulary
|
|
16
|
+
and runs over a bound `TimeSeries`, with content-addressed caching,
|
|
17
|
+
provenance, and per-node timings on every response. Underneath sits a
|
|
18
|
+
small pull-based evaluation engine: nodes with typed ports, memoized
|
|
19
|
+
results, and change propagation that stops as soon as a value stops
|
|
20
|
+
changing.
|
|
21
|
+
|
|
22
|
+
**Docs: [pond-ts.org/docs/process](https://pond-ts.org/docs/process/)** —
|
|
23
|
+
tutorial, fluent authoring, the request/response contract, hosts and
|
|
24
|
+
sources, caching and budgets.
|
|
25
|
+
|
|
26
|
+
```sh
|
|
27
|
+
npm install @pond-ts/process pond-ts
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
`pond-ts` is a peer dependency.
|
|
31
|
+
|
|
32
|
+
## When _not_ to use this
|
|
33
|
+
|
|
34
|
+
Chaining is pond's mental model and stays the right default:
|
|
35
|
+
|
|
36
|
+
```ts
|
|
37
|
+
const out = series
|
|
38
|
+
.rolling('5m', { cpu: 'avg' })
|
|
39
|
+
.aggregate(Sequence.every('1h'), {
|
|
40
|
+
cpu: 'max',
|
|
41
|
+
});
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
That is clearer than any graph, and pond's design notes deliberately
|
|
45
|
+
[resist operator-graph vocabulary](https://github.com/pond-ts/pond/blob/main/docs/rfcs/streaming.md) for the
|
|
46
|
+
core API — you do not submit a job graph to a runtime.
|
|
47
|
+
|
|
48
|
+
This package exists for the case chaining genuinely cannot express: when
|
|
49
|
+
**the pipeline itself is data**. Assembled at runtime from config,
|
|
50
|
+
reshaped by a user in an editor, or fanning one expensive computation out
|
|
51
|
+
to several consumers that each want a different slice. If your pipeline is
|
|
52
|
+
known when you write the code, chain it and skip this package.
|
|
53
|
+
|
|
54
|
+
## Fluent plans
|
|
55
|
+
|
|
56
|
+
Bind authoring to a registry when application code constructs the graph.
|
|
57
|
+
The registry's literal type supplies the operation methods, params, secondary
|
|
58
|
+
input roles, and multi-output suffixes:
|
|
59
|
+
|
|
60
|
+
```ts
|
|
61
|
+
const graph = process(registry, 'ACME_5m').as('bands_and_stretch');
|
|
62
|
+
const close = graph.column('close');
|
|
63
|
+
|
|
64
|
+
const bands = close.bollinger({
|
|
65
|
+
as: 'bands',
|
|
66
|
+
period: 20,
|
|
67
|
+
stdDev: 2,
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
const width = bands.output('Upper').subtract({
|
|
71
|
+
as: 'width',
|
|
72
|
+
right: bands.output('Lower'),
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
const request = graph.outputs({
|
|
76
|
+
bands: bands.columns(),
|
|
77
|
+
upper: bands.output('Upper').columns(),
|
|
78
|
+
latestWidth: width.last(),
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
const result = host.run(request);
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
`period: '20'`, `bands.output('Banana')`, or an omitted `right` input are
|
|
85
|
+
compile errors. The result is nevertheless plain slot-plan data: the fluent
|
|
86
|
+
surface has no resolver or cache of its own, and a model-composed JSON request
|
|
87
|
+
lands on the same nodes.
|
|
88
|
+
|
|
89
|
+
Use `plan(from).add(...)` when the operation itself is dynamic and cannot be
|
|
90
|
+
known to TypeScript.
|
|
91
|
+
|
|
92
|
+
## Remote sources
|
|
93
|
+
|
|
94
|
+
A graph may name an opaque asynchronous source instead of a preloaded dataset.
|
|
95
|
+
The request carries its name and params; URLs, credentials, and loader code stay
|
|
96
|
+
on the host:
|
|
97
|
+
|
|
98
|
+
```ts
|
|
99
|
+
const marketBars = defineSource({
|
|
100
|
+
name: 'market.bars',
|
|
101
|
+
async load(
|
|
102
|
+
{
|
|
103
|
+
symbol,
|
|
104
|
+
interval,
|
|
105
|
+
}: {
|
|
106
|
+
symbol: string;
|
|
107
|
+
interval: '1m' | '5m' | '1h';
|
|
108
|
+
},
|
|
109
|
+
{ previous },
|
|
110
|
+
) {
|
|
111
|
+
const response = await fetch(
|
|
112
|
+
`${MARKET_API}/bars?symbol=${symbol}&interval=${interval}`,
|
|
113
|
+
{
|
|
114
|
+
headers: {
|
|
115
|
+
Authorization: `Bearer ${MARKET_TOKEN}`,
|
|
116
|
+
...(previous && { 'If-None-Match': previous.revision }),
|
|
117
|
+
},
|
|
118
|
+
},
|
|
119
|
+
);
|
|
120
|
+
if (response.status === 304) return previous!;
|
|
121
|
+
return {
|
|
122
|
+
value: TimeSeries.fromJSON(await response.json()),
|
|
123
|
+
revision: response.headers.get('etag')!,
|
|
124
|
+
};
|
|
125
|
+
},
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
const sources = createSourceRegistry().define(marketBars);
|
|
129
|
+
const host = createHost({ registry, sources, units });
|
|
130
|
+
|
|
131
|
+
const graph = process(
|
|
132
|
+
registry,
|
|
133
|
+
marketBars.ref({ symbol: 'ACME', interval: '5m' }),
|
|
134
|
+
);
|
|
135
|
+
const close = graph.column('close');
|
|
136
|
+
const average = close.sma({ as: 'average', period: 20 });
|
|
137
|
+
|
|
138
|
+
const result = await host.runAsync(
|
|
139
|
+
graph.outputs({ average: average.columns(), latest: average.last() }),
|
|
140
|
+
);
|
|
141
|
+
```
|
|
142
|
+
|
|
143
|
+
The canonical source reference chooses a long-lived bound graph. The loader's
|
|
144
|
+
`revision` decides freshness: an equal revision preserves every cached node; a
|
|
145
|
+
new revision updates the source in place and lets ordinary graph invalidation
|
|
146
|
+
run. Use synchronous `host.run()` for string-keyed datasets added with
|
|
147
|
+
`host.add()`.
|
|
148
|
+
|
|
149
|
+
## Worker pool (Node)
|
|
150
|
+
|
|
151
|
+
`@pond-ts/process/pool` runs **whole requests** across worker threads, each
|
|
152
|
+
holding a long-lived `Host`. It scales throughput under concurrent load; it
|
|
153
|
+
does not make one request faster.
|
|
154
|
+
|
|
155
|
+
```ts
|
|
156
|
+
// setup.mjs — imported by BOTH isolates, because a registry is functions
|
|
157
|
+
// and functions do not survive structured clone.
|
|
158
|
+
export default function setup() {
|
|
159
|
+
return { registry, datasets: { px: series } };
|
|
160
|
+
}
|
|
161
|
+
```
|
|
162
|
+
|
|
163
|
+
```ts
|
|
164
|
+
import { HostPool } from '@pond-ts/process/pool';
|
|
165
|
+
|
|
166
|
+
const pool = await HostPool.start({
|
|
167
|
+
setup: new URL('./setup.mjs', import.meta.url),
|
|
168
|
+
size: 4,
|
|
169
|
+
});
|
|
170
|
+
const result = await pool.run({ from: 'px', process: plan, select });
|
|
171
|
+
await pool.close();
|
|
172
|
+
```
|
|
173
|
+
|
|
174
|
+
Requests run with `assemble: false` — the pool answers `columns` (which cross
|
|
175
|
+
as transferable buffers) and the caller assembles a `TimeSeries` if it wants
|
|
176
|
+
one. Pass an `affinity` key as the second argument to pin related requests to
|
|
177
|
+
one worker, so its warm nodes get reused.
|
|
178
|
+
|
|
179
|
+
**When it pays — it is about cache-hit rate, not request size.** Measured
|
|
180
|
+
at 32 requests over 8 workers (`node packages/process/scripts/perf-pool.mjs`):
|
|
181
|
+
**3.1–4.0× on distinct requests** at every size from 0.5 ms to 10 ms each, and
|
|
182
|
+
**~0.01× on repeated ones**. In-process, a re-asked question is a memo hit that
|
|
183
|
+
returns the same column for nothing; a pool copies and ships every answer
|
|
184
|
+
however cheap it was, and each worker warms its own graph. Pooling and caching
|
|
185
|
+
compete rather than compose.
|
|
186
|
+
|
|
187
|
+
**Check your ops before you reach for the pool.** The same rolling mean writing
|
|
188
|
+
a `Float64Array` instead of `new Array(n)` runs **482 ms single-threaded where
|
|
189
|
+
the boxed version needs 632 ms across eight workers** — fixing the op beat
|
|
190
|
+
adding eight cores. Boxing also parallelises worse, contending on memory
|
|
191
|
+
bandwidth and per-isolate GC. A high pool speedup can be a symptom of a slow
|
|
192
|
+
op.
|
|
193
|
+
|
|
194
|
+
## Quick start
|
|
195
|
+
|
|
196
|
+
```ts
|
|
197
|
+
import { source, derive } from '@pond-ts/process';
|
|
198
|
+
|
|
199
|
+
const raw = source<TimeSeries<Schema>>();
|
|
200
|
+
|
|
201
|
+
const hourly = derive({ s: raw.out.value }, ({ s }) =>
|
|
202
|
+
s.aggregate(Sequence.every('1h'), { cpu: 'avg' }),
|
|
203
|
+
);
|
|
204
|
+
const peak = derive({ s: hourly.out.value }, ({ s }) => s.column('cpu').max());
|
|
205
|
+
|
|
206
|
+
raw.set(series);
|
|
207
|
+
peak.out.value.get(); // aggregates once, caches
|
|
208
|
+
peak.out.value.get(); // cache hit — nothing recomputes
|
|
209
|
+
```
|
|
210
|
+
|
|
211
|
+
## How evaluation works
|
|
212
|
+
|
|
213
|
+
Two mechanisms doing two different jobs:
|
|
214
|
+
|
|
215
|
+
- **Dirty marking (push).** Setting a source marks everything downstream
|
|
216
|
+
as "revalidate before answering," cutting off at nodes already marked.
|
|
217
|
+
A change costs O(affected nodes) regardless of graph size.
|
|
218
|
+
- **Version stamps (pull).** Each outlet's version increments only when a
|
|
219
|
+
recomputed value _actually differs_. A dirty node whose input versions
|
|
220
|
+
all match skips `compute` entirely.
|
|
221
|
+
|
|
222
|
+
The second is the point. A source change that produces an identical
|
|
223
|
+
downstream value stops the cascade there, so expensive transforms below it
|
|
224
|
+
never run:
|
|
225
|
+
|
|
226
|
+
```ts
|
|
227
|
+
const level = source<number>();
|
|
228
|
+
const bucket = derive({ x: level.out.value }, ({ x }) => Math.floor(x / 10), {
|
|
229
|
+
equals: (a, b) => a === b,
|
|
230
|
+
});
|
|
231
|
+
const expensive = derive({ b: bucket.out.value }, ({ b }) => heavyWork(b));
|
|
232
|
+
|
|
233
|
+
level.set(11);
|
|
234
|
+
expensive.out.value.get(); // computes
|
|
235
|
+
level.set(13); // different input, same bucket
|
|
236
|
+
expensive.out.value.get(); // cache hit — heavyWork never re-runs
|
|
237
|
+
```
|
|
238
|
+
|
|
239
|
+
Equality defaults to `Object.is`, which is right for immutable pond values
|
|
240
|
+
— a transform that changed something returns a new instance. Supply
|
|
241
|
+
`equals` where a node produces scalars or small records. Note that a
|
|
242
|
+
`true` from `equals` also **keeps the old value** and discards the new
|
|
243
|
+
one, so compare everything a consumer can observe, not just an id.
|
|
244
|
+
|
|
245
|
+
## Types are enforced, not documented
|
|
246
|
+
|
|
247
|
+
Ports are typed fields, so mismatches are compile errors:
|
|
248
|
+
|
|
249
|
+
```ts
|
|
250
|
+
text.out.value.connect(add.in.a); // ✗ Outlet<string> → Inlet<number>
|
|
251
|
+
add.in.nope; // ✗ 'nope' is not a declared input
|
|
252
|
+
```
|
|
253
|
+
|
|
254
|
+
## Live sources
|
|
255
|
+
|
|
256
|
+
`fromLive` binds a pond `LiveSeries` / `LiveView` into a graph. Events
|
|
257
|
+
**invalidate**; they don't snapshot. A burst of 10,000 events costs one
|
|
258
|
+
dirty mark each and exactly one `toTimeSeries()` at the next pull:
|
|
259
|
+
|
|
260
|
+
```ts
|
|
261
|
+
const feed = fromLive(liveSeries);
|
|
262
|
+
const hourly = derive({ s: feed.out.value }, ({ s }) =>
|
|
263
|
+
s.aggregate(Sequence.every('1h'), { cpu: 'avg' }),
|
|
264
|
+
);
|
|
265
|
+
|
|
266
|
+
// ... 10k events arrive ...
|
|
267
|
+
hourly.out.value.get(); // one snapshot, one aggregate
|
|
268
|
+
feed.dispose(); // unsubscribe
|
|
269
|
+
```
|
|
270
|
+
|
|
271
|
+
That keeps the layer on the right side of pond's split: incremental
|
|
272
|
+
per-event work stays in the live layer, and the graph composes whole-value
|
|
273
|
+
batch transforms over snapshots.
|
|
274
|
+
|
|
275
|
+
### There is no partial invalidation — bind the aggregation instead
|
|
276
|
+
|
|
277
|
+
A dirty node recomputes from a _whole_ snapshot. The pipeline above
|
|
278
|
+
therefore re-aggregates every retained event on every pull, even though
|
|
279
|
+
only the tail moved. The graph does not track which rows changed, and
|
|
280
|
+
deliberately doesn't try to: pond's live layer already does incremental
|
|
281
|
+
per-event computation, and reimplementing it behind ports would be a
|
|
282
|
+
second engine to keep correct.
|
|
283
|
+
|
|
284
|
+
So push the windowed work down and bind _its_ output:
|
|
285
|
+
|
|
286
|
+
```ts
|
|
287
|
+
const feed = fromLive(live.aggregate(Sequence.every('1h'), { cpu: 'avg' }));
|
|
288
|
+
const peak = derive({ s: feed.out.value }, ({ s }) => s.column('cpu').max());
|
|
289
|
+
```
|
|
290
|
+
|
|
291
|
+
`LiveAggregation` maintains its buckets per event, so a pull materializes
|
|
292
|
+
bucket count instead of event count. At 200k events through a 50k-event
|
|
293
|
+
buffer, pulling every 1k events: **9.05 ms/pull re-aggregating the buffer
|
|
294
|
+
vs 0.04 ms/pull off the live aggregation — 235×**, and the gap widens with
|
|
295
|
+
buffer size (O(retained events) vs O(buckets)).
|
|
296
|
+
|
|
297
|
+
**Read the tradeoff before switching.** A live aggregation exposes _closed_
|
|
298
|
+
buckets only. Data is the clock, so the newest bucket is invisible until an
|
|
299
|
+
event crosses its end — two hours of minute data ending at 1h59m reads as
|
|
300
|
+
one row this way and two by re-aggregating the buffer. If the currently
|
|
301
|
+
filling bucket must be on screen, stay on the buffer and pay for it, or use
|
|
302
|
+
a `Trigger` so buckets close on a schedule you control.
|
|
303
|
+
|
|
304
|
+
## Multi-output nodes
|
|
305
|
+
|
|
306
|
+
`derive` covers single-output nodes. `defineNode` declares a reusable node
|
|
307
|
+
type, with as many outputs as you like — all computed in one pass:
|
|
308
|
+
|
|
309
|
+
```ts
|
|
310
|
+
const Extent = defineNode({
|
|
311
|
+
kind: 'extent',
|
|
312
|
+
inputs: { series: port<TimeSeries<Schema>>() },
|
|
313
|
+
outputs: { min: port<number>(), max: port<number>() },
|
|
314
|
+
compute: ({ series }) => ({
|
|
315
|
+
min: series.column('cpu').min(),
|
|
316
|
+
max: series.column('cpu').max(),
|
|
317
|
+
}),
|
|
318
|
+
});
|
|
319
|
+
|
|
320
|
+
const extent = Extent();
|
|
321
|
+
hourly.out.value.connect(extent.in.series);
|
|
322
|
+
extent.out.min.get(); // computes both outputs once
|
|
323
|
+
```
|
|
324
|
+
|
|
325
|
+
## Inspecting a graph
|
|
326
|
+
|
|
327
|
+
`Graph` is a read-only view over already-wired nodes — evaluation never
|
|
328
|
+
consults it.
|
|
329
|
+
|
|
330
|
+
```ts
|
|
331
|
+
const graph = Graph.from(peak); // discovers every reachable node
|
|
332
|
+
graph.order(); // dependency order
|
|
333
|
+
graph.toJSON(); // structure: nodes, ports, edges
|
|
334
|
+
```
|
|
335
|
+
|
|
336
|
+
`toJSON()` is a description, not a serialization — there is no `fromJSON`.
|
|
337
|
+
Rebuilding a graph needs a `kind` → factory registry and per-node config in
|
|
338
|
+
the dump; neither exists yet.
|
|
339
|
+
|
|
340
|
+
## Errors
|
|
341
|
+
|
|
342
|
+
A node caches the error its `compute` threw and rethrows it without
|
|
343
|
+
re-running until an input changes, so a broken node stays cheap to poll.
|
|
344
|
+
`node.error` exposes it. Cycles are rejected by `connect()`, so the graph
|
|
345
|
+
is acyclic by construction and evaluation never guards against recursion.
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
// @pond-ts/process ships as ES modules only. This stub is the `require` target in
|
|
4
|
+
// the package's `exports` map so that CommonJS consumers get a clear,
|
|
5
|
+
// actionable error instead of Node's cryptic `ERR_PACKAGE_PATH_NOT_EXPORTED`.
|
|
6
|
+
//
|
|
7
|
+
// It is copied verbatim into `dist/` during `prepack` (see package.json) so it
|
|
8
|
+
// rides along in the published tarball; the source of truth lives at the
|
|
9
|
+
// package root and is never touched by `tsc`.
|
|
10
|
+
|
|
11
|
+
throw new Error(
|
|
12
|
+
'@pond-ts/process is an ES module package and cannot be loaded with require(). ' +
|
|
13
|
+
"Use `import { source } from '@pond-ts/process'` instead, or a dynamic " +
|
|
14
|
+
"`await import('@pond-ts/process')` from CommonJS. See https://nodejs.org/api/esm.html.",
|
|
15
|
+
);
|
package/dist/column.d.ts
ADDED
|
@@ -0,0 +1,197 @@
|
|
|
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, TimeSeries } from 'pond-ts';
|
|
33
|
+
import type { Column, SeriesSchema } from 'pond-ts';
|
|
34
|
+
/**
|
|
35
|
+
* Retained size of a column value, in bytes.
|
|
36
|
+
*
|
|
37
|
+
* Approximate by construction — it counts the backing buffers a packed
|
|
38
|
+
* column owns, which is what a cache budget is actually trying to bound,
|
|
39
|
+
* and ignores per-object overhead. A chunked column reports the sum of
|
|
40
|
+
* its chunks. A column whose kind this cannot size returns `0` rather
|
|
41
|
+
* than guessing, so a budget treats it as free instead of evicting on a
|
|
42
|
+
* fabricated number.
|
|
43
|
+
*
|
|
44
|
+
* Bytes rather than entries is the point: a 500k-row result and a scalar
|
|
45
|
+
* both count as one entry, and only one of them matters.
|
|
46
|
+
*/
|
|
47
|
+
export declare function columnBytes(column: Column): number;
|
|
48
|
+
/**
|
|
49
|
+
* Packs loose values into a `Float64Column`.
|
|
50
|
+
*
|
|
51
|
+
* For an op whose kernel returns an array — the financial kernels are
|
|
52
|
+
* array-out (`rollingValues`, `emaValues`) — this confines the boxing to
|
|
53
|
+
* one pass, instead of the boxed array being retained as the node value.
|
|
54
|
+
*
|
|
55
|
+
* `NaN` is treated as **missing**, matching what a study's warm-up
|
|
56
|
+
* means, and never packed as a defined cell: core's reducer non-finite
|
|
57
|
+
* policy requires that a column flagged `allFinite` contain no `NaN`,
|
|
58
|
+
* and a wrongly-set flag makes reducers take an unguarded path and
|
|
59
|
+
* silently return a wrong result. `allFinite` is therefore computed by
|
|
60
|
+
* inspecting every defined cell, not assumed.
|
|
61
|
+
*/
|
|
62
|
+
export declare function packColumn(values: ArrayLike<number | undefined | null>): Float64Column;
|
|
63
|
+
/**
|
|
64
|
+
* Appends a column to a series under `name` — the renderer path, where a
|
|
65
|
+
* caller genuinely wants one `TimeSeries` carrying several studies.
|
|
66
|
+
*
|
|
67
|
+
* **A gapless column round-trips without boxing**, via the column's own
|
|
68
|
+
* `Float64Array`. A column *with* gaps cannot: core's `withColumn` takes
|
|
69
|
+
* values, not a column, and rejects a non-finite cell, so a warm-up has
|
|
70
|
+
* to be expressed as `undefined` in a boxed array. Core appends columns
|
|
71
|
+
* directly internally (`withColumnAppended`) but does not expose it;
|
|
72
|
+
* until it does, a gapped column pays one boxing pass here.
|
|
73
|
+
*
|
|
74
|
+
* Since most studies have a warm-up, that fallback is the common case —
|
|
75
|
+
* which is exactly why assembly should be requested rather than assumed
|
|
76
|
+
* ([PND-PROCTERM]). A facts-only request never calls this.
|
|
77
|
+
*/
|
|
78
|
+
export declare function appendColumn<S extends SeriesSchema>(series: TimeSeries<S>, name: string, column: Column): TimeSeries<SeriesSchema>;
|
|
79
|
+
/**
|
|
80
|
+
* The buffer pair a packed numeric column **is** — for moving one across
|
|
81
|
+
* an isolate boundary ([PND-PROCPAR]).
|
|
82
|
+
*
|
|
83
|
+
* `RunResult.columns` is already described as the wire-shaped answer: a
|
|
84
|
+
* `Float64Array` plus a validity bitmap. This makes that literal, so a
|
|
85
|
+
* worker can hand a result back as two transferable buffers rather than
|
|
86
|
+
* 500k boxed values (48.6 ms per answer boxed vs 0.5 ms transferred).
|
|
87
|
+
*
|
|
88
|
+
* **The buffers are copies, deliberately.** A column's own buffers are
|
|
89
|
+
* shared with the node's memo, and transferring a buffer *detaches* it in
|
|
90
|
+
* the sending isolate — which would silently empty the cache the worker
|
|
91
|
+
* exists to keep warm. One `slice()` per surfaced column is the price of
|
|
92
|
+
* the cache staying valid, and it is still the cheap direction.
|
|
93
|
+
*
|
|
94
|
+
* Falls back to `undefined` for a column this cannot express (chunked
|
|
95
|
+
* storage, or a non-numeric kind), so a caller can box that one rather
|
|
96
|
+
* than the pool failing over a column it did not need packed.
|
|
97
|
+
*/
|
|
98
|
+
export interface ColumnBuffers {
|
|
99
|
+
readonly length: number;
|
|
100
|
+
readonly values: Float64Array;
|
|
101
|
+
/** Absent ⇒ every cell defined (the framework's no-bitmap convention). */
|
|
102
|
+
readonly bits?: Uint8Array;
|
|
103
|
+
readonly definedCount: number;
|
|
104
|
+
readonly allFinite: boolean;
|
|
105
|
+
}
|
|
106
|
+
export declare function columnBuffers(column: Column): ColumnBuffers | undefined;
|
|
107
|
+
/** Rebuilds a column from {@link columnBuffers}, adopting both buffers. */
|
|
108
|
+
export declare function columnFromBuffers(wire: ColumnBuffers): Float64Column;
|
|
109
|
+
/**
|
|
110
|
+
* A **zero-copy** read view over a packed numeric column — [PND-PROCCOL].
|
|
111
|
+
*
|
|
112
|
+
* The counterpart to {@link columnBuffers}, which copies because its
|
|
113
|
+
* result crosses a thread. This one borrows: `values` and `bits` are
|
|
114
|
+
* `subarray`s of the column's own storage, valid only while the memo
|
|
115
|
+
* that produced the column holds it. Read, never retain.
|
|
116
|
+
*
|
|
117
|
+
* `undefined` for anything not packed numeric — a string column, or a
|
|
118
|
+
* value that arrived boxed — so a caller keeps its slow path.
|
|
119
|
+
*
|
|
120
|
+
* ## Why a view rather than `scan`
|
|
121
|
+
*
|
|
122
|
+
* Measured folding a max over 500k cells:
|
|
123
|
+
*
|
|
124
|
+
* | read path | ms |
|
|
125
|
+
* | ----------------------------- | ---- |
|
|
126
|
+
* | boxed `(number\|undefined)[]` | 0.91 |
|
|
127
|
+
* | `Column.scan()` | 4.27 |
|
|
128
|
+
* | buffer + bitmap | 0.96 |
|
|
129
|
+
*
|
|
130
|
+
* The columnar form is **not faster to read** — a buffer walk only
|
|
131
|
+
* reaches parity with the boxed array, and `scan` is 4.7× slower than
|
|
132
|
+
* either because it takes a callback per cell. The case for it is that
|
|
133
|
+
* it does not *allocate*: no `Array` of 500k boxed slots per version,
|
|
134
|
+
* and nothing at all for a fold like `latest`, which reads one cell.
|
|
135
|
+
* Core's design principles recommend `scan` as the columnar read path,
|
|
136
|
+
* which is worth revisiting on this evidence.
|
|
137
|
+
*/
|
|
138
|
+
export interface ColumnView {
|
|
139
|
+
readonly length: number;
|
|
140
|
+
/** Column storage. A cell is meaningful only where {@link ColumnView.defined}. */
|
|
141
|
+
readonly values: Float64Array;
|
|
142
|
+
/** Absent ⇒ every cell is defined. */
|
|
143
|
+
readonly bits?: Uint8Array;
|
|
144
|
+
readonly definedCount: number;
|
|
145
|
+
defined(i: number): boolean;
|
|
146
|
+
at(i: number): number | undefined;
|
|
147
|
+
}
|
|
148
|
+
export declare function columnView(column: Column): ColumnView | undefined;
|
|
149
|
+
/**
|
|
150
|
+
* A writable output buffer for a ranged recompute — [PND-PROCRANGE].
|
|
151
|
+
*
|
|
152
|
+
* The rows an op keeps unchanged are already here, values **and**
|
|
153
|
+
* validity, copied as blocks. The op fills `[from, to)` and nothing else.
|
|
154
|
+
*
|
|
155
|
+
* This exists because carrying a prefix forward correctly is harder than
|
|
156
|
+
* it looks and the obvious shortcut is silently wrong. Packed storage
|
|
157
|
+
* holds `0` at a missing cell, not `NaN`, so copying only `values` turns
|
|
158
|
+
* every warm-up gap in the prefix into a defined zero — measured at 1,875
|
|
159
|
+
* wrong cells on a 5-study pass, caught by a from-scratch comparison
|
|
160
|
+
* rather than by any type. Validity has to move with the values, and
|
|
161
|
+
* doing that per cell is the `O(n)` walk the whole ticket exists to
|
|
162
|
+
* remove.
|
|
163
|
+
*/
|
|
164
|
+
export interface RangeOutput {
|
|
165
|
+
/** Length `to`. `[0, from)` already carries the previous output. */
|
|
166
|
+
readonly values: Float64Array;
|
|
167
|
+
/**
|
|
168
|
+
* Validity bits, LSB-first, already carrying `[0, from)`.
|
|
169
|
+
*
|
|
170
|
+
* Exposed so an op can write a run of cells as bytes rather than
|
|
171
|
+
* through {@link RangeOutput.set} per cell; most ops want `set`.
|
|
172
|
+
*/
|
|
173
|
+
readonly bits: Uint8Array;
|
|
174
|
+
/** Marks a cell present, with its value. */
|
|
175
|
+
set(i: number, v: number): void;
|
|
176
|
+
/**
|
|
177
|
+
* Marks a cell missing.
|
|
178
|
+
*
|
|
179
|
+
* **Only needed to undo a {@link RangeOutput.set} made in the same
|
|
180
|
+
* pass.** Cells in `[from, to)` start unset and an unwritten cell
|
|
181
|
+
* seals as missing, so clearing one the op never set does nothing —
|
|
182
|
+
* which is why an empty `clear` passed the whole suite until a
|
|
183
|
+
* set-then-clear case was written (Layer 2, PR #573).
|
|
184
|
+
*/
|
|
185
|
+
clear(i: number): void;
|
|
186
|
+
}
|
|
187
|
+
/** Bytes needed for `length` validity bits. */
|
|
188
|
+
export declare function validityByteCount(length: number): number;
|
|
189
|
+
/**
|
|
190
|
+
* Prepares a {@link RangeOutput} of `length`, carrying `[0, keep)` from
|
|
191
|
+
* `prior` — a `Float64Array.set` for the values and a byte-wise copy for
|
|
192
|
+
* the bitmap, with the straddling byte masked.
|
|
193
|
+
*/
|
|
194
|
+
export declare function prepareRange(length: number, keep: number, prior: ColumnView | undefined): RangeOutput;
|
|
195
|
+
/** Seals a {@link RangeOutput} into a column, counting validity once. */
|
|
196
|
+
export declare function sealRange(out: RangeOutput, length: number): Float64Column;
|
|
197
|
+
//# sourceMappingURL=column.d.ts.map
|