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