@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/README.md +31 -4
- package/dist/destinations.cjs.map +1 -1
- package/dist/destinations.js.map +1 -1
- package/dist/index.cjs +230 -45
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +12 -2
- package/dist/index.d.ts +12 -2
- package/dist/index.js +229 -46
- package/dist/index.js.map +1 -1
- package/dist/server.cjs +303 -55
- 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 +301 -56
- package/dist/server.js.map +1 -1
- package/dist/testing.cjs +302 -54
- package/dist/testing.cjs.map +1 -1
- package/dist/testing.js +302 -54
- package/dist/testing.js.map +1 -1
- package/package.json +1 -1
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
|
|
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
|
-
|
|
268
|
-
|
|
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 (!
|
|
271
|
-
if (node.fn === "avg") return finite(total /
|
|
272
|
-
return
|
|
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
|
|
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 (
|
|
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
|
-
|
|
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
|
|
364
|
-
const
|
|
365
|
-
|
|
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
|
-
|
|
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
|
|
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 (
|
|
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
|
|
381
|
-
|
|
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 ?
|
|
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)
|
|
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
|
-
|
|
1049
|
-
|
|
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 } =
|
|
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
|
-
|
|
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
|
-
|
|
1216
|
-
|
|
1217
|
-
unsettled
|
|
1218
|
-
|
|
1219
|
-
|
|
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" ||
|
|
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 } =
|
|
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
|
|
1338
|
-
const
|
|
1339
|
-
|
|
1340
|
-
|
|
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
|
-
|
|
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
|
-
|
|
1887
|
-
|
|
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
|
-
|
|
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
|