@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/server.d.cts CHANGED
@@ -15,8 +15,26 @@ interface SubmissionOptions {
15
15
  /** `false` checks a draft: formats only, nothing required. Default true. */
16
16
  complete?: boolean | undefined;
17
17
  validators?: Record<string, FieldValidator> | undefined;
18
- /** Most events accepted, counted after `compactEvents`. Default 10 000. */
18
+ /**
19
+ * Most events accepted, counted after `compactEvents`. Default `MAX_EVENTS`
20
+ * (10,000). At most `RAW_EVENTS_PER_EVENT` (10) times as many may be sent.
21
+ */
19
22
  maxEvents?: number | undefined;
23
+ /**
24
+ * Most JSON values in the answers, answer events and hidden inputs
25
+ * together (each number, text, true / false, list item, row and cell
26
+ * counts one; a run of answers to one field, as typing in a table sends,
27
+ * counts its last only). Default `MAX_SUBMISSION_VALUES` (200,000).
28
+ */
29
+ maxValues?: number | undefined;
30
+ /**
31
+ * Most work replaying the events may take: one unit per block each time
32
+ * the path is walked, one per calculated field each time the answers
33
+ * settle, and a formula's size (its numbers, answers and operators) each
34
+ * time it is worked out. Default `MAX_REPLAY_WORK` (1,000,000). Only the
35
+ * replay counts: the submit that follows it does not.
36
+ */
37
+ maxReplayWork?: number | undefined;
20
38
  }
21
39
  interface CheckedSubmission {
22
40
  status: "draft" | "completed";
@@ -43,6 +61,15 @@ type SubmissionResult = {
43
61
  };
44
62
  /** Default for `SubmissionOptions.maxEvents`. */
45
63
  declare const MAX_EVENTS = 10000;
64
+ /** Default for `SubmissionOptions.maxValues`. */
65
+ declare const MAX_SUBMISSION_VALUES = 200000;
66
+ /** Default for `SubmissionOptions.maxReplayWork`. */
67
+ declare const MAX_REPLAY_WORK = 1000000;
68
+ /**
69
+ * Most events sent, before compaction, as a multiple of `maxEvents`: a
70
+ * keystroke log compacts to far fewer, but every event sent is looked at.
71
+ */
72
+ declare const RAW_EVENTS_PER_EVENT = 10;
46
73
  /**
47
74
  * Check a submission against the published definition. Never throws on a
48
75
  * malformed submission: it returns `{ ok: false, errors: { "": … } }`.
@@ -54,4 +81,4 @@ declare function validateSubmission(definition: FormDefinition, submission: Subm
54
81
  */
55
82
  declare function publicDefinition(definition: FormDefinition): FormDefinition;
56
83
 
57
- export { type CheckedSubmission, MAX_EVENTS, type SubmissionInput, type SubmissionOptions, type SubmissionResult, publicDefinition, validateSubmission };
84
+ export { type CheckedSubmission, MAX_EVENTS, MAX_REPLAY_WORK, MAX_SUBMISSION_VALUES, RAW_EVENTS_PER_EVENT, type SubmissionInput, type SubmissionOptions, type SubmissionResult, publicDefinition, validateSubmission };
package/dist/server.d.ts CHANGED
@@ -15,8 +15,26 @@ interface SubmissionOptions {
15
15
  /** `false` checks a draft: formats only, nothing required. Default true. */
16
16
  complete?: boolean | undefined;
17
17
  validators?: Record<string, FieldValidator> | undefined;
18
- /** Most events accepted, counted after `compactEvents`. Default 10 000. */
18
+ /**
19
+ * Most events accepted, counted after `compactEvents`. Default `MAX_EVENTS`
20
+ * (10,000). At most `RAW_EVENTS_PER_EVENT` (10) times as many may be sent.
21
+ */
19
22
  maxEvents?: number | undefined;
23
+ /**
24
+ * Most JSON values in the answers, answer events and hidden inputs
25
+ * together (each number, text, true / false, list item, row and cell
26
+ * counts one; a run of answers to one field, as typing in a table sends,
27
+ * counts its last only). Default `MAX_SUBMISSION_VALUES` (200,000).
28
+ */
29
+ maxValues?: number | undefined;
30
+ /**
31
+ * Most work replaying the events may take: one unit per block each time
32
+ * the path is walked, one per calculated field each time the answers
33
+ * settle, and a formula's size (its numbers, answers and operators) each
34
+ * time it is worked out. Default `MAX_REPLAY_WORK` (1,000,000). Only the
35
+ * replay counts: the submit that follows it does not.
36
+ */
37
+ maxReplayWork?: number | undefined;
20
38
  }
21
39
  interface CheckedSubmission {
22
40
  status: "draft" | "completed";
@@ -43,6 +61,15 @@ type SubmissionResult = {
43
61
  };
44
62
  /** Default for `SubmissionOptions.maxEvents`. */
45
63
  declare const MAX_EVENTS = 10000;
64
+ /** Default for `SubmissionOptions.maxValues`. */
65
+ declare const MAX_SUBMISSION_VALUES = 200000;
66
+ /** Default for `SubmissionOptions.maxReplayWork`. */
67
+ declare const MAX_REPLAY_WORK = 1000000;
68
+ /**
69
+ * Most events sent, before compaction, as a multiple of `maxEvents`: a
70
+ * keystroke log compacts to far fewer, but every event sent is looked at.
71
+ */
72
+ declare const RAW_EVENTS_PER_EVENT = 10;
46
73
  /**
47
74
  * Check a submission against the published definition. Never throws on a
48
75
  * malformed submission: it returns `{ ok: false, errors: { "": … } }`.
@@ -54,4 +81,4 @@ declare function validateSubmission(definition: FormDefinition, submission: Subm
54
81
  */
55
82
  declare function publicDefinition(definition: FormDefinition): FormDefinition;
56
83
 
57
- export { type CheckedSubmission, MAX_EVENTS, type SubmissionInput, type SubmissionOptions, type SubmissionResult, publicDefinition, validateSubmission };
84
+ export { type CheckedSubmission, MAX_EVENTS, MAX_REPLAY_WORK, MAX_SUBMISSION_VALUES, RAW_EVENTS_PER_EVENT, type SubmissionInput, type SubmissionOptions, type SubmissionResult, publicDefinition, validateSubmission };
package/dist/server.js CHANGED
@@ -32,6 +32,7 @@ function tokenize(source) {
32
32
  i++;
33
33
  while (isDigit(source[i] ?? "")) i++;
34
34
  }
35
+ if (isIdStart(source[i] ?? "")) throw new FormulaError("Write a number in digits only, like 1000000.", at);
35
36
  const v = Number(source.slice(at, i));
36
37
  if (!Number.isFinite(v)) throw new FormulaError("This number is too large.", at);
37
38
  tokens.push({ t: "num", v, at });
@@ -230,6 +231,27 @@ function numbersIn(values, out = []) {
230
231
  }
231
232
  return out;
232
233
  }
234
+ var listTotals = /* @__PURE__ */ new WeakMap();
235
+ var ownLists = /* @__PURE__ */ new WeakSet();
236
+ function ownFormulaList(list) {
237
+ Object.freeze(list);
238
+ ownLists.add(list);
239
+ return list;
240
+ }
241
+ function totalsOf(list) {
242
+ const own2 = ownLists.has(list);
243
+ let t = own2 ? listTotals.get(list) : void 0;
244
+ if (t) return t;
245
+ const nums = numbersIn(list);
246
+ t = { sum: 0, count: nums.length, min: Infinity, max: -Infinity };
247
+ for (const n of nums) {
248
+ t.sum += n;
249
+ if (n < t.min) t.min = n;
250
+ if (n > t.max) t.max = n;
251
+ }
252
+ if (own2) listTotals.set(list, t);
253
+ return t;
254
+ }
233
255
  function compare(op, a, b) {
234
256
  const blank2 = (v) => v === null || Array.isArray(v) || typeof v === "string" && v.trim() === "";
235
257
  if (blank2(a) || blank2(b)) return op === "!=";
@@ -254,7 +276,7 @@ function evaluateFormula(ast, read) {
254
276
  case "text":
255
277
  return node.value;
256
278
  case "ref":
257
- return read(node.column !== void 0 ? { key: node.key, column: node.column } : { key: node.key });
279
+ return read(node);
258
280
  case "unary": {
259
281
  const n = formulaNumber(scalar(run(node.arg)));
260
282
  return n === null ? null : -n;
@@ -262,12 +284,31 @@ function evaluateFormula(ast, read) {
262
284
  case "if":
263
285
  return truthy(run(node.test)) ? run(node.then) : run(node.else);
264
286
  case "call": {
265
- const nums = numbersIn(node.args.map(run));
266
- const total = nums.reduce((a, b) => a + b, 0);
287
+ let total = 0;
288
+ let count = 0;
289
+ let min = Infinity;
290
+ let max = -Infinity;
291
+ for (const arg of node.args) {
292
+ const v = run(arg);
293
+ if (Array.isArray(v)) {
294
+ const t = totalsOf(v);
295
+ total += t.sum;
296
+ count += t.count;
297
+ if (t.min < min) min = t.min;
298
+ if (t.max > max) max = t.max;
299
+ continue;
300
+ }
301
+ const n = formulaNumber(v);
302
+ if (n === null) continue;
303
+ total += n;
304
+ count++;
305
+ if (n < min) min = n;
306
+ if (n > max) max = n;
307
+ }
267
308
  if (node.fn === "sum") return finite(total);
268
- if (!nums.length) return null;
269
- if (node.fn === "avg") return finite(total / nums.length);
270
- return nums.reduce((a, b) => node.fn === "min" ? Math.min(a, b) : Math.max(a, b));
309
+ if (!count) return null;
310
+ if (node.fn === "avg") return finite(total / count);
311
+ return node.fn === "min" ? min : max;
271
312
  }
272
313
  case "binary": {
273
314
  const a = run(node.left);
@@ -294,6 +335,7 @@ function evaluateFormula(ast, read) {
294
335
 
295
336
  // src/calculated.ts
296
337
  var CALCULATED_TYPE = "calculated";
338
+ var MAX_FORM_FORMULA_LENGTH = 2e4;
297
339
  var MAX_CALCULATED_DECIMALS = 10;
298
340
  function calculatedProps(field) {
299
341
  const p = field.props ?? {};
@@ -310,8 +352,7 @@ var round = (n, decimals) => {
310
352
  const r = Number(n.toFixed(decimals));
311
353
  return Object.is(r, -0) ? 0 : r;
312
354
  };
313
- function calculatedValue(result, field) {
314
- const { format, decimals } = calculatedProps(field);
355
+ function valueAs(result, { format, decimals }) {
315
356
  if (result === null || Array.isArray(result)) return void 0;
316
357
  if (format === "number" || format === "percent") {
317
358
  const n = formulaNumber(result);
@@ -332,24 +373,48 @@ function formatCalculated(value, field) {
332
373
  return typeof value === "string" ? value : "";
333
374
  }
334
375
  var MAX_FORMULA_ITEMS = 1e4;
376
+ var MAX_SETTLE_ITEMS = 5e4;
377
+ var extract = (value, column) => column !== void 0 ? value.flatMap((row) => {
378
+ if (!row || typeof row !== "object" || Array.isArray(row)) return [];
379
+ const cell = row[column];
380
+ return typeof cell === "string" || typeof cell === "number" ? [cell] : [];
381
+ }) : value.flatMap((v) => typeof v === "string" || typeof v === "number" || typeof v === "boolean" ? [v] : []);
335
382
  function formulaInput(value, column) {
336
383
  if (value === void 0 || value === null) return null;
337
384
  if (Array.isArray(value) && value.length > MAX_FORMULA_ITEMS) return null;
338
- if (column !== void 0) {
339
- if (!Array.isArray(value)) return null;
340
- return value.flatMap((row) => {
341
- if (!row || typeof row !== "object" || Array.isArray(row)) return [];
342
- const cell = row[column];
343
- return typeof cell === "string" || typeof cell === "number" ? [cell] : [];
344
- });
345
- }
346
- if (Array.isArray(value)) {
347
- return value.flatMap((v) => typeof v === "string" || typeof v === "number" || typeof v === "boolean" ? [v] : []);
348
- }
385
+ if (Array.isArray(value)) return extract(value, void 0);
349
386
  return typeof value === "object" ? null : value;
350
387
  }
388
+ function refNodes(node, out = []) {
389
+ if (node.kind === "ref") out.push(node);
390
+ else if (node.kind === "unary") refNodes(node.arg, out);
391
+ else if (node.kind === "binary") refNodes(node.left, out), refNodes(node.right, out);
392
+ else if (node.kind === "if") refNodes(node.test, out), refNodes(node.then, out), refNodes(node.else, out);
393
+ else if (node.kind === "call") for (const a of node.args) refNodes(a, out);
394
+ return out;
395
+ }
396
+ function formulaSize(node) {
397
+ if (node.kind === "unary") return 1 + formulaSize(node.arg);
398
+ if (node.kind === "binary") return 1 + formulaSize(node.left) + formulaSize(node.right);
399
+ if (node.kind === "if") return 1 + formulaSize(node.test) + formulaSize(node.then) + formulaSize(node.else);
400
+ if (node.kind === "call") return node.args.reduce((n, a) => n + formulaSize(a), 1);
401
+ return 1;
402
+ }
403
+ var ReplayWorkExceeded = class extends Error {
404
+ constructor(limit) {
405
+ super(`Replaying the response would take more than ${limit} steps of work.`);
406
+ this.limit = limit;
407
+ }
408
+ limit;
409
+ };
410
+ function spend(budget, amount) {
411
+ if (!budget) return;
412
+ budget.used += amount;
413
+ if (budget.used > budget.max) throw new ReplayWorkExceeded(budget.max);
414
+ }
351
415
  var UNREAD = /* @__PURE__ */ Symbol("unread");
352
- function calculator(definition) {
416
+ var OVER = /* @__PURE__ */ Symbol("over");
417
+ function calculator(definition, work) {
353
418
  const compiled = [];
354
419
  const idByKey = /* @__PURE__ */ new Map();
355
420
  for (const block of definition.blocks) {
@@ -358,35 +423,106 @@ function calculator(definition) {
358
423
  }
359
424
  for (const block of definition.blocks) {
360
425
  if (block.kind !== "field" || !isCalculated(block)) continue;
361
- const parsed = parseFormula(calculatedProps(block).formula);
362
- const reads = parsed.ok ? parsed.refs.map((r) => idByKey.get(r.key)) : [];
363
- compiled.push({ field: block, ast: parsed.ok ? parsed.ast : null, reads, last: null });
426
+ const props = calculatedProps(block);
427
+ const parsed = parseFormula(props.formula);
428
+ const reads = [];
429
+ const slots = /* @__PURE__ */ new Map();
430
+ const byKey = /* @__PURE__ */ new Map();
431
+ for (const ref of parsed.ok ? refNodes(parsed.ast) : []) {
432
+ const id = idByKey.get(ref.key);
433
+ const key = `${id ?? ""}\0${ref.column ?? ""}`;
434
+ let slot = byKey.get(key);
435
+ if (slot === void 0) {
436
+ slot = reads.push({ id, column: ref.column, key }) - 1;
437
+ byKey.set(key, slot);
438
+ }
439
+ slots.set(ref, slot);
440
+ }
441
+ compiled.push({ field: block, props, ast: parsed.ok ? parsed.ast : null, size: parsed.ok ? formulaSize(parsed.ast) : 0, reads, slots, last: null });
364
442
  }
443
+ const extracted = /* @__PURE__ */ new WeakMap();
444
+ const cachedList = (value, column) => {
445
+ let byColumn = extracted.get(value);
446
+ if (!byColumn) extracted.set(value, byColumn = /* @__PURE__ */ new Map());
447
+ const key = column ?? "";
448
+ let list = byColumn.get(key);
449
+ if (!list) {
450
+ byColumn.set(key, list = ownFormulaList(extract(value, column)));
451
+ }
452
+ return list;
453
+ };
454
+ const readAnswer = (value, column) => {
455
+ if (Array.isArray(value)) return cachedList(value, column);
456
+ return column !== void 0 ? null : formulaInput(value);
457
+ };
458
+ const forget = (value) => {
459
+ if (!value || typeof value !== "object") return;
460
+ if (Array.isArray(value)) extracted.delete(value);
461
+ for (const entry of compiled) if (entry.last?.inputs.includes(value)) entry.last = null;
462
+ };
365
463
  const withCalculated = (values, path) => {
366
464
  if (!compiled.length) return values;
367
465
  const visible = new Set(path);
368
466
  let next = values;
369
- const input = (id) => id === void 0 || !visible.has(id) || !Object.prototype.hasOwnProperty.call(next, id) ? UNREAD : next[id];
467
+ let copied = false;
468
+ let budget = MAX_SETTLE_ITEMS;
469
+ const within = /* @__PURE__ */ new Map();
470
+ const input = ({ id }) => {
471
+ if (id === void 0 || !visible.has(id) || !Object.prototype.hasOwnProperty.call(next, id)) return UNREAD;
472
+ const value = next[id];
473
+ if (!Array.isArray(value)) return value;
474
+ let ok = within.get(id);
475
+ if (ok === void 0) {
476
+ ok = value.length <= MAX_FORMULA_ITEMS && value.length <= budget;
477
+ if (ok) budget -= value.length;
478
+ within.set(id, ok);
479
+ }
480
+ return ok ? value : OVER;
481
+ };
370
482
  for (const entry of compiled) {
371
483
  const { field, ast } = entry;
372
- const inputs = entry.reads.map(input);
484
+ const { reads } = entry;
485
+ spend(work, 1);
486
+ const inputs = new Array(reads.length);
487
+ let over = false;
488
+ let same = entry.last !== null;
489
+ for (let i = 0; i < reads.length; i++) {
490
+ const v = input(reads[i]);
491
+ inputs[i] = v;
492
+ if (v === OVER) over = true;
493
+ if (same && entry.last.inputs[i] !== v) same = false;
494
+ }
373
495
  let value;
374
- if (entry.last && entry.last.inputs.length === inputs.length && entry.last.inputs.every((v, i) => v === inputs[i])) {
496
+ if (same) {
375
497
  value = entry.last.value;
376
498
  } else {
499
+ spend(work, entry.size);
500
+ const resolved = new Array(inputs.length);
377
501
  const read = (ref) => {
378
- const raw = input(idByKey.get(ref.key));
379
- return raw === UNREAD ? null : formulaInput(raw, ref.column);
502
+ const slot = entry.slots.get(ref);
503
+ if (slot === void 0) return null;
504
+ let v = resolved[slot];
505
+ if (v === void 0) {
506
+ const raw = inputs[slot];
507
+ v = resolved[slot] = raw === UNREAD ? null : readAnswer(raw, reads[slot].column);
508
+ }
509
+ return v;
380
510
  };
381
- value = ast ? calculatedValue(evaluateFormula(ast, read), field) : void 0;
511
+ value = ast && !over ? valueAs(evaluateFormula(ast, read), entry.props) : void 0;
382
512
  entry.last = { inputs, value };
383
513
  }
384
514
  const current = Object.prototype.hasOwnProperty.call(next, field.id) ? next[field.id] : void 0;
385
- if (current !== value) next = { ...next, [field.id]: value };
515
+ if (current !== value) {
516
+ if (!copied) {
517
+ next = { ...next };
518
+ copied = true;
519
+ }
520
+ next[field.id] = value;
521
+ }
386
522
  }
387
523
  return next;
388
524
  };
389
- return { active: compiled.length > 0, withCalculated };
525
+ return { active: compiled.length > 0, ids: new Set(compiled.map((c) => c.field.id)), withCalculated, forget };
390
526
  }
391
527
  var numberOps = ["eq", "neq", "lt", "lte", "gt", "gte", "in", "answered", "empty"];
392
528
  var calculatedFieldType = {
@@ -1043,8 +1179,8 @@ function rulesAllow(def, blockId, scope) {
1043
1179
  }
1044
1180
  function walkPath(def, scope) {
1045
1181
  const list = steps(def);
1046
- const index = new Map(list.map((b, i2) => [b.id, i2]));
1047
- const position = new Map(def.blocks.map((b, i2) => [b.id, i2]));
1182
+ let index;
1183
+ let position;
1048
1184
  const jumps = (def.logic ?? []).filter(
1049
1185
  (r) => r.effect === "jump"
1050
1186
  );
@@ -1068,6 +1204,7 @@ function walkPath(def, scope) {
1068
1204
  ending = jump.to.ending;
1069
1205
  break;
1070
1206
  }
1207
+ index ??= new Map(list.map((b, at) => [b.id, at]));
1071
1208
  const target = index.get(jump.to);
1072
1209
  if (target !== void 0 && target > i) {
1073
1210
  i = nextVisible(target);
@@ -1077,6 +1214,7 @@ function walkPath(def, scope) {
1077
1214
  i = nextVisible(i + 1);
1078
1215
  }
1079
1216
  const scopeAt = (blockId, inclusive = false) => {
1217
+ position ??= new Map(def.blocks.map((b, i2) => [b.id, i2]));
1080
1218
  const at = position.get(blockId) ?? -1;
1081
1219
  const values = /* @__PURE__ */ Object.create(null);
1082
1220
  for (const id of path) {
@@ -1139,10 +1277,19 @@ function validateField(field, value, options = {}) {
1139
1277
  }
1140
1278
 
1141
1279
  // src/engine.ts
1280
+ function readsAny(node, ids) {
1281
+ if (Array.isArray(node)) return node.some((n) => readsAny(n, ids));
1282
+ if (!node || typeof node !== "object") return false;
1283
+ for (const [k, v] of Object.entries(node)) {
1284
+ if (k === "field" && typeof v === "string" && ids.has(v)) return true;
1285
+ if (typeof v === "object" && readsAny(v, ids)) return true;
1286
+ }
1287
+ return false;
1288
+ }
1142
1289
  function createForm(definition, options = {}) {
1143
1290
  return build(definition, options).engine;
1144
1291
  }
1145
- function build(definition, options, replaying = false) {
1292
+ function build(definition, options, replaying = false, budget) {
1146
1293
  const fields = fieldMap(definition);
1147
1294
  const byKey = new Map([...fields.values()].map((f) => [f.key, f]));
1148
1295
  const blocks = new Map(definition.blocks.map((b) => [b.id, b]));
@@ -1160,15 +1307,24 @@ function build(definition, options, replaying = false) {
1160
1307
  if (!declared.has(name)) initialHidden[name] = value;
1161
1308
  }
1162
1309
  Object.assign(initialHidden, coerceHidden(definition, options.hidden).values);
1163
- const calc = calculator(definition);
1310
+ const calc = calculator(definition, budget);
1311
+ const pathOf = (values, hidden) => {
1312
+ spend(budget, definition.blocks.length);
1313
+ return computePath(definition, scopeOf(values, hidden));
1314
+ };
1315
+ const logicReadsCalculated = calc.active && readsAny(definition.logic, calc.ids);
1164
1316
  const settle = (values, hidden) => {
1165
- let { path } = computePath(definition, scopeOf(values, hidden));
1317
+ let { path } = pathOf(values, hidden);
1166
1318
  if (!calc.active) return { values, path };
1319
+ if (!logicReadsCalculated) return { values: calc.withCalculated(values, path), path };
1167
1320
  for (let round2 = 0; round2 < 4; round2++) {
1168
1321
  const next = calc.withCalculated(values, path);
1169
1322
  if (next === values) break;
1170
1323
  values = next;
1171
- path = computePath(definition, scopeOf(values, hidden)).path;
1324
+ const again = pathOf(values, hidden).path;
1325
+ const same = again.length === path.length && again.every((id, i) => id === path[i]);
1326
+ path = again;
1327
+ if (same) break;
1172
1328
  }
1173
1329
  return { values, path };
1174
1330
  };
@@ -1210,14 +1366,30 @@ function build(definition, options, replaying = false) {
1210
1366
  return errors;
1211
1367
  };
1212
1368
  let unsettled = false;
1213
- const settleNow = () => {
1214
- if (!unsettled) return;
1215
- unsettled = false;
1216
- const { values, path } = settle(state.values, state.hidden);
1217
- state = { ...state, values, path };
1369
+ let formulasStale = false;
1370
+ const settleNow = (needValues = true) => {
1371
+ if (unsettled && calc.active && !logicReadsCalculated && !needValues) {
1372
+ unsettled = false;
1373
+ formulasStale = true;
1374
+ state = { ...state, path: pathOf(state.values, state.hidden).path };
1375
+ return;
1376
+ }
1377
+ if (unsettled) {
1378
+ unsettled = false;
1379
+ formulasStale = false;
1380
+ const { values, path } = settle(state.values, state.hidden);
1381
+ state = { ...state, values, path };
1382
+ } else if (formulasStale && needValues) {
1383
+ formulasStale = false;
1384
+ state = { ...state, values: calc.withCalculated(state.values, state.path) };
1385
+ }
1218
1386
  };
1219
1387
  const answer = (id, value, event) => {
1220
- if (state.status === "completed" || own(state.values, id) === value || isCalculated(fields.get(id))) return;
1388
+ if (state.status === "completed" || isCalculated(fields.get(id))) return;
1389
+ if (own(state.values, id) === value) {
1390
+ if (value === null || typeof value !== "object") return;
1391
+ calc.forget(value);
1392
+ }
1221
1393
  const errors = { ...state.errors };
1222
1394
  delete errors[id];
1223
1395
  if (batch) {
@@ -1283,7 +1455,7 @@ function build(definition, options, replaying = false) {
1283
1455
  emit({ ...state, errors, current: firstError, events });
1284
1456
  return false;
1285
1457
  }
1286
- const { ending } = computePath(definition, scopeOf(state.values, state.hidden));
1458
+ const { ending } = pathOf(state.values, state.hidden);
1287
1459
  emit({ ...state, errors: {}, status: "completed", ending, events });
1288
1460
  return true;
1289
1461
  },
@@ -1315,10 +1487,11 @@ function build(definition, options, replaying = false) {
1315
1487
  answer(event.field, event.value, logged);
1316
1488
  }
1317
1489
  } else if (event.type === "next") {
1318
- settleNow();
1490
+ settleNow(false);
1491
+ if (formulasStale && isCalculated(fields.get(state.current ?? ""))) settleNow(true);
1319
1492
  engine.next();
1320
1493
  } else if (event.type === "back") {
1321
- settleNow();
1494
+ settleNow(false);
1322
1495
  engine.back();
1323
1496
  } else if (event.type === "submit") {
1324
1497
  settleNow();
@@ -1332,10 +1505,16 @@ function build(definition, options, replaying = false) {
1332
1505
  };
1333
1506
  return { engine, apply, finish };
1334
1507
  }
1335
- function replay(definition, events, options = {}) {
1336
- const { engine, apply, finish } = build(definition, { ...options, initialValues: void 0, clock: () => void 0 }, true);
1337
- for (const event of events) apply(event);
1338
- finish();
1508
+ function replayWithin(definition, events, options, maxWork) {
1509
+ const budget = maxWork === void 0 ? void 0 : { used: 0, max: maxWork };
1510
+ const { engine, apply, finish } = build(definition, { ...options, initialValues: void 0, clock: () => void 0 }, true, budget);
1511
+ try {
1512
+ for (const event of events) apply(event);
1513
+ finish();
1514
+ } finally {
1515
+ if (budget) budget.used;
1516
+ }
1517
+ if (budget) budget.max = Infinity;
1339
1518
  return engine;
1340
1519
  }
1341
1520
 
@@ -1601,9 +1780,15 @@ function parseForm(input) {
1601
1780
  issue(`${path}.kind`, "Unknown block kind.");
1602
1781
  }
1603
1782
  });
1783
+ let formulaChars = 0;
1604
1784
  input.blocks.forEach((block, i) => {
1605
1785
  if (!isObject2(block) || block.kind !== "field" || block.type !== CALCULATED_TYPE) return;
1606
1786
  const path = `blocks[${i}].props.formula`;
1787
+ const source = isObject2(block.props) && typeof block.props.formula === "string" ? block.props.formula : "";
1788
+ formulaChars += source.length;
1789
+ if (formulaChars > MAX_FORM_FORMULA_LENGTH && formulaChars - source.length <= MAX_FORM_FORMULA_LENGTH) {
1790
+ 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.`);
1791
+ }
1607
1792
  if (block.required === true) {
1608
1793
  warnings.push({ path: `blocks[${i}].required`, message: "A calculated field is worked out, not answered, so it is never required." });
1609
1794
  }
@@ -1833,6 +2018,41 @@ function parseForm(input) {
1833
2018
 
1834
2019
  // src/server.ts
1835
2020
  var MAX_EVENTS = 1e4;
2021
+ var MAX_SUBMISSION_VALUES = 2e5;
2022
+ var MAX_REPLAY_WORK = 1e6;
2023
+ var RAW_EVENTS_PER_EVENT = 10;
2024
+ function countValues(value, limit) {
2025
+ let count = 0;
2026
+ const stack = [value];
2027
+ while (stack.length && count <= limit) {
2028
+ const v = stack.pop();
2029
+ if (v === void 0) continue;
2030
+ count++;
2031
+ if (Array.isArray(v)) for (const item of v) stack.push(item);
2032
+ else if (v !== null && typeof v === "object") for (const k in v) stack.push(v[k]);
2033
+ }
2034
+ return count;
2035
+ }
2036
+ var answerTo = (e) => isPlainObject(e) && e.type === "answer" && typeof e.field === "string" ? e.field : void 0;
2037
+ function leanEvents(events) {
2038
+ const out = [];
2039
+ for (let i = 0; i < events.length; i++) {
2040
+ const field = answerTo(events[i]);
2041
+ if (field !== void 0 && answerTo(events[i + 1]) === field) continue;
2042
+ out.push(events[i]);
2043
+ }
2044
+ return out;
2045
+ }
2046
+ function submissionValues(input, limit) {
2047
+ let count = countValues(input.hidden, limit);
2048
+ if (Array.isArray(input.events)) {
2049
+ for (const e of input.events) {
2050
+ if (count > limit) break;
2051
+ if (answerTo(e) !== void 0) count += countValues(e.value, limit - count);
2052
+ }
2053
+ } else count += countValues(input.answers, limit - count);
2054
+ return count;
2055
+ }
1836
2056
  var MALFORMED = { ok: false, errors: { "": "Malformed submission." } };
1837
2057
  var parsedDefinitions = /* @__PURE__ */ new WeakMap();
1838
2058
  function checkDefinition(definition) {
@@ -1862,7 +2082,24 @@ function wellFormed(input) {
1862
2082
  function validateSubmission(definition, submission, options = {}) {
1863
2083
  const invalid = checkDefinition(definition);
1864
2084
  if (invalid) return { ok: false, errors: { "": invalid } };
1865
- const input = wellFormed(submission);
2085
+ if (!isPlainObject(submission)) return MALFORMED;
2086
+ const maxEvents = options.maxEvents ?? MAX_EVENTS;
2087
+ let sent = submission;
2088
+ if (Array.isArray(submission.events)) {
2089
+ const maxRaw = maxEvents * RAW_EVENTS_PER_EVENT;
2090
+ if (submission.events.length > maxRaw) {
2091
+ return { ok: false, errors: { "": `Too many events: ${submission.events.length} sent; the limit is ${maxRaw} before compaction.` } };
2092
+ }
2093
+ sent = { ...submission, events: leanEvents(submission.events) };
2094
+ }
2095
+ const maxValues = options.maxValues ?? MAX_SUBMISSION_VALUES;
2096
+ if (submissionValues(sent, maxValues) > maxValues) {
2097
+ return {
2098
+ ok: false,
2099
+ errors: { "": `Too much data: the answers hold more than ${maxValues} values (items, rows and cells); the limit is ${maxValues}.` }
2100
+ };
2101
+ }
2102
+ const input = wellFormed(sent);
1866
2103
  if (!input) return MALFORMED;
1867
2104
  const ignored = /* @__PURE__ */ new Set();
1868
2105
  const client = coerceHidden(definition, input.hidden, { sources: ["url", "host"] });
@@ -1881,12 +2118,20 @@ function validateSubmission(definition, submission, options = {}) {
1881
2118
  let engine;
1882
2119
  if (input.events) {
1883
2120
  const events = compactEvents(input.events);
1884
- const max = options.maxEvents ?? MAX_EVENTS;
1885
- if (events.length > max) {
1886
- return { ok: false, errors: { "": `Too many events: ${events.length} after compaction; the limit is ${max}.` } };
2121
+ if (events.length > maxEvents) {
2122
+ return { ok: false, errors: { "": `Too many events: ${events.length} after compaction; the limit is ${maxEvents}.` } };
1887
2123
  }
1888
2124
  const kept = events.filter((e) => e.type !== "answer" || accept(e.field) !== false);
1889
- engine = replay(definition, kept, { hidden: engineHidden, validators: options.validators });
2125
+ const maxWork = options.maxReplayWork ?? MAX_REPLAY_WORK;
2126
+ try {
2127
+ engine = replayWithin(definition, kept, { hidden: engineHidden, validators: options.validators }, maxWork);
2128
+ } catch (e) {
2129
+ if (!(e instanceof ReplayWorkExceeded)) throw e;
2130
+ return {
2131
+ ok: false,
2132
+ errors: { "": `Too much work: replaying these events (the path and calculated fields) would take more than ${maxWork} steps; the limit is ${maxWork}.` }
2133
+ };
2134
+ }
1890
2135
  } else {
1891
2136
  const answers2 = {};
1892
2137
  for (const id of Object.keys(input.answers ?? {})) {
@@ -1953,6 +2198,6 @@ function publicDefinition(definition) {
1953
2198
  return JSON.parse(JSON.stringify(definition));
1954
2199
  }
1955
2200
 
1956
- export { MAX_EVENTS, publicDefinition, validateSubmission };
2201
+ export { MAX_EVENTS, MAX_REPLAY_WORK, MAX_SUBMISSION_VALUES, RAW_EVENTS_PER_EVENT, publicDefinition, validateSubmission };
1957
2202
  //# sourceMappingURL=server.js.map
1958
2203
  //# sourceMappingURL=server.js.map