@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
@@ -0,0 +1,360 @@
1
+ /**
2
+ * `run` — one entry point, one response. [PND-DEMOM0], [PND-PROCTERM].
3
+ *
4
+ * The request carries a plan **and** what it wants back. A renderer asks
5
+ * for columns; an agent asks for facts; a legend chip is a fact riding
6
+ * alongside columns in the same pass. Collapsing the request rather than
7
+ * forking the terminal is what makes that one call.
8
+ *
9
+ * Two things measured earlier are load-bearing here:
10
+ *
11
+ * - **Facts read node values directly.** Assembling a `TimeSeries` so a
12
+ * reduction has a column to read cost 52× more, and 441× once facts
13
+ * memoize. Assembly happens only when `columns` is asked for.
14
+ * - **Assembly resolves a closure.** "Needed" is not "selected with
15
+ * `columns`": a reduction reads a column too, and `crossings`'s
16
+ * `against` names a second one. Assembling only the column-selectors
17
+ * produced a fact with *no value* rather than an error — silent, which
18
+ * is worse than a throw.
19
+ */
20
+ import { appendColumn } from '../column.js';
21
+ import { ProcessError } from '../errors.js';
22
+ import { columnsOf, explain, refToId, unitOf } from './identity.js';
23
+ import { expandSlots } from './slots.js';
24
+ import { specId } from './identity.js';
25
+ import { specOf } from './types.js';
26
+ // ── run ──────────────────────────────────────────────────────
27
+ /**
28
+ * Reduces either request form to the one the resolver already handles.
29
+ *
30
+ * A slot request expands to exactly the nested plan its equivalent would
31
+ * have been written as, so both land on identical ids — that equality is
32
+ * the contract, and it is why nothing downstream of here knows slots
33
+ * exist ([PND-PROCSLOT]).
34
+ */
35
+ function normalize(graph, request) {
36
+ if (!('nodes' in request)) {
37
+ return {
38
+ plan: request.plan,
39
+ select: request.select ?? [],
40
+ slotOf: new Map(),
41
+ };
42
+ }
43
+ const columns = graph.series.schema.slice(1).map((c) => c.name);
44
+ const expanded = expandSlots(request.nodes, columns);
45
+ // First slot wins. Two slots may legally resolve to ONE id — the
46
+ // registry-free builder cannot canonicalize defaults, so `shape()`
47
+ // and `shape({points: 40})` arrive as two slots naming the same
48
+ // computation. Attribution must not depend on which happened to be
49
+ // declared last; "the first slot to name it labels it" is the rule.
50
+ const slotOf = new Map();
51
+ for (const [slot, spec] of expanded) {
52
+ const id = specId(graph.registry, spec);
53
+ if (!slotOf.has(id))
54
+ slotOf.set(id, slot);
55
+ }
56
+ const select = Object.entries(request.outputs ?? {}).map(([name, sel]) => {
57
+ // `on` names a slot here. Anything else is left alone, so an id
58
+ // string still works — a follow-up can cite what the last response
59
+ // returned without re-deriving it.
60
+ const on = typeof sel.on === 'string' && expanded.has(sel.on)
61
+ ? expanded.get(sel.on)
62
+ : sel.on;
63
+ return { ...sel, on, name };
64
+ });
65
+ return { plan: [...expanded.values()], select, slotOf };
66
+ }
67
+ export function run(graph, request) {
68
+ const { onError = 'throw', assemble = true } = request;
69
+ const registry = graph.registry;
70
+ const skipped = [];
71
+ const resolved = [];
72
+ // Slot expansion happens before anything resolves, so its failures
73
+ // used to escape the error policy entirely — a mistyped input came
74
+ // back as a thrown 500 rather than a `skipped` reason an agent could
75
+ // read and retry against. Every other class of bad plan is
76
+ // collectable; there was no argument for this one being different.
77
+ let normalized;
78
+ try {
79
+ normalized = normalize(graph, request);
80
+ }
81
+ catch (e) {
82
+ if (onError === 'throw')
83
+ throw e;
84
+ return {
85
+ outputs: {},
86
+ facts: [],
87
+ explain: {},
88
+ skipped: [{ reason: e instanceof Error ? e.message : String(e) }],
89
+ nodes: [],
90
+ };
91
+ }
92
+ const { plan, select, slotOf } = normalized;
93
+ const fail = (entry) => {
94
+ if (onError === 'throw')
95
+ throw new ProcessError(entry.reason);
96
+ skipped.push(entry);
97
+ };
98
+ // ── resolve the plan ───────────────────────────────────────
99
+ for (const spec of plan) {
100
+ try {
101
+ const compiled = graph.compile(spec);
102
+ resolved.push({ id: compiled.id, spec });
103
+ }
104
+ catch (e) {
105
+ fail({
106
+ spec: {
107
+ op: spec.op,
108
+ params: { ...(spec.params ?? {}) },
109
+ inputs: spec.inputs,
110
+ },
111
+ reason: e instanceof Error ? e.message : String(e),
112
+ });
113
+ }
114
+ }
115
+ const explainMap = {};
116
+ for (const { id, spec } of resolved)
117
+ explainMap[id] = explain(registry, spec);
118
+ // ── work out what the selection actually needs ──────────────
119
+ // Every id any selector mentions, including a `crossings` `against`.
120
+ const needed = new Map(); // id -> report in `outputs`
121
+ const selectors = [];
122
+ for (const sel of select) {
123
+ let id;
124
+ try {
125
+ id = refToId(registry, sel.on);
126
+ // An inline spec is a complete description of a computation, so
127
+ // resolve it whether or not the plan also lists it at top level.
128
+ // Requiring both was redundant bookkeeping that no schema could
129
+ // express — so it lived in prose, and a caller composing against
130
+ // the schema alone duly selected a spec it had not listed, and got
131
+ // a skip instead of an answer ([PND-PROCSCHEMA], M5).
132
+ if (typeof sel.on !== 'string' && graph.get(id) === undefined) {
133
+ const compiled = graph.compile(sel.on);
134
+ resolved.push({ id: compiled.id, spec: sel.on });
135
+ explainMap[id] = explain(registry, sel.on);
136
+ }
137
+ }
138
+ catch (e) {
139
+ fail({ select: sel, reason: e instanceof Error ? e.message : String(e) });
140
+ continue;
141
+ }
142
+ selectors.push({ sel, id });
143
+ // Surfacing a column-producing node means surfacing its columns.
144
+ // There is no longer a `columns: true` to opt into, because what a
145
+ // selector yields is decided by the node it points at.
146
+ const compiled = graph.get(id);
147
+ const isFoldNode = compiled?.fold === true;
148
+ if (!isFoldNode)
149
+ needed.set(id, true);
150
+ }
151
+ const outputs = {};
152
+ const timings = [];
153
+ const timed = new Set();
154
+ let assembled;
155
+ let drawn;
156
+ const columnCache = new Map();
157
+ /**
158
+ * Pulls a node's inputs before the node itself, so the time recorded
159
+ * against it is its own compute rather than an ancestor's. Without
160
+ * this a leaf absorbs the whole subtree's cost and the badge lies.
161
+ */
162
+ const warm = (id) => {
163
+ if (timed.has(id))
164
+ return;
165
+ timed.add(id);
166
+ const compiled = graph.get(id);
167
+ if (compiled === undefined)
168
+ return;
169
+ // Lineage for the whole closure, not just the plan's top level. A
170
+ // nested spec is a node in `nodes` and will be a node in the M4
171
+ // pipeline view, and both label from here — leaving it out meant a
172
+ // badge with a raw id under it.
173
+ explainMap[id] ??= explain(registry, compiled.spec);
174
+ // Resolved on the way down, so an edge names the id the consumer
175
+ // will see in `nodes` rather than a spec it would have to hash.
176
+ const upstream = compiled.spec.inputs.map((input) => {
177
+ if (typeof input === 'string')
178
+ return input;
179
+ const upId = refToId(registry, specOf(input));
180
+ warm(upId);
181
+ return upId;
182
+ });
183
+ const declared = registry.outputsOf(registry.get(compiled.spec.op));
184
+ const wasDirty = compiled.node.dirty;
185
+ const t0 = performance.now();
186
+ // A fold is pulled exactly like a column node — same memo, same
187
+ // version check. That equivalence is the point of [PND-PROCFOLD]:
188
+ // the badge row now covers the part callers actually read.
189
+ if (compiled.fold)
190
+ graph.factOf(compiled);
191
+ else
192
+ graph.columnOf(compiled, declared[0].id);
193
+ const ms = performance.now() - t0;
194
+ timings.push({
195
+ id,
196
+ ...(slotOf.has(id) && { slot: slotOf.get(id) }),
197
+ pulled: true,
198
+ cached: !wasDirty,
199
+ ms: Math.round(ms * 1000) / 1000,
200
+ inputs: upstream,
201
+ });
202
+ };
203
+ /**
204
+ * Records a resolved node the request never pulled, inputs first.
205
+ *
206
+ * The pipeline is the plan, not the subset one selector happened to
207
+ * reach. Reading `node.dirty` is free — no value is produced — so this
208
+ * costs nothing beyond the walk.
209
+ */
210
+ const record = (id) => {
211
+ if (timed.has(id))
212
+ return;
213
+ timed.add(id);
214
+ const compiled = graph.get(id);
215
+ if (compiled === undefined)
216
+ return;
217
+ explainMap[id] ??= explain(registry, compiled.spec);
218
+ const upstream = compiled.spec.inputs.map((input) => {
219
+ if (typeof input === 'string')
220
+ return input;
221
+ const upId = refToId(registry, specOf(input));
222
+ record(upId);
223
+ return upId;
224
+ });
225
+ timings.push({
226
+ id,
227
+ ...(slotOf.has(id) && { slot: slotOf.get(id) }),
228
+ pulled: false,
229
+ cached: !compiled.node.dirty,
230
+ ms: 0,
231
+ inputs: upstream,
232
+ });
233
+ };
234
+ const columnFor = (id, suffix) => {
235
+ const key = id + suffix;
236
+ const hit = columnCache.get(key);
237
+ if (hit)
238
+ return hit;
239
+ const compiled = graph.get(id);
240
+ if (compiled === undefined)
241
+ throw new ProcessError(`'${id}' is not in this plan`);
242
+ warm(id);
243
+ const col = graph.columnOf(compiled, suffix);
244
+ columnCache.set(key, col);
245
+ return col;
246
+ };
247
+ // ── resolve the surfaced columns, and assemble only if asked ───
248
+ if (needed.size > 0) {
249
+ drawn = {};
250
+ if (assemble)
251
+ assembled = graph.series;
252
+ for (const [id, report] of needed) {
253
+ const compiled = graph.get(id);
254
+ if (compiled === undefined)
255
+ continue;
256
+ const cols = columnsOf(registry, compiled.spec, id);
257
+ if (report)
258
+ outputs[id] = [];
259
+ const selections = selectors.filter((selection) => selection.id === id);
260
+ for (const { sel } of selections) {
261
+ const declared = registry.outputsOf(registry.get(compiled.spec.op));
262
+ // A selector naming an output the node does not declare used to
263
+ // filter every iteration below and surface NOTHING — no columns,
264
+ // no error, no `skipped` entry. Silent is the worst of the three.
265
+ if (sel.output !== undefined &&
266
+ !declared.some((o) => o.id === sel.output)) {
267
+ const have = declared.map((o) => `'${o.id}'`).join(', ');
268
+ fail({
269
+ select: sel,
270
+ reason: `'${compiled.spec.op}' has no output '${sel.output}' (has ${have})`,
271
+ });
272
+ continue;
273
+ }
274
+ // Pulling a column runs op code, which can throw like anything
275
+ // else a request does — so it answers to the same error policy
276
+ // the fact loop already honours. Under 'throw' the original
277
+ // error propagates untouched.
278
+ try {
279
+ declared.forEach((o, n) => {
280
+ if (sel.output !== undefined && o.id !== sel.output)
281
+ return;
282
+ const columnName = cols[n];
283
+ const col = columnFor(id, o.id);
284
+ // Several selectors may surface the same node or column under
285
+ // different caller names. Report every selection, but materialize
286
+ // each physical column once.
287
+ if (drawn[columnName] === undefined) {
288
+ drawn[columnName] = col;
289
+ if (assemble)
290
+ assembled = appendColumn(assembled, columnName, col);
291
+ }
292
+ if (report) {
293
+ outputs[id].push({
294
+ column: columnName,
295
+ unit: unitOf(registry, compiled.spec, graph.units, n),
296
+ ...(sel.name !== undefined && { name: sel.name }),
297
+ });
298
+ }
299
+ });
300
+ }
301
+ catch (e) {
302
+ if (onError === 'throw')
303
+ throw e;
304
+ fail({
305
+ select: sel,
306
+ reason: e instanceof Error ? e.message : String(e),
307
+ });
308
+ }
309
+ }
310
+ }
311
+ }
312
+ // ── facts are pulled from fold nodes, like any other value ──
313
+ const facts = [];
314
+ for (const { sel, id } of selectors) {
315
+ const compiled = graph.get(id);
316
+ if (compiled === undefined) {
317
+ fail({ select: sel, reason: `'${id}' is not in this plan` });
318
+ continue;
319
+ }
320
+ if (!compiled.fold)
321
+ continue;
322
+ try {
323
+ warm(id);
324
+ // Provenance wins. The body is spread FIRST and its reserved keys
325
+ // are dropped, so a custom fold cannot masquerade as another node,
326
+ // rename an output the caller did not, or forge a unit — `id`,
327
+ // `name`, `op` and `unit` always mean what the graph says.
328
+ const body = { ...graph.factOf(compiled) };
329
+ for (const key of ['id', 'name', 'op', 'unit'])
330
+ delete body[key];
331
+ facts.push({
332
+ ...body,
333
+ id,
334
+ ...(sel.name !== undefined && { name: sel.name }),
335
+ op: compiled.spec.op,
336
+ unit: unitOf(registry, compiled.spec, graph.units),
337
+ });
338
+ }
339
+ catch (e) {
340
+ fail({ select: sel, reason: e instanceof Error ? e.message : String(e) });
341
+ }
342
+ }
343
+ // Anything the plan resolved but nothing selected. After the pulls, so
344
+ // a node that *was* read keeps its timing rather than being shadowed.
345
+ for (const { id } of resolved)
346
+ record(id);
347
+ // The budget is enforced once the run has resolved, not during it:
348
+ // evicting a node this run is about to read would only recompile it.
349
+ graph.enforceBudget();
350
+ return {
351
+ ...(assembled !== undefined && { series: assembled }),
352
+ ...(drawn !== undefined && { columns: drawn }),
353
+ outputs,
354
+ facts,
355
+ explain: explainMap,
356
+ skipped,
357
+ nodes: timings,
358
+ };
359
+ }
360
+ //# sourceMappingURL=run.js.map
@@ -0,0 +1,65 @@
1
+ /**
2
+ * Slots — [PND-PROCSLOT].
3
+ *
4
+ * A plan written as nested specs uses **one identity for two jobs**. A
5
+ * node's id is derived from its op, params and inputs, so it keys the
6
+ * cache correctly — and changes the moment a param does, even though the
7
+ * topology has not. Moving a `period` from 20 to 50 leaves a
8
+ * structurally identical plan in which every downstream id is different.
9
+ *
10
+ * A **slot** is the missing identity: a caller-assigned name for a
11
+ * position in the graph, stable across a param edit.
12
+ *
13
+ * ```jsonc
14
+ * {
15
+ * "bb": { "op": "bollinger", "params": { "period": 20 }, "in": ["px"] },
16
+ * "z": { "op": "zscore", "params": { "period": 20 }, "in": ["px"] }
17
+ * }
18
+ * ```
19
+ *
20
+ * Slots are an **alias layer, not a replacement**. `specId` remains the
21
+ * cache key, because it is what makes a node found again across requests,
22
+ * sessions and callers — a saved view composed months ago and a fresh
23
+ * compose land on the same node precisely because the id is derived
24
+ * rather than assigned. One caller's `bb` means nothing to another's.
25
+ *
26
+ * That is why this file only **expands**: a slot graph becomes the nested
27
+ * `Spec` form the rest of the layer already resolves, so a slot plan and
28
+ * the equivalent nested plan produce identical ids by construction, and
29
+ * neither `compile` nor `specId` needs to know slots exist.
30
+ */
31
+ import { ProcessError } from '../errors.js';
32
+ import type { ParamValue, Spec } from './types.js';
33
+ /** Thrown when a slot graph cannot be expanded. */
34
+ export declare class SlotError extends ProcessError {
35
+ }
36
+ /** One node in a slot graph. `in` names source columns or other slots. */
37
+ export interface SlotDef {
38
+ readonly op: string;
39
+ readonly params?: Readonly<Record<string, ParamValue>>;
40
+ /**
41
+ * Inputs, in the op's declared order. Each entry is a source column
42
+ * name or another slot's name — **not** an inline spec. Being unable
43
+ * to nest here is the point: nesting is what slots replace.
44
+ */
45
+ readonly in: readonly string[];
46
+ }
47
+ /** A graph keyed by caller-assigned names. */
48
+ export type Slots = Readonly<Record<string, SlotDef>>;
49
+ /**
50
+ * Expands a slot graph into the nested `Spec` form.
51
+ *
52
+ * `columns` is the bound source's column names, needed for two checks
53
+ * that are far cheaper here than as a failure later:
54
+ *
55
+ * - a slot may not take the name of a column, because an input string
56
+ * would then be ambiguous and the column would be shadowed silently
57
+ * (the alternative, a sigil like `"@bb"`, adds syntax to a format that
58
+ * has none — [PND-PROCSLOT] prefers the validation);
59
+ * - an input naming neither a slot nor a column is a typo, and saying so
60
+ * with both lists beats a downstream "column not found".
61
+ *
62
+ * @throws {SlotError} on a name collision, an unknown reference, or a cycle.
63
+ */
64
+ export declare function expandSlots(slots: Slots, columns: readonly string[]): Map<string, Spec>;
65
+ //# sourceMappingURL=slots.d.ts.map
@@ -0,0 +1,114 @@
1
+ /**
2
+ * Slots — [PND-PROCSLOT].
3
+ *
4
+ * A plan written as nested specs uses **one identity for two jobs**. A
5
+ * node's id is derived from its op, params and inputs, so it keys the
6
+ * cache correctly — and changes the moment a param does, even though the
7
+ * topology has not. Moving a `period` from 20 to 50 leaves a
8
+ * structurally identical plan in which every downstream id is different.
9
+ *
10
+ * A **slot** is the missing identity: a caller-assigned name for a
11
+ * position in the graph, stable across a param edit.
12
+ *
13
+ * ```jsonc
14
+ * {
15
+ * "bb": { "op": "bollinger", "params": { "period": 20 }, "in": ["px"] },
16
+ * "z": { "op": "zscore", "params": { "period": 20 }, "in": ["px"] }
17
+ * }
18
+ * ```
19
+ *
20
+ * Slots are an **alias layer, not a replacement**. `specId` remains the
21
+ * cache key, because it is what makes a node found again across requests,
22
+ * sessions and callers — a saved view composed months ago and a fresh
23
+ * compose land on the same node precisely because the id is derived
24
+ * rather than assigned. One caller's `bb` means nothing to another's.
25
+ *
26
+ * That is why this file only **expands**: a slot graph becomes the nested
27
+ * `Spec` form the rest of the layer already resolves, so a slot plan and
28
+ * the equivalent nested plan produce identical ids by construction, and
29
+ * neither `compile` nor `specId` needs to know slots exist.
30
+ */
31
+ import { ProcessError } from '../errors.js';
32
+ /** Thrown when a slot graph cannot be expanded. */
33
+ export class SlotError extends ProcessError {
34
+ }
35
+ /**
36
+ * Expands a slot graph into the nested `Spec` form.
37
+ *
38
+ * `columns` is the bound source's column names, needed for two checks
39
+ * that are far cheaper here than as a failure later:
40
+ *
41
+ * - a slot may not take the name of a column, because an input string
42
+ * would then be ambiguous and the column would be shadowed silently
43
+ * (the alternative, a sigil like `"@bb"`, adds syntax to a format that
44
+ * has none — [PND-PROCSLOT] prefers the validation);
45
+ * - an input naming neither a slot nor a column is a typo, and saying so
46
+ * with both lists beats a downstream "column not found".
47
+ *
48
+ * @throws {SlotError} on a name collision, an unknown reference, or a cycle.
49
+ */
50
+ export function expandSlots(slots, columns) {
51
+ const names = Object.keys(slots);
52
+ const columnSet = new Set(columns);
53
+ for (const name of names) {
54
+ if (columnSet.has(name)) {
55
+ throw new SlotError(`slot '${name}' collides with a source column of the same name — rename the slot, or an input naming '${name}' would be ambiguous`);
56
+ }
57
+ }
58
+ const done = new Map();
59
+ // Insertion-ordered, so the cycle message reads as a path rather than
60
+ // an unordered set of names.
61
+ const visiting = [];
62
+ const expand = (name) => {
63
+ const cached = done.get(name);
64
+ if (cached !== undefined)
65
+ return cached;
66
+ const at = visiting.indexOf(name);
67
+ if (at !== -1) {
68
+ throw new SlotError(`slot cycle: ${[...visiting.slice(at), name].join(' → ')}`);
69
+ }
70
+ const def = slots[name];
71
+ visiting.push(name);
72
+ const inputs = def.in.map((ref) => {
73
+ if (Object.hasOwn(slots, ref))
74
+ return expand(ref);
75
+ if (columnSet.has(ref))
76
+ return ref;
77
+ // `bb#Lower` — one named output of a multi-output slot. A suffix
78
+ // rather than a nested object because `in` is a list of strings
79
+ // and keeping it that way is what makes the slot schema flat, with
80
+ // no recursive `$ref` to make portable ([PND-PROCSLOT]).
81
+ const hash = ref.lastIndexOf('#');
82
+ if (hash > 0) {
83
+ const upstream = ref.slice(0, hash);
84
+ if (Object.hasOwn(slots, upstream)) {
85
+ return { from: expand(upstream), output: ref.slice(hash + 1) };
86
+ }
87
+ }
88
+ // Name the `#` spelling when the reference looks like an attempt
89
+ // at one. A model reaching for a band's upper line writes
90
+ // `bb.Upper` on the first try — reasonably — and a list of valid
91
+ // slots does not tell it what it got wrong.
92
+ const guess = /^(.+)[.:/](.+)$/.exec(ref);
93
+ const hint = guess !== null && Object.hasOwn(slots, guess[1])
94
+ ? ` — to read one output of slot '${guess[1]}', write '${guess[1]}#${guess[2]}'`
95
+ : '';
96
+ throw new SlotError(`slot '${name}' names '${ref}', which is neither a slot nor a column${hint} — slots are ${quoted(names)}; columns are ${quoted(columns)}`);
97
+ });
98
+ visiting.pop();
99
+ const spec = {
100
+ op: def.op,
101
+ ...(def.params !== undefined && { params: def.params }),
102
+ inputs,
103
+ };
104
+ done.set(name, spec);
105
+ return spec;
106
+ };
107
+ for (const name of names)
108
+ expand(name);
109
+ return done;
110
+ }
111
+ function quoted(values) {
112
+ return values.length === 0 ? 'none' : values.map((v) => `'${v}'`).join(', ');
113
+ }
114
+ //# sourceMappingURL=slots.js.map
@@ -0,0 +1,49 @@
1
+ /**
2
+ * Opaque, asynchronous data sources.
3
+ *
4
+ * A request carries only `{ source, params }`. The loader stays on the host,
5
+ * where credentials, URLs, retries and cache policy belong. The canonical
6
+ * source id chooses a long-lived bound graph; `revision` decides whether that
7
+ * graph's source value must be invalidated.
8
+ */
9
+ import { ProcessError } from '../errors.js';
10
+ import type { SeriesSchema, TimeSeries } from 'pond-ts';
11
+ import type { ParamValue } from './types.js';
12
+ export type SourceParams = Readonly<Record<string, ParamValue>>;
13
+ /** JSON-safe source identity carried by a request. */
14
+ export interface SourceRef<Name extends string = string, Params extends SourceParams = SourceParams> {
15
+ readonly source: Name;
16
+ readonly params: Params;
17
+ }
18
+ export interface LoadedSource {
19
+ readonly value: TimeSeries<SeriesSchema>;
20
+ /**
21
+ * Version of the remote value: an ETag, cursor, object version, or another
22
+ * stable token. Equal revisions deliberately preserve every graph cache.
23
+ */
24
+ readonly revision: string;
25
+ }
26
+ export interface SourceLoadContext {
27
+ readonly previous?: LoadedSource;
28
+ }
29
+ export interface SourceDef<Name extends string = string, Params extends SourceParams = SourceParams> {
30
+ readonly name: Name;
31
+ readonly load: (params: Params, context: SourceLoadContext) => Promise<LoadedSource>;
32
+ ref(params: Params): SourceRef<Name, Params>;
33
+ }
34
+ /** Defines a typed source token and its host-side loader. */
35
+ export declare function defineSource<const Name extends string, const Params extends SourceParams>(definition: {
36
+ readonly name: Name;
37
+ readonly load: SourceDef<Name, Params>['load'];
38
+ }): SourceDef<Name, Params>;
39
+ export declare class UnknownSourceError extends ProcessError {
40
+ }
41
+ export declare class SourceRegistry {
42
+ #private;
43
+ define<const Name extends string, const Params extends SourceParams>(source: SourceDef<Name, Params>): this;
44
+ load(ref: SourceRef, previous?: LoadedSource): Promise<LoadedSource>;
45
+ }
46
+ export declare function createSourceRegistry(): SourceRegistry;
47
+ /** Canonical, order-independent identity for one source invocation. */
48
+ export declare function sourceId(ref: SourceRef): string;
49
+ //# sourceMappingURL=source.d.ts.map
@@ -0,0 +1,54 @@
1
+ /**
2
+ * Opaque, asynchronous data sources.
3
+ *
4
+ * A request carries only `{ source, params }`. The loader stays on the host,
5
+ * where credentials, URLs, retries and cache policy belong. The canonical
6
+ * source id chooses a long-lived bound graph; `revision` decides whether that
7
+ * graph's source value must be invalidated.
8
+ */
9
+ import { ProcessError } from '../errors.js';
10
+ /** Defines a typed source token and its host-side loader. */
11
+ export function defineSource(definition) {
12
+ return {
13
+ ...definition,
14
+ ref: (params) => ({ source: definition.name, params }),
15
+ };
16
+ }
17
+ export class UnknownSourceError extends ProcessError {
18
+ }
19
+ export class SourceRegistry {
20
+ #sources = new Map();
21
+ define(source) {
22
+ this.#sources.set(source.name, {
23
+ name: source.name,
24
+ ref: (params) => ({ source: source.name, params }),
25
+ load: (params, context) => source.load(params, context),
26
+ });
27
+ return this;
28
+ }
29
+ async load(ref, previous) {
30
+ const source = this.#sources.get(ref.source);
31
+ if (source === undefined) {
32
+ const have = [...this.#sources.keys()].map((k) => `'${k}'`).join(', ');
33
+ throw new UnknownSourceError(`unknown source '${ref.source}'${have ? ` — have ${have}` : ''}`);
34
+ }
35
+ return source.load(ref.params, {
36
+ ...(previous !== undefined && { previous }),
37
+ });
38
+ }
39
+ }
40
+ export function createSourceRegistry() {
41
+ return new SourceRegistry();
42
+ }
43
+ /** Canonical, order-independent identity for one source invocation. */
44
+ export function sourceId(ref) {
45
+ const params = Object.keys(ref.params)
46
+ .sort()
47
+ .map((key) => {
48
+ const value = ref.params[key];
49
+ return `${encodeURIComponent(key)}=${encodeURIComponent(`${typeof value}:${JSON.stringify(value)}`)}`;
50
+ })
51
+ .join('&');
52
+ return `source:${encodeURIComponent(ref.source)}${params ? `?${params}` : ''}`;
53
+ }
54
+ //# sourceMappingURL=source.js.map