@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.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
|
@@ -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
|
|
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
|
-
|
|
266
|
-
|
|
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 (!
|
|
269
|
-
if (node.fn === "avg") return finite(total /
|
|
270
|
-
return
|
|
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
|
|
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 (
|
|
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
|
-
|
|
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
|
|
362
|
-
const
|
|
363
|
-
|
|
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
|
-
|
|
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
|
|
484
|
+
const { reads } = entry;
|
|
485
|
+
spend(work, 1);
|
|
486
|
+
const inputs = new Array(reads.length);
|
|
487
|
+
let over = false;
|
|
488
|
+
let same = entry.last !== null;
|
|
489
|
+
for (let i = 0; i < reads.length; i++) {
|
|
490
|
+
const v = input(reads[i]);
|
|
491
|
+
inputs[i] = v;
|
|
492
|
+
if (v === OVER) over = true;
|
|
493
|
+
if (same && entry.last.inputs[i] !== v) same = false;
|
|
494
|
+
}
|
|
373
495
|
let value;
|
|
374
|
-
if (
|
|
496
|
+
if (same) {
|
|
375
497
|
value = entry.last.value;
|
|
376
498
|
} else {
|
|
499
|
+
spend(work, entry.size);
|
|
500
|
+
const resolved = new Array(inputs.length);
|
|
377
501
|
const read = (ref) => {
|
|
378
|
-
const
|
|
379
|
-
|
|
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 ?
|
|
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)
|
|
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
|
-
|
|
1047
|
-
|
|
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 } =
|
|
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
|
-
|
|
1324
|
+
const again = pathOf(values, hidden).path;
|
|
1325
|
+
const same = again.length === path.length && again.every((id, i) => id === path[i]);
|
|
1326
|
+
path = again;
|
|
1327
|
+
if (same) break;
|
|
1172
1328
|
}
|
|
1173
1329
|
return { values, path };
|
|
1174
1330
|
};
|
|
@@ -1210,14 +1366,30 @@ function build(definition, options, replaying = false) {
|
|
|
1210
1366
|
return errors;
|
|
1211
1367
|
};
|
|
1212
1368
|
let unsettled = false;
|
|
1213
|
-
|
|
1214
|
-
|
|
1215
|
-
unsettled
|
|
1216
|
-
|
|
1217
|
-
|
|
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" ||
|
|
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 } =
|
|
1458
|
+
const { ending } = pathOf(state.values, state.hidden);
|
|
1287
1459
|
emit({ ...state, errors: {}, status: "completed", ending, events });
|
|
1288
1460
|
return true;
|
|
1289
1461
|
},
|
|
@@ -1315,10 +1487,11 @@ function build(definition, options, replaying = false) {
|
|
|
1315
1487
|
answer(event.field, event.value, logged);
|
|
1316
1488
|
}
|
|
1317
1489
|
} else if (event.type === "next") {
|
|
1318
|
-
settleNow();
|
|
1490
|
+
settleNow(false);
|
|
1491
|
+
if (formulasStale && isCalculated(fields.get(state.current ?? ""))) settleNow(true);
|
|
1319
1492
|
engine.next();
|
|
1320
1493
|
} else if (event.type === "back") {
|
|
1321
|
-
settleNow();
|
|
1494
|
+
settleNow(false);
|
|
1322
1495
|
engine.back();
|
|
1323
1496
|
} else if (event.type === "submit") {
|
|
1324
1497
|
settleNow();
|
|
@@ -1332,10 +1505,16 @@ function build(definition, options, replaying = false) {
|
|
|
1332
1505
|
};
|
|
1333
1506
|
return { engine, apply, finish };
|
|
1334
1507
|
}
|
|
1335
|
-
function
|
|
1336
|
-
const
|
|
1337
|
-
|
|
1338
|
-
|
|
1508
|
+
function replayWithin(definition, events, options, maxWork) {
|
|
1509
|
+
const budget = maxWork === void 0 ? void 0 : { used: 0, max: maxWork };
|
|
1510
|
+
const { engine, apply, finish } = build(definition, { ...options, initialValues: void 0, clock: () => void 0 }, true, budget);
|
|
1511
|
+
try {
|
|
1512
|
+
for (const event of events) apply(event);
|
|
1513
|
+
finish();
|
|
1514
|
+
} finally {
|
|
1515
|
+
if (budget) budget.used;
|
|
1516
|
+
}
|
|
1517
|
+
if (budget) budget.max = Infinity;
|
|
1339
1518
|
return engine;
|
|
1340
1519
|
}
|
|
1341
1520
|
|
|
@@ -1601,9 +1780,15 @@ function parseForm(input) {
|
|
|
1601
1780
|
issue(`${path}.kind`, "Unknown block kind.");
|
|
1602
1781
|
}
|
|
1603
1782
|
});
|
|
1783
|
+
let formulaChars = 0;
|
|
1604
1784
|
input.blocks.forEach((block, i) => {
|
|
1605
1785
|
if (!isObject2(block) || block.kind !== "field" || block.type !== CALCULATED_TYPE) return;
|
|
1606
1786
|
const path = `blocks[${i}].props.formula`;
|
|
1787
|
+
const source = isObject2(block.props) && typeof block.props.formula === "string" ? block.props.formula : "";
|
|
1788
|
+
formulaChars += source.length;
|
|
1789
|
+
if (formulaChars > MAX_FORM_FORMULA_LENGTH && formulaChars - source.length <= MAX_FORM_FORMULA_LENGTH) {
|
|
1790
|
+
issue(path, `The form's formulas together are over ${MAX_FORM_FORMULA_LENGTH} characters; shorten some, or work part out once in an earlier calculated field.`);
|
|
1791
|
+
}
|
|
1607
1792
|
if (block.required === true) {
|
|
1608
1793
|
warnings.push({ path: `blocks[${i}].required`, message: "A calculated field is worked out, not answered, so it is never required." });
|
|
1609
1794
|
}
|
|
@@ -1833,6 +2018,41 @@ function parseForm(input) {
|
|
|
1833
2018
|
|
|
1834
2019
|
// src/server.ts
|
|
1835
2020
|
var MAX_EVENTS = 1e4;
|
|
2021
|
+
var MAX_SUBMISSION_VALUES = 2e5;
|
|
2022
|
+
var MAX_REPLAY_WORK = 1e6;
|
|
2023
|
+
var RAW_EVENTS_PER_EVENT = 10;
|
|
2024
|
+
function countValues(value, limit) {
|
|
2025
|
+
let count = 0;
|
|
2026
|
+
const stack = [value];
|
|
2027
|
+
while (stack.length && count <= limit) {
|
|
2028
|
+
const v = stack.pop();
|
|
2029
|
+
if (v === void 0) continue;
|
|
2030
|
+
count++;
|
|
2031
|
+
if (Array.isArray(v)) for (const item of v) stack.push(item);
|
|
2032
|
+
else if (v !== null && typeof v === "object") for (const k in v) stack.push(v[k]);
|
|
2033
|
+
}
|
|
2034
|
+
return count;
|
|
2035
|
+
}
|
|
2036
|
+
var answerTo = (e) => isPlainObject(e) && e.type === "answer" && typeof e.field === "string" ? e.field : void 0;
|
|
2037
|
+
function leanEvents(events) {
|
|
2038
|
+
const out = [];
|
|
2039
|
+
for (let i = 0; i < events.length; i++) {
|
|
2040
|
+
const field = answerTo(events[i]);
|
|
2041
|
+
if (field !== void 0 && answerTo(events[i + 1]) === field) continue;
|
|
2042
|
+
out.push(events[i]);
|
|
2043
|
+
}
|
|
2044
|
+
return out;
|
|
2045
|
+
}
|
|
2046
|
+
function submissionValues(input, limit) {
|
|
2047
|
+
let count = countValues(input.hidden, limit);
|
|
2048
|
+
if (Array.isArray(input.events)) {
|
|
2049
|
+
for (const e of input.events) {
|
|
2050
|
+
if (count > limit) break;
|
|
2051
|
+
if (answerTo(e) !== void 0) count += countValues(e.value, limit - count);
|
|
2052
|
+
}
|
|
2053
|
+
} else count += countValues(input.answers, limit - count);
|
|
2054
|
+
return count;
|
|
2055
|
+
}
|
|
1836
2056
|
var MALFORMED = { ok: false, errors: { "": "Malformed submission." } };
|
|
1837
2057
|
var parsedDefinitions = /* @__PURE__ */ new WeakMap();
|
|
1838
2058
|
function checkDefinition(definition) {
|
|
@@ -1862,7 +2082,24 @@ function wellFormed(input) {
|
|
|
1862
2082
|
function validateSubmission(definition, submission, options = {}) {
|
|
1863
2083
|
const invalid = checkDefinition(definition);
|
|
1864
2084
|
if (invalid) return { ok: false, errors: { "": invalid } };
|
|
1865
|
-
|
|
2085
|
+
if (!isPlainObject(submission)) return MALFORMED;
|
|
2086
|
+
const maxEvents = options.maxEvents ?? MAX_EVENTS;
|
|
2087
|
+
let sent = submission;
|
|
2088
|
+
if (Array.isArray(submission.events)) {
|
|
2089
|
+
const maxRaw = maxEvents * RAW_EVENTS_PER_EVENT;
|
|
2090
|
+
if (submission.events.length > maxRaw) {
|
|
2091
|
+
return { ok: false, errors: { "": `Too many events: ${submission.events.length} sent; the limit is ${maxRaw} before compaction.` } };
|
|
2092
|
+
}
|
|
2093
|
+
sent = { ...submission, events: leanEvents(submission.events) };
|
|
2094
|
+
}
|
|
2095
|
+
const maxValues = options.maxValues ?? MAX_SUBMISSION_VALUES;
|
|
2096
|
+
if (submissionValues(sent, maxValues) > maxValues) {
|
|
2097
|
+
return {
|
|
2098
|
+
ok: false,
|
|
2099
|
+
errors: { "": `Too much data: the answers hold more than ${maxValues} values (items, rows and cells); the limit is ${maxValues}.` }
|
|
2100
|
+
};
|
|
2101
|
+
}
|
|
2102
|
+
const input = wellFormed(sent);
|
|
1866
2103
|
if (!input) return MALFORMED;
|
|
1867
2104
|
const ignored = /* @__PURE__ */ new Set();
|
|
1868
2105
|
const client = coerceHidden(definition, input.hidden, { sources: ["url", "host"] });
|
|
@@ -1881,12 +2118,20 @@ function validateSubmission(definition, submission, options = {}) {
|
|
|
1881
2118
|
let engine;
|
|
1882
2119
|
if (input.events) {
|
|
1883
2120
|
const events = compactEvents(input.events);
|
|
1884
|
-
|
|
1885
|
-
|
|
1886
|
-
return { ok: false, errors: { "": `Too many events: ${events.length} after compaction; the limit is ${max}.` } };
|
|
2121
|
+
if (events.length > maxEvents) {
|
|
2122
|
+
return { ok: false, errors: { "": `Too many events: ${events.length} after compaction; the limit is ${maxEvents}.` } };
|
|
1887
2123
|
}
|
|
1888
2124
|
const kept = events.filter((e) => e.type !== "answer" || accept(e.field) !== false);
|
|
1889
|
-
|
|
2125
|
+
const maxWork = options.maxReplayWork ?? MAX_REPLAY_WORK;
|
|
2126
|
+
try {
|
|
2127
|
+
engine = replayWithin(definition, kept, { hidden: engineHidden, validators: options.validators }, maxWork);
|
|
2128
|
+
} catch (e) {
|
|
2129
|
+
if (!(e instanceof ReplayWorkExceeded)) throw e;
|
|
2130
|
+
return {
|
|
2131
|
+
ok: false,
|
|
2132
|
+
errors: { "": `Too much work: replaying these events (the path and calculated fields) would take more than ${maxWork} steps; the limit is ${maxWork}.` }
|
|
2133
|
+
};
|
|
2134
|
+
}
|
|
1890
2135
|
} else {
|
|
1891
2136
|
const answers2 = {};
|
|
1892
2137
|
for (const id of Object.keys(input.answers ?? {})) {
|
|
@@ -1953,6 +2198,6 @@ function publicDefinition(definition) {
|
|
|
1953
2198
|
return JSON.parse(JSON.stringify(definition));
|
|
1954
2199
|
}
|
|
1955
2200
|
|
|
1956
|
-
export { MAX_EVENTS, publicDefinition, validateSubmission };
|
|
2201
|
+
export { MAX_EVENTS, MAX_REPLAY_WORK, MAX_SUBMISSION_VALUES, RAW_EVENTS_PER_EVENT, publicDefinition, validateSubmission };
|
|
1957
2202
|
//# sourceMappingURL=server.js.map
|
|
1958
2203
|
//# sourceMappingURL=server.js.map
|