@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.cjs CHANGED
@@ -417,7 +417,9 @@ var round = (n, decimals) => {
417
417
  return Object.is(r, -0) ? 0 : r;
418
418
  };
419
419
  function calculatedValue(result, field2) {
420
- const { format, decimals } = calculatedProps(field2);
420
+ return valueAs(result, calculatedProps(field2));
421
+ }
422
+ function valueAs(result, { format, decimals }) {
421
423
  if (result === null || Array.isArray(result)) return void 0;
422
424
  if (format === "number" || format === "percent") {
423
425
  const n = formulaNumber(result);
@@ -444,19 +446,6 @@ var extract = (value, column) => column !== void 0 ? value.flatMap((row) => {
444
446
  const cell = row[column];
445
447
  return typeof cell === "string" || typeof cell === "number" ? [cell] : [];
446
448
  }) : value.flatMap((v) => typeof v === "string" || typeof v === "number" || typeof v === "boolean" ? [v] : []);
447
- var extracted = /* @__PURE__ */ new WeakMap();
448
- function cachedList(value, column) {
449
- let byColumn = extracted.get(value);
450
- if (!byColumn) extracted.set(value, byColumn = /* @__PURE__ */ new Map());
451
- const key = column ?? "";
452
- let list = byColumn.get(key);
453
- if (!list) byColumn.set(key, list = ownFormulaList(extract(value, column)));
454
- return list;
455
- }
456
- function readAnswer(value, column) {
457
- if (Array.isArray(value)) return cachedList(value, column);
458
- return column !== void 0 ? null : formulaInput(value);
459
- }
460
449
  function formulaInput(value, column) {
461
450
  if (value === void 0 || value === null) return null;
462
451
  if (Array.isArray(value) && value.length > MAX_FORMULA_ITEMS) return null;
@@ -475,9 +464,19 @@ function refNodes(node, out = []) {
475
464
  else if (node.kind === "call") for (const a of node.args) refNodes(a, out);
476
465
  return out;
477
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
+ }
478
477
  var UNREAD = /* @__PURE__ */ Symbol("unread");
479
478
  var OVER = /* @__PURE__ */ Symbol("over");
480
- function calculator(definition) {
479
+ function calculator(definition, work) {
481
480
  const compiled = [];
482
481
  const idByKey = /* @__PURE__ */ new Map();
483
482
  for (const block of definition.blocks) {
@@ -486,7 +485,8 @@ function calculator(definition) {
486
485
  }
487
486
  for (const block of definition.blocks) {
488
487
  if (block.kind !== "field" || !isCalculated(block)) continue;
489
- const parsed = parseFormula(calculatedProps(block).formula);
488
+ const props2 = calculatedProps(block);
489
+ const parsed = parseFormula(props2.formula);
490
490
  const reads = [];
491
491
  const slots = /* @__PURE__ */ new Map();
492
492
  const byKey = /* @__PURE__ */ new Map();
@@ -500,33 +500,64 @@ function calculator(definition) {
500
500
  }
501
501
  slots.set(ref, slot);
502
502
  }
503
- compiled.push({ field: block, ast: parsed.ok ? parsed.ast : null, reads, slots, last: null });
503
+ compiled.push({ field: block, props: props2, ast: parsed.ok ? parsed.ast : null, size: parsed.ok ? formulaSize(parsed.ast) : 0, reads, slots, last: null });
504
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
+ };
505
525
  const withCalculated = (values, path) => {
506
526
  if (!compiled.length) return values;
507
527
  const visible = new Set(path);
508
528
  let next = values;
529
+ let copied = false;
509
530
  let budget = MAX_SETTLE_ITEMS;
510
531
  const within = /* @__PURE__ */ new Map();
511
- const input = ({ id, key }) => {
532
+ const input = ({ id }) => {
512
533
  if (id === void 0 || !visible.has(id) || !Object.prototype.hasOwnProperty.call(next, id)) return UNREAD;
513
534
  const value = next[id];
514
535
  if (!Array.isArray(value)) return value;
515
- let ok = within.get(key);
536
+ let ok = within.get(id);
516
537
  if (ok === void 0) {
517
538
  ok = value.length <= MAX_FORMULA_ITEMS && value.length <= budget;
518
539
  if (ok) budget -= value.length;
519
- within.set(key, ok);
540
+ within.set(id, ok);
520
541
  }
521
542
  return ok ? value : OVER;
522
543
  };
523
544
  for (const entry of compiled) {
524
545
  const { field: field2, ast } = entry;
525
- 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
+ }
526
556
  let value;
527
- if (entry.last && entry.last.inputs.length === inputs.length && entry.last.inputs.every((v, i) => v === inputs[i])) {
557
+ if (same) {
528
558
  value = entry.last.value;
529
559
  } else {
560
+ spend(work, entry.size);
530
561
  const resolved = new Array(inputs.length);
531
562
  const read = (ref) => {
532
563
  const slot = entry.slots.get(ref);
@@ -534,19 +565,25 @@ function calculator(definition) {
534
565
  let v = resolved[slot];
535
566
  if (v === void 0) {
536
567
  const raw = inputs[slot];
537
- v = resolved[slot] = raw === UNREAD ? null : readAnswer(raw, entry.reads[slot].column);
568
+ v = resolved[slot] = raw === UNREAD ? null : readAnswer(raw, reads[slot].column);
538
569
  }
539
570
  return v;
540
571
  };
541
- value = ast && !inputs.includes(OVER) ? calculatedValue(evaluateFormula(ast, read), field2) : void 0;
572
+ value = ast && !over ? valueAs(evaluateFormula(ast, read), entry.props) : void 0;
542
573
  entry.last = { inputs, value };
543
574
  }
544
575
  const current = Object.prototype.hasOwnProperty.call(next, field2.id) ? next[field2.id] : void 0;
545
- 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
+ }
546
583
  }
547
584
  return next;
548
585
  };
549
- return { active: compiled.length > 0, withCalculated };
586
+ return { active: compiled.length > 0, ids: new Set(compiled.map((c) => c.field.id)), withCalculated, forget };
550
587
  }
551
588
  var numberOps = ["eq", "neq", "lt", "lte", "gt", "gte", "in", "answered", "empty"];
552
589
  var calculatedFieldType = {
@@ -1181,8 +1218,8 @@ function rulesAllow(def, blockId, scope) {
1181
1218
  }
1182
1219
  function walkPath(def, scope) {
1183
1220
  const list = steps(def);
1184
- const index = new Map(list.map((b, i2) => [b.id, i2]));
1185
- const position = new Map(def.blocks.map((b, i2) => [b.id, i2]));
1221
+ let index;
1222
+ let position;
1186
1223
  const jumps = (def.logic ?? []).filter(
1187
1224
  (r) => r.effect === "jump"
1188
1225
  );
@@ -1206,6 +1243,7 @@ function walkPath(def, scope) {
1206
1243
  ending = jump.to.ending;
1207
1244
  break;
1208
1245
  }
1246
+ index ??= new Map(list.map((b, at) => [b.id, at]));
1209
1247
  const target = index.get(jump.to);
1210
1248
  if (target !== void 0 && target > i) {
1211
1249
  i = nextVisible(target);
@@ -1215,6 +1253,7 @@ function walkPath(def, scope) {
1215
1253
  i = nextVisible(i + 1);
1216
1254
  }
1217
1255
  const scopeAt = (blockId, inclusive = false) => {
1256
+ position ??= new Map(def.blocks.map((b, i2) => [b.id, i2]));
1218
1257
  const at = position.get(blockId) ?? -1;
1219
1258
  const values = /* @__PURE__ */ Object.create(null);
1220
1259
  for (const id of path) {
@@ -2026,10 +2065,19 @@ function validateField(field2, value, options = {}) {
2026
2065
  }
2027
2066
 
2028
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
+ }
2029
2077
  function createForm(definition, options = {}) {
2030
2078
  return build(definition, options).engine;
2031
2079
  }
2032
- function build(definition, options, replaying = false) {
2080
+ function build(definition, options, replaying = false, budget) {
2033
2081
  const fields = fieldMap(definition);
2034
2082
  const byKey = new Map([...fields.values()].map((f2) => [f2.key, f2]));
2035
2083
  const blocks = new Map(definition.blocks.map((b) => [b.id, b]));
@@ -2047,15 +2095,24 @@ function build(definition, options, replaying = false) {
2047
2095
  if (!declared.has(name)) initialHidden[name] = value;
2048
2096
  }
2049
2097
  Object.assign(initialHidden, coerceHidden(definition, options.hidden).values);
2050
- 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);
2051
2104
  const settle = (values, hidden) => {
2052
- let { path } = computePath(definition, scopeOf(values, hidden));
2105
+ let { path } = pathOf(values, hidden);
2053
2106
  if (!calc.active) return { values, path };
2107
+ if (!logicReadsCalculated) return { values: calc.withCalculated(values, path), path };
2054
2108
  for (let round2 = 0; round2 < 4; round2++) {
2055
2109
  const next = calc.withCalculated(values, path);
2056
2110
  if (next === values) break;
2057
2111
  values = next;
2058
- 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;
2059
2116
  }
2060
2117
  return { values, path };
2061
2118
  };
@@ -2097,14 +2154,30 @@ function build(definition, options, replaying = false) {
2097
2154
  return errors;
2098
2155
  };
2099
2156
  let unsettled = false;
2100
- const settleNow = () => {
2101
- if (!unsettled) return;
2102
- unsettled = false;
2103
- const { values, path } = settle(state.values, state.hidden);
2104
- 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
+ }
2105
2174
  };
2106
2175
  const answer = (id, value, event) => {
2107
- 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
+ }
2108
2181
  const errors = { ...state.errors };
2109
2182
  delete errors[id];
2110
2183
  if (batch) {
@@ -2170,7 +2243,7 @@ function build(definition, options, replaying = false) {
2170
2243
  emit({ ...state, errors, current: firstError, events });
2171
2244
  return false;
2172
2245
  }
2173
- const { ending } = computePath(definition, scopeOf(state.values, state.hidden));
2246
+ const { ending } = pathOf(state.values, state.hidden);
2174
2247
  emit({ ...state, errors: {}, status: "completed", ending, events });
2175
2248
  return true;
2176
2249
  },
@@ -2202,10 +2275,11 @@ function build(definition, options, replaying = false) {
2202
2275
  answer(event.field, event.value, logged);
2203
2276
  }
2204
2277
  } else if (event.type === "next") {
2205
- settleNow();
2278
+ settleNow(false);
2279
+ if (formulasStale && isCalculated(fields.get(state.current ?? ""))) settleNow(true);
2206
2280
  engine.next();
2207
2281
  } else if (event.type === "back") {
2208
- settleNow();
2282
+ settleNow(false);
2209
2283
  engine.back();
2210
2284
  } else if (event.type === "submit") {
2211
2285
  settleNow();
@@ -2220,9 +2294,16 @@ function build(definition, options, replaying = false) {
2220
2294
  return { engine, apply, finish };
2221
2295
  }
2222
2296
  function replay(definition, events, options = {}) {
2223
- const { engine, apply, finish } = build(definition, { ...options, initialValues: void 0, clock: () => void 0 }, true);
2224
- for (const event of events) apply(event);
2225
- 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
+ }
2226
2307
  return engine;
2227
2308
  }
2228
2309