@truecalc/workbook 8.2.0 → 8.2.1

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/README.md CHANGED
@@ -215,6 +215,80 @@ const result = wb.resolved('Sheet1', 'A2');
215
215
  // => { type: 'number', value: 20 }
216
216
  ```
217
217
 
218
+ ### `wb.precedentsOf(sheet, cell, maxDepth?, maxNodes?)`
219
+
220
+ What a cell **reads** — the answer to "where does this number come from?".
221
+
222
+ - `sheet` — sheet name
223
+ - `cell` — A1-style cell reference
224
+ - `maxDepth` — how many levels to follow. Default `1` (direct precedents only),
225
+ clamped to `64`.
226
+ - `maxNodes` — cap on the number of precedents returned. Default `1000`,
227
+ clamped to `10000`.
228
+
229
+ Returns `{ cell, precedents, truncated, truncatedBy? }`. Each precedent is
230
+ `{ depth, reference }`, where `reference` is a discriminated union tagged by
231
+ `kind`:
232
+
233
+ | `kind` | Shape |
234
+ |--------------|-------------------------------------------------------------------|
235
+ | `cell` | `{ kind: 'cell', sheet: 'Inputs', a1: 'A1' }` |
236
+ | `range` | `{ kind: 'range', sheet: 'Inputs', range: 'A1:A20' }` |
237
+ | `name` | `{ kind: 'name', name: 'TaxRate', target: { kind: 'cell', ... } }`|
238
+ | `unresolved` | `{ kind: 'unresolved', text: 'Nope!A1' }` |
239
+
240
+ A range is one node, never expanded into its member cells. Every `cell` and
241
+ `range` carries its own `sheet`, so cross-sheet precedents stay cross-sheet.
242
+ `precedents` is always an array — a literal, an empty cell and a constant
243
+ formula all return `[]`.
244
+
245
+ ```js
246
+ wb.addSheet('Inputs');
247
+ wb.addSheet('Report');
248
+ wb.set('Inputs', 'A1', '10');
249
+ wb.set('Report', 'B1', '=Inputs!A1 * 2');
250
+
251
+ wb.precedentsOf('Report', 'B1');
252
+ // => { cell: { sheet: 'Report', a1: 'B1' },
253
+ // precedents: [ { depth: 1, reference: { kind: 'cell', sheet: 'Inputs', a1: 'A1' } } ],
254
+ // truncated: false }
255
+ ```
256
+
257
+ ### `wb.dependentsOf(sheet, cell, maxDepth?, maxNodes?)`
258
+
259
+ What **reads** a cell — the answer to "what breaks if I change this?".
260
+ Same arguments and bounds as `precedentsOf`.
261
+
262
+ Returns `{ cell, dependents, truncated, truncatedBy? }`, where each dependent is
263
+ `{ depth, sheet, a1 }`. Dependents are always concrete formula cells, and
264
+ include cells that reach this one through a range containing it or a named range
265
+ targeting it. `dependents` is always an array; `[]` means nothing reads the cell.
266
+
267
+ ```js
268
+ wb.dependentsOf('Inputs', 'A1');
269
+ // => { cell: { sheet: 'Inputs', a1: 'A1' },
270
+ // dependents: [ { depth: 1, sheet: 'Report', a1: 'B1' } ],
271
+ // truncated: false }
272
+ ```
273
+
274
+ ### Bounds and freshness
275
+
276
+ Every dependency query is bounded on both depth and node count and **always
277
+ reports whether it stopped early**: `truncated` is `true` and `truncatedBy` is
278
+ `'maxDepth'` or `'maxNodes'`. Branch on `truncated` — it is always present;
279
+ `truncatedBy` appears only when truncated. A request above the ceilings is
280
+ clamped, which is safe precisely because the clamp still sets `truncated`.
281
+
282
+ The queries read the workbook's **current** formulas, named ranges and sheet
283
+ names on every call. They need no `recalc()`, they reflect every `set` /
284
+ `clear` / `defineName` since the last one, and they can never return a stale
285
+ graph. The trade is cost: each call rebuilds the graph, `O(formula cells)` —
286
+ an upper bound expected to improve, not a fixed contract — and `dependentsOf`
287
+ additionally costs `O(distinct range nodes + names)` per node it walks, since
288
+ finding what reads a cell means testing every range and every name against
289
+ it. Neither call is a cheap accessor; a host driving these from the UI should
290
+ debounce rather than call on every selection change.
291
+
218
292
  ### `wb.toJSON()`
219
293
 
220
294
  Serializes the entire workbook state (sheets, cell values, formulas, named ranges)
package/package.json CHANGED
@@ -2,7 +2,7 @@
2
2
  "name": "@truecalc/workbook",
3
3
  "type": "module",
4
4
  "description": "Spreadsheet workbook for the browser — full Workbook API compiled to WebAssembly",
5
- "version": "8.2.0",
5
+ "version": "8.2.1",
6
6
  "license": "MIT",
7
7
  "repository": {
8
8
  "type": "git",
@@ -1,5 +1,128 @@
1
1
  /* tslint:disable */
2
2
  /* eslint-disable */
3
+ /**
4
+ * A cell identified the way a JavaScript caller addresses one: the sheet name
5
+ * **as written in the workbook** (original case, not the case-folded key the
6
+ * graph indexes by) plus a plain A1 address.
7
+ */
8
+ export interface CellNode {
9
+ /**
10
+ * Sheet name in the workbook\'s own casing — pass it straight back to
11
+ * `get`, `set` or another dependency query.
12
+ */
13
+ sheet: string;
14
+ /**
15
+ * Plain uppercase A1 address (`A1`, `BC42`) within `sheet`.
16
+ */
17
+ a1: string;
18
+ }
19
+
20
+ /**
21
+ * Answer to: what breaks if I change this cell?
22
+ */
23
+ export interface DependentsResult {
24
+ /**
25
+ * The cell that was queried, echoed back in canonical form.
26
+ */
27
+ cell: CellNode;
28
+ /**
29
+ * The formula cells that would have to recalculate, nearest depth first.
30
+ * **Always present**, empty when nothing reads the cell.
31
+ */
32
+ dependents: DependentNode[];
33
+ /**
34
+ * `true` when the walk stopped before exhausting the graph.
35
+ */
36
+ truncated: boolean;
37
+ /**
38
+ * Which bound stopped the walk. Present exactly when `truncated` is
39
+ * `true`. As with [`PrecedentsResult::truncated_by`], reports
40
+ * `maxNodes` only when the node cap actually blocked an available
41
+ * dependent from being emitted, not merely because the emitted count
42
+ * happens to equal `maxNodes`.
43
+ */
44
+ truncatedBy?: "maxNodes" | "maxDepth";
45
+ }
46
+
47
+ /**
48
+ * Answer to: what does this cell read?
49
+ */
50
+ export interface PrecedentsResult {
51
+ /**
52
+ * The cell that was queried, echoed back in canonical form.
53
+ */
54
+ cell: CellNode;
55
+ /**
56
+ * The precedents found, nearest depth first. **Always present**: a
57
+ * literal cell, an empty cell and a formula with no references all
58
+ * return an empty array — never a missing field — so absence of
59
+ * precedents is never confused with absence of an answer.
60
+ */
61
+ precedents: PrecedentNode[];
62
+ /**
63
+ * `true` when the walk stopped before exhausting the graph, i.e. the
64
+ * array above is a prefix of the real answer, not the whole of it.
65
+ */
66
+ truncated: boolean;
67
+ /**
68
+ * Which bound stopped the walk. Present exactly when `truncated` is
69
+ * `true` — branch on `truncated`, which is always present; this is the
70
+ * detail, not the signal. Reports `maxNodes` only when the node cap
71
+ * actually blocked an available precedent from being emitted; if depth
72
+ * is what kept the walk from going further — even when the emitted
73
+ * count happens to equal `maxNodes` — this reports `maxDepth`, the
74
+ * bound that actually ended the walk.
75
+ */
76
+ truncatedBy?: "maxNodes" | "maxDepth";
77
+ }
78
+
79
+ /**
80
+ * One dependent, with how far it sits from the queried cell. Dependents are
81
+ * always concrete formula cells — only a formula can read something.
82
+ */
83
+ export interface DependentNode {
84
+ /**
85
+ * `1` = reads the queried cell directly (as a cell, through a range that
86
+ * contains it, or through a named range whose target contains it), `2` =
87
+ * reads something that reads it, and so on. Always the shortest distance.
88
+ */
89
+ depth: number;
90
+ /**
91
+ * Sheet name in the workbook\'s own casing.
92
+ */
93
+ sheet: string;
94
+ /**
95
+ * Plain uppercase A1 address within `sheet`.
96
+ */
97
+ a1: string;
98
+ }
99
+
100
+ /**
101
+ * One precedent, with how far it sits from the queried cell.
102
+ */
103
+ export interface PrecedentNode {
104
+ /**
105
+ * `1` = read directly by the queried cell, `2` = read by something the
106
+ * queried cell reads, and so on. Always the **shortest** distance: a
107
+ * precedent reachable by several paths is reported once, at its nearest
108
+ * depth.
109
+ */
110
+ depth: number;
111
+ /**
112
+ * What is read.
113
+ */
114
+ reference: PrecedentRef;
115
+ }
116
+
117
+ /**
118
+ * One thing a formula reads, after sheet and name resolution.
119
+ *
120
+ * A range is reported as a **single** range node, never expanded into its
121
+ * cells: `SUM(A1:A100000)` is one precedent, which is what keeps the answer
122
+ * bounded and is also how the engine itself stores the edge.
123
+ */
124
+ export type PrecedentRef = { kind: "cell"; sheet: string; a1: string } | { kind: "range"; sheet: string; range: string } | { kind: "name"; name: string; target: NameTargetRef } | { kind: "unresolved"; text: string };
125
+
3
126
  /**
4
127
  * Result of [`translate_formula`]: either the rewritten `formula` text or an
5
128
  * `error` message (mutually exclusive). Mirrors the `TranslateResult` shape
@@ -11,6 +134,12 @@ export interface TranslateResult {
11
134
  error?: string;
12
135
  }
13
136
 
137
+ /**
138
+ * What a named range currently points at, carried alongside the name so a
139
+ * caller never has to resolve the indirection itself.
140
+ */
141
+ export type NameTargetRef = { kind: "cell"; sheet: string; a1: string } | { kind: "range"; sheet: string; range: string } | { kind: "unresolved" };
142
+
14
143
 
15
144
  /**
16
145
  * A spreadsheet workbook exposed to JavaScript.
@@ -40,6 +169,29 @@ export class JsWorkbook {
40
169
  * (current-row) structured references against it.
41
170
  */
42
171
  defineTable(name: string, ref_str: string): void;
172
+ /**
173
+ * What **reads** the cell at `a1` on `sheet` — its dependents, i.e. what
174
+ * would have to recalculate (and could visibly change) if you edited it.
175
+ *
176
+ * Returns `{ cell, dependents: [{ depth, sheet, a1 }], truncated,
177
+ * truncatedBy? }`. Dependents are always concrete formula cells and
178
+ * include cells that reach this one through a range that contains it or
179
+ * through a named range whose target contains it — the caller never has
180
+ * to reason about range compression or name indirection.
181
+ *
182
+ * `dependents` is always an array; `[]` means nothing reads the cell.
183
+ *
184
+ * Bounds, truncation reporting and freshness are exactly as for
185
+ * [`precedentsOf`](Self::precedents_of), with one addition: finding what
186
+ * reads a cell means testing every distinct range node and every name
187
+ * against it, so each emitted (or expanded) node costs `O(distinct
188
+ * ranges + names)` on top of the `O(formula cells)` graph build — a
189
+ * workbook with many named ranges or range formulas makes this call
190
+ * noticeably more expensive than `precedentsOf` at the same size.
191
+ *
192
+ * Throws on an unknown sheet or a malformed A1 address.
193
+ */
194
+ dependentsOf(sheet: string, a1: string, max_depth?: number | null, max_nodes?: number | null): DependentsResult;
43
195
  /**
44
196
  * Deserializes a workbook from its canonical JSON string.
45
197
  */
@@ -51,6 +203,36 @@ export class JsWorkbook {
51
203
  * string (e.g. `"excel"`) for Excel-compatible behavior.
52
204
  */
53
205
  constructor(engine: string);
206
+ /**
207
+ * What the cell at `a1` on `sheet` **reads** — its precedents.
208
+ *
209
+ * Answers: where does this number come from? Returns
210
+ * `{ cell, precedents: [{ depth, reference }], truncated, truncatedBy? }`.
211
+ * Each `reference` is a tagged union: `cell`, `range` (never expanded into
212
+ * its members), `name` (carrying what the name currently targets), or
213
+ * `unresolved` (a dangling sheet or name, the reason the cell will show
214
+ * `#REF!` / `#NAME?`). Every `cell` / `range` carries its own `sheet`, so
215
+ * a cross-sheet precedent stays cross-sheet.
216
+ *
217
+ * `precedents` is always an array: a literal, an empty cell and a
218
+ * constant formula all return `[]`.
219
+ *
220
+ * `maxDepth` defaults to `1` (direct precedents only) and is clamped to
221
+ * `1..=64`; `maxNodes` defaults to `1000` and is clamped to `10000`.
222
+ * **If the walk stops at either bound, `truncated` is `true` and
223
+ * `truncatedBy` says which one** — the returned array is then a prefix of
224
+ * the answer, not the answer. `truncated` is always present; branch on
225
+ * it, not on `truncatedBy`.
226
+ *
227
+ * The graph is rebuilt from the workbook's current formulas on every
228
+ * call, so the result is never stale: it reflects every `set` / `clear` /
229
+ * `defineName` since the last `recalc`, and is meaningful before any
230
+ * `recalc` at all. Building the graph costs `O(formula cells)` per call —
231
+ * an upper bound expected to improve, not a fixed contract.
232
+ *
233
+ * Throws on an unknown sheet or a malformed A1 address.
234
+ */
235
+ precedentsOf(sheet: string, a1: string, max_depth?: number | null, max_nodes?: number | null): PrecedentsResult;
54
236
  /**
55
237
  * Runs a full recalculation against the given context JSON.
56
238
  *
@@ -134,8 +316,10 @@ export interface InitOutput {
134
316
  readonly jsworkbook_clear: (a: number, b: number, c: number, d: number, e: number) => void;
135
317
  readonly jsworkbook_defineName: (a: number, b: number, c: number, d: number, e: number, f: number) => void;
136
318
  readonly jsworkbook_defineTable: (a: number, b: number, c: number, d: number, e: number, f: number) => void;
319
+ readonly jsworkbook_dependentsOf: (a: number, b: number, c: number, d: number, e: number, f: number, g: number, h: number) => void;
137
320
  readonly jsworkbook_fromJSON: (a: number, b: number, c: number) => void;
138
321
  readonly jsworkbook_new: (a: number, b: number) => number;
322
+ readonly jsworkbook_precedentsOf: (a: number, b: number, c: number, d: number, e: number, f: number, g: number, h: number) => void;
139
323
  readonly jsworkbook_recalc: (a: number, b: number, c: number, d: number) => void;
140
324
  readonly jsworkbook_redefineName: (a: number, b: number, c: number, d: number, e: number, f: number) => void;
141
325
  readonly jsworkbook_redefineTable: (a: number, b: number, c: number, d: number, e: number, f: number) => void;
@@ -102,6 +102,52 @@ export class JsWorkbook {
102
102
  wasm.__wbindgen_add_to_stack_pointer(16);
103
103
  }
104
104
  }
105
+ /**
106
+ * What **reads** the cell at `a1` on `sheet` — its dependents, i.e. what
107
+ * would have to recalculate (and could visibly change) if you edited it.
108
+ *
109
+ * Returns `{ cell, dependents: [{ depth, sheet, a1 }], truncated,
110
+ * truncatedBy? }`. Dependents are always concrete formula cells and
111
+ * include cells that reach this one through a range that contains it or
112
+ * through a named range whose target contains it — the caller never has
113
+ * to reason about range compression or name indirection.
114
+ *
115
+ * `dependents` is always an array; `[]` means nothing reads the cell.
116
+ *
117
+ * Bounds, truncation reporting and freshness are exactly as for
118
+ * [`precedentsOf`](Self::precedents_of), with one addition: finding what
119
+ * reads a cell means testing every distinct range node and every name
120
+ * against it, so each emitted (or expanded) node costs `O(distinct
121
+ * ranges + names)` on top of the `O(formula cells)` graph build — a
122
+ * workbook with many named ranges or range formulas makes this call
123
+ * noticeably more expensive than `precedentsOf` at the same size.
124
+ *
125
+ * Throws on an unknown sheet or a malformed A1 address.
126
+ * @param {string} sheet
127
+ * @param {string} a1
128
+ * @param {number | null} [max_depth]
129
+ * @param {number | null} [max_nodes]
130
+ * @returns {DependentsResult}
131
+ */
132
+ dependentsOf(sheet, a1, max_depth, max_nodes) {
133
+ try {
134
+ const retptr = wasm.__wbindgen_add_to_stack_pointer(-16);
135
+ const ptr0 = passStringToWasm0(sheet, wasm.__wbindgen_export, wasm.__wbindgen_export2);
136
+ const len0 = WASM_VECTOR_LEN;
137
+ const ptr1 = passStringToWasm0(a1, wasm.__wbindgen_export, wasm.__wbindgen_export2);
138
+ const len1 = WASM_VECTOR_LEN;
139
+ wasm.jsworkbook_dependentsOf(retptr, this.__wbg_ptr, ptr0, len0, ptr1, len1, isLikeNone(max_depth) ? 0x100000001 : (max_depth) >>> 0, isLikeNone(max_nodes) ? 0x100000001 : (max_nodes) >>> 0);
140
+ var r0 = getDataViewMemory0().getInt32(retptr + 4 * 0, true);
141
+ var r1 = getDataViewMemory0().getInt32(retptr + 4 * 1, true);
142
+ var r2 = getDataViewMemory0().getInt32(retptr + 4 * 2, true);
143
+ if (r2) {
144
+ throw takeObject(r1);
145
+ }
146
+ return takeObject(r0);
147
+ } finally {
148
+ wasm.__wbindgen_add_to_stack_pointer(16);
149
+ }
150
+ }
105
151
  /**
106
152
  * Deserializes a workbook from its canonical JSON string.
107
153
  * @param {string} s
@@ -139,6 +185,59 @@ export class JsWorkbook {
139
185
  JsWorkbookFinalization.register(this, this.__wbg_ptr, this);
140
186
  return this;
141
187
  }
188
+ /**
189
+ * What the cell at `a1` on `sheet` **reads** — its precedents.
190
+ *
191
+ * Answers: where does this number come from? Returns
192
+ * `{ cell, precedents: [{ depth, reference }], truncated, truncatedBy? }`.
193
+ * Each `reference` is a tagged union: `cell`, `range` (never expanded into
194
+ * its members), `name` (carrying what the name currently targets), or
195
+ * `unresolved` (a dangling sheet or name, the reason the cell will show
196
+ * `#REF!` / `#NAME?`). Every `cell` / `range` carries its own `sheet`, so
197
+ * a cross-sheet precedent stays cross-sheet.
198
+ *
199
+ * `precedents` is always an array: a literal, an empty cell and a
200
+ * constant formula all return `[]`.
201
+ *
202
+ * `maxDepth` defaults to `1` (direct precedents only) and is clamped to
203
+ * `1..=64`; `maxNodes` defaults to `1000` and is clamped to `10000`.
204
+ * **If the walk stops at either bound, `truncated` is `true` and
205
+ * `truncatedBy` says which one** — the returned array is then a prefix of
206
+ * the answer, not the answer. `truncated` is always present; branch on
207
+ * it, not on `truncatedBy`.
208
+ *
209
+ * The graph is rebuilt from the workbook's current formulas on every
210
+ * call, so the result is never stale: it reflects every `set` / `clear` /
211
+ * `defineName` since the last `recalc`, and is meaningful before any
212
+ * `recalc` at all. Building the graph costs `O(formula cells)` per call —
213
+ * an upper bound expected to improve, not a fixed contract.
214
+ *
215
+ * Throws on an unknown sheet or a malformed A1 address.
216
+ * @param {string} sheet
217
+ * @param {string} a1
218
+ * @param {number | null} [max_depth]
219
+ * @param {number | null} [max_nodes]
220
+ * @returns {PrecedentsResult}
221
+ */
222
+ precedentsOf(sheet, a1, max_depth, max_nodes) {
223
+ try {
224
+ const retptr = wasm.__wbindgen_add_to_stack_pointer(-16);
225
+ const ptr0 = passStringToWasm0(sheet, wasm.__wbindgen_export, wasm.__wbindgen_export2);
226
+ const len0 = WASM_VECTOR_LEN;
227
+ const ptr1 = passStringToWasm0(a1, wasm.__wbindgen_export, wasm.__wbindgen_export2);
228
+ const len1 = WASM_VECTOR_LEN;
229
+ wasm.jsworkbook_precedentsOf(retptr, this.__wbg_ptr, ptr0, len0, ptr1, len1, isLikeNone(max_depth) ? 0x100000001 : (max_depth) >>> 0, isLikeNone(max_nodes) ? 0x100000001 : (max_nodes) >>> 0);
230
+ var r0 = getDataViewMemory0().getInt32(retptr + 4 * 0, true);
231
+ var r1 = getDataViewMemory0().getInt32(retptr + 4 * 1, true);
232
+ var r2 = getDataViewMemory0().getInt32(retptr + 4 * 2, true);
233
+ if (r2) {
234
+ throw takeObject(r1);
235
+ }
236
+ return takeObject(r0);
237
+ } finally {
238
+ wasm.__wbindgen_add_to_stack_pointer(16);
239
+ }
240
+ }
142
241
  /**
143
242
  * Runs a full recalculation against the given context JSON.
144
243
  *
@@ -387,14 +486,26 @@ function __wbg_get_imports() {
387
486
  __wbg___wbindgen_throw_6b64449b9b9ed33c: function(arg0, arg1) {
388
487
  throw new Error(getStringFromWasm0(arg0, arg1));
389
488
  },
489
+ __wbg_new_682678e2f47e32bc: function() {
490
+ const ret = new Array();
491
+ return addHeapObject(ret);
492
+ },
390
493
  __wbg_new_aa8d0fa9762c29bd: function() {
391
494
  const ret = new Object();
392
495
  return addHeapObject(ret);
393
496
  },
497
+ __wbg_set_3bf1de9fab0cd644: function(arg0, arg1, arg2) {
498
+ getObject(arg0)[arg1 >>> 0] = takeObject(arg2);
499
+ },
394
500
  __wbg_set_6be42768c690e380: function(arg0, arg1, arg2) {
395
501
  getObject(arg0)[takeObject(arg1)] = takeObject(arg2);
396
502
  },
397
- __wbindgen_cast_0000000000000001: function(arg0, arg1) {
503
+ __wbindgen_cast_0000000000000001: function(arg0) {
504
+ // Cast intrinsic for `F64 -> Externref`.
505
+ const ret = arg0;
506
+ return addHeapObject(ret);
507
+ },
508
+ __wbindgen_cast_0000000000000002: function(arg0, arg1) {
398
509
  // Cast intrinsic for `Ref(String) -> Externref`.
399
510
  const ret = getStringFromWasm0(arg0, arg1);
400
511
  return addHeapObject(ret);
@@ -460,6 +571,10 @@ heap.push(undefined, null, true, false);
460
571
 
461
572
  let heap_next = heap.length;
462
573
 
574
+ function isLikeNone(x) {
575
+ return x === undefined || x === null;
576
+ }
577
+
463
578
  function passStringToWasm0(arg, malloc, realloc) {
464
579
  if (realloc === undefined) {
465
580
  const buf = cachedTextEncoder.encode(arg);
Binary file