@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/README.md +22 -5
- package/dist/destinations.cjs.map +1 -1
- package/dist/destinations.js.map +1 -1
- package/dist/index.cjs +125 -44
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.js +125 -44
- package/dist/index.js.map +1 -1
- package/dist/server.cjs +201 -51
- package/dist/server.cjs.map +1 -1
- package/dist/server.d.cts +29 -2
- package/dist/server.d.ts +29 -2
- package/dist/server.js +199 -52
- package/dist/server.js.map +1 -1
- package/dist/testing.cjs +200 -50
- package/dist/testing.cjs.map +1 -1
- package/dist/testing.js +200 -50
- package/dist/testing.js.map +1 -1
- package/package.json +1 -1
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
|
-
/**
|
|
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
|
-
/**
|
|
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
|
@@ -352,8 +352,7 @@ var round = (n, decimals) => {
|
|
|
352
352
|
const r = Number(n.toFixed(decimals));
|
|
353
353
|
return Object.is(r, -0) ? 0 : r;
|
|
354
354
|
};
|
|
355
|
-
function
|
|
356
|
-
const { format, decimals } = calculatedProps(field);
|
|
355
|
+
function valueAs(result, { format, decimals }) {
|
|
357
356
|
if (result === null || Array.isArray(result)) return void 0;
|
|
358
357
|
if (format === "number" || format === "percent") {
|
|
359
358
|
const n = formulaNumber(result);
|
|
@@ -380,19 +379,6 @@ var extract = (value, column) => column !== void 0 ? value.flatMap((row) => {
|
|
|
380
379
|
const cell = row[column];
|
|
381
380
|
return typeof cell === "string" || typeof cell === "number" ? [cell] : [];
|
|
382
381
|
}) : value.flatMap((v) => typeof v === "string" || typeof v === "number" || typeof v === "boolean" ? [v] : []);
|
|
383
|
-
var extracted = /* @__PURE__ */ new WeakMap();
|
|
384
|
-
function cachedList(value, column) {
|
|
385
|
-
let byColumn = extracted.get(value);
|
|
386
|
-
if (!byColumn) extracted.set(value, byColumn = /* @__PURE__ */ new Map());
|
|
387
|
-
const key = column ?? "";
|
|
388
|
-
let list = byColumn.get(key);
|
|
389
|
-
if (!list) byColumn.set(key, list = ownFormulaList(extract(value, column)));
|
|
390
|
-
return list;
|
|
391
|
-
}
|
|
392
|
-
function readAnswer(value, column) {
|
|
393
|
-
if (Array.isArray(value)) return cachedList(value, column);
|
|
394
|
-
return column !== void 0 ? null : formulaInput(value);
|
|
395
|
-
}
|
|
396
382
|
function formulaInput(value, column) {
|
|
397
383
|
if (value === void 0 || value === null) return null;
|
|
398
384
|
if (Array.isArray(value) && value.length > MAX_FORMULA_ITEMS) return null;
|
|
@@ -407,9 +393,28 @@ function refNodes(node, out = []) {
|
|
|
407
393
|
else if (node.kind === "call") for (const a of node.args) refNodes(a, out);
|
|
408
394
|
return out;
|
|
409
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
|
+
}
|
|
410
415
|
var UNREAD = /* @__PURE__ */ Symbol("unread");
|
|
411
416
|
var OVER = /* @__PURE__ */ Symbol("over");
|
|
412
|
-
function calculator(definition) {
|
|
417
|
+
function calculator(definition, work) {
|
|
413
418
|
const compiled = [];
|
|
414
419
|
const idByKey = /* @__PURE__ */ new Map();
|
|
415
420
|
for (const block of definition.blocks) {
|
|
@@ -418,7 +423,8 @@ function calculator(definition) {
|
|
|
418
423
|
}
|
|
419
424
|
for (const block of definition.blocks) {
|
|
420
425
|
if (block.kind !== "field" || !isCalculated(block)) continue;
|
|
421
|
-
const
|
|
426
|
+
const props = calculatedProps(block);
|
|
427
|
+
const parsed = parseFormula(props.formula);
|
|
422
428
|
const reads = [];
|
|
423
429
|
const slots = /* @__PURE__ */ new Map();
|
|
424
430
|
const byKey = /* @__PURE__ */ new Map();
|
|
@@ -432,33 +438,65 @@ function calculator(definition) {
|
|
|
432
438
|
}
|
|
433
439
|
slots.set(ref, slot);
|
|
434
440
|
}
|
|
435
|
-
compiled.push({ field: block, ast: parsed.ok ? parsed.ast : null, reads, slots, last: null });
|
|
441
|
+
compiled.push({ field: block, props, ast: parsed.ok ? parsed.ast : null, size: parsed.ok ? formulaSize(parsed.ast) : 0, reads, slots, last: null });
|
|
436
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
|
+
};
|
|
437
463
|
const withCalculated = (values, path) => {
|
|
438
464
|
if (!compiled.length) return values;
|
|
439
465
|
const visible = new Set(path);
|
|
440
466
|
let next = values;
|
|
467
|
+
let copied = false;
|
|
441
468
|
let budget = MAX_SETTLE_ITEMS;
|
|
442
469
|
const within = /* @__PURE__ */ new Map();
|
|
443
|
-
const input = ({ id
|
|
470
|
+
const input = ({ id }) => {
|
|
444
471
|
if (id === void 0 || !visible.has(id) || !Object.prototype.hasOwnProperty.call(next, id)) return UNREAD;
|
|
445
472
|
const value = next[id];
|
|
446
473
|
if (!Array.isArray(value)) return value;
|
|
447
|
-
let ok = within.get(
|
|
474
|
+
let ok = within.get(id);
|
|
448
475
|
if (ok === void 0) {
|
|
449
476
|
ok = value.length <= MAX_FORMULA_ITEMS && value.length <= budget;
|
|
450
477
|
if (ok) budget -= value.length;
|
|
451
|
-
within.set(
|
|
478
|
+
within.set(id, ok);
|
|
452
479
|
}
|
|
453
480
|
return ok ? value : OVER;
|
|
454
481
|
};
|
|
455
482
|
for (const entry of compiled) {
|
|
456
483
|
const { field, ast } = entry;
|
|
457
|
-
const
|
|
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
|
+
}
|
|
458
495
|
let value;
|
|
459
|
-
if (
|
|
496
|
+
if (same) {
|
|
460
497
|
value = entry.last.value;
|
|
461
498
|
} else {
|
|
499
|
+
spend(work, entry.size);
|
|
462
500
|
const resolved = new Array(inputs.length);
|
|
463
501
|
const read = (ref) => {
|
|
464
502
|
const slot = entry.slots.get(ref);
|
|
@@ -466,19 +504,25 @@ function calculator(definition) {
|
|
|
466
504
|
let v = resolved[slot];
|
|
467
505
|
if (v === void 0) {
|
|
468
506
|
const raw = inputs[slot];
|
|
469
|
-
v = resolved[slot] = raw === UNREAD ? null : readAnswer(raw,
|
|
507
|
+
v = resolved[slot] = raw === UNREAD ? null : readAnswer(raw, reads[slot].column);
|
|
470
508
|
}
|
|
471
509
|
return v;
|
|
472
510
|
};
|
|
473
|
-
value = ast && !
|
|
511
|
+
value = ast && !over ? valueAs(evaluateFormula(ast, read), entry.props) : void 0;
|
|
474
512
|
entry.last = { inputs, value };
|
|
475
513
|
}
|
|
476
514
|
const current = Object.prototype.hasOwnProperty.call(next, field.id) ? next[field.id] : void 0;
|
|
477
|
-
if (current !== value)
|
|
515
|
+
if (current !== value) {
|
|
516
|
+
if (!copied) {
|
|
517
|
+
next = { ...next };
|
|
518
|
+
copied = true;
|
|
519
|
+
}
|
|
520
|
+
next[field.id] = value;
|
|
521
|
+
}
|
|
478
522
|
}
|
|
479
523
|
return next;
|
|
480
524
|
};
|
|
481
|
-
return { active: compiled.length > 0, withCalculated };
|
|
525
|
+
return { active: compiled.length > 0, ids: new Set(compiled.map((c) => c.field.id)), withCalculated, forget };
|
|
482
526
|
}
|
|
483
527
|
var numberOps = ["eq", "neq", "lt", "lte", "gt", "gte", "in", "answered", "empty"];
|
|
484
528
|
var calculatedFieldType = {
|
|
@@ -1135,8 +1179,8 @@ function rulesAllow(def, blockId, scope) {
|
|
|
1135
1179
|
}
|
|
1136
1180
|
function walkPath(def, scope) {
|
|
1137
1181
|
const list = steps(def);
|
|
1138
|
-
|
|
1139
|
-
|
|
1182
|
+
let index;
|
|
1183
|
+
let position;
|
|
1140
1184
|
const jumps = (def.logic ?? []).filter(
|
|
1141
1185
|
(r) => r.effect === "jump"
|
|
1142
1186
|
);
|
|
@@ -1160,6 +1204,7 @@ function walkPath(def, scope) {
|
|
|
1160
1204
|
ending = jump.to.ending;
|
|
1161
1205
|
break;
|
|
1162
1206
|
}
|
|
1207
|
+
index ??= new Map(list.map((b, at) => [b.id, at]));
|
|
1163
1208
|
const target = index.get(jump.to);
|
|
1164
1209
|
if (target !== void 0 && target > i) {
|
|
1165
1210
|
i = nextVisible(target);
|
|
@@ -1169,6 +1214,7 @@ function walkPath(def, scope) {
|
|
|
1169
1214
|
i = nextVisible(i + 1);
|
|
1170
1215
|
}
|
|
1171
1216
|
const scopeAt = (blockId, inclusive = false) => {
|
|
1217
|
+
position ??= new Map(def.blocks.map((b, i2) => [b.id, i2]));
|
|
1172
1218
|
const at = position.get(blockId) ?? -1;
|
|
1173
1219
|
const values = /* @__PURE__ */ Object.create(null);
|
|
1174
1220
|
for (const id of path) {
|
|
@@ -1231,10 +1277,19 @@ function validateField(field, value, options = {}) {
|
|
|
1231
1277
|
}
|
|
1232
1278
|
|
|
1233
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
|
+
}
|
|
1234
1289
|
function createForm(definition, options = {}) {
|
|
1235
1290
|
return build(definition, options).engine;
|
|
1236
1291
|
}
|
|
1237
|
-
function build(definition, options, replaying = false) {
|
|
1292
|
+
function build(definition, options, replaying = false, budget) {
|
|
1238
1293
|
const fields = fieldMap(definition);
|
|
1239
1294
|
const byKey = new Map([...fields.values()].map((f) => [f.key, f]));
|
|
1240
1295
|
const blocks = new Map(definition.blocks.map((b) => [b.id, b]));
|
|
@@ -1252,15 +1307,24 @@ function build(definition, options, replaying = false) {
|
|
|
1252
1307
|
if (!declared.has(name)) initialHidden[name] = value;
|
|
1253
1308
|
}
|
|
1254
1309
|
Object.assign(initialHidden, coerceHidden(definition, options.hidden).values);
|
|
1255
|
-
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);
|
|
1256
1316
|
const settle = (values, hidden) => {
|
|
1257
|
-
let { path } =
|
|
1317
|
+
let { path } = pathOf(values, hidden);
|
|
1258
1318
|
if (!calc.active) return { values, path };
|
|
1319
|
+
if (!logicReadsCalculated) return { values: calc.withCalculated(values, path), path };
|
|
1259
1320
|
for (let round2 = 0; round2 < 4; round2++) {
|
|
1260
1321
|
const next = calc.withCalculated(values, path);
|
|
1261
1322
|
if (next === values) break;
|
|
1262
1323
|
values = next;
|
|
1263
|
-
|
|
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;
|
|
1264
1328
|
}
|
|
1265
1329
|
return { values, path };
|
|
1266
1330
|
};
|
|
@@ -1302,14 +1366,30 @@ function build(definition, options, replaying = false) {
|
|
|
1302
1366
|
return errors;
|
|
1303
1367
|
};
|
|
1304
1368
|
let unsettled = false;
|
|
1305
|
-
|
|
1306
|
-
|
|
1307
|
-
unsettled
|
|
1308
|
-
|
|
1309
|
-
|
|
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
|
+
}
|
|
1310
1386
|
};
|
|
1311
1387
|
const answer = (id, value, event) => {
|
|
1312
|
-
if (state.status === "completed" ||
|
|
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
|
+
}
|
|
1313
1393
|
const errors = { ...state.errors };
|
|
1314
1394
|
delete errors[id];
|
|
1315
1395
|
if (batch) {
|
|
@@ -1375,7 +1455,7 @@ function build(definition, options, replaying = false) {
|
|
|
1375
1455
|
emit({ ...state, errors, current: firstError, events });
|
|
1376
1456
|
return false;
|
|
1377
1457
|
}
|
|
1378
|
-
const { ending } =
|
|
1458
|
+
const { ending } = pathOf(state.values, state.hidden);
|
|
1379
1459
|
emit({ ...state, errors: {}, status: "completed", ending, events });
|
|
1380
1460
|
return true;
|
|
1381
1461
|
},
|
|
@@ -1407,10 +1487,11 @@ function build(definition, options, replaying = false) {
|
|
|
1407
1487
|
answer(event.field, event.value, logged);
|
|
1408
1488
|
}
|
|
1409
1489
|
} else if (event.type === "next") {
|
|
1410
|
-
settleNow();
|
|
1490
|
+
settleNow(false);
|
|
1491
|
+
if (formulasStale && isCalculated(fields.get(state.current ?? ""))) settleNow(true);
|
|
1411
1492
|
engine.next();
|
|
1412
1493
|
} else if (event.type === "back") {
|
|
1413
|
-
settleNow();
|
|
1494
|
+
settleNow(false);
|
|
1414
1495
|
engine.back();
|
|
1415
1496
|
} else if (event.type === "submit") {
|
|
1416
1497
|
settleNow();
|
|
@@ -1424,10 +1505,16 @@ function build(definition, options, replaying = false) {
|
|
|
1424
1505
|
};
|
|
1425
1506
|
return { engine, apply, finish };
|
|
1426
1507
|
}
|
|
1427
|
-
function
|
|
1428
|
-
const
|
|
1429
|
-
|
|
1430
|
-
|
|
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;
|
|
1431
1518
|
return engine;
|
|
1432
1519
|
}
|
|
1433
1520
|
|
|
@@ -1931,6 +2018,41 @@ function parseForm(input) {
|
|
|
1931
2018
|
|
|
1932
2019
|
// src/server.ts
|
|
1933
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
|
+
}
|
|
1934
2056
|
var MALFORMED = { ok: false, errors: { "": "Malformed submission." } };
|
|
1935
2057
|
var parsedDefinitions = /* @__PURE__ */ new WeakMap();
|
|
1936
2058
|
function checkDefinition(definition) {
|
|
@@ -1960,7 +2082,24 @@ function wellFormed(input) {
|
|
|
1960
2082
|
function validateSubmission(definition, submission, options = {}) {
|
|
1961
2083
|
const invalid = checkDefinition(definition);
|
|
1962
2084
|
if (invalid) return { ok: false, errors: { "": invalid } };
|
|
1963
|
-
|
|
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);
|
|
1964
2103
|
if (!input) return MALFORMED;
|
|
1965
2104
|
const ignored = /* @__PURE__ */ new Set();
|
|
1966
2105
|
const client = coerceHidden(definition, input.hidden, { sources: ["url", "host"] });
|
|
@@ -1979,12 +2118,20 @@ function validateSubmission(definition, submission, options = {}) {
|
|
|
1979
2118
|
let engine;
|
|
1980
2119
|
if (input.events) {
|
|
1981
2120
|
const events = compactEvents(input.events);
|
|
1982
|
-
|
|
1983
|
-
|
|
1984
|
-
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}.` } };
|
|
1985
2123
|
}
|
|
1986
2124
|
const kept = events.filter((e) => e.type !== "answer" || accept(e.field) !== false);
|
|
1987
|
-
|
|
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
|
+
}
|
|
1988
2135
|
} else {
|
|
1989
2136
|
const answers2 = {};
|
|
1990
2137
|
for (const id of Object.keys(input.answers ?? {})) {
|
|
@@ -2051,6 +2198,6 @@ function publicDefinition(definition) {
|
|
|
2051
2198
|
return JSON.parse(JSON.stringify(definition));
|
|
2052
2199
|
}
|
|
2053
2200
|
|
|
2054
|
-
export { MAX_EVENTS, publicDefinition, validateSubmission };
|
|
2201
|
+
export { MAX_EVENTS, MAX_REPLAY_WORK, MAX_SUBMISSION_VALUES, RAW_EVENTS_PER_EVENT, publicDefinition, validateSubmission };
|
|
2055
2202
|
//# sourceMappingURL=server.js.map
|
|
2056
2203
|
//# sourceMappingURL=server.js.map
|