@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/testing.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 same2 = 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 (same2 && entry.last.inputs[i] !== v) same2 = 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 (same2) {
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 same2 = again.length === path.length && again.every((id, i) => id === path[i]);
1326
+ path = again;
1327
+ if (same2) 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();
@@ -1333,9 +1506,18 @@ function build(definition, options, replaying = false) {
1333
1506
  return { engine, apply, finish };
1334
1507
  }
1335
1508
  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();
1509
+ return replayWithin(definition, events, options);
1510
+ }
1511
+ function replayWithin(definition, events, options, maxWork) {
1512
+ const budget = maxWork === void 0 ? void 0 : { used: 0, max: maxWork };
1513
+ const { engine, apply, finish } = build(definition, { ...options, initialValues: void 0, clock: () => void 0 }, true, budget);
1514
+ try {
1515
+ for (const event of events) apply(event);
1516
+ finish();
1517
+ } finally {
1518
+ if (budget) budget.used;
1519
+ }
1520
+ if (budget) budget.max = Infinity;
1339
1521
  return engine;
1340
1522
  }
1341
1523
 
@@ -1780,9 +1962,15 @@ function parseForm(input) {
1780
1962
  issue(`${path}.kind`, "Unknown block kind.");
1781
1963
  }
1782
1964
  });
1965
+ let formulaChars = 0;
1783
1966
  input.blocks.forEach((block, i) => {
1784
1967
  if (!isObject2(block) || block.kind !== "field" || block.type !== CALCULATED_TYPE) return;
1785
1968
  const path = `blocks[${i}].props.formula`;
1969
+ const source = isObject2(block.props) && typeof block.props.formula === "string" ? block.props.formula : "";
1970
+ formulaChars += source.length;
1971
+ if (formulaChars > MAX_FORM_FORMULA_LENGTH && formulaChars - source.length <= MAX_FORM_FORMULA_LENGTH) {
1972
+ 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.`);
1973
+ }
1786
1974
  if (block.required === true) {
1787
1975
  warnings.push({ path: `blocks[${i}].required`, message: "A calculated field is worked out, not answered, so it is never required." });
1788
1976
  }
@@ -2012,6 +2200,41 @@ function parseForm(input) {
2012
2200
 
2013
2201
  // src/server.ts
2014
2202
  var MAX_EVENTS = 1e4;
2203
+ var MAX_SUBMISSION_VALUES = 2e5;
2204
+ var MAX_REPLAY_WORK = 1e6;
2205
+ var RAW_EVENTS_PER_EVENT = 10;
2206
+ function countValues(value, limit) {
2207
+ let count = 0;
2208
+ const stack = [value];
2209
+ while (stack.length && count <= limit) {
2210
+ const v = stack.pop();
2211
+ if (v === void 0) continue;
2212
+ count++;
2213
+ if (Array.isArray(v)) for (const item of v) stack.push(item);
2214
+ else if (v !== null && typeof v === "object") for (const k in v) stack.push(v[k]);
2215
+ }
2216
+ return count;
2217
+ }
2218
+ var answerTo = (e) => isPlainObject(e) && e.type === "answer" && typeof e.field === "string" ? e.field : void 0;
2219
+ function leanEvents(events) {
2220
+ const out = [];
2221
+ for (let i = 0; i < events.length; i++) {
2222
+ const field = answerTo(events[i]);
2223
+ if (field !== void 0 && answerTo(events[i + 1]) === field) continue;
2224
+ out.push(events[i]);
2225
+ }
2226
+ return out;
2227
+ }
2228
+ function submissionValues(input, limit) {
2229
+ let count = countValues(input.hidden, limit);
2230
+ if (Array.isArray(input.events)) {
2231
+ for (const e of input.events) {
2232
+ if (count > limit) break;
2233
+ if (answerTo(e) !== void 0) count += countValues(e.value, limit - count);
2234
+ }
2235
+ } else count += countValues(input.answers, limit - count);
2236
+ return count;
2237
+ }
2015
2238
  var MALFORMED = { ok: false, errors: { "": "Malformed submission." } };
2016
2239
  var parsedDefinitions = /* @__PURE__ */ new WeakMap();
2017
2240
  function checkDefinition(definition) {
@@ -2041,7 +2264,24 @@ function wellFormed(input) {
2041
2264
  function validateSubmission(definition, submission, options = {}) {
2042
2265
  const invalid = checkDefinition(definition);
2043
2266
  if (invalid) return { ok: false, errors: { "": invalid } };
2044
- const input = wellFormed(submission);
2267
+ if (!isPlainObject(submission)) return MALFORMED;
2268
+ const maxEvents = options.maxEvents ?? MAX_EVENTS;
2269
+ let sent = submission;
2270
+ if (Array.isArray(submission.events)) {
2271
+ const maxRaw = maxEvents * RAW_EVENTS_PER_EVENT;
2272
+ if (submission.events.length > maxRaw) {
2273
+ return { ok: false, errors: { "": `Too many events: ${submission.events.length} sent; the limit is ${maxRaw} before compaction.` } };
2274
+ }
2275
+ sent = { ...submission, events: leanEvents(submission.events) };
2276
+ }
2277
+ const maxValues = options.maxValues ?? MAX_SUBMISSION_VALUES;
2278
+ if (submissionValues(sent, maxValues) > maxValues) {
2279
+ return {
2280
+ ok: false,
2281
+ errors: { "": `Too much data: the answers hold more than ${maxValues} values (items, rows and cells); the limit is ${maxValues}.` }
2282
+ };
2283
+ }
2284
+ const input = wellFormed(sent);
2045
2285
  if (!input) return MALFORMED;
2046
2286
  const ignored = /* @__PURE__ */ new Set();
2047
2287
  const client = coerceHidden(definition, input.hidden, { sources: ["url", "host"] });
@@ -2060,12 +2300,20 @@ function validateSubmission(definition, submission, options = {}) {
2060
2300
  let engine;
2061
2301
  if (input.events) {
2062
2302
  const events = compactEvents(input.events);
2063
- const max = options.maxEvents ?? MAX_EVENTS;
2064
- if (events.length > max) {
2065
- return { ok: false, errors: { "": `Too many events: ${events.length} after compaction; the limit is ${max}.` } };
2303
+ if (events.length > maxEvents) {
2304
+ return { ok: false, errors: { "": `Too many events: ${events.length} after compaction; the limit is ${maxEvents}.` } };
2066
2305
  }
2067
2306
  const kept = events.filter((e) => e.type !== "answer" || accept(e.field) !== false);
2068
- engine = replay(definition, kept, { hidden: engineHidden, validators: options.validators });
2307
+ const maxWork = options.maxReplayWork ?? MAX_REPLAY_WORK;
2308
+ try {
2309
+ engine = replayWithin(definition, kept, { hidden: engineHidden, validators: options.validators }, maxWork);
2310
+ } catch (e) {
2311
+ if (!(e instanceof ReplayWorkExceeded)) throw e;
2312
+ return {
2313
+ ok: false,
2314
+ errors: { "": `Too much work: replaying these events (the path and calculated fields) would take more than ${maxWork} steps; the limit is ${maxWork}.` }
2315
+ };
2316
+ }
2069
2317
  } else {
2070
2318
  const answers2 = {};
2071
2319
  for (const id of Object.keys(input.answers ?? {})) {