@pond-ts/process 0.54.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (57) hide show
  1. package/CHANGELOG.md +5914 -0
  2. package/LICENSE +21 -0
  3. package/README.md +345 -0
  4. package/dist/cjs-fallback.cjs +15 -0
  5. package/dist/column.d.ts +197 -0
  6. package/dist/column.js +306 -0
  7. package/dist/errors.d.ts +22 -0
  8. package/dist/errors.js +25 -0
  9. package/dist/graph.d.ts +89 -0
  10. package/dist/graph.js +133 -0
  11. package/dist/index.d.ts +59 -0
  12. package/dist/index.js +45 -0
  13. package/dist/node.d.ts +151 -0
  14. package/dist/node.js +268 -0
  15. package/dist/plan/builder.d.ts +138 -0
  16. package/dist/plan/builder.js +166 -0
  17. package/dist/plan/fluent.d.ts +93 -0
  18. package/dist/plan/fluent.js +140 -0
  19. package/dist/plan/folds.d.ts +25 -0
  20. package/dist/plan/folds.js +190 -0
  21. package/dist/plan/graph.d.ts +171 -0
  22. package/dist/plan/graph.js +658 -0
  23. package/dist/plan/history.d.ts +61 -0
  24. package/dist/plan/history.js +82 -0
  25. package/dist/plan/host.d.ts +173 -0
  26. package/dist/plan/host.js +234 -0
  27. package/dist/plan/identity.d.ts +81 -0
  28. package/dist/plan/identity.js +158 -0
  29. package/dist/plan/params.d.ts +15 -0
  30. package/dist/plan/params.js +26 -0
  31. package/dist/plan/registry.d.ts +162 -0
  32. package/dist/plan/registry.js +422 -0
  33. package/dist/plan/run.d.ts +211 -0
  34. package/dist/plan/run.js +360 -0
  35. package/dist/plan/slots.d.ts +65 -0
  36. package/dist/plan/slots.js +114 -0
  37. package/dist/plan/source.d.ts +49 -0
  38. package/dist/plan/source.js +54 -0
  39. package/dist/plan/types.d.ts +376 -0
  40. package/dist/plan/types.js +20 -0
  41. package/dist/pool/index.d.ts +15 -0
  42. package/dist/pool/index.js +12 -0
  43. package/dist/pool/pool.d.ts +92 -0
  44. package/dist/pool/pool.js +237 -0
  45. package/dist/pool/protocol.d.ts +48 -0
  46. package/dist/pool/protocol.js +9 -0
  47. package/dist/pool/wire.d.ts +52 -0
  48. package/dist/pool/wire.js +95 -0
  49. package/dist/pool/worker.d.ts +22 -0
  50. package/dist/pool/worker.js +80 -0
  51. package/dist/port.d.ts +79 -0
  52. package/dist/port.js +222 -0
  53. package/dist/source.d.ts +161 -0
  54. package/dist/source.js +182 -0
  55. package/dist/types.d.ts +77 -0
  56. package/dist/types.js +26 -0
  57. package/package.json +50 -0
package/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
+ );
@@ -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