@marktiderman/genesis-forms 0.4.0 → 0.4.2

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/dist/index.d.cts CHANGED
@@ -553,6 +553,8 @@ declare const CALCULATED_TYPE = "calculated";
553
553
  */
554
554
  declare const COMPUTED_FIELD_TYPES: readonly ["calculated"];
555
555
  type CalculatedFormat = "number" | "percent" | "text";
556
+ /** Longest the formulas of one form may be, all together (`parseForm` checks it). */
557
+ declare const MAX_FORM_FORMULA_LENGTH = 20000;
556
558
  /** Most decimal places a calculated value may keep. */
557
559
  declare const MAX_CALCULATED_DECIMALS = 10;
558
560
  interface CalculatedProps {
@@ -577,10 +579,18 @@ declare function formatCalculated(value: Json | undefined, field: FieldBlock): s
577
579
  * server can't make every formula over it slow.
578
580
  */
579
581
  declare const MAX_FORMULA_ITEMS = 10000;
582
+ /**
583
+ * Most list items and table rows all of a form's formulas read in one go
584
+ * (each list or table counted once, however many of its columns are read, in form order). A formula that reads
585
+ * an answer past the limit, or one over `MAX_FORMULA_ITEMS`, has no value
586
+ * (blank, never a partial total), so many big answers can't make working
587
+ * the values out slow either.
588
+ */
589
+ declare const MAX_SETTLE_ITEMS = 50000;
580
590
  /**
581
591
  * The value a formula reads for an answer: numbers and text as they are, a
582
592
  * list's items, one table column's cells. A list or table of more than
583
- * `MAX_FORMULA_ITEMS` reads as blank.
593
+ * `MAX_FORMULA_ITEMS` reads as blank. Each call builds a new list.
584
594
  */
585
595
  declare function formulaInput(value: Json | undefined, column?: string): FormulaValue;
586
596
 
@@ -666,4 +676,4 @@ declare function explain(source: FormEngine | ExplainInput, blockId: string, opt
666
676
  /** A condition as a plain-English clause: `contact_me is not Yes`. */
667
677
  declare function describeCondition(condition: Condition, def: FormDefinition, fields?: Map<string, FieldBlock>): string;
668
678
 
669
- export { type AnswersOf, Block, CALCULATED_TYPE, COMPUTED_FIELD_TYPES, type CalculatedFormat, type CalculatedProps, type ChoiceFieldOptions, type ChoiceOption, type CoerceHiddenOptions, type CoercedHidden, Condition, ContentBlock, Ending, type ExplainInput, type ExplainOptions, type Explanation, FORMULA_FUNCTIONS, FieldBlock, type FieldOptions, type FieldSpec, type FieldTypeManifest, type FieldValidation, FieldValidator, type Form, FormDefinition, FormEngine, type FormInput, type FormIssue, FormLayout, FormSettings, type FormulaFunction, type FormulaNode, type FormulaParseResult, type FormulaReader, type FormulaRef, type FormulaValue, HIGHLIGHT_NOTE_MESSAGE, HiddenInput, type Highlight, Json, type JumpTrace, type KeyResolver, LogicRule, MAX_CALCULATED_DECIMALS, MAX_FORMULA_ITEMS, MAX_FORMULA_LENGTH, MAX_HIDDEN_STRING_LENGTH, MAX_HIGHLIGHT_NOTE_LENGTH, MAX_HIGHLIGHT_QUOTE_LENGTH, MAX_TABLE_CELL_LENGTH, MAX_TABLE_COLUMNS, MAX_TIMESTAMP_SECONDS, type MediaNote, Operator, PageBreakBlock, type ParseResult, type PathResult, type RuleSpec, type RuleTrace, STRUCTURED_FIELD_TYPES, type TableColumn, calculatedProps, calculatedValue, canonicalJson, choiceOptions, coerceHidden, coerceHiddenValue, computePath, dateLimits, defineForm, describeCondition, evaluate, evaluateFormula, explain, f, fingerprint, formatCalculated, formatTimestamp, formulaInput, formulaNumber, formulaRefs, getFieldType, isEmptyValue, isIsoDate, isSensitive, listFieldTypes, listLimits, normalizeValue, numberLimits, parseForm, parseFormula, parseTimestamp, ratingDisplay, recall, registerFieldType, renameFormulaKey, ruleVisible, scaleLabels, steps, summarizeValue, tableColumns, tableLimits, when };
679
+ export { type AnswersOf, Block, CALCULATED_TYPE, COMPUTED_FIELD_TYPES, type CalculatedFormat, type CalculatedProps, type ChoiceFieldOptions, type ChoiceOption, type CoerceHiddenOptions, type CoercedHidden, Condition, ContentBlock, Ending, type ExplainInput, type ExplainOptions, type Explanation, FORMULA_FUNCTIONS, FieldBlock, type FieldOptions, type FieldSpec, type FieldTypeManifest, type FieldValidation, FieldValidator, type Form, FormDefinition, FormEngine, type FormInput, type FormIssue, FormLayout, FormSettings, type FormulaFunction, type FormulaNode, type FormulaParseResult, type FormulaReader, type FormulaRef, type FormulaValue, HIGHLIGHT_NOTE_MESSAGE, HiddenInput, type Highlight, Json, type JumpTrace, type KeyResolver, LogicRule, MAX_CALCULATED_DECIMALS, MAX_FORMULA_ITEMS, MAX_FORMULA_LENGTH, MAX_FORM_FORMULA_LENGTH, MAX_HIDDEN_STRING_LENGTH, MAX_HIGHLIGHT_NOTE_LENGTH, MAX_HIGHLIGHT_QUOTE_LENGTH, MAX_SETTLE_ITEMS, MAX_TABLE_CELL_LENGTH, MAX_TABLE_COLUMNS, MAX_TIMESTAMP_SECONDS, type MediaNote, Operator, PageBreakBlock, type ParseResult, type PathResult, type RuleSpec, type RuleTrace, STRUCTURED_FIELD_TYPES, type TableColumn, calculatedProps, calculatedValue, canonicalJson, choiceOptions, coerceHidden, coerceHiddenValue, computePath, dateLimits, defineForm, describeCondition, evaluate, evaluateFormula, explain, f, fingerprint, formatCalculated, formatTimestamp, formulaInput, formulaNumber, formulaRefs, getFieldType, isEmptyValue, isIsoDate, isSensitive, listFieldTypes, listLimits, normalizeValue, numberLimits, parseForm, parseFormula, parseTimestamp, ratingDisplay, recall, registerFieldType, renameFormulaKey, ruleVisible, scaleLabels, steps, summarizeValue, tableColumns, tableLimits, when };
package/dist/index.d.ts CHANGED
@@ -553,6 +553,8 @@ declare const CALCULATED_TYPE = "calculated";
553
553
  */
554
554
  declare const COMPUTED_FIELD_TYPES: readonly ["calculated"];
555
555
  type CalculatedFormat = "number" | "percent" | "text";
556
+ /** Longest the formulas of one form may be, all together (`parseForm` checks it). */
557
+ declare const MAX_FORM_FORMULA_LENGTH = 20000;
556
558
  /** Most decimal places a calculated value may keep. */
557
559
  declare const MAX_CALCULATED_DECIMALS = 10;
558
560
  interface CalculatedProps {
@@ -577,10 +579,18 @@ declare function formatCalculated(value: Json | undefined, field: FieldBlock): s
577
579
  * server can't make every formula over it slow.
578
580
  */
579
581
  declare const MAX_FORMULA_ITEMS = 10000;
582
+ /**
583
+ * Most list items and table rows all of a form's formulas read in one go
584
+ * (each list or table counted once, however many of its columns are read, in form order). A formula that reads
585
+ * an answer past the limit, or one over `MAX_FORMULA_ITEMS`, has no value
586
+ * (blank, never a partial total), so many big answers can't make working
587
+ * the values out slow either.
588
+ */
589
+ declare const MAX_SETTLE_ITEMS = 50000;
580
590
  /**
581
591
  * The value a formula reads for an answer: numbers and text as they are, a
582
592
  * list's items, one table column's cells. A list or table of more than
583
- * `MAX_FORMULA_ITEMS` reads as blank.
593
+ * `MAX_FORMULA_ITEMS` reads as blank. Each call builds a new list.
584
594
  */
585
595
  declare function formulaInput(value: Json | undefined, column?: string): FormulaValue;
586
596
 
@@ -666,4 +676,4 @@ declare function explain(source: FormEngine | ExplainInput, blockId: string, opt
666
676
  /** A condition as a plain-English clause: `contact_me is not Yes`. */
667
677
  declare function describeCondition(condition: Condition, def: FormDefinition, fields?: Map<string, FieldBlock>): string;
668
678
 
669
- export { type AnswersOf, Block, CALCULATED_TYPE, COMPUTED_FIELD_TYPES, type CalculatedFormat, type CalculatedProps, type ChoiceFieldOptions, type ChoiceOption, type CoerceHiddenOptions, type CoercedHidden, Condition, ContentBlock, Ending, type ExplainInput, type ExplainOptions, type Explanation, FORMULA_FUNCTIONS, FieldBlock, type FieldOptions, type FieldSpec, type FieldTypeManifest, type FieldValidation, FieldValidator, type Form, FormDefinition, FormEngine, type FormInput, type FormIssue, FormLayout, FormSettings, type FormulaFunction, type FormulaNode, type FormulaParseResult, type FormulaReader, type FormulaRef, type FormulaValue, HIGHLIGHT_NOTE_MESSAGE, HiddenInput, type Highlight, Json, type JumpTrace, type KeyResolver, LogicRule, MAX_CALCULATED_DECIMALS, MAX_FORMULA_ITEMS, MAX_FORMULA_LENGTH, MAX_HIDDEN_STRING_LENGTH, MAX_HIGHLIGHT_NOTE_LENGTH, MAX_HIGHLIGHT_QUOTE_LENGTH, MAX_TABLE_CELL_LENGTH, MAX_TABLE_COLUMNS, MAX_TIMESTAMP_SECONDS, type MediaNote, Operator, PageBreakBlock, type ParseResult, type PathResult, type RuleSpec, type RuleTrace, STRUCTURED_FIELD_TYPES, type TableColumn, calculatedProps, calculatedValue, canonicalJson, choiceOptions, coerceHidden, coerceHiddenValue, computePath, dateLimits, defineForm, describeCondition, evaluate, evaluateFormula, explain, f, fingerprint, formatCalculated, formatTimestamp, formulaInput, formulaNumber, formulaRefs, getFieldType, isEmptyValue, isIsoDate, isSensitive, listFieldTypes, listLimits, normalizeValue, numberLimits, parseForm, parseFormula, parseTimestamp, ratingDisplay, recall, registerFieldType, renameFormulaKey, ruleVisible, scaleLabels, steps, summarizeValue, tableColumns, tableLimits, when };
679
+ export { type AnswersOf, Block, CALCULATED_TYPE, COMPUTED_FIELD_TYPES, type CalculatedFormat, type CalculatedProps, type ChoiceFieldOptions, type ChoiceOption, type CoerceHiddenOptions, type CoercedHidden, Condition, ContentBlock, Ending, type ExplainInput, type ExplainOptions, type Explanation, FORMULA_FUNCTIONS, FieldBlock, type FieldOptions, type FieldSpec, type FieldTypeManifest, type FieldValidation, FieldValidator, type Form, FormDefinition, FormEngine, type FormInput, type FormIssue, FormLayout, FormSettings, type FormulaFunction, type FormulaNode, type FormulaParseResult, type FormulaReader, type FormulaRef, type FormulaValue, HIGHLIGHT_NOTE_MESSAGE, HiddenInput, type Highlight, Json, type JumpTrace, type KeyResolver, LogicRule, MAX_CALCULATED_DECIMALS, MAX_FORMULA_ITEMS, MAX_FORMULA_LENGTH, MAX_FORM_FORMULA_LENGTH, MAX_HIDDEN_STRING_LENGTH, MAX_HIGHLIGHT_NOTE_LENGTH, MAX_HIGHLIGHT_QUOTE_LENGTH, MAX_SETTLE_ITEMS, MAX_TABLE_CELL_LENGTH, MAX_TABLE_COLUMNS, MAX_TIMESTAMP_SECONDS, type MediaNote, Operator, PageBreakBlock, type ParseResult, type PathResult, type RuleSpec, type RuleTrace, STRUCTURED_FIELD_TYPES, type TableColumn, calculatedProps, calculatedValue, canonicalJson, choiceOptions, coerceHidden, coerceHiddenValue, computePath, dateLimits, defineForm, describeCondition, evaluate, evaluateFormula, explain, f, fingerprint, formatCalculated, formatTimestamp, formulaInput, formulaNumber, formulaRefs, getFieldType, isEmptyValue, isIsoDate, isSensitive, listFieldTypes, listLimits, normalizeValue, numberLimits, parseForm, parseFormula, parseTimestamp, ratingDisplay, recall, registerFieldType, renameFormulaKey, ruleVisible, scaleLabels, steps, summarizeValue, tableColumns, tableLimits, when };
package/dist/index.js CHANGED
@@ -62,6 +62,7 @@ function tokenize(source) {
62
62
  i++;
63
63
  while (isDigit(source[i] ?? "")) i++;
64
64
  }
65
+ if (isIdStart(source[i] ?? "")) throw new FormulaError("Write a number in digits only, like 1000000.", at);
65
66
  const v = Number(source.slice(at, i));
66
67
  if (!Number.isFinite(v)) throw new FormulaError("This number is too large.", at);
67
68
  tokens.push({ t: "num", v, at });
@@ -291,6 +292,27 @@ function numbersIn(values, out = []) {
291
292
  }
292
293
  return out;
293
294
  }
295
+ var listTotals = /* @__PURE__ */ new WeakMap();
296
+ var ownLists = /* @__PURE__ */ new WeakSet();
297
+ function ownFormulaList(list) {
298
+ Object.freeze(list);
299
+ ownLists.add(list);
300
+ return list;
301
+ }
302
+ function totalsOf(list) {
303
+ const own2 = ownLists.has(list);
304
+ let t = own2 ? listTotals.get(list) : void 0;
305
+ if (t) return t;
306
+ const nums = numbersIn(list);
307
+ t = { sum: 0, count: nums.length, min: Infinity, max: -Infinity };
308
+ for (const n of nums) {
309
+ t.sum += n;
310
+ if (n < t.min) t.min = n;
311
+ if (n > t.max) t.max = n;
312
+ }
313
+ if (own2) listTotals.set(list, t);
314
+ return t;
315
+ }
294
316
  function compare(op, a, b) {
295
317
  const blank2 = (v) => v === null || Array.isArray(v) || typeof v === "string" && v.trim() === "";
296
318
  if (blank2(a) || blank2(b)) return op === "!=";
@@ -315,7 +337,7 @@ function evaluateFormula(ast, read) {
315
337
  case "text":
316
338
  return node.value;
317
339
  case "ref":
318
- return read(node.column !== void 0 ? { key: node.key, column: node.column } : { key: node.key });
340
+ return read(node);
319
341
  case "unary": {
320
342
  const n = formulaNumber(scalar(run(node.arg)));
321
343
  return n === null ? null : -n;
@@ -323,12 +345,31 @@ function evaluateFormula(ast, read) {
323
345
  case "if":
324
346
  return truthy(run(node.test)) ? run(node.then) : run(node.else);
325
347
  case "call": {
326
- const nums = numbersIn(node.args.map(run));
327
- const total = nums.reduce((a, b) => a + b, 0);
348
+ let total = 0;
349
+ let count = 0;
350
+ let min = Infinity;
351
+ let max = -Infinity;
352
+ for (const arg of node.args) {
353
+ const v = run(arg);
354
+ if (Array.isArray(v)) {
355
+ const t = totalsOf(v);
356
+ total += t.sum;
357
+ count += t.count;
358
+ if (t.min < min) min = t.min;
359
+ if (t.max > max) max = t.max;
360
+ continue;
361
+ }
362
+ const n = formulaNumber(v);
363
+ if (n === null) continue;
364
+ total += n;
365
+ count++;
366
+ if (n < min) min = n;
367
+ if (n > max) max = n;
368
+ }
328
369
  if (node.fn === "sum") return finite(total);
329
- if (!nums.length) return null;
330
- if (node.fn === "avg") return finite(total / nums.length);
331
- return nums.reduce((a, b) => node.fn === "min" ? Math.min(a, b) : Math.max(a, b));
370
+ if (!count) return null;
371
+ if (node.fn === "avg") return finite(total / count);
372
+ return node.fn === "min" ? min : max;
332
373
  }
333
374
  case "binary": {
334
375
  const a = run(node.left);
@@ -356,6 +397,7 @@ function evaluateFormula(ast, read) {
356
397
  // src/calculated.ts
357
398
  var CALCULATED_TYPE = "calculated";
358
399
  var COMPUTED_FIELD_TYPES = [CALCULATED_TYPE];
400
+ var MAX_FORM_FORMULA_LENGTH = 2e4;
359
401
  var MAX_CALCULATED_DECIMALS = 10;
360
402
  function calculatedProps(field2) {
361
403
  const p = field2.props ?? {};
@@ -373,7 +415,9 @@ var round = (n, decimals) => {
373
415
  return Object.is(r, -0) ? 0 : r;
374
416
  };
375
417
  function calculatedValue(result, field2) {
376
- const { format, decimals } = calculatedProps(field2);
418
+ return valueAs(result, calculatedProps(field2));
419
+ }
420
+ function valueAs(result, { format, decimals }) {
377
421
  if (result === null || Array.isArray(result)) return void 0;
378
422
  if (format === "number" || format === "percent") {
379
423
  const n = formulaNumber(result);
@@ -394,24 +438,43 @@ function formatCalculated(value, field2) {
394
438
  return typeof value === "string" ? value : "";
395
439
  }
396
440
  var MAX_FORMULA_ITEMS = 1e4;
441
+ var MAX_SETTLE_ITEMS = 5e4;
442
+ var extract = (value, column) => column !== void 0 ? value.flatMap((row) => {
443
+ if (!row || typeof row !== "object" || Array.isArray(row)) return [];
444
+ const cell = row[column];
445
+ return typeof cell === "string" || typeof cell === "number" ? [cell] : [];
446
+ }) : value.flatMap((v) => typeof v === "string" || typeof v === "number" || typeof v === "boolean" ? [v] : []);
397
447
  function formulaInput(value, column) {
398
448
  if (value === void 0 || value === null) return null;
399
449
  if (Array.isArray(value) && value.length > MAX_FORMULA_ITEMS) return null;
400
450
  if (column !== void 0) {
401
451
  if (!Array.isArray(value)) return null;
402
- return value.flatMap((row) => {
403
- if (!row || typeof row !== "object" || Array.isArray(row)) return [];
404
- const cell = row[column];
405
- return typeof cell === "string" || typeof cell === "number" ? [cell] : [];
406
- });
407
- }
408
- if (Array.isArray(value)) {
409
- return value.flatMap((v) => typeof v === "string" || typeof v === "number" || typeof v === "boolean" ? [v] : []);
452
+ return extract(value, column);
410
453
  }
454
+ if (Array.isArray(value)) return extract(value, void 0);
411
455
  return typeof value === "object" ? null : value;
412
456
  }
457
+ function refNodes(node, out = []) {
458
+ if (node.kind === "ref") out.push(node);
459
+ else if (node.kind === "unary") refNodes(node.arg, out);
460
+ else if (node.kind === "binary") refNodes(node.left, out), refNodes(node.right, out);
461
+ else if (node.kind === "if") refNodes(node.test, out), refNodes(node.then, out), refNodes(node.else, out);
462
+ else if (node.kind === "call") for (const a of node.args) refNodes(a, out);
463
+ return out;
464
+ }
465
+ function formulaSize(node) {
466
+ if (node.kind === "unary") return 1 + formulaSize(node.arg);
467
+ if (node.kind === "binary") return 1 + formulaSize(node.left) + formulaSize(node.right);
468
+ if (node.kind === "if") return 1 + formulaSize(node.test) + formulaSize(node.then) + formulaSize(node.else);
469
+ if (node.kind === "call") return node.args.reduce((n, a) => n + formulaSize(a), 1);
470
+ return 1;
471
+ }
472
+ function spend(budget, amount) {
473
+ return;
474
+ }
413
475
  var UNREAD = /* @__PURE__ */ Symbol("unread");
414
- function calculator(definition) {
476
+ var OVER = /* @__PURE__ */ Symbol("over");
477
+ function calculator(definition, work) {
415
478
  const compiled = [];
416
479
  const idByKey = /* @__PURE__ */ new Map();
417
480
  for (const block of definition.blocks) {
@@ -420,35 +483,105 @@ function calculator(definition) {
420
483
  }
421
484
  for (const block of definition.blocks) {
422
485
  if (block.kind !== "field" || !isCalculated(block)) continue;
423
- const parsed = parseFormula(calculatedProps(block).formula);
424
- const reads = parsed.ok ? parsed.refs.map((r) => idByKey.get(r.key)) : [];
425
- compiled.push({ field: block, ast: parsed.ok ? parsed.ast : null, reads, last: null });
486
+ const props2 = calculatedProps(block);
487
+ const parsed = parseFormula(props2.formula);
488
+ const reads = [];
489
+ const slots = /* @__PURE__ */ new Map();
490
+ const byKey = /* @__PURE__ */ new Map();
491
+ for (const ref of parsed.ok ? refNodes(parsed.ast) : []) {
492
+ const id = idByKey.get(ref.key);
493
+ const key = `${id ?? ""}\0${ref.column ?? ""}`;
494
+ let slot = byKey.get(key);
495
+ if (slot === void 0) {
496
+ slot = reads.push({ id, column: ref.column, key }) - 1;
497
+ byKey.set(key, slot);
498
+ }
499
+ slots.set(ref, slot);
500
+ }
501
+ compiled.push({ field: block, props: props2, ast: parsed.ok ? parsed.ast : null, size: parsed.ok ? formulaSize(parsed.ast) : 0, reads, slots, last: null });
426
502
  }
503
+ const extracted = /* @__PURE__ */ new WeakMap();
504
+ const cachedList = (value, column) => {
505
+ let byColumn = extracted.get(value);
506
+ if (!byColumn) extracted.set(value, byColumn = /* @__PURE__ */ new Map());
507
+ const key = column ?? "";
508
+ let list = byColumn.get(key);
509
+ if (!list) {
510
+ byColumn.set(key, list = ownFormulaList(extract(value, column)));
511
+ }
512
+ return list;
513
+ };
514
+ const readAnswer = (value, column) => {
515
+ if (Array.isArray(value)) return cachedList(value, column);
516
+ return column !== void 0 ? null : formulaInput(value);
517
+ };
518
+ const forget = (value) => {
519
+ if (!value || typeof value !== "object") return;
520
+ if (Array.isArray(value)) extracted.delete(value);
521
+ for (const entry of compiled) if (entry.last?.inputs.includes(value)) entry.last = null;
522
+ };
427
523
  const withCalculated = (values, path) => {
428
524
  if (!compiled.length) return values;
429
525
  const visible = new Set(path);
430
526
  let next = values;
431
- const input = (id) => id === void 0 || !visible.has(id) || !Object.prototype.hasOwnProperty.call(next, id) ? UNREAD : next[id];
527
+ let copied = false;
528
+ let budget = MAX_SETTLE_ITEMS;
529
+ const within = /* @__PURE__ */ new Map();
530
+ const input = ({ id }) => {
531
+ if (id === void 0 || !visible.has(id) || !Object.prototype.hasOwnProperty.call(next, id)) return UNREAD;
532
+ const value = next[id];
533
+ if (!Array.isArray(value)) return value;
534
+ let ok = within.get(id);
535
+ if (ok === void 0) {
536
+ ok = value.length <= MAX_FORMULA_ITEMS && value.length <= budget;
537
+ if (ok) budget -= value.length;
538
+ within.set(id, ok);
539
+ }
540
+ return ok ? value : OVER;
541
+ };
432
542
  for (const entry of compiled) {
433
543
  const { field: field2, ast } = entry;
434
- const inputs = entry.reads.map(input);
544
+ const { reads } = entry;
545
+ const inputs = new Array(reads.length);
546
+ let over = false;
547
+ let same = entry.last !== null;
548
+ for (let i = 0; i < reads.length; i++) {
549
+ const v = input(reads[i]);
550
+ inputs[i] = v;
551
+ if (v === OVER) over = true;
552
+ if (same && entry.last.inputs[i] !== v) same = false;
553
+ }
435
554
  let value;
436
- if (entry.last && entry.last.inputs.length === inputs.length && entry.last.inputs.every((v, i) => v === inputs[i])) {
555
+ if (same) {
437
556
  value = entry.last.value;
438
557
  } else {
558
+ spend(work, entry.size);
559
+ const resolved = new Array(inputs.length);
439
560
  const read = (ref) => {
440
- const raw = input(idByKey.get(ref.key));
441
- return raw === UNREAD ? null : formulaInput(raw, ref.column);
561
+ const slot = entry.slots.get(ref);
562
+ if (slot === void 0) return null;
563
+ let v = resolved[slot];
564
+ if (v === void 0) {
565
+ const raw = inputs[slot];
566
+ v = resolved[slot] = raw === UNREAD ? null : readAnswer(raw, reads[slot].column);
567
+ }
568
+ return v;
442
569
  };
443
- value = ast ? calculatedValue(evaluateFormula(ast, read), field2) : void 0;
570
+ value = ast && !over ? valueAs(evaluateFormula(ast, read), entry.props) : void 0;
444
571
  entry.last = { inputs, value };
445
572
  }
446
573
  const current = Object.prototype.hasOwnProperty.call(next, field2.id) ? next[field2.id] : void 0;
447
- if (current !== value) next = { ...next, [field2.id]: value };
574
+ if (current !== value) {
575
+ if (!copied) {
576
+ next = { ...next };
577
+ copied = true;
578
+ }
579
+ next[field2.id] = value;
580
+ }
448
581
  }
449
582
  return next;
450
583
  };
451
- return { active: compiled.length > 0, withCalculated };
584
+ return { active: compiled.length > 0, ids: new Set(compiled.map((c) => c.field.id)), withCalculated, forget };
452
585
  }
453
586
  var numberOps = ["eq", "neq", "lt", "lte", "gt", "gte", "in", "answered", "empty"];
454
587
  var calculatedFieldType = {
@@ -1083,8 +1216,8 @@ function rulesAllow(def, blockId, scope) {
1083
1216
  }
1084
1217
  function walkPath(def, scope) {
1085
1218
  const list = steps(def);
1086
- const index = new Map(list.map((b, i2) => [b.id, i2]));
1087
- const position = new Map(def.blocks.map((b, i2) => [b.id, i2]));
1219
+ let index;
1220
+ let position;
1088
1221
  const jumps = (def.logic ?? []).filter(
1089
1222
  (r) => r.effect === "jump"
1090
1223
  );
@@ -1108,6 +1241,7 @@ function walkPath(def, scope) {
1108
1241
  ending = jump.to.ending;
1109
1242
  break;
1110
1243
  }
1244
+ index ??= new Map(list.map((b, at) => [b.id, at]));
1111
1245
  const target = index.get(jump.to);
1112
1246
  if (target !== void 0 && target > i) {
1113
1247
  i = nextVisible(target);
@@ -1117,6 +1251,7 @@ function walkPath(def, scope) {
1117
1251
  i = nextVisible(i + 1);
1118
1252
  }
1119
1253
  const scopeAt = (blockId, inclusive = false) => {
1254
+ position ??= new Map(def.blocks.map((b, i2) => [b.id, i2]));
1120
1255
  const at = position.get(blockId) ?? -1;
1121
1256
  const values = /* @__PURE__ */ Object.create(null);
1122
1257
  for (const id of path) {
@@ -1421,9 +1556,15 @@ function parseForm(input) {
1421
1556
  issue(`${path}.kind`, "Unknown block kind.");
1422
1557
  }
1423
1558
  });
1559
+ let formulaChars = 0;
1424
1560
  input.blocks.forEach((block, i) => {
1425
1561
  if (!isObject2(block) || block.kind !== "field" || block.type !== CALCULATED_TYPE) return;
1426
1562
  const path = `blocks[${i}].props.formula`;
1563
+ const source = isObject2(block.props) && typeof block.props.formula === "string" ? block.props.formula : "";
1564
+ formulaChars += source.length;
1565
+ if (formulaChars > MAX_FORM_FORMULA_LENGTH && formulaChars - source.length <= MAX_FORM_FORMULA_LENGTH) {
1566
+ issue(path, `The form's formulas together are over ${MAX_FORM_FORMULA_LENGTH} characters; shorten some, or work part out once in an earlier calculated field.`);
1567
+ }
1427
1568
  if (block.required === true) {
1428
1569
  warnings.push({ path: `blocks[${i}].required`, message: "A calculated field is worked out, not answered, so it is never required." });
1429
1570
  }
@@ -1922,10 +2063,19 @@ function validateField(field2, value, options = {}) {
1922
2063
  }
1923
2064
 
1924
2065
  // src/engine.ts
2066
+ function readsAny(node, ids) {
2067
+ if (Array.isArray(node)) return node.some((n) => readsAny(n, ids));
2068
+ if (!node || typeof node !== "object") return false;
2069
+ for (const [k, v] of Object.entries(node)) {
2070
+ if (k === "field" && typeof v === "string" && ids.has(v)) return true;
2071
+ if (typeof v === "object" && readsAny(v, ids)) return true;
2072
+ }
2073
+ return false;
2074
+ }
1925
2075
  function createForm(definition, options = {}) {
1926
2076
  return build(definition, options).engine;
1927
2077
  }
1928
- function build(definition, options, replaying = false) {
2078
+ function build(definition, options, replaying = false, budget) {
1929
2079
  const fields = fieldMap(definition);
1930
2080
  const byKey = new Map([...fields.values()].map((f2) => [f2.key, f2]));
1931
2081
  const blocks = new Map(definition.blocks.map((b) => [b.id, b]));
@@ -1943,15 +2093,24 @@ function build(definition, options, replaying = false) {
1943
2093
  if (!declared.has(name)) initialHidden[name] = value;
1944
2094
  }
1945
2095
  Object.assign(initialHidden, coerceHidden(definition, options.hidden).values);
1946
- const calc = calculator(definition);
2096
+ const calc = calculator(definition, budget);
2097
+ const pathOf = (values, hidden) => {
2098
+ spend(budget, definition.blocks.length);
2099
+ return computePath(definition, scopeOf(values, hidden));
2100
+ };
2101
+ const logicReadsCalculated = calc.active && readsAny(definition.logic, calc.ids);
1947
2102
  const settle = (values, hidden) => {
1948
- let { path } = computePath(definition, scopeOf(values, hidden));
2103
+ let { path } = pathOf(values, hidden);
1949
2104
  if (!calc.active) return { values, path };
2105
+ if (!logicReadsCalculated) return { values: calc.withCalculated(values, path), path };
1950
2106
  for (let round2 = 0; round2 < 4; round2++) {
1951
2107
  const next = calc.withCalculated(values, path);
1952
2108
  if (next === values) break;
1953
2109
  values = next;
1954
- path = computePath(definition, scopeOf(values, hidden)).path;
2110
+ const again = pathOf(values, hidden).path;
2111
+ const same = again.length === path.length && again.every((id, i) => id === path[i]);
2112
+ path = again;
2113
+ if (same) break;
1955
2114
  }
1956
2115
  return { values, path };
1957
2116
  };
@@ -1993,14 +2152,30 @@ function build(definition, options, replaying = false) {
1993
2152
  return errors;
1994
2153
  };
1995
2154
  let unsettled = false;
1996
- const settleNow = () => {
1997
- if (!unsettled) return;
1998
- unsettled = false;
1999
- const { values, path } = settle(state.values, state.hidden);
2000
- state = { ...state, values, path };
2155
+ let formulasStale = false;
2156
+ const settleNow = (needValues = true) => {
2157
+ if (unsettled && calc.active && !logicReadsCalculated && !needValues) {
2158
+ unsettled = false;
2159
+ formulasStale = true;
2160
+ state = { ...state, path: pathOf(state.values, state.hidden).path };
2161
+ return;
2162
+ }
2163
+ if (unsettled) {
2164
+ unsettled = false;
2165
+ formulasStale = false;
2166
+ const { values, path } = settle(state.values, state.hidden);
2167
+ state = { ...state, values, path };
2168
+ } else if (formulasStale && needValues) {
2169
+ formulasStale = false;
2170
+ state = { ...state, values: calc.withCalculated(state.values, state.path) };
2171
+ }
2001
2172
  };
2002
2173
  const answer = (id, value, event) => {
2003
- if (state.status === "completed" || own(state.values, id) === value || isCalculated(fields.get(id))) return;
2174
+ if (state.status === "completed" || isCalculated(fields.get(id))) return;
2175
+ if (own(state.values, id) === value) {
2176
+ if (value === null || typeof value !== "object") return;
2177
+ calc.forget(value);
2178
+ }
2004
2179
  const errors = { ...state.errors };
2005
2180
  delete errors[id];
2006
2181
  if (batch) {
@@ -2066,7 +2241,7 @@ function build(definition, options, replaying = false) {
2066
2241
  emit({ ...state, errors, current: firstError, events });
2067
2242
  return false;
2068
2243
  }
2069
- const { ending } = computePath(definition, scopeOf(state.values, state.hidden));
2244
+ const { ending } = pathOf(state.values, state.hidden);
2070
2245
  emit({ ...state, errors: {}, status: "completed", ending, events });
2071
2246
  return true;
2072
2247
  },
@@ -2098,10 +2273,11 @@ function build(definition, options, replaying = false) {
2098
2273
  answer(event.field, event.value, logged);
2099
2274
  }
2100
2275
  } else if (event.type === "next") {
2101
- settleNow();
2276
+ settleNow(false);
2277
+ if (formulasStale && isCalculated(fields.get(state.current ?? ""))) settleNow(true);
2102
2278
  engine.next();
2103
2279
  } else if (event.type === "back") {
2104
- settleNow();
2280
+ settleNow(false);
2105
2281
  engine.back();
2106
2282
  } else if (event.type === "submit") {
2107
2283
  settleNow();
@@ -2116,9 +2292,16 @@ function build(definition, options, replaying = false) {
2116
2292
  return { engine, apply, finish };
2117
2293
  }
2118
2294
  function replay(definition, events, options = {}) {
2119
- const { engine, apply, finish } = build(definition, { ...options, initialValues: void 0, clock: () => void 0 }, true);
2120
- for (const event of events) apply(event);
2121
- finish();
2295
+ return replayWithin(definition, events, options);
2296
+ }
2297
+ function replayWithin(definition, events, options, maxWork) {
2298
+ const budget = void 0 ;
2299
+ const { engine, apply, finish } = build(definition, { ...options, initialValues: void 0, clock: () => void 0 }, true, budget);
2300
+ try {
2301
+ for (const event of events) apply(event);
2302
+ finish();
2303
+ } finally {
2304
+ }
2122
2305
  return engine;
2123
2306
  }
2124
2307
 
@@ -2258,6 +2441,6 @@ function describeCondition(condition, def, fields = fieldMap(def)) {
2258
2441
  return `${left} ${op} ${right}`;
2259
2442
  }
2260
2443
 
2261
- export { CALCULATED_TYPE, COMPUTED_FIELD_TYPES, FORMULA_FUNCTIONS, HIGHLIGHT_NOTE_MESSAGE, MAX_CALCULATED_DECIMALS, MAX_FORMULA_ITEMS, MAX_FORMULA_LENGTH, MAX_HIDDEN_STRING_LENGTH, MAX_HIGHLIGHT_NOTE_LENGTH, MAX_HIGHLIGHT_QUOTE_LENGTH, MAX_TABLE_CELL_LENGTH, MAX_TABLE_COLUMNS, MAX_TIMESTAMP_SECONDS, REQUIRED_MESSAGE, STRUCTURED_FIELD_TYPES, calculatedProps, calculatedValue, canonicalJson, choiceOptions, coerceHidden, coerceHiddenValue, computePath, createForm, dateLimits, defineForm, describeCondition, evaluate, evaluateFormula, explain, f, fingerprint, formatCalculated, formatTimestamp, formulaInput, formulaNumber, formulaRefs, getFieldType, isEmptyValue, isFormEvent, isIsoDate, isJson, isSensitive, listFieldTypes, listLimits, normalizeValue, numberLimits, parseForm, parseFormula, parseTimestamp, ratingDisplay, recall, registerFieldType, renameFormulaKey, replay, ruleVisible, scaleLabels, steps, summarizeValue, tableColumns, tableLimits, validateField, when };
2444
+ export { CALCULATED_TYPE, COMPUTED_FIELD_TYPES, FORMULA_FUNCTIONS, HIGHLIGHT_NOTE_MESSAGE, MAX_CALCULATED_DECIMALS, MAX_FORMULA_ITEMS, MAX_FORMULA_LENGTH, MAX_FORM_FORMULA_LENGTH, MAX_HIDDEN_STRING_LENGTH, MAX_HIGHLIGHT_NOTE_LENGTH, MAX_HIGHLIGHT_QUOTE_LENGTH, MAX_SETTLE_ITEMS, MAX_TABLE_CELL_LENGTH, MAX_TABLE_COLUMNS, MAX_TIMESTAMP_SECONDS, REQUIRED_MESSAGE, STRUCTURED_FIELD_TYPES, calculatedProps, calculatedValue, canonicalJson, choiceOptions, coerceHidden, coerceHiddenValue, computePath, createForm, dateLimits, defineForm, describeCondition, evaluate, evaluateFormula, explain, f, fingerprint, formatCalculated, formatTimestamp, formulaInput, formulaNumber, formulaRefs, getFieldType, isEmptyValue, isFormEvent, isIsoDate, isJson, isSensitive, listFieldTypes, listLimits, normalizeValue, numberLimits, parseForm, parseFormula, parseTimestamp, ratingDisplay, recall, registerFieldType, renameFormulaKey, replay, ruleVisible, scaleLabels, steps, summarizeValue, tableColumns, tableLimits, validateField, when };
2262
2445
  //# sourceMappingURL=index.js.map
2263
2446
  //# sourceMappingURL=index.js.map