@marktiderman/genesis-forms 0.4.1 → 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
@@ -581,7 +581,7 @@ declare function formatCalculated(value: Json | undefined, field: FieldBlock): s
581
581
  declare const MAX_FORMULA_ITEMS = 10000;
582
582
  /**
583
583
  * Most list items and table rows all of a form's formulas read in one go
584
- * (each answer and column counted once, in form order). A formula that reads
584
+ * (each list or table counted once, however many of its columns are read, in form order). A formula that reads
585
585
  * an answer past the limit, or one over `MAX_FORMULA_ITEMS`, has no value
586
586
  * (blank, never a partial total), so many big answers can't make working
587
587
  * the values out slow either.
package/dist/index.d.ts CHANGED
@@ -581,7 +581,7 @@ declare function formatCalculated(value: Json | undefined, field: FieldBlock): s
581
581
  declare const MAX_FORMULA_ITEMS = 10000;
582
582
  /**
583
583
  * Most list items and table rows all of a form's formulas read in one go
584
- * (each answer and column counted once, in form order). A formula that reads
584
+ * (each list or table counted once, however many of its columns are read, in form order). A formula that reads
585
585
  * an answer past the limit, or one over `MAX_FORMULA_ITEMS`, has no value
586
586
  * (blank, never a partial total), so many big answers can't make working
587
587
  * the values out slow either.
package/dist/index.js CHANGED
@@ -415,7 +415,9 @@ var round = (n, decimals) => {
415
415
  return Object.is(r, -0) ? 0 : r;
416
416
  };
417
417
  function calculatedValue(result, field2) {
418
- const { format, decimals } = calculatedProps(field2);
418
+ return valueAs(result, calculatedProps(field2));
419
+ }
420
+ function valueAs(result, { format, decimals }) {
419
421
  if (result === null || Array.isArray(result)) return void 0;
420
422
  if (format === "number" || format === "percent") {
421
423
  const n = formulaNumber(result);
@@ -442,19 +444,6 @@ var extract = (value, column) => column !== void 0 ? value.flatMap((row) => {
442
444
  const cell = row[column];
443
445
  return typeof cell === "string" || typeof cell === "number" ? [cell] : [];
444
446
  }) : value.flatMap((v) => typeof v === "string" || typeof v === "number" || typeof v === "boolean" ? [v] : []);
445
- var extracted = /* @__PURE__ */ new WeakMap();
446
- function cachedList(value, column) {
447
- let byColumn = extracted.get(value);
448
- if (!byColumn) extracted.set(value, byColumn = /* @__PURE__ */ new Map());
449
- const key = column ?? "";
450
- let list = byColumn.get(key);
451
- if (!list) byColumn.set(key, list = ownFormulaList(extract(value, column)));
452
- return list;
453
- }
454
- function readAnswer(value, column) {
455
- if (Array.isArray(value)) return cachedList(value, column);
456
- return column !== void 0 ? null : formulaInput(value);
457
- }
458
447
  function formulaInput(value, column) {
459
448
  if (value === void 0 || value === null) return null;
460
449
  if (Array.isArray(value) && value.length > MAX_FORMULA_ITEMS) return null;
@@ -473,9 +462,19 @@ function refNodes(node, out = []) {
473
462
  else if (node.kind === "call") for (const a of node.args) refNodes(a, out);
474
463
  return out;
475
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
+ }
476
475
  var UNREAD = /* @__PURE__ */ Symbol("unread");
477
476
  var OVER = /* @__PURE__ */ Symbol("over");
478
- function calculator(definition) {
477
+ function calculator(definition, work) {
479
478
  const compiled = [];
480
479
  const idByKey = /* @__PURE__ */ new Map();
481
480
  for (const block of definition.blocks) {
@@ -484,7 +483,8 @@ function calculator(definition) {
484
483
  }
485
484
  for (const block of definition.blocks) {
486
485
  if (block.kind !== "field" || !isCalculated(block)) continue;
487
- const parsed = parseFormula(calculatedProps(block).formula);
486
+ const props2 = calculatedProps(block);
487
+ const parsed = parseFormula(props2.formula);
488
488
  const reads = [];
489
489
  const slots = /* @__PURE__ */ new Map();
490
490
  const byKey = /* @__PURE__ */ new Map();
@@ -498,33 +498,64 @@ function calculator(definition) {
498
498
  }
499
499
  slots.set(ref, slot);
500
500
  }
501
- compiled.push({ field: block, ast: parsed.ok ? parsed.ast : null, reads, slots, last: null });
501
+ compiled.push({ field: block, props: props2, ast: parsed.ok ? parsed.ast : null, size: parsed.ok ? formulaSize(parsed.ast) : 0, reads, slots, last: null });
502
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
+ };
503
523
  const withCalculated = (values, path) => {
504
524
  if (!compiled.length) return values;
505
525
  const visible = new Set(path);
506
526
  let next = values;
527
+ let copied = false;
507
528
  let budget = MAX_SETTLE_ITEMS;
508
529
  const within = /* @__PURE__ */ new Map();
509
- const input = ({ id, key }) => {
530
+ const input = ({ id }) => {
510
531
  if (id === void 0 || !visible.has(id) || !Object.prototype.hasOwnProperty.call(next, id)) return UNREAD;
511
532
  const value = next[id];
512
533
  if (!Array.isArray(value)) return value;
513
- let ok = within.get(key);
534
+ let ok = within.get(id);
514
535
  if (ok === void 0) {
515
536
  ok = value.length <= MAX_FORMULA_ITEMS && value.length <= budget;
516
537
  if (ok) budget -= value.length;
517
- within.set(key, ok);
538
+ within.set(id, ok);
518
539
  }
519
540
  return ok ? value : OVER;
520
541
  };
521
542
  for (const entry of compiled) {
522
543
  const { field: field2, ast } = entry;
523
- 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
+ }
524
554
  let value;
525
- if (entry.last && entry.last.inputs.length === inputs.length && entry.last.inputs.every((v, i) => v === inputs[i])) {
555
+ if (same) {
526
556
  value = entry.last.value;
527
557
  } else {
558
+ spend(work, entry.size);
528
559
  const resolved = new Array(inputs.length);
529
560
  const read = (ref) => {
530
561
  const slot = entry.slots.get(ref);
@@ -532,19 +563,25 @@ function calculator(definition) {
532
563
  let v = resolved[slot];
533
564
  if (v === void 0) {
534
565
  const raw = inputs[slot];
535
- v = resolved[slot] = raw === UNREAD ? null : readAnswer(raw, entry.reads[slot].column);
566
+ v = resolved[slot] = raw === UNREAD ? null : readAnswer(raw, reads[slot].column);
536
567
  }
537
568
  return v;
538
569
  };
539
- value = ast && !inputs.includes(OVER) ? calculatedValue(evaluateFormula(ast, read), field2) : void 0;
570
+ value = ast && !over ? valueAs(evaluateFormula(ast, read), entry.props) : void 0;
540
571
  entry.last = { inputs, value };
541
572
  }
542
573
  const current = Object.prototype.hasOwnProperty.call(next, field2.id) ? next[field2.id] : void 0;
543
- 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
+ }
544
581
  }
545
582
  return next;
546
583
  };
547
- return { active: compiled.length > 0, withCalculated };
584
+ return { active: compiled.length > 0, ids: new Set(compiled.map((c) => c.field.id)), withCalculated, forget };
548
585
  }
549
586
  var numberOps = ["eq", "neq", "lt", "lte", "gt", "gte", "in", "answered", "empty"];
550
587
  var calculatedFieldType = {
@@ -1179,8 +1216,8 @@ function rulesAllow(def, blockId, scope) {
1179
1216
  }
1180
1217
  function walkPath(def, scope) {
1181
1218
  const list = steps(def);
1182
- const index = new Map(list.map((b, i2) => [b.id, i2]));
1183
- const position = new Map(def.blocks.map((b, i2) => [b.id, i2]));
1219
+ let index;
1220
+ let position;
1184
1221
  const jumps = (def.logic ?? []).filter(
1185
1222
  (r) => r.effect === "jump"
1186
1223
  );
@@ -1204,6 +1241,7 @@ function walkPath(def, scope) {
1204
1241
  ending = jump.to.ending;
1205
1242
  break;
1206
1243
  }
1244
+ index ??= new Map(list.map((b, at) => [b.id, at]));
1207
1245
  const target = index.get(jump.to);
1208
1246
  if (target !== void 0 && target > i) {
1209
1247
  i = nextVisible(target);
@@ -1213,6 +1251,7 @@ function walkPath(def, scope) {
1213
1251
  i = nextVisible(i + 1);
1214
1252
  }
1215
1253
  const scopeAt = (blockId, inclusive = false) => {
1254
+ position ??= new Map(def.blocks.map((b, i2) => [b.id, i2]));
1216
1255
  const at = position.get(blockId) ?? -1;
1217
1256
  const values = /* @__PURE__ */ Object.create(null);
1218
1257
  for (const id of path) {
@@ -2024,10 +2063,19 @@ function validateField(field2, value, options = {}) {
2024
2063
  }
2025
2064
 
2026
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
+ }
2027
2075
  function createForm(definition, options = {}) {
2028
2076
  return build(definition, options).engine;
2029
2077
  }
2030
- function build(definition, options, replaying = false) {
2078
+ function build(definition, options, replaying = false, budget) {
2031
2079
  const fields = fieldMap(definition);
2032
2080
  const byKey = new Map([...fields.values()].map((f2) => [f2.key, f2]));
2033
2081
  const blocks = new Map(definition.blocks.map((b) => [b.id, b]));
@@ -2045,15 +2093,24 @@ function build(definition, options, replaying = false) {
2045
2093
  if (!declared.has(name)) initialHidden[name] = value;
2046
2094
  }
2047
2095
  Object.assign(initialHidden, coerceHidden(definition, options.hidden).values);
2048
- 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);
2049
2102
  const settle = (values, hidden) => {
2050
- let { path } = computePath(definition, scopeOf(values, hidden));
2103
+ let { path } = pathOf(values, hidden);
2051
2104
  if (!calc.active) return { values, path };
2105
+ if (!logicReadsCalculated) return { values: calc.withCalculated(values, path), path };
2052
2106
  for (let round2 = 0; round2 < 4; round2++) {
2053
2107
  const next = calc.withCalculated(values, path);
2054
2108
  if (next === values) break;
2055
2109
  values = next;
2056
- 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;
2057
2114
  }
2058
2115
  return { values, path };
2059
2116
  };
@@ -2095,14 +2152,30 @@ function build(definition, options, replaying = false) {
2095
2152
  return errors;
2096
2153
  };
2097
2154
  let unsettled = false;
2098
- const settleNow = () => {
2099
- if (!unsettled) return;
2100
- unsettled = false;
2101
- const { values, path } = settle(state.values, state.hidden);
2102
- 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
+ }
2103
2172
  };
2104
2173
  const answer = (id, value, event) => {
2105
- 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
+ }
2106
2179
  const errors = { ...state.errors };
2107
2180
  delete errors[id];
2108
2181
  if (batch) {
@@ -2168,7 +2241,7 @@ function build(definition, options, replaying = false) {
2168
2241
  emit({ ...state, errors, current: firstError, events });
2169
2242
  return false;
2170
2243
  }
2171
- const { ending } = computePath(definition, scopeOf(state.values, state.hidden));
2244
+ const { ending } = pathOf(state.values, state.hidden);
2172
2245
  emit({ ...state, errors: {}, status: "completed", ending, events });
2173
2246
  return true;
2174
2247
  },
@@ -2200,10 +2273,11 @@ function build(definition, options, replaying = false) {
2200
2273
  answer(event.field, event.value, logged);
2201
2274
  }
2202
2275
  } else if (event.type === "next") {
2203
- settleNow();
2276
+ settleNow(false);
2277
+ if (formulasStale && isCalculated(fields.get(state.current ?? ""))) settleNow(true);
2204
2278
  engine.next();
2205
2279
  } else if (event.type === "back") {
2206
- settleNow();
2280
+ settleNow(false);
2207
2281
  engine.back();
2208
2282
  } else if (event.type === "submit") {
2209
2283
  settleNow();
@@ -2218,9 +2292,16 @@ function build(definition, options, replaying = false) {
2218
2292
  return { engine, apply, finish };
2219
2293
  }
2220
2294
  function replay(definition, events, options = {}) {
2221
- const { engine, apply, finish } = build(definition, { ...options, initialValues: void 0, clock: () => void 0 }, true);
2222
- for (const event of events) apply(event);
2223
- 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
+ }
2224
2305
  return engine;
2225
2306
  }
2226
2307