@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/index.d.cts
CHANGED
|
@@ -581,7 +581,7 @@ declare function formatCalculated(value: Json | undefined, field: FieldBlock): s
|
|
|
581
581
|
declare const MAX_FORMULA_ITEMS = 10000;
|
|
582
582
|
/**
|
|
583
583
|
* Most list items and table rows all of a form's formulas read in one go
|
|
584
|
-
* (each
|
|
584
|
+
* (each list or table counted once, however many of its columns are read, in form order). A formula that reads
|
|
585
585
|
* an answer past the limit, or one over `MAX_FORMULA_ITEMS`, has no value
|
|
586
586
|
* (blank, never a partial total), so many big answers can't make working
|
|
587
587
|
* the values out slow either.
|
package/dist/index.d.ts
CHANGED
|
@@ -581,7 +581,7 @@ declare function formatCalculated(value: Json | undefined, field: FieldBlock): s
|
|
|
581
581
|
declare const MAX_FORMULA_ITEMS = 10000;
|
|
582
582
|
/**
|
|
583
583
|
* Most list items and table rows all of a form's formulas read in one go
|
|
584
|
-
* (each
|
|
584
|
+
* (each list or table counted once, however many of its columns are read, in form order). A formula that reads
|
|
585
585
|
* an answer past the limit, or one over `MAX_FORMULA_ITEMS`, has no value
|
|
586
586
|
* (blank, never a partial total), so many big answers can't make working
|
|
587
587
|
* the values out slow either.
|
package/dist/index.js
CHANGED
|
@@ -415,7 +415,9 @@ var round = (n, decimals) => {
|
|
|
415
415
|
return Object.is(r, -0) ? 0 : r;
|
|
416
416
|
};
|
|
417
417
|
function calculatedValue(result, field2) {
|
|
418
|
-
|
|
418
|
+
return valueAs(result, calculatedProps(field2));
|
|
419
|
+
}
|
|
420
|
+
function valueAs(result, { format, decimals }) {
|
|
419
421
|
if (result === null || Array.isArray(result)) return void 0;
|
|
420
422
|
if (format === "number" || format === "percent") {
|
|
421
423
|
const n = formulaNumber(result);
|
|
@@ -442,19 +444,6 @@ var extract = (value, column) => column !== void 0 ? value.flatMap((row) => {
|
|
|
442
444
|
const cell = row[column];
|
|
443
445
|
return typeof cell === "string" || typeof cell === "number" ? [cell] : [];
|
|
444
446
|
}) : value.flatMap((v) => typeof v === "string" || typeof v === "number" || typeof v === "boolean" ? [v] : []);
|
|
445
|
-
var extracted = /* @__PURE__ */ new WeakMap();
|
|
446
|
-
function 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) byColumn.set(key, list = ownFormulaList(extract(value, column)));
|
|
452
|
-
return list;
|
|
453
|
-
}
|
|
454
|
-
function readAnswer(value, column) {
|
|
455
|
-
if (Array.isArray(value)) return cachedList(value, column);
|
|
456
|
-
return column !== void 0 ? null : formulaInput(value);
|
|
457
|
-
}
|
|
458
447
|
function formulaInput(value, column) {
|
|
459
448
|
if (value === void 0 || value === null) return null;
|
|
460
449
|
if (Array.isArray(value) && value.length > MAX_FORMULA_ITEMS) return null;
|
|
@@ -473,9 +462,19 @@ function refNodes(node, out = []) {
|
|
|
473
462
|
else if (node.kind === "call") for (const a of node.args) refNodes(a, out);
|
|
474
463
|
return out;
|
|
475
464
|
}
|
|
465
|
+
function formulaSize(node) {
|
|
466
|
+
if (node.kind === "unary") return 1 + formulaSize(node.arg);
|
|
467
|
+
if (node.kind === "binary") return 1 + formulaSize(node.left) + formulaSize(node.right);
|
|
468
|
+
if (node.kind === "if") return 1 + formulaSize(node.test) + formulaSize(node.then) + formulaSize(node.else);
|
|
469
|
+
if (node.kind === "call") return node.args.reduce((n, a) => n + formulaSize(a), 1);
|
|
470
|
+
return 1;
|
|
471
|
+
}
|
|
472
|
+
function spend(budget, amount) {
|
|
473
|
+
return;
|
|
474
|
+
}
|
|
476
475
|
var UNREAD = /* @__PURE__ */ Symbol("unread");
|
|
477
476
|
var OVER = /* @__PURE__ */ Symbol("over");
|
|
478
|
-
function calculator(definition) {
|
|
477
|
+
function calculator(definition, work) {
|
|
479
478
|
const compiled = [];
|
|
480
479
|
const idByKey = /* @__PURE__ */ new Map();
|
|
481
480
|
for (const block of definition.blocks) {
|
|
@@ -484,7 +483,8 @@ function calculator(definition) {
|
|
|
484
483
|
}
|
|
485
484
|
for (const block of definition.blocks) {
|
|
486
485
|
if (block.kind !== "field" || !isCalculated(block)) continue;
|
|
487
|
-
const
|
|
486
|
+
const props2 = calculatedProps(block);
|
|
487
|
+
const parsed = parseFormula(props2.formula);
|
|
488
488
|
const reads = [];
|
|
489
489
|
const slots = /* @__PURE__ */ new Map();
|
|
490
490
|
const byKey = /* @__PURE__ */ new Map();
|
|
@@ -498,33 +498,64 @@ function calculator(definition) {
|
|
|
498
498
|
}
|
|
499
499
|
slots.set(ref, slot);
|
|
500
500
|
}
|
|
501
|
-
compiled.push({ field: block, ast: parsed.ok ? parsed.ast : null, reads, slots, last: null });
|
|
501
|
+
compiled.push({ field: block, props: props2, ast: parsed.ok ? parsed.ast : null, size: parsed.ok ? formulaSize(parsed.ast) : 0, reads, slots, last: null });
|
|
502
502
|
}
|
|
503
|
+
const extracted = /* @__PURE__ */ new WeakMap();
|
|
504
|
+
const cachedList = (value, column) => {
|
|
505
|
+
let byColumn = extracted.get(value);
|
|
506
|
+
if (!byColumn) extracted.set(value, byColumn = /* @__PURE__ */ new Map());
|
|
507
|
+
const key = column ?? "";
|
|
508
|
+
let list = byColumn.get(key);
|
|
509
|
+
if (!list) {
|
|
510
|
+
byColumn.set(key, list = ownFormulaList(extract(value, column)));
|
|
511
|
+
}
|
|
512
|
+
return list;
|
|
513
|
+
};
|
|
514
|
+
const readAnswer = (value, column) => {
|
|
515
|
+
if (Array.isArray(value)) return cachedList(value, column);
|
|
516
|
+
return column !== void 0 ? null : formulaInput(value);
|
|
517
|
+
};
|
|
518
|
+
const forget = (value) => {
|
|
519
|
+
if (!value || typeof value !== "object") return;
|
|
520
|
+
if (Array.isArray(value)) extracted.delete(value);
|
|
521
|
+
for (const entry of compiled) if (entry.last?.inputs.includes(value)) entry.last = null;
|
|
522
|
+
};
|
|
503
523
|
const withCalculated = (values, path) => {
|
|
504
524
|
if (!compiled.length) return values;
|
|
505
525
|
const visible = new Set(path);
|
|
506
526
|
let next = values;
|
|
527
|
+
let copied = false;
|
|
507
528
|
let budget = MAX_SETTLE_ITEMS;
|
|
508
529
|
const within = /* @__PURE__ */ new Map();
|
|
509
|
-
const input = ({ id
|
|
530
|
+
const input = ({ id }) => {
|
|
510
531
|
if (id === void 0 || !visible.has(id) || !Object.prototype.hasOwnProperty.call(next, id)) return UNREAD;
|
|
511
532
|
const value = next[id];
|
|
512
533
|
if (!Array.isArray(value)) return value;
|
|
513
|
-
let ok = within.get(
|
|
534
|
+
let ok = within.get(id);
|
|
514
535
|
if (ok === void 0) {
|
|
515
536
|
ok = value.length <= MAX_FORMULA_ITEMS && value.length <= budget;
|
|
516
537
|
if (ok) budget -= value.length;
|
|
517
|
-
within.set(
|
|
538
|
+
within.set(id, ok);
|
|
518
539
|
}
|
|
519
540
|
return ok ? value : OVER;
|
|
520
541
|
};
|
|
521
542
|
for (const entry of compiled) {
|
|
522
543
|
const { field: field2, ast } = entry;
|
|
523
|
-
const
|
|
544
|
+
const { reads } = entry;
|
|
545
|
+
const inputs = new Array(reads.length);
|
|
546
|
+
let over = false;
|
|
547
|
+
let same = entry.last !== null;
|
|
548
|
+
for (let i = 0; i < reads.length; i++) {
|
|
549
|
+
const v = input(reads[i]);
|
|
550
|
+
inputs[i] = v;
|
|
551
|
+
if (v === OVER) over = true;
|
|
552
|
+
if (same && entry.last.inputs[i] !== v) same = false;
|
|
553
|
+
}
|
|
524
554
|
let value;
|
|
525
|
-
if (
|
|
555
|
+
if (same) {
|
|
526
556
|
value = entry.last.value;
|
|
527
557
|
} else {
|
|
558
|
+
spend(work, entry.size);
|
|
528
559
|
const resolved = new Array(inputs.length);
|
|
529
560
|
const read = (ref) => {
|
|
530
561
|
const slot = entry.slots.get(ref);
|
|
@@ -532,19 +563,25 @@ function calculator(definition) {
|
|
|
532
563
|
let v = resolved[slot];
|
|
533
564
|
if (v === void 0) {
|
|
534
565
|
const raw = inputs[slot];
|
|
535
|
-
v = resolved[slot] = raw === UNREAD ? null : readAnswer(raw,
|
|
566
|
+
v = resolved[slot] = raw === UNREAD ? null : readAnswer(raw, reads[slot].column);
|
|
536
567
|
}
|
|
537
568
|
return v;
|
|
538
569
|
};
|
|
539
|
-
value = ast && !
|
|
570
|
+
value = ast && !over ? valueAs(evaluateFormula(ast, read), entry.props) : void 0;
|
|
540
571
|
entry.last = { inputs, value };
|
|
541
572
|
}
|
|
542
573
|
const current = Object.prototype.hasOwnProperty.call(next, field2.id) ? next[field2.id] : void 0;
|
|
543
|
-
if (current !== value)
|
|
574
|
+
if (current !== value) {
|
|
575
|
+
if (!copied) {
|
|
576
|
+
next = { ...next };
|
|
577
|
+
copied = true;
|
|
578
|
+
}
|
|
579
|
+
next[field2.id] = value;
|
|
580
|
+
}
|
|
544
581
|
}
|
|
545
582
|
return next;
|
|
546
583
|
};
|
|
547
|
-
return { active: compiled.length > 0, withCalculated };
|
|
584
|
+
return { active: compiled.length > 0, ids: new Set(compiled.map((c) => c.field.id)), withCalculated, forget };
|
|
548
585
|
}
|
|
549
586
|
var numberOps = ["eq", "neq", "lt", "lte", "gt", "gte", "in", "answered", "empty"];
|
|
550
587
|
var calculatedFieldType = {
|
|
@@ -1179,8 +1216,8 @@ function rulesAllow(def, blockId, scope) {
|
|
|
1179
1216
|
}
|
|
1180
1217
|
function walkPath(def, scope) {
|
|
1181
1218
|
const list = steps(def);
|
|
1182
|
-
|
|
1183
|
-
|
|
1219
|
+
let index;
|
|
1220
|
+
let position;
|
|
1184
1221
|
const jumps = (def.logic ?? []).filter(
|
|
1185
1222
|
(r) => r.effect === "jump"
|
|
1186
1223
|
);
|
|
@@ -1204,6 +1241,7 @@ function walkPath(def, scope) {
|
|
|
1204
1241
|
ending = jump.to.ending;
|
|
1205
1242
|
break;
|
|
1206
1243
|
}
|
|
1244
|
+
index ??= new Map(list.map((b, at) => [b.id, at]));
|
|
1207
1245
|
const target = index.get(jump.to);
|
|
1208
1246
|
if (target !== void 0 && target > i) {
|
|
1209
1247
|
i = nextVisible(target);
|
|
@@ -1213,6 +1251,7 @@ function walkPath(def, scope) {
|
|
|
1213
1251
|
i = nextVisible(i + 1);
|
|
1214
1252
|
}
|
|
1215
1253
|
const scopeAt = (blockId, inclusive = false) => {
|
|
1254
|
+
position ??= new Map(def.blocks.map((b, i2) => [b.id, i2]));
|
|
1216
1255
|
const at = position.get(blockId) ?? -1;
|
|
1217
1256
|
const values = /* @__PURE__ */ Object.create(null);
|
|
1218
1257
|
for (const id of path) {
|
|
@@ -2024,10 +2063,19 @@ function validateField(field2, value, options = {}) {
|
|
|
2024
2063
|
}
|
|
2025
2064
|
|
|
2026
2065
|
// src/engine.ts
|
|
2066
|
+
function readsAny(node, ids) {
|
|
2067
|
+
if (Array.isArray(node)) return node.some((n) => readsAny(n, ids));
|
|
2068
|
+
if (!node || typeof node !== "object") return false;
|
|
2069
|
+
for (const [k, v] of Object.entries(node)) {
|
|
2070
|
+
if (k === "field" && typeof v === "string" && ids.has(v)) return true;
|
|
2071
|
+
if (typeof v === "object" && readsAny(v, ids)) return true;
|
|
2072
|
+
}
|
|
2073
|
+
return false;
|
|
2074
|
+
}
|
|
2027
2075
|
function createForm(definition, options = {}) {
|
|
2028
2076
|
return build(definition, options).engine;
|
|
2029
2077
|
}
|
|
2030
|
-
function build(definition, options, replaying = false) {
|
|
2078
|
+
function build(definition, options, replaying = false, budget) {
|
|
2031
2079
|
const fields = fieldMap(definition);
|
|
2032
2080
|
const byKey = new Map([...fields.values()].map((f2) => [f2.key, f2]));
|
|
2033
2081
|
const blocks = new Map(definition.blocks.map((b) => [b.id, b]));
|
|
@@ -2045,15 +2093,24 @@ function build(definition, options, replaying = false) {
|
|
|
2045
2093
|
if (!declared.has(name)) initialHidden[name] = value;
|
|
2046
2094
|
}
|
|
2047
2095
|
Object.assign(initialHidden, coerceHidden(definition, options.hidden).values);
|
|
2048
|
-
const calc = calculator(definition);
|
|
2096
|
+
const calc = calculator(definition, budget);
|
|
2097
|
+
const pathOf = (values, hidden) => {
|
|
2098
|
+
spend(budget, definition.blocks.length);
|
|
2099
|
+
return computePath(definition, scopeOf(values, hidden));
|
|
2100
|
+
};
|
|
2101
|
+
const logicReadsCalculated = calc.active && readsAny(definition.logic, calc.ids);
|
|
2049
2102
|
const settle = (values, hidden) => {
|
|
2050
|
-
let { path } =
|
|
2103
|
+
let { path } = pathOf(values, hidden);
|
|
2051
2104
|
if (!calc.active) return { values, path };
|
|
2105
|
+
if (!logicReadsCalculated) return { values: calc.withCalculated(values, path), path };
|
|
2052
2106
|
for (let round2 = 0; round2 < 4; round2++) {
|
|
2053
2107
|
const next = calc.withCalculated(values, path);
|
|
2054
2108
|
if (next === values) break;
|
|
2055
2109
|
values = next;
|
|
2056
|
-
|
|
2110
|
+
const again = pathOf(values, hidden).path;
|
|
2111
|
+
const same = again.length === path.length && again.every((id, i) => id === path[i]);
|
|
2112
|
+
path = again;
|
|
2113
|
+
if (same) break;
|
|
2057
2114
|
}
|
|
2058
2115
|
return { values, path };
|
|
2059
2116
|
};
|
|
@@ -2095,14 +2152,30 @@ function build(definition, options, replaying = false) {
|
|
|
2095
2152
|
return errors;
|
|
2096
2153
|
};
|
|
2097
2154
|
let unsettled = false;
|
|
2098
|
-
|
|
2099
|
-
|
|
2100
|
-
unsettled
|
|
2101
|
-
|
|
2102
|
-
|
|
2155
|
+
let formulasStale = false;
|
|
2156
|
+
const settleNow = (needValues = true) => {
|
|
2157
|
+
if (unsettled && calc.active && !logicReadsCalculated && !needValues) {
|
|
2158
|
+
unsettled = false;
|
|
2159
|
+
formulasStale = true;
|
|
2160
|
+
state = { ...state, path: pathOf(state.values, state.hidden).path };
|
|
2161
|
+
return;
|
|
2162
|
+
}
|
|
2163
|
+
if (unsettled) {
|
|
2164
|
+
unsettled = false;
|
|
2165
|
+
formulasStale = false;
|
|
2166
|
+
const { values, path } = settle(state.values, state.hidden);
|
|
2167
|
+
state = { ...state, values, path };
|
|
2168
|
+
} else if (formulasStale && needValues) {
|
|
2169
|
+
formulasStale = false;
|
|
2170
|
+
state = { ...state, values: calc.withCalculated(state.values, state.path) };
|
|
2171
|
+
}
|
|
2103
2172
|
};
|
|
2104
2173
|
const answer = (id, value, event) => {
|
|
2105
|
-
if (state.status === "completed" ||
|
|
2174
|
+
if (state.status === "completed" || isCalculated(fields.get(id))) return;
|
|
2175
|
+
if (own(state.values, id) === value) {
|
|
2176
|
+
if (value === null || typeof value !== "object") return;
|
|
2177
|
+
calc.forget(value);
|
|
2178
|
+
}
|
|
2106
2179
|
const errors = { ...state.errors };
|
|
2107
2180
|
delete errors[id];
|
|
2108
2181
|
if (batch) {
|
|
@@ -2168,7 +2241,7 @@ function build(definition, options, replaying = false) {
|
|
|
2168
2241
|
emit({ ...state, errors, current: firstError, events });
|
|
2169
2242
|
return false;
|
|
2170
2243
|
}
|
|
2171
|
-
const { ending } =
|
|
2244
|
+
const { ending } = pathOf(state.values, state.hidden);
|
|
2172
2245
|
emit({ ...state, errors: {}, status: "completed", ending, events });
|
|
2173
2246
|
return true;
|
|
2174
2247
|
},
|
|
@@ -2200,10 +2273,11 @@ function build(definition, options, replaying = false) {
|
|
|
2200
2273
|
answer(event.field, event.value, logged);
|
|
2201
2274
|
}
|
|
2202
2275
|
} else if (event.type === "next") {
|
|
2203
|
-
settleNow();
|
|
2276
|
+
settleNow(false);
|
|
2277
|
+
if (formulasStale && isCalculated(fields.get(state.current ?? ""))) settleNow(true);
|
|
2204
2278
|
engine.next();
|
|
2205
2279
|
} else if (event.type === "back") {
|
|
2206
|
-
settleNow();
|
|
2280
|
+
settleNow(false);
|
|
2207
2281
|
engine.back();
|
|
2208
2282
|
} else if (event.type === "submit") {
|
|
2209
2283
|
settleNow();
|
|
@@ -2218,9 +2292,16 @@ function build(definition, options, replaying = false) {
|
|
|
2218
2292
|
return { engine, apply, finish };
|
|
2219
2293
|
}
|
|
2220
2294
|
function replay(definition, events, options = {}) {
|
|
2221
|
-
|
|
2222
|
-
|
|
2223
|
-
|
|
2295
|
+
return replayWithin(definition, events, options);
|
|
2296
|
+
}
|
|
2297
|
+
function replayWithin(definition, events, options, maxWork) {
|
|
2298
|
+
const budget = void 0 ;
|
|
2299
|
+
const { engine, apply, finish } = build(definition, { ...options, initialValues: void 0, clock: () => void 0 }, true, budget);
|
|
2300
|
+
try {
|
|
2301
|
+
for (const event of events) apply(event);
|
|
2302
|
+
finish();
|
|
2303
|
+
} finally {
|
|
2304
|
+
}
|
|
2224
2305
|
return engine;
|
|
2225
2306
|
}
|
|
2226
2307
|
|