@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.cjs CHANGED
@@ -64,6 +64,7 @@ function tokenize(source) {
64
64
  i++;
65
65
  while (isDigit(source[i] ?? "")) i++;
66
66
  }
67
+ if (isIdStart(source[i] ?? "")) throw new FormulaError("Write a number in digits only, like 1000000.", at);
67
68
  const v = Number(source.slice(at, i));
68
69
  if (!Number.isFinite(v)) throw new FormulaError("This number is too large.", at);
69
70
  tokens.push({ t: "num", v, at });
@@ -293,6 +294,27 @@ function numbersIn(values, out = []) {
293
294
  }
294
295
  return out;
295
296
  }
297
+ var listTotals = /* @__PURE__ */ new WeakMap();
298
+ var ownLists = /* @__PURE__ */ new WeakSet();
299
+ function ownFormulaList(list) {
300
+ Object.freeze(list);
301
+ ownLists.add(list);
302
+ return list;
303
+ }
304
+ function totalsOf(list) {
305
+ const own2 = ownLists.has(list);
306
+ let t = own2 ? listTotals.get(list) : void 0;
307
+ if (t) return t;
308
+ const nums = numbersIn(list);
309
+ t = { sum: 0, count: nums.length, min: Infinity, max: -Infinity };
310
+ for (const n of nums) {
311
+ t.sum += n;
312
+ if (n < t.min) t.min = n;
313
+ if (n > t.max) t.max = n;
314
+ }
315
+ if (own2) listTotals.set(list, t);
316
+ return t;
317
+ }
296
318
  function compare(op, a, b) {
297
319
  const blank2 = (v) => v === null || Array.isArray(v) || typeof v === "string" && v.trim() === "";
298
320
  if (blank2(a) || blank2(b)) return op === "!=";
@@ -317,7 +339,7 @@ function evaluateFormula(ast, read) {
317
339
  case "text":
318
340
  return node.value;
319
341
  case "ref":
320
- return read(node.column !== void 0 ? { key: node.key, column: node.column } : { key: node.key });
342
+ return read(node);
321
343
  case "unary": {
322
344
  const n = formulaNumber(scalar(run(node.arg)));
323
345
  return n === null ? null : -n;
@@ -325,12 +347,31 @@ function evaluateFormula(ast, read) {
325
347
  case "if":
326
348
  return truthy(run(node.test)) ? run(node.then) : run(node.else);
327
349
  case "call": {
328
- const nums = numbersIn(node.args.map(run));
329
- const total = nums.reduce((a, b) => a + b, 0);
350
+ let total = 0;
351
+ let count = 0;
352
+ let min = Infinity;
353
+ let max = -Infinity;
354
+ for (const arg of node.args) {
355
+ const v = run(arg);
356
+ if (Array.isArray(v)) {
357
+ const t = totalsOf(v);
358
+ total += t.sum;
359
+ count += t.count;
360
+ if (t.min < min) min = t.min;
361
+ if (t.max > max) max = t.max;
362
+ continue;
363
+ }
364
+ const n = formulaNumber(v);
365
+ if (n === null) continue;
366
+ total += n;
367
+ count++;
368
+ if (n < min) min = n;
369
+ if (n > max) max = n;
370
+ }
330
371
  if (node.fn === "sum") return finite(total);
331
- if (!nums.length) return null;
332
- if (node.fn === "avg") return finite(total / nums.length);
333
- return nums.reduce((a, b) => node.fn === "min" ? Math.min(a, b) : Math.max(a, b));
372
+ if (!count) return null;
373
+ if (node.fn === "avg") return finite(total / count);
374
+ return node.fn === "min" ? min : max;
334
375
  }
335
376
  case "binary": {
336
377
  const a = run(node.left);
@@ -358,6 +399,7 @@ function evaluateFormula(ast, read) {
358
399
  // src/calculated.ts
359
400
  var CALCULATED_TYPE = "calculated";
360
401
  var COMPUTED_FIELD_TYPES = [CALCULATED_TYPE];
402
+ var MAX_FORM_FORMULA_LENGTH = 2e4;
361
403
  var MAX_CALCULATED_DECIMALS = 10;
362
404
  function calculatedProps(field2) {
363
405
  const p = field2.props ?? {};
@@ -375,7 +417,9 @@ var round = (n, decimals) => {
375
417
  return Object.is(r, -0) ? 0 : r;
376
418
  };
377
419
  function calculatedValue(result, field2) {
378
- const { format, decimals } = calculatedProps(field2);
420
+ return valueAs(result, calculatedProps(field2));
421
+ }
422
+ function valueAs(result, { format, decimals }) {
379
423
  if (result === null || Array.isArray(result)) return void 0;
380
424
  if (format === "number" || format === "percent") {
381
425
  const n = formulaNumber(result);
@@ -396,24 +440,43 @@ function formatCalculated(value, field2) {
396
440
  return typeof value === "string" ? value : "";
397
441
  }
398
442
  var MAX_FORMULA_ITEMS = 1e4;
443
+ var MAX_SETTLE_ITEMS = 5e4;
444
+ var extract = (value, column) => column !== void 0 ? value.flatMap((row) => {
445
+ if (!row || typeof row !== "object" || Array.isArray(row)) return [];
446
+ const cell = row[column];
447
+ return typeof cell === "string" || typeof cell === "number" ? [cell] : [];
448
+ }) : value.flatMap((v) => typeof v === "string" || typeof v === "number" || typeof v === "boolean" ? [v] : []);
399
449
  function formulaInput(value, column) {
400
450
  if (value === void 0 || value === null) return null;
401
451
  if (Array.isArray(value) && value.length > MAX_FORMULA_ITEMS) return null;
402
452
  if (column !== void 0) {
403
453
  if (!Array.isArray(value)) return null;
404
- return value.flatMap((row) => {
405
- if (!row || typeof row !== "object" || Array.isArray(row)) return [];
406
- const cell = row[column];
407
- return typeof cell === "string" || typeof cell === "number" ? [cell] : [];
408
- });
409
- }
410
- if (Array.isArray(value)) {
411
- return value.flatMap((v) => typeof v === "string" || typeof v === "number" || typeof v === "boolean" ? [v] : []);
454
+ return extract(value, column);
412
455
  }
456
+ if (Array.isArray(value)) return extract(value, void 0);
413
457
  return typeof value === "object" ? null : value;
414
458
  }
459
+ function refNodes(node, out = []) {
460
+ if (node.kind === "ref") out.push(node);
461
+ else if (node.kind === "unary") refNodes(node.arg, out);
462
+ else if (node.kind === "binary") refNodes(node.left, out), refNodes(node.right, out);
463
+ else if (node.kind === "if") refNodes(node.test, out), refNodes(node.then, out), refNodes(node.else, out);
464
+ else if (node.kind === "call") for (const a of node.args) refNodes(a, out);
465
+ return out;
466
+ }
467
+ function formulaSize(node) {
468
+ if (node.kind === "unary") return 1 + formulaSize(node.arg);
469
+ if (node.kind === "binary") return 1 + formulaSize(node.left) + formulaSize(node.right);
470
+ if (node.kind === "if") return 1 + formulaSize(node.test) + formulaSize(node.then) + formulaSize(node.else);
471
+ if (node.kind === "call") return node.args.reduce((n, a) => n + formulaSize(a), 1);
472
+ return 1;
473
+ }
474
+ function spend(budget, amount) {
475
+ return;
476
+ }
415
477
  var UNREAD = /* @__PURE__ */ Symbol("unread");
416
- function calculator(definition) {
478
+ var OVER = /* @__PURE__ */ Symbol("over");
479
+ function calculator(definition, work) {
417
480
  const compiled = [];
418
481
  const idByKey = /* @__PURE__ */ new Map();
419
482
  for (const block of definition.blocks) {
@@ -422,35 +485,105 @@ function calculator(definition) {
422
485
  }
423
486
  for (const block of definition.blocks) {
424
487
  if (block.kind !== "field" || !isCalculated(block)) continue;
425
- const parsed = parseFormula(calculatedProps(block).formula);
426
- const reads = parsed.ok ? parsed.refs.map((r) => idByKey.get(r.key)) : [];
427
- compiled.push({ field: block, ast: parsed.ok ? parsed.ast : null, reads, last: null });
488
+ const props2 = calculatedProps(block);
489
+ const parsed = parseFormula(props2.formula);
490
+ const reads = [];
491
+ const slots = /* @__PURE__ */ new Map();
492
+ const byKey = /* @__PURE__ */ new Map();
493
+ for (const ref of parsed.ok ? refNodes(parsed.ast) : []) {
494
+ const id = idByKey.get(ref.key);
495
+ const key = `${id ?? ""}\0${ref.column ?? ""}`;
496
+ let slot = byKey.get(key);
497
+ if (slot === void 0) {
498
+ slot = reads.push({ id, column: ref.column, key }) - 1;
499
+ byKey.set(key, slot);
500
+ }
501
+ slots.set(ref, slot);
502
+ }
503
+ compiled.push({ field: block, props: props2, ast: parsed.ok ? parsed.ast : null, size: parsed.ok ? formulaSize(parsed.ast) : 0, reads, slots, last: null });
428
504
  }
505
+ const extracted = /* @__PURE__ */ new WeakMap();
506
+ const cachedList = (value, column) => {
507
+ let byColumn = extracted.get(value);
508
+ if (!byColumn) extracted.set(value, byColumn = /* @__PURE__ */ new Map());
509
+ const key = column ?? "";
510
+ let list = byColumn.get(key);
511
+ if (!list) {
512
+ byColumn.set(key, list = ownFormulaList(extract(value, column)));
513
+ }
514
+ return list;
515
+ };
516
+ const readAnswer = (value, column) => {
517
+ if (Array.isArray(value)) return cachedList(value, column);
518
+ return column !== void 0 ? null : formulaInput(value);
519
+ };
520
+ const forget = (value) => {
521
+ if (!value || typeof value !== "object") return;
522
+ if (Array.isArray(value)) extracted.delete(value);
523
+ for (const entry of compiled) if (entry.last?.inputs.includes(value)) entry.last = null;
524
+ };
429
525
  const withCalculated = (values, path) => {
430
526
  if (!compiled.length) return values;
431
527
  const visible = new Set(path);
432
528
  let next = values;
433
- const input = (id) => id === void 0 || !visible.has(id) || !Object.prototype.hasOwnProperty.call(next, id) ? UNREAD : next[id];
529
+ let copied = false;
530
+ let budget = MAX_SETTLE_ITEMS;
531
+ const within = /* @__PURE__ */ new Map();
532
+ const input = ({ id }) => {
533
+ if (id === void 0 || !visible.has(id) || !Object.prototype.hasOwnProperty.call(next, id)) return UNREAD;
534
+ const value = next[id];
535
+ if (!Array.isArray(value)) return value;
536
+ let ok = within.get(id);
537
+ if (ok === void 0) {
538
+ ok = value.length <= MAX_FORMULA_ITEMS && value.length <= budget;
539
+ if (ok) budget -= value.length;
540
+ within.set(id, ok);
541
+ }
542
+ return ok ? value : OVER;
543
+ };
434
544
  for (const entry of compiled) {
435
545
  const { field: field2, ast } = entry;
436
- const inputs = entry.reads.map(input);
546
+ const { reads } = entry;
547
+ const inputs = new Array(reads.length);
548
+ let over = false;
549
+ let same = entry.last !== null;
550
+ for (let i = 0; i < reads.length; i++) {
551
+ const v = input(reads[i]);
552
+ inputs[i] = v;
553
+ if (v === OVER) over = true;
554
+ if (same && entry.last.inputs[i] !== v) same = false;
555
+ }
437
556
  let value;
438
- if (entry.last && entry.last.inputs.length === inputs.length && entry.last.inputs.every((v, i) => v === inputs[i])) {
557
+ if (same) {
439
558
  value = entry.last.value;
440
559
  } else {
560
+ spend(work, entry.size);
561
+ const resolved = new Array(inputs.length);
441
562
  const read = (ref) => {
442
- const raw = input(idByKey.get(ref.key));
443
- return raw === UNREAD ? null : formulaInput(raw, ref.column);
563
+ const slot = entry.slots.get(ref);
564
+ if (slot === void 0) return null;
565
+ let v = resolved[slot];
566
+ if (v === void 0) {
567
+ const raw = inputs[slot];
568
+ v = resolved[slot] = raw === UNREAD ? null : readAnswer(raw, reads[slot].column);
569
+ }
570
+ return v;
444
571
  };
445
- value = ast ? calculatedValue(evaluateFormula(ast, read), field2) : void 0;
572
+ value = ast && !over ? valueAs(evaluateFormula(ast, read), entry.props) : void 0;
446
573
  entry.last = { inputs, value };
447
574
  }
448
575
  const current = Object.prototype.hasOwnProperty.call(next, field2.id) ? next[field2.id] : void 0;
449
- if (current !== value) next = { ...next, [field2.id]: value };
576
+ if (current !== value) {
577
+ if (!copied) {
578
+ next = { ...next };
579
+ copied = true;
580
+ }
581
+ next[field2.id] = value;
582
+ }
450
583
  }
451
584
  return next;
452
585
  };
453
- return { active: compiled.length > 0, withCalculated };
586
+ return { active: compiled.length > 0, ids: new Set(compiled.map((c) => c.field.id)), withCalculated, forget };
454
587
  }
455
588
  var numberOps = ["eq", "neq", "lt", "lte", "gt", "gte", "in", "answered", "empty"];
456
589
  var calculatedFieldType = {
@@ -1085,8 +1218,8 @@ function rulesAllow(def, blockId, scope) {
1085
1218
  }
1086
1219
  function walkPath(def, scope) {
1087
1220
  const list = steps(def);
1088
- const index = new Map(list.map((b, i2) => [b.id, i2]));
1089
- const position = new Map(def.blocks.map((b, i2) => [b.id, i2]));
1221
+ let index;
1222
+ let position;
1090
1223
  const jumps = (def.logic ?? []).filter(
1091
1224
  (r) => r.effect === "jump"
1092
1225
  );
@@ -1110,6 +1243,7 @@ function walkPath(def, scope) {
1110
1243
  ending = jump.to.ending;
1111
1244
  break;
1112
1245
  }
1246
+ index ??= new Map(list.map((b, at) => [b.id, at]));
1113
1247
  const target = index.get(jump.to);
1114
1248
  if (target !== void 0 && target > i) {
1115
1249
  i = nextVisible(target);
@@ -1119,6 +1253,7 @@ function walkPath(def, scope) {
1119
1253
  i = nextVisible(i + 1);
1120
1254
  }
1121
1255
  const scopeAt = (blockId, inclusive = false) => {
1256
+ position ??= new Map(def.blocks.map((b, i2) => [b.id, i2]));
1122
1257
  const at = position.get(blockId) ?? -1;
1123
1258
  const values = /* @__PURE__ */ Object.create(null);
1124
1259
  for (const id of path) {
@@ -1423,9 +1558,15 @@ function parseForm(input) {
1423
1558
  issue(`${path}.kind`, "Unknown block kind.");
1424
1559
  }
1425
1560
  });
1561
+ let formulaChars = 0;
1426
1562
  input.blocks.forEach((block, i) => {
1427
1563
  if (!isObject2(block) || block.kind !== "field" || block.type !== CALCULATED_TYPE) return;
1428
1564
  const path = `blocks[${i}].props.formula`;
1565
+ const source = isObject2(block.props) && typeof block.props.formula === "string" ? block.props.formula : "";
1566
+ formulaChars += source.length;
1567
+ if (formulaChars > MAX_FORM_FORMULA_LENGTH && formulaChars - source.length <= MAX_FORM_FORMULA_LENGTH) {
1568
+ 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.`);
1569
+ }
1429
1570
  if (block.required === true) {
1430
1571
  warnings.push({ path: `blocks[${i}].required`, message: "A calculated field is worked out, not answered, so it is never required." });
1431
1572
  }
@@ -1924,10 +2065,19 @@ function validateField(field2, value, options = {}) {
1924
2065
  }
1925
2066
 
1926
2067
  // src/engine.ts
2068
+ function readsAny(node, ids) {
2069
+ if (Array.isArray(node)) return node.some((n) => readsAny(n, ids));
2070
+ if (!node || typeof node !== "object") return false;
2071
+ for (const [k, v] of Object.entries(node)) {
2072
+ if (k === "field" && typeof v === "string" && ids.has(v)) return true;
2073
+ if (typeof v === "object" && readsAny(v, ids)) return true;
2074
+ }
2075
+ return false;
2076
+ }
1927
2077
  function createForm(definition, options = {}) {
1928
2078
  return build(definition, options).engine;
1929
2079
  }
1930
- function build(definition, options, replaying = false) {
2080
+ function build(definition, options, replaying = false, budget) {
1931
2081
  const fields = fieldMap(definition);
1932
2082
  const byKey = new Map([...fields.values()].map((f2) => [f2.key, f2]));
1933
2083
  const blocks = new Map(definition.blocks.map((b) => [b.id, b]));
@@ -1945,15 +2095,24 @@ function build(definition, options, replaying = false) {
1945
2095
  if (!declared.has(name)) initialHidden[name] = value;
1946
2096
  }
1947
2097
  Object.assign(initialHidden, coerceHidden(definition, options.hidden).values);
1948
- const calc = calculator(definition);
2098
+ const calc = calculator(definition, budget);
2099
+ const pathOf = (values, hidden) => {
2100
+ spend(budget, definition.blocks.length);
2101
+ return computePath(definition, scopeOf(values, hidden));
2102
+ };
2103
+ const logicReadsCalculated = calc.active && readsAny(definition.logic, calc.ids);
1949
2104
  const settle = (values, hidden) => {
1950
- let { path } = computePath(definition, scopeOf(values, hidden));
2105
+ let { path } = pathOf(values, hidden);
1951
2106
  if (!calc.active) return { values, path };
2107
+ if (!logicReadsCalculated) return { values: calc.withCalculated(values, path), path };
1952
2108
  for (let round2 = 0; round2 < 4; round2++) {
1953
2109
  const next = calc.withCalculated(values, path);
1954
2110
  if (next === values) break;
1955
2111
  values = next;
1956
- path = computePath(definition, scopeOf(values, hidden)).path;
2112
+ const again = pathOf(values, hidden).path;
2113
+ const same = again.length === path.length && again.every((id, i) => id === path[i]);
2114
+ path = again;
2115
+ if (same) break;
1957
2116
  }
1958
2117
  return { values, path };
1959
2118
  };
@@ -1995,14 +2154,30 @@ function build(definition, options, replaying = false) {
1995
2154
  return errors;
1996
2155
  };
1997
2156
  let unsettled = false;
1998
- const settleNow = () => {
1999
- if (!unsettled) return;
2000
- unsettled = false;
2001
- const { values, path } = settle(state.values, state.hidden);
2002
- state = { ...state, values, path };
2157
+ let formulasStale = false;
2158
+ const settleNow = (needValues = true) => {
2159
+ if (unsettled && calc.active && !logicReadsCalculated && !needValues) {
2160
+ unsettled = false;
2161
+ formulasStale = true;
2162
+ state = { ...state, path: pathOf(state.values, state.hidden).path };
2163
+ return;
2164
+ }
2165
+ if (unsettled) {
2166
+ unsettled = false;
2167
+ formulasStale = false;
2168
+ const { values, path } = settle(state.values, state.hidden);
2169
+ state = { ...state, values, path };
2170
+ } else if (formulasStale && needValues) {
2171
+ formulasStale = false;
2172
+ state = { ...state, values: calc.withCalculated(state.values, state.path) };
2173
+ }
2003
2174
  };
2004
2175
  const answer = (id, value, event) => {
2005
- if (state.status === "completed" || own(state.values, id) === value || isCalculated(fields.get(id))) return;
2176
+ if (state.status === "completed" || isCalculated(fields.get(id))) return;
2177
+ if (own(state.values, id) === value) {
2178
+ if (value === null || typeof value !== "object") return;
2179
+ calc.forget(value);
2180
+ }
2006
2181
  const errors = { ...state.errors };
2007
2182
  delete errors[id];
2008
2183
  if (batch) {
@@ -2068,7 +2243,7 @@ function build(definition, options, replaying = false) {
2068
2243
  emit({ ...state, errors, current: firstError, events });
2069
2244
  return false;
2070
2245
  }
2071
- const { ending } = computePath(definition, scopeOf(state.values, state.hidden));
2246
+ const { ending } = pathOf(state.values, state.hidden);
2072
2247
  emit({ ...state, errors: {}, status: "completed", ending, events });
2073
2248
  return true;
2074
2249
  },
@@ -2100,10 +2275,11 @@ function build(definition, options, replaying = false) {
2100
2275
  answer(event.field, event.value, logged);
2101
2276
  }
2102
2277
  } else if (event.type === "next") {
2103
- settleNow();
2278
+ settleNow(false);
2279
+ if (formulasStale && isCalculated(fields.get(state.current ?? ""))) settleNow(true);
2104
2280
  engine.next();
2105
2281
  } else if (event.type === "back") {
2106
- settleNow();
2282
+ settleNow(false);
2107
2283
  engine.back();
2108
2284
  } else if (event.type === "submit") {
2109
2285
  settleNow();
@@ -2118,9 +2294,16 @@ function build(definition, options, replaying = false) {
2118
2294
  return { engine, apply, finish };
2119
2295
  }
2120
2296
  function replay(definition, events, options = {}) {
2121
- const { engine, apply, finish } = build(definition, { ...options, initialValues: void 0, clock: () => void 0 }, true);
2122
- for (const event of events) apply(event);
2123
- finish();
2297
+ return replayWithin(definition, events, options);
2298
+ }
2299
+ function replayWithin(definition, events, options, maxWork) {
2300
+ const budget = void 0 ;
2301
+ const { engine, apply, finish } = build(definition, { ...options, initialValues: void 0, clock: () => void 0 }, true, budget);
2302
+ try {
2303
+ for (const event of events) apply(event);
2304
+ finish();
2305
+ } finally {
2306
+ }
2124
2307
  return engine;
2125
2308
  }
2126
2309
 
@@ -2267,9 +2450,11 @@ exports.HIGHLIGHT_NOTE_MESSAGE = HIGHLIGHT_NOTE_MESSAGE;
2267
2450
  exports.MAX_CALCULATED_DECIMALS = MAX_CALCULATED_DECIMALS;
2268
2451
  exports.MAX_FORMULA_ITEMS = MAX_FORMULA_ITEMS;
2269
2452
  exports.MAX_FORMULA_LENGTH = MAX_FORMULA_LENGTH;
2453
+ exports.MAX_FORM_FORMULA_LENGTH = MAX_FORM_FORMULA_LENGTH;
2270
2454
  exports.MAX_HIDDEN_STRING_LENGTH = MAX_HIDDEN_STRING_LENGTH;
2271
2455
  exports.MAX_HIGHLIGHT_NOTE_LENGTH = MAX_HIGHLIGHT_NOTE_LENGTH;
2272
2456
  exports.MAX_HIGHLIGHT_QUOTE_LENGTH = MAX_HIGHLIGHT_QUOTE_LENGTH;
2457
+ exports.MAX_SETTLE_ITEMS = MAX_SETTLE_ITEMS;
2273
2458
  exports.MAX_TABLE_CELL_LENGTH = MAX_TABLE_CELL_LENGTH;
2274
2459
  exports.MAX_TABLE_COLUMNS = MAX_TABLE_COLUMNS;
2275
2460
  exports.MAX_TIMESTAMP_SECONDS = MAX_TIMESTAMP_SECONDS;