@marktiderman/genesis-forms 0.7.0 → 0.9.0

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.
Files changed (36) hide show
  1. package/README.md +386 -8
  2. package/dist/destinations.cjs +856 -53
  3. package/dist/destinations.cjs.map +1 -1
  4. package/dist/destinations.d.cts +452 -18
  5. package/dist/destinations.d.ts +452 -18
  6. package/dist/destinations.js +826 -54
  7. package/dist/destinations.js.map +1 -1
  8. package/dist/{engine-C5tb3mkf.d.cts → engine-BaHPC2-1.d.cts} +2 -2
  9. package/dist/{engine-DcNQ-6oJ.d.ts → engine-Da99Tv-E.d.ts} +2 -2
  10. package/dist/index.cjs +996 -167
  11. package/dist/index.cjs.map +1 -1
  12. package/dist/index.d.cts +288 -11
  13. package/dist/index.d.ts +288 -11
  14. package/dist/index.js +975 -168
  15. package/dist/index.js.map +1 -1
  16. package/dist/server.cjs +563 -173
  17. package/dist/server.cjs.map +1 -1
  18. package/dist/server.d.cts +3 -474
  19. package/dist/server.d.ts +3 -474
  20. package/dist/server.js +558 -174
  21. package/dist/server.js.map +1 -1
  22. package/dist/submit-protocol-CFi-0Qd0.d.ts +840 -0
  23. package/dist/submit-protocol-D0cOckmK.d.cts +840 -0
  24. package/dist/testing.cjs +250 -154
  25. package/dist/testing.cjs.map +1 -1
  26. package/dist/testing.d.cts +4 -4
  27. package/dist/testing.d.ts +4 -4
  28. package/dist/testing.js +250 -154
  29. package/dist/testing.js.map +1 -1
  30. package/dist/{types-oAckGR_Z.d.cts → types-C0pUfew3.d.cts} +8 -2
  31. package/dist/{types-oAckGR_Z.d.ts → types-C0pUfew3.d.ts} +8 -2
  32. package/dist/{validate-BFpNJbjf.d.cts → validate-DpmBssFk.d.ts} +258 -140
  33. package/dist/{validate-qwkdcr3H.d.ts → validate-U5LoW3eR.d.cts} +258 -140
  34. package/package.json +2 -2
  35. package/dist/upload-protocol-Bw0CWRJc.d.cts +0 -122
  36. package/dist/upload-protocol-PX0jroTV.d.ts +0 -122
package/dist/index.js CHANGED
@@ -28,6 +28,45 @@ function fingerprint(value) {
28
28
  return (4294967296 * (2097151 & h2) + (h1 >>> 0)).toString(16).padStart(14, "0");
29
29
  }
30
30
 
31
+ // src/props-check.ts
32
+ var RESERVED_NAMES = new Set(Object.getOwnPropertyNames(Object.prototype));
33
+ var isPlainRecord = (v) => !!v && typeof v === "object" && !Array.isArray(v);
34
+ var isWholeNumber = (v) => typeof v === "number" && Number.isInteger(v);
35
+ var isFiniteNumber = (v) => typeof v === "number" && Number.isFinite(v);
36
+ function propsChecker(prefix, report) {
37
+ const at = (path) => path ? `${prefix}.${path}` : prefix;
38
+ const issue = (path, message2) => report(at(path), message2);
39
+ return {
40
+ issue,
41
+ text(v, path, nonEmpty = false) {
42
+ if (typeof v !== "string" || nonEmpty && !v) issue(path, nonEmpty ? "Required." : "Required text.");
43
+ },
44
+ optionalText(v, path) {
45
+ if (v !== void 0 && typeof v !== "string") issue(path, "Must be text.");
46
+ },
47
+ optionalBoolean(v, path) {
48
+ if (v !== void 0 && typeof v !== "boolean") issue(path, "Must be true or false.");
49
+ },
50
+ optionalInt(v, path, floor) {
51
+ if (v !== void 0 && !(isWholeNumber(v) && v >= floor)) issue(path, `Must be a whole number of ${floor} or more.`);
52
+ },
53
+ ordered(low, high, path, what) {
54
+ if (isWholeNumber(low) && isWholeNumber(high) && low > high) issue(path, `Can't be more than the ${what}.`);
55
+ },
56
+ name(v, path, what) {
57
+ if (typeof v !== "string" || !v) {
58
+ issue(path, `${what} must be a non-empty string.`);
59
+ return false;
60
+ }
61
+ if (RESERVED_NAMES.has(v)) {
62
+ issue(path, `"${v}" is reserved and cannot be used as ${what.toLowerCase()}.`);
63
+ return false;
64
+ }
65
+ return true;
66
+ }
67
+ };
68
+ }
69
+
31
70
  // src/formula.ts
32
71
  var FORMULA_FUNCTIONS = ["sum", "avg", "min", "max"];
33
72
  var MAX_FORMULA_LENGTH = 2e3;
@@ -37,8 +76,8 @@ var TWO_CHAR = /* @__PURE__ */ new Set(["==", "!=", "<=", ">="]);
37
76
  var ONE_CHAR = /* @__PURE__ */ new Set(["+", "-", "*", "/", "<", ">", "(", ")", "?", ":", ",", ".", "=", "\u2212", "\xD7", "\xF7", "\u2260", "\u2264", "\u2265"]);
38
77
  var COMPARISONS = /* @__PURE__ */ new Set(["==", "!=", "<", "<=", ">", ">="]);
39
78
  var FormulaError = class extends Error {
40
- constructor(message, at) {
41
- super(message);
79
+ constructor(message2, at) {
80
+ super(message2);
42
81
  this.at = at;
43
82
  }
44
83
  at;
@@ -116,8 +155,8 @@ function parseFormula(source) {
116
155
  const tok = peek();
117
156
  return tok.t === "op" && tok.v === v;
118
157
  };
119
- const expect = (v, message) => {
120
- if (!isOp(v)) throw new FormulaError(message, peek().at);
158
+ const expect = (v, message2) => {
159
+ if (!isOp(v)) throw new FormulaError(message2, peek().at);
121
160
  pos++;
122
161
  };
123
162
  const describe = (tok) => tok.t === "end" ? "the formula ends there" : `found "${String(tok.v)}"`;
@@ -591,6 +630,17 @@ var calculatedFieldType = {
591
630
  value: { json: true },
592
631
  operators: numberOps,
593
632
  a11y: { semantics: "control" },
633
+ // A number, unless the format says text; without a format it can hold either, so it isn't known.
634
+ literal: (props2) => props2.format === "text" ? "string" : props2.format === "number" || props2.format === "percent" ? "number" : void 0,
635
+ // The formula itself is checked once every key is known (`parseForm`).
636
+ checkProps: (props2, check) => {
637
+ if (props2.format !== void 0 && props2.format !== "number" && props2.format !== "percent" && props2.format !== "text") {
638
+ check.issue("format", "Must be number, percent or text.");
639
+ }
640
+ if (props2.decimals !== void 0 && !(isWholeNumber(props2.decimals) && props2.decimals >= 0 && props2.decimals <= MAX_CALCULATED_DECIMALS)) {
641
+ check.issue("decimals", `Must be a whole number from 0 to ${MAX_CALCULATED_DECIMALS}.`);
642
+ }
643
+ },
594
644
  // The engine works the value out; any number, text or true / false it made is valid.
595
645
  validate: (value) => typeof value === "number" || typeof value === "string" || typeof value === "boolean" ? null : "This value is worked out from other answers.",
596
646
  summarize: (value, field2) => formatCalculated(value, field2)
@@ -703,12 +753,42 @@ var MAX_HIGHLIGHT_QUOTE_LENGTH = 2e3;
703
753
  var HIGHLIGHT_NOTE_MESSAGE = "Add a note to each highlight.";
704
754
  var sourceText = (field2) => typeof field2.props?.text === "string" && field2.props.text ? field2.props.text : void 0;
705
755
  var STRUCTURED_FIELD_TYPES = ["list", "table", "media-notes", "text-highlights"];
756
+ var listProps = (props2, check) => {
757
+ check.optionalInt(props2.min, "min", 0);
758
+ check.optionalInt(props2.max, "max", 1);
759
+ check.optionalInt(props2.itemMaxLength, "itemMaxLength", 1);
760
+ check.optionalText(props2.placeholder, "placeholder");
761
+ check.ordered(props2.min, props2.max ?? 10, "min", "most items");
762
+ };
763
+ var tableProps = (props2, check) => {
764
+ const columns = props2.columns;
765
+ if (!Array.isArray(columns) || columns.length < 1 || columns.length > MAX_TABLE_COLUMNS) {
766
+ check.issue("columns", `Must be a list of 1 to ${MAX_TABLE_COLUMNS} columns.`);
767
+ } else {
768
+ const seen = /* @__PURE__ */ new Set();
769
+ columns.forEach((c, j) => {
770
+ const at = `columns[${j}]`;
771
+ if (!isPlainRecord(c)) return check.issue(at, "Each column needs a key and a label.");
772
+ if (check.name(c.key, `${at}.key`, "A column key")) {
773
+ if (seen.has(c.key)) check.issue(`${at}.key`, `Duplicate column key "${c.key}".`);
774
+ seen.add(c.key);
775
+ }
776
+ check.text(c.label, `${at}.label`);
777
+ check.optionalBoolean(c.long, `${at}.long`);
778
+ });
779
+ }
780
+ check.optionalInt(props2.minRows, "minRows", 0);
781
+ check.optionalInt(props2.maxRows, "maxRows", 1);
782
+ check.ordered(props2.minRows, props2.maxRows ?? 3, "minRows", "most rows");
783
+ };
706
784
  var structuredFieldTypes = [
707
785
  {
708
786
  type: "list",
709
787
  label: "List",
710
788
  category: "text",
711
789
  value: { json: true },
790
+ literal: "string",
791
+ checkProps: listProps,
712
792
  // logic.ts `contains` on an array is exact item equality, same as multi-choice, so it is offered.
713
793
  operators: ["contains", "answered", "empty"],
714
794
  a11y: { semantics: "group" },
@@ -734,6 +814,7 @@ var structuredFieldTypes = [
734
814
  label: "Table",
735
815
  category: "advanced",
736
816
  value: { json: true },
817
+ checkProps: tableProps,
737
818
  operators: ["answered", "empty"],
738
819
  a11y: { semantics: "group" },
739
820
  isEmpty: (value) => emptyList(value, (r) => isObject(r) && Object.values(r).every(blankText)),
@@ -777,6 +858,10 @@ var structuredFieldTypes = [
777
858
  label: "Notes at moments",
778
859
  category: "media",
779
860
  value: { json: true },
861
+ checkProps: (props2, check) => {
862
+ check.optionalInt(props2.maxNotes, "maxNotes", 1);
863
+ check.optionalInt(props2.textMaxLength, "textMaxLength", 1);
864
+ },
780
865
  operators: ["answered", "empty"],
781
866
  a11y: { semantics: "group" },
782
867
  isEmpty: (value) => emptyList(value, (n) => isObject(n) && blankText(n.text)),
@@ -800,6 +885,10 @@ var structuredFieldTypes = [
800
885
  label: "Highlights",
801
886
  category: "media",
802
887
  value: { json: true },
888
+ checkProps: (props2, check) => {
889
+ check.optionalText(props2.text, "text");
890
+ check.optionalInt(props2.maxHighlights, "maxHighlights", 1);
891
+ },
803
892
  operators: ["answered", "empty"],
804
893
  a11y: { semantics: "group" },
805
894
  isEmpty: (value) => emptyList(value, () => false),
@@ -892,6 +981,22 @@ var photoFieldType = {
892
981
  value: { json: true },
893
982
  operators: ["answered", "empty"],
894
983
  a11y: { semantics: "group" },
984
+ checkProps: (props2, check) => {
985
+ if (props2.maxFiles !== void 0 && !(isWholeNumber(props2.maxFiles) && props2.maxFiles >= 1 && props2.maxFiles <= MAX_PHOTO_FILES)) {
986
+ check.issue("maxFiles", `Must be a whole number from 1 to ${MAX_PHOTO_FILES}.`);
987
+ }
988
+ if (props2.maxSizeMb !== void 0 && !(isWholeNumber(props2.maxSizeMb) && props2.maxSizeMb >= 1 && props2.maxSizeMb <= MAX_PHOTO_SIZE_MB)) {
989
+ check.issue("maxSizeMb", `Must be a whole number from 1 to ${MAX_PHOTO_SIZE_MB}.`);
990
+ }
991
+ if (props2.formats !== void 0) {
992
+ const formats = props2.formats;
993
+ if (!Array.isArray(formats) || formats.length === 0 || !formats.every(isPhotoFormat)) {
994
+ check.issue("formats", "Must be a list of one or more of jpeg, png, heic and webp.");
995
+ } else if (new Set(formats).size !== formats.length) {
996
+ check.issue("formats", "Name each format once.");
997
+ }
998
+ }
999
+ },
895
1000
  isEmpty: (value) => value === void 0 || value === null || Array.isArray(value) && value.length === 0,
896
1001
  validate: (value, field2) => {
897
1002
  if (!Array.isArray(value) || !value.every(isPhotoRef)) return "Add a photo.";
@@ -909,18 +1014,48 @@ var photoFieldType = {
909
1014
  };
910
1015
 
911
1016
  // src/fields.ts
912
- var REGISTRY = /* @__PURE__ */ Symbol.for("@marktiderman/genesis-forms/field-types");
1017
+ var VALIDATOR_FAILED_MESSAGE = "This answer couldn't be checked.";
1018
+ var REGISTRY = /* @__PURE__ */ Symbol.for("@marktiderman/genesis-forms/field-types@2");
1019
+ var BUILT_IN = /* @__PURE__ */ Symbol.for("@marktiderman/genesis-forms/built-in-field-types@2");
913
1020
  var globalScope = globalThis;
914
1021
  var registry = globalScope[REGISTRY] ??= /* @__PURE__ */ new Map();
915
- function registerFieldType(manifest) {
916
- if (!manifest.type) throw new Error("registerFieldType: a field type needs a `type`.");
1022
+ var builtIn = globalScope[BUILT_IN] ??= /* @__PURE__ */ new Set();
1023
+ var freezeRecord = (v) => v && typeof v === "object" && !Array.isArray(v) ? Object.freeze({ ...v }) : v;
1024
+ function frozen(manifest) {
1025
+ const copy = { ...manifest };
1026
+ for (const key of ["choice", "a11y", "value"]) if (key in copy) copy[key] = freezeRecord(copy[key]);
1027
+ if (Array.isArray(copy.operators)) {
1028
+ const operators = Object.freeze([...copy.operators]);
1029
+ Object.defineProperty(copy, "operators", { get: () => [...operators], enumerable: true });
1030
+ }
1031
+ return Object.freeze(copy);
1032
+ }
1033
+ function registerFieldType(manifest, options = {}) {
1034
+ if (!manifest || typeof manifest.type !== "string" || !manifest.type) {
1035
+ throw new Error("registerFieldType: a field type needs a `type`.");
1036
+ }
917
1037
  const value = manifest.value;
918
- if (!value.json && (typeof value.serialize !== "function" || typeof value.deserialize !== "function")) {
1038
+ if (value && !value.json && (typeof value.serialize !== "function" || typeof value.deserialize !== "function")) {
919
1039
  throw new Error(
920
1040
  `registerFieldType: "${manifest.type}" stores a non-JSON value, so it must provide both serialize and deserialize.`
921
1041
  );
922
1042
  }
923
- registry.set(manifest.type, manifest);
1043
+ if (!value || !Array.isArray(manifest.operators)) {
1044
+ throw new Error(`registerFieldType: "${manifest.type}" needs a \`value\` and a list of \`operators\`.`);
1045
+ }
1046
+ if (manifest.choice !== void 0 && typeof manifest.choice?.multiple !== "boolean") {
1047
+ throw new Error(`registerFieldType: "${manifest.type}" has a \`choice\` without \`multiple: true | false\`.`);
1048
+ }
1049
+ if (builtIn.has(manifest.type) && options.replace !== true) {
1050
+ throw new Error(
1051
+ `registerFieldType: "${manifest.type}" is a built-in field type; pass { replace: true } to replace it for every form in this process.`
1052
+ );
1053
+ }
1054
+ registry.set(manifest.type, frozen(withChoiceDefaults(manifest)));
1055
+ }
1056
+ function registerBuiltIn(manifest) {
1057
+ builtIn.add(manifest.type);
1058
+ if (!registry.has(manifest.type)) registry.set(manifest.type, frozen(manifest));
924
1059
  }
925
1060
  function getFieldType(type) {
926
1061
  return registry.get(type);
@@ -928,6 +1063,67 @@ function getFieldType(type) {
928
1063
  function listFieldTypes() {
929
1064
  return [...registry.values()];
930
1065
  }
1066
+ function isBuiltInFieldType(type) {
1067
+ return builtIn.has(type);
1068
+ }
1069
+ function choiceKind(type) {
1070
+ return registry.get(type)?.choice;
1071
+ }
1072
+ function isChoiceField(block) {
1073
+ return !!block && block.kind === "field" && typeof block.type === "string" && choiceKind(block.type) !== void 0;
1074
+ }
1075
+ function literalType(type, props2) {
1076
+ const literal = registry.get(type)?.literal;
1077
+ if (typeof literal !== "function") return literal;
1078
+ try {
1079
+ return literal(props2);
1080
+ } catch {
1081
+ return void 0;
1082
+ }
1083
+ }
1084
+ var CONTENT_REGISTRY = /* @__PURE__ */ Symbol.for("@marktiderman/genesis-forms/content-types@1");
1085
+ var contentScope = globalThis;
1086
+ var contentRegistry = contentScope[CONTENT_REGISTRY] ??= /* @__PURE__ */ new Map();
1087
+ var BUILT_IN_CONTENT = /* @__PURE__ */ new Set(["heading", "text"]);
1088
+ function registerContentType(manifest, options = {}) {
1089
+ if (!manifest || typeof manifest.type !== "string" || !manifest.type) {
1090
+ throw new Error("registerContentType: a content type needs a `type`.");
1091
+ }
1092
+ if (typeof manifest.label !== "string") throw new Error(`registerContentType: "${manifest.type}" needs a \`label\`.`);
1093
+ for (const hook of ["checkProps", "text"]) {
1094
+ if (manifest[hook] !== void 0 && typeof manifest[hook] !== "function") {
1095
+ throw new Error(`registerContentType: "${manifest.type}" has a \`${hook}\` that isn't a function.`);
1096
+ }
1097
+ }
1098
+ if (BUILT_IN_CONTENT.has(manifest.type) && options.replace !== true) {
1099
+ throw new Error(
1100
+ `registerContentType: "${manifest.type}" is a built-in content type; pass { replace: true } to replace it for every form in this process.`
1101
+ );
1102
+ }
1103
+ contentRegistry.set(manifest.type, frozen(manifest));
1104
+ }
1105
+ function getContentType(type) {
1106
+ return contentRegistry.get(type);
1107
+ }
1108
+ function listContentTypes() {
1109
+ return [...contentRegistry.values()];
1110
+ }
1111
+ function contentText(block) {
1112
+ const text = contentRegistry.get(block.type)?.text;
1113
+ if (!text) return block.text;
1114
+ try {
1115
+ const shown2 = text(block);
1116
+ return typeof shown2 === "string" ? shown2 : block.text;
1117
+ } catch {
1118
+ return block.text;
1119
+ }
1120
+ }
1121
+ for (const manifest of [
1122
+ { type: "heading", label: "Heading" },
1123
+ { type: "text", label: "Text" }
1124
+ ]) {
1125
+ if (!contentRegistry.has(manifest.type)) contentRegistry.set(manifest.type, frozen(manifest));
1126
+ }
931
1127
  function isEmptyValue(value, field2) {
932
1128
  const manifest = field2 ? registry.get(field2.type) : void 0;
933
1129
  if (manifest?.isEmpty) return manifest.isEmpty(value);
@@ -1113,6 +1309,27 @@ function isPhoneNumber(value) {
1113
1309
  }
1114
1310
  return digits >= 7 && digits <= 15;
1115
1311
  }
1312
+ var oneOptionValidate = (value, field2) => typeof value === "string" && (choiceOptions(field2).some((o) => o.value === value) || typedOther(field2, value)) ? null : "Choose one of the options.";
1313
+ var oneOptionSummary = (value, field2) => optionLabel(field2, String(value));
1314
+ var optionsValidate = (value, field2) => {
1315
+ const allowed = new Set(choiceOptions(field2).map((o) => o.value));
1316
+ const others = Array.isArray(value) ? value.filter((v) => typeof v === "string" && !allowed.has(v)) : [];
1317
+ return Array.isArray(value) && new Set(value).size === value.length && value.every((v) => typeof v === "string") && (others.length === 0 || others.length === 1 && typedOther(field2, others[0])) ? null : "Choose from the options.";
1318
+ };
1319
+ var optionsSummary = (value, field2) => Array.isArray(value) ? value.map((v) => optionLabel(field2, String(v))).join(", ") : String(value);
1320
+ function withChoiceDefaults(manifest) {
1321
+ if (!manifest.choice || manifest.validate && manifest.summarize) return manifest;
1322
+ const multiple = manifest.choice.multiple;
1323
+ return {
1324
+ ...manifest,
1325
+ validate: manifest.validate ?? (multiple ? optionsValidate : oneOptionValidate),
1326
+ summarize: manifest.summarize ?? (multiple ? optionsSummary : oneOptionSummary)
1327
+ };
1328
+ }
1329
+ var scaleProps = (props2, check) => {
1330
+ check.optionalText(props2.lowLabel, "lowLabel");
1331
+ check.optionalText(props2.highLabel, "highLabel");
1332
+ };
1116
1333
  var builtins = [
1117
1334
  {
1118
1335
  type: "short-text",
@@ -1121,6 +1338,7 @@ var builtins = [
1121
1338
  value: { json: true },
1122
1339
  operators: textOps,
1123
1340
  a11y: { semantics: "control" },
1341
+ literal: "string",
1124
1342
  validate: textValidate(500)
1125
1343
  },
1126
1344
  {
@@ -1130,6 +1348,7 @@ var builtins = [
1130
1348
  value: { json: true },
1131
1349
  operators: textOps,
1132
1350
  a11y: { semantics: "control" },
1351
+ literal: "string",
1133
1352
  validate: textValidate(5e3)
1134
1353
  },
1135
1354
  {
@@ -1139,6 +1358,7 @@ var builtins = [
1139
1358
  value: { json: true },
1140
1359
  operators: textOps,
1141
1360
  a11y: { semantics: "control", autocomplete: "email", inputMode: "email" },
1361
+ literal: "string",
1142
1362
  validate: (value) => typeof value === "string" && isEmailAddress(value.trim()) ? null : "Enter an email address like name@example.com.",
1143
1363
  normalize: (value) => typeof value === "string" ? value.trim() : value
1144
1364
  },
@@ -1149,6 +1369,7 @@ var builtins = [
1149
1369
  value: { json: true },
1150
1370
  operators: ["eq", "neq", "answered", "empty"],
1151
1371
  a11y: { semantics: "control", autocomplete: "tel", inputMode: "tel" },
1372
+ literal: "string",
1152
1373
  validate: (value) => typeof value === "string" && isPhoneNumber(value.trim()) ? null : "Enter a phone number with 7 to 15 digits.",
1153
1374
  normalize: (value) => typeof value === "string" ? value.trim() : value,
1154
1375
  summarize: (value) => String(value)
@@ -1160,6 +1381,13 @@ var builtins = [
1160
1381
  value: { json: true },
1161
1382
  operators: numberOps2,
1162
1383
  a11y: { semantics: "group" },
1384
+ literal: "number",
1385
+ checkProps: (props2, check) => {
1386
+ scaleProps(props2, check);
1387
+ if (props2.display !== void 0 && props2.display !== "numbers" && props2.display !== "stars") {
1388
+ check.issue("display", "Must be numbers or stars.");
1389
+ }
1390
+ },
1163
1391
  validate: (value, field2) => {
1164
1392
  const { min, max } = intRange(field2, 1, 5);
1165
1393
  return Number.isInteger(value) && value >= min && value <= max ? null : `Choose a rating from ${min} to ${max}.`;
@@ -1177,6 +1405,8 @@ var builtins = [
1177
1405
  value: { json: true },
1178
1406
  operators: numberOps2,
1179
1407
  a11y: { semantics: "group" },
1408
+ literal: "number",
1409
+ checkProps: scaleProps,
1180
1410
  validate: (value) => Number.isInteger(value) && value >= 0 && value <= 10 ? null : "Choose a score from 0 to 10.",
1181
1411
  summarize: (value, field2) => {
1182
1412
  const end = scaleEndLabel(field2, value, 0, 10);
@@ -1190,8 +1420,10 @@ var builtins = [
1190
1420
  value: { json: true },
1191
1421
  operators: ["eq", "neq", "in", "answered", "empty"],
1192
1422
  a11y: { semantics: "group" },
1193
- validate: (value, field2) => typeof value === "string" && (choiceOptions(field2).some((o) => o.value === value) || typedOther(field2, value)) ? null : "Choose one of the options.",
1194
- summarize: (value, field2) => optionLabel(field2, String(value))
1423
+ literal: "string",
1424
+ choice: { multiple: false },
1425
+ validate: oneOptionValidate,
1426
+ summarize: oneOptionSummary
1195
1427
  },
1196
1428
  {
1197
1429
  type: "multi-choice",
@@ -1200,12 +1432,10 @@ var builtins = [
1200
1432
  value: { json: true },
1201
1433
  operators: ["contains", "answered", "empty"],
1202
1434
  a11y: { semantics: "group" },
1203
- validate: (value, field2) => {
1204
- const allowed = new Set(choiceOptions(field2).map((o) => o.value));
1205
- const others = Array.isArray(value) ? value.filter((v) => typeof v === "string" && !allowed.has(v)) : [];
1206
- return Array.isArray(value) && new Set(value).size === value.length && value.every((v) => typeof v === "string") && (others.length === 0 || others.length === 1 && typedOther(field2, others[0])) ? null : "Choose from the options.";
1207
- },
1208
- summarize: (value, field2) => Array.isArray(value) ? value.map((v) => optionLabel(field2, String(v))).join(", ") : String(value)
1435
+ literal: "string",
1436
+ choice: { multiple: true },
1437
+ validate: optionsValidate,
1438
+ summarize: optionsSummary
1209
1439
  },
1210
1440
  {
1211
1441
  type: "yes-no",
@@ -1214,6 +1444,7 @@ var builtins = [
1214
1444
  value: { json: true },
1215
1445
  operators: ["eq", "neq", "answered", "empty"],
1216
1446
  a11y: { semantics: "group" },
1447
+ literal: "boolean",
1217
1448
  validate: (value) => typeof value === "boolean" ? null : "Choose yes or no.",
1218
1449
  summarize: (value) => value ? "Yes" : "No"
1219
1450
  },
@@ -1224,8 +1455,10 @@ var builtins = [
1224
1455
  value: { json: true },
1225
1456
  operators: ["eq", "neq", "in", "answered", "empty"],
1226
1457
  a11y: { semantics: "control" },
1227
- validate: (value, field2) => typeof value === "string" && (choiceOptions(field2).some((o) => o.value === value) || typedOther(field2, value)) ? null : "Choose one of the options.",
1228
- summarize: (value, field2) => optionLabel(field2, String(value))
1458
+ literal: "string",
1459
+ choice: { multiple: false },
1460
+ validate: oneOptionValidate,
1461
+ summarize: oneOptionSummary
1229
1462
  },
1230
1463
  {
1231
1464
  type: "number",
@@ -1234,6 +1467,16 @@ var builtins = [
1234
1467
  value: { json: true },
1235
1468
  operators: numberOps2,
1236
1469
  a11y: { semantics: "control", inputMode: "decimal" },
1470
+ literal: "number",
1471
+ checkProps: (props2, check) => {
1472
+ for (const k of ["min", "max", "step"]) {
1473
+ if (props2[k] !== void 0 && !isFiniteNumber(props2[k])) check.issue(k, "Must be a number.");
1474
+ }
1475
+ if (isFiniteNumber(props2.step) && props2.step <= 0) check.issue("step", "Must be above 0.");
1476
+ if (isFiniteNumber(props2.min) && isFiniteNumber(props2.max) && props2.min > props2.max) {
1477
+ check.issue("min", "Can't be more than the largest number.");
1478
+ }
1479
+ },
1237
1480
  validate: (value, field2) => {
1238
1481
  if (typeof value !== "number" || !Number.isFinite(value)) return "Enter a number.";
1239
1482
  const { min, max, step } = numberLimits(field2);
@@ -1260,6 +1503,17 @@ var builtins = [
1260
1503
  value: { json: true },
1261
1504
  operators: ["eq", "neq", "answered", "empty"],
1262
1505
  a11y: { semantics: "control" },
1506
+ literal: "string",
1507
+ checkProps: (props2, check) => {
1508
+ for (const k of ["min", "max"]) {
1509
+ if (props2[k] !== void 0 && !(typeof props2[k] === "string" && isIsoDate(props2[k]))) {
1510
+ check.issue(k, "Must be a date like 2026-10-22.");
1511
+ }
1512
+ }
1513
+ if (typeof props2.min === "string" && typeof props2.max === "string" && isIsoDate(props2.min) && isIsoDate(props2.max) && props2.min > props2.max) {
1514
+ check.issue("min", "Can't be after the latest date.");
1515
+ }
1516
+ },
1263
1517
  validate: (value, field2) => {
1264
1518
  if (typeof value !== "string" || !isIsoDate(value.trim())) return "Enter a date like 2026-10-22.";
1265
1519
  const date = value.trim();
@@ -1282,6 +1536,8 @@ var builtins = [
1282
1536
  value: { json: true },
1283
1537
  operators: ["answered", "empty"],
1284
1538
  a11y: { semantics: "group" },
1539
+ literal: "boolean",
1540
+ checkProps: (props2, check) => check.optionalText(props2.text, "text"),
1285
1541
  // Unchecked is unanswered, so a required consent must be checked.
1286
1542
  isEmpty: (value) => value === void 0 || value === null || value === false,
1287
1543
  validate: (value) => value === true ? null : "Check the box to agree.",
@@ -1334,7 +1590,7 @@ function dateLimits(field2) {
1334
1590
  if (typeof max === "string" && isIsoDate(max)) out.max = max;
1335
1591
  return out;
1336
1592
  }
1337
- for (const manifest of [...builtins, ...structuredFieldTypes, calculatedFieldType, photoFieldType]) registerFieldType(manifest);
1593
+ for (const manifest of [...builtins, ...structuredFieldTypes, calculatedFieldType, photoFieldType]) registerBuiltIn(manifest);
1338
1594
 
1339
1595
  // src/hidden.ts
1340
1596
  var MAX_HIDDEN_STRING_LENGTH = 1e3;
@@ -1510,7 +1766,8 @@ function fieldMap(def) {
1510
1766
 
1511
1767
  // src/options.ts
1512
1768
  var CHOICE_FIELD_TYPES = /* @__PURE__ */ new Set(["single-choice", "multi-choice", "dropdown"]);
1513
- var isChoice = (block) => !!block && block.kind === "field" && CHOICE_FIELD_TYPES.has(block.type);
1769
+ var isChoice = isChoiceField;
1770
+ var isMultiple = (field2) => choiceKind(field2.type)?.multiple === true;
1514
1771
  function put(record, key, value) {
1515
1772
  Object.defineProperty(record, key, { value, enumerable: true, writable: true, configurable: true });
1516
1773
  }
@@ -1605,7 +1862,7 @@ function isPrefillParam(param) {
1605
1862
  return typeof param === "string" && param.length <= MAX_PREFILL_PARAM_LENGTH && /^[A-Za-z0-9_.-]+$/.test(param);
1606
1863
  }
1607
1864
  function prefillSetting(field2) {
1608
- if (!CHOICE_FIELD_TYPES.has(field2.type)) return void 0;
1865
+ if (!choiceKind(field2.type)) return void 0;
1609
1866
  const raw = field2.props?.prefill;
1610
1867
  if (!raw || typeof raw !== "object" || Array.isArray(raw) || !isPrefillParam(raw.param)) return void 0;
1611
1868
  return { param: raw.param, hideWhenPrefilled: raw.hideWhenPrefilled === true };
@@ -1633,7 +1890,7 @@ function prefillFromLink(definition, params, values = {}) {
1633
1890
  if (!raw.length) continue;
1634
1891
  const shown2 = new Set(visibleOptions(definition, block, seen).map((o) => o.value));
1635
1892
  let value;
1636
- if (block.type === "multi-choice") {
1893
+ if (isMultiple(block)) {
1637
1894
  const picked = [];
1638
1895
  for (const r of raw) {
1639
1896
  for (const v of shown2.has(r) ? [r] : r.split(",").map((s) => s.trim())) {
@@ -1682,6 +1939,21 @@ function snapshotOf(byValue, value) {
1682
1939
  function answerSnapshots(definition, answers) {
1683
1940
  const out = {};
1684
1941
  for (const block of definition.blocks) {
1942
+ if (block.kind !== "field") continue;
1943
+ const manifest = getFieldType(block.type);
1944
+ const custom = manifest?.choice ? void 0 : manifest?.snapshot;
1945
+ if (custom) {
1946
+ const value2 = own(answers, block.id);
1947
+ if (isEmptyValue(value2, block)) continue;
1948
+ let snapshot2;
1949
+ try {
1950
+ snapshot2 = custom(value2, block);
1951
+ } catch {
1952
+ snapshot2 = void 0;
1953
+ }
1954
+ if (snapshot2 !== void 0) put(out, block.id, snapshot2);
1955
+ continue;
1956
+ }
1685
1957
  if (!isChoice(block) || !optionsSource(block)) continue;
1686
1958
  const value = own(answers, block.id);
1687
1959
  if (isEmptyValue(value, block)) continue;
@@ -1699,6 +1971,22 @@ function answersFromSnapshots(definition, snapshots) {
1699
1971
  const answers = {};
1700
1972
  const missing = {};
1701
1973
  for (const block of definition.blocks) {
1974
+ if (block.kind !== "field") continue;
1975
+ const manifest = getFieldType(block.type);
1976
+ const custom = manifest?.choice ? void 0 : manifest?.fromSnapshot;
1977
+ if (custom) {
1978
+ const raw2 = own(snapshots, block.id);
1979
+ if (raw2 === void 0) continue;
1980
+ let restored;
1981
+ try {
1982
+ restored = custom(raw2, block);
1983
+ } catch {
1984
+ restored = void 0;
1985
+ }
1986
+ if (restored?.value !== void 0) put(answers, block.id, restored.value);
1987
+ if (Array.isArray(restored?.missing) && restored.missing.length) put(missing, block.id, restored.missing);
1988
+ continue;
1989
+ }
1702
1990
  if (!isChoice(block)) continue;
1703
1991
  const raw = own(snapshots, block.id);
1704
1992
  const list = (Array.isArray(raw) ? raw : raw === void 0 ? [] : [raw]).flatMap((s) => {
@@ -1721,7 +2009,7 @@ function answersFromSnapshots(definition, snapshots) {
1721
2009
  if (value === void 0) lost.push(s);
1722
2010
  else if (!found.includes(value)) found.push(value);
1723
2011
  }
1724
- if (found.length) put(answers, block.id, block.type === "multi-choice" ? found : found[0]);
2012
+ if (found.length) put(answers, block.id, isMultiple(block) ? found : found[0]);
1725
2013
  if (lost.length) put(missing, block.id, lost);
1726
2014
  }
1727
2015
  return { answers, missing };
@@ -1758,25 +2046,6 @@ function shown(v) {
1758
2046
  }
1759
2047
  var isObject2 = (v) => !!v && typeof v === "object" && !Array.isArray(v);
1760
2048
  var isInt = (v) => typeof v === "number" && Number.isInteger(v);
1761
- var LITERAL_TYPE = {
1762
- "short-text": "string",
1763
- "long-text": "string",
1764
- email: "string",
1765
- phone: "string",
1766
- rating: "number",
1767
- nps: "number",
1768
- "single-choice": "string",
1769
- "multi-choice": "string",
1770
- "yes-no": "boolean",
1771
- list: "string",
1772
- dropdown: "string",
1773
- number: "number",
1774
- date: "string",
1775
- consent: "boolean",
1776
- // A calculated field holds a number unless its format says text; see `literalType`.
1777
- calculated: "number"
1778
- };
1779
- var CHOICE_TYPES = /* @__PURE__ */ new Set(["single-choice", "multi-choice", "dropdown"]);
1780
2049
  var ORDERING = /* @__PURE__ */ new Set(["lt", "lte", "gt", "gte"]);
1781
2050
  var OPERATORS = /* @__PURE__ */ new Set([
1782
2051
  "eq",
@@ -1790,13 +2059,16 @@ var OPERATORS = /* @__PURE__ */ new Set([
1790
2059
  "answered",
1791
2060
  "empty"
1792
2061
  ]);
1793
- var RESERVED = new Set(Object.getOwnPropertyNames(Object.prototype));
1794
2062
  var MAX_CONDITION_DEPTH = 32;
2063
+ function contentKinds() {
2064
+ const names = listContentTypes().map((c) => c.type);
2065
+ return names.length > 1 ? `${names.slice(0, -1).join(", ")} or ${names[names.length - 1]}` : names[0] ?? "a registered content type";
2066
+ }
1795
2067
  function parseForm(input) {
1796
2068
  const issues = [];
1797
2069
  const warnings = [];
1798
2070
  const fail = () => ({ ok: false, issues, warnings });
1799
- const issue = (path, message) => void issues.push({ path, message });
2071
+ const issue = (path, message2) => void issues.push({ path, message: message2 });
1800
2072
  const text = (v, path, nonEmpty = false) => {
1801
2073
  if (typeof v !== "string" || nonEmpty && !v) issue(path, nonEmpty ? "Required." : "Required text.");
1802
2074
  };
@@ -1821,7 +2093,7 @@ function parseForm(input) {
1821
2093
  }
1822
2094
  const fits = (x) => typeof x === "string" && x.length <= OPTION_LIMITS.attributeValue;
1823
2095
  for (const [name2, v] of Object.entries(attributes)) {
1824
- if (!name2.trim() || name2.length > OPTION_LIMITS.attributeName || RESERVED.has(name2)) {
2096
+ if (!name2.trim() || name2.length > OPTION_LIMITS.attributeName || RESERVED_NAMES.has(name2)) {
1825
2097
  issue(`${at}.attributes`, `${shown(name2)} isn't a usable attribute name (at most ${OPTION_LIMITS.attributeName} characters).`);
1826
2098
  } else if (!fits(v) && !(Array.isArray(v) && v.length <= OPTION_LIMITS.attributeItems && v.every(fits))) {
1827
2099
  issue(
@@ -1836,104 +2108,18 @@ function parseForm(input) {
1836
2108
  issue(path, `${what} must be a non-empty string.`);
1837
2109
  return false;
1838
2110
  }
1839
- if (RESERVED.has(v)) {
2111
+ if (RESERVED_NAMES.has(v)) {
1840
2112
  issue(path, `"${v}" is reserved and cannot be used as ${what.toLowerCase()}.`);
1841
2113
  return false;
1842
2114
  }
1843
2115
  return true;
1844
2116
  };
1845
- const optionalInt = (v, path, floor) => {
1846
- if (v !== void 0 && !(isInt(v) && v >= floor)) issue(path, `Must be a whole number of ${floor} or more.`);
1847
- };
1848
- const ordered = (low, high, path, what) => {
1849
- if (isInt(low) && isInt(high) && low > high) issue(path, `Can't be more than the ${what}.`);
1850
- };
1851
- const structuredProps = (type, props2, path) => {
1852
- if (type === "list") {
1853
- optionalInt(props2.min, `${path}.min`, 0);
1854
- optionalInt(props2.max, `${path}.max`, 1);
1855
- optionalInt(props2.itemMaxLength, `${path}.itemMaxLength`, 1);
1856
- optionalText(props2.placeholder, `${path}.placeholder`);
1857
- ordered(props2.min, props2.max ?? 10, `${path}.min`, "most items");
1858
- } else if (type === "table") {
1859
- const columns = props2.columns;
1860
- if (!Array.isArray(columns) || columns.length < 1 || columns.length > MAX_TABLE_COLUMNS) {
1861
- issue(`${path}.columns`, `Must be a list of 1 to ${MAX_TABLE_COLUMNS} columns.`);
1862
- } else {
1863
- const seen = /* @__PURE__ */ new Set();
1864
- columns.forEach((c, j) => {
1865
- const at = `${path}.columns[${j}]`;
1866
- if (!isObject2(c)) return issue(at, "Each column needs a key and a label.");
1867
- if (name(c.key, `${at}.key`, "A column key")) {
1868
- if (seen.has(c.key)) issue(`${at}.key`, `Duplicate column key "${c.key}".`);
1869
- seen.add(c.key);
1870
- }
1871
- text(c.label, `${at}.label`);
1872
- optionalBoolean(c.long, `${at}.long`);
1873
- });
1874
- }
1875
- optionalInt(props2.minRows, `${path}.minRows`, 0);
1876
- optionalInt(props2.maxRows, `${path}.maxRows`, 1);
1877
- ordered(props2.minRows, props2.maxRows ?? 3, `${path}.minRows`, "most rows");
1878
- } else if (type === "media-notes") {
1879
- optionalInt(props2.maxNotes, `${path}.maxNotes`, 1);
1880
- optionalInt(props2.textMaxLength, `${path}.textMaxLength`, 1);
1881
- } else if (type === "text-highlights") {
1882
- optionalText(props2.text, `${path}.text`);
1883
- optionalInt(props2.maxHighlights, `${path}.maxHighlights`, 1);
1884
- } else if (type === CALCULATED_TYPE) {
1885
- if (props2.format !== void 0 && props2.format !== "number" && props2.format !== "percent" && props2.format !== "text") {
1886
- issue(`${path}.format`, "Must be number, percent or text.");
1887
- }
1888
- if (props2.decimals !== void 0 && !(isInt(props2.decimals) && props2.decimals >= 0 && props2.decimals <= MAX_CALCULATED_DECIMALS)) {
1889
- issue(`${path}.decimals`, `Must be a whole number from 0 to ${MAX_CALCULATED_DECIMALS}.`);
1890
- }
1891
- }
1892
- };
1893
- const isFiniteNumber = (v) => typeof v === "number" && Number.isFinite(v);
1894
- const fieldProps = (type, props2, path) => {
1895
- if (type === "rating" || type === "nps") {
1896
- optionalText(props2.lowLabel, `${path}.lowLabel`);
1897
- optionalText(props2.highLabel, `${path}.highLabel`);
1898
- }
1899
- if (type === "rating" && props2.display !== void 0 && props2.display !== "numbers" && props2.display !== "stars") {
1900
- issue(`${path}.display`, "Must be numbers or stars.");
1901
- }
1902
- if (type === "number") {
1903
- for (const k of ["min", "max", "step"]) {
1904
- if (props2[k] !== void 0 && !isFiniteNumber(props2[k])) issue(`${path}.${k}`, "Must be a number.");
1905
- }
1906
- if (isFiniteNumber(props2.step) && props2.step <= 0) issue(`${path}.step`, "Must be above 0.");
1907
- if (isFiniteNumber(props2.min) && isFiniteNumber(props2.max) && props2.min > props2.max) {
1908
- issue(`${path}.min`, "Can't be more than the largest number.");
1909
- }
1910
- }
1911
- if (type === "date") {
1912
- for (const k of ["min", "max"]) {
1913
- if (props2[k] !== void 0 && !(typeof props2[k] === "string" && isIsoDate(props2[k]))) {
1914
- issue(`${path}.${k}`, "Must be a date like 2026-10-22.");
1915
- }
1916
- }
1917
- if (typeof props2.min === "string" && typeof props2.max === "string" && isIsoDate(props2.min) && isIsoDate(props2.max) && props2.min > props2.max) {
1918
- issue(`${path}.min`, "Can't be after the latest date.");
1919
- }
1920
- }
1921
- if (type === "consent") optionalText(props2.text, `${path}.text`);
1922
- if (type === PHOTO_TYPE) {
1923
- if (props2.maxFiles !== void 0 && !(isInt(props2.maxFiles) && props2.maxFiles >= 1 && props2.maxFiles <= MAX_PHOTO_FILES)) {
1924
- issue(`${path}.maxFiles`, `Must be a whole number from 1 to ${MAX_PHOTO_FILES}.`);
1925
- }
1926
- if (props2.maxSizeMb !== void 0 && !(isInt(props2.maxSizeMb) && props2.maxSizeMb >= 1 && props2.maxSizeMb <= MAX_PHOTO_SIZE_MB)) {
1927
- issue(`${path}.maxSizeMb`, `Must be a whole number from 1 to ${MAX_PHOTO_SIZE_MB}.`);
1928
- }
1929
- if (props2.formats !== void 0) {
1930
- const formats = props2.formats;
1931
- if (!Array.isArray(formats) || formats.length === 0 || !formats.every(isPhotoFormat)) {
1932
- issue(`${path}.formats`, "Must be a list of one or more of jpeg, png, heic and webp.");
1933
- } else if (new Set(formats).size !== formats.length) {
1934
- issue(`${path}.formats`, "Name each format once.");
1935
- }
1936
- }
2117
+ const kindProps = (check, props2, path) => {
2118
+ if (!check) return;
2119
+ try {
2120
+ check(props2, propsChecker(path, issue));
2121
+ } catch {
2122
+ issue(path, "These settings couldn't be checked.");
1937
2123
  }
1938
2124
  };
1939
2125
  if (!isObject2(input)) {
@@ -1972,7 +2158,7 @@ function parseForm(input) {
1972
2158
  const order = /* @__PURE__ */ new Map();
1973
2159
  const tableColumnKeys = /* @__PURE__ */ new Map();
1974
2160
  const choices = /* @__PURE__ */ new Map();
1975
- const calculatedLiteral = /* @__PURE__ */ new Map();
2161
+ const literals = /* @__PURE__ */ new Map();
1976
2162
  input.blocks.forEach((block, i) => {
1977
2163
  const path = `blocks[${i}]`;
1978
2164
  if (!isObject2(block) || typeof block.id !== "string" || !block.id) {
@@ -2000,7 +2186,7 @@ function parseForm(input) {
2000
2186
  if (from !== void 0) {
2001
2187
  if (!isObject2(from) || from.source !== "host" || typeof from.key !== "string" || !from.key) {
2002
2188
  issue(`${path}.props.optionsFrom`, 'Must be { source: "host", key } with a non-empty key.');
2003
- } else if (typeof block.type === "string" && !CHOICE_TYPES.has(block.type)) {
2189
+ } else if (typeof block.type === "string" && !choiceKind(block.type)) {
2004
2190
  warnings.push({ path: `${path}.props.optionsFrom`, message: "Only single choice, multiple choice and dropdown questions take options from the app." });
2005
2191
  }
2006
2192
  const filter = isObject2(from) ? from.filter : void 0;
@@ -2022,7 +2208,7 @@ function parseForm(input) {
2022
2208
  );
2023
2209
  } else {
2024
2210
  optionalBoolean(prefill.hideWhenPrefilled, `${path}.props.prefill.hideWhenPrefilled`);
2025
- if (typeof block.type === "string" && !CHOICE_TYPES.has(block.type)) {
2211
+ if (typeof block.type === "string" && !choiceKind(block.type)) {
2026
2212
  warnings.push({ path: `${path}.props.prefill`, message: "Only single choice, multiple choice and dropdown questions are filled in from a link." });
2027
2213
  }
2028
2214
  }
@@ -2053,7 +2239,7 @@ function parseForm(input) {
2053
2239
  if (other !== void 0) {
2054
2240
  if (other !== true && other !== false && !(isObject2(other) && (other.label === void 0 || typeof other.label === "string"))) {
2055
2241
  issue(`${path}.props.other`, "Must be true, false or { label }.");
2056
- } else if (other !== false && typeof block.type === "string" && !CHOICE_TYPES.has(block.type)) {
2242
+ } else if (other !== false && typeof block.type === "string" && !choiceKind(block.type)) {
2057
2243
  warnings.push({ path: `${path}.props.other`, message: 'Only single choice, multiple choice and dropdown questions take an "Other" answer.' });
2058
2244
  }
2059
2245
  }
@@ -2066,19 +2252,14 @@ function parseForm(input) {
2066
2252
  }
2067
2253
  }
2068
2254
  if (typeof block.type === "string" && (block.props === void 0 || isObject2(block.props))) {
2069
- structuredProps(block.type, block.props ?? {}, `${path}.props`);
2070
- fieldProps(block.type, block.props ?? {}, `${path}.props`);
2255
+ kindProps(getFieldType(block.type)?.checkProps, block.props ?? {}, `${path}.props`);
2071
2256
  }
2072
2257
  if (validKey) {
2073
2258
  if (keys.has(block.key)) issue(`${path}.key`, `Duplicate key "${String(block.key)}".`);
2074
2259
  keys.set(block.key, block.id);
2075
2260
  }
2076
2261
  fieldTypes.set(block.id, typeof block.type === "string" ? block.type : "");
2077
- if (block.type === CALCULATED_TYPE) {
2078
- const format = isObject2(block.props) ? block.props.format : void 0;
2079
- if (format === "text") calculatedLiteral.set(block.id, "string");
2080
- else if (format !== "number" && format !== "percent") calculatedLiteral.set(block.id, void 0);
2081
- }
2262
+ literals.set(block.id, typeof block.type === "string" ? literalType(block.type, isObject2(block.props) ? block.props : {}) : void 0);
2082
2263
  if (block.type === "table" && isObject2(block.props) && Array.isArray(block.props.columns)) {
2083
2264
  tableColumnKeys.set(
2084
2265
  block.id,
@@ -2086,8 +2267,13 @@ function parseForm(input) {
2086
2267
  );
2087
2268
  }
2088
2269
  } else if (block.kind === "content") {
2089
- if (block.type !== "heading" && block.type !== "text") issue(`${path}.type`, "Must be heading or text.");
2270
+ const content = typeof block.type === "string" ? getContentType(block.type) : void 0;
2271
+ if (!content) issue(`${path}.type`, `Must be ${contentKinds()}.`);
2090
2272
  text(block.text, `${path}.text`);
2273
+ if (content?.checkProps) {
2274
+ if (block.props === void 0 || isObject2(block.props) && isJson(block.props)) kindProps(content.checkProps, block.props ?? {}, `${path}.props`);
2275
+ else issue(`${path}.props`, "Must be a JSON object.");
2276
+ }
2091
2277
  } else if (block.kind === "page-break") {
2092
2278
  optionalText(block.title, `${path}.title`);
2093
2279
  } else {
@@ -2217,8 +2403,8 @@ function parseForm(input) {
2217
2403
  if (typeof left.field === "string" && fieldTypes.has(left.field)) {
2218
2404
  const fieldType = fieldTypes.get(left.field);
2219
2405
  what = `"${keyOf(left.field)}"`;
2220
- type = calculatedLiteral.has(left.field) ? calculatedLiteral.get(left.field) : LITERAL_TYPE[fieldType];
2221
- if (CHOICE_TYPES.has(fieldType)) options = choices.get(left.field);
2406
+ type = literals.get(left.field);
2407
+ if (choiceKind(fieldType)) options = choices.get(left.field);
2222
2408
  } else if (typeof left.hidden === "string" && hiddenTypes.has(left.hidden)) {
2223
2409
  what = `hidden input "${left.hidden}"`;
2224
2410
  type = hiddenTypes.get(left.hidden);
@@ -2239,8 +2425,8 @@ function parseForm(input) {
2239
2425
  type = "number";
2240
2426
  }
2241
2427
  for (const v of values) {
2242
- const message = type !== void 0 && typeof v !== type ? `${what} holds a ${type}, but ${shown(v)} is not one; this comparison is ${outcome}.` : options && typeof v === "string" && !options.has(v) ? `${shown(v)} is not an option of ${what}; this comparison is ${outcome}.` : null;
2243
- if (message) warnings.push({ path: `${path}.right`, message });
2428
+ const message2 = type !== void 0 && typeof v !== type ? `${what} holds a ${type}, but ${shown(v)} is not one; this comparison is ${outcome}.` : options && typeof v === "string" && !options.has(v) ? `${shown(v)} is not an option of ${what}; this comparison is ${outcome}.` : null;
2429
+ if (message2) warnings.push({ path: `${path}.right`, message: message2 });
2244
2430
  }
2245
2431
  };
2246
2432
  const keyOf = (id) => [...keys].find(([, v]) => v === id)?.[0] ?? id;
@@ -2555,17 +2741,17 @@ function defineForm(input) {
2555
2741
  if (input.endings?.length) definition.endings = input.endings;
2556
2742
  if (input.settings) definition.settings = input.settings;
2557
2743
  if (input.meta) definition.meta = input.meta;
2558
- const frozen = JSON.parse(JSON.stringify(definition));
2559
- const parsed = parseForm(frozen);
2744
+ const frozen2 = JSON.parse(JSON.stringify(definition));
2745
+ const parsed = parseForm(frozen2);
2560
2746
  if (!parsed.ok) {
2561
2747
  const list = parsed.issues.map((i) => ` - ${i.path ? `${i.path}: ` : ""}${i.message}`).join("\n");
2562
2748
  throw new Error(`defineForm "${input.id}" is not a valid form:
2563
2749
  ${list}`);
2564
2750
  }
2565
2751
  return {
2566
- definition: frozen,
2567
- toJSON: () => JSON.parse(JSON.stringify(frozen)),
2568
- hash: () => fingerprint(frozen),
2752
+ definition: frozen2,
2753
+ toJSON: () => JSON.parse(JSON.stringify(frozen2)),
2754
+ hash: () => fingerprint(frozen2),
2569
2755
  $answers: void 0,
2570
2756
  $hidden: void 0
2571
2757
  };
@@ -2588,8 +2774,13 @@ function validateField(field2, value, options = {}) {
2588
2774
  }
2589
2775
  const manifest = getFieldType(field2.type);
2590
2776
  if (!manifest) return `Unknown field type "${field2.type}".`;
2591
- const builtIn = manifest.validate?.(value, field2) ?? null;
2592
- if (builtIn) return builtIn;
2777
+ let builtIn2;
2778
+ try {
2779
+ builtIn2 = manifest.validate?.(value, field2) ?? null;
2780
+ } catch {
2781
+ builtIn2 = VALIDATOR_FAILED_MESSAGE;
2782
+ }
2783
+ if (builtIn2) return builtIn2;
2593
2784
  return options.custom ? runCustom(options.custom, value) : null;
2594
2785
  }
2595
2786
 
@@ -2683,9 +2874,9 @@ function build(given, options, replaying = false, budget) {
2683
2874
  const field2 = fields.get(id);
2684
2875
  if (!field2 || !isVisibleIn(s, id)) continue;
2685
2876
  const value = own(s.values, id);
2686
- let message = validateField(field2, value, { custom: options.validators?.[id] });
2687
- if (!message && filters) message = filteredChoiceError(definition, field2, value, shown2 ??= answersOnPath(s));
2688
- if (message) errors[id] = message;
2877
+ let message2 = validateField(field2, value, { custom: options.validators?.[id] });
2878
+ if (!message2 && filters) message2 = filteredChoiceError(definition, field2, value, shown2 ??= answersOnPath(s));
2879
+ if (message2) errors[id] = message2;
2689
2880
  }
2690
2881
  return errors;
2691
2882
  };
@@ -2909,6 +3100,40 @@ function formProgress(engine) {
2909
3100
  // src/upload-protocol.ts
2910
3101
  var MAX_UPLOAD_REQUEST_BYTES = 4e3;
2911
3102
 
3103
+ // src/merge.ts
3104
+ function put2(record, key, value) {
3105
+ Object.defineProperty(record, key, { value, enumerable: true, writable: true, configurable: true });
3106
+ }
3107
+ function mergeResponses(definition, base, patch, options = {}) {
3108
+ const def = withHostOptions(definition, options.hostOptions);
3109
+ const fields = fieldMap(def);
3110
+ const keepable = (field2) => !!field2 && !isCalculated(field2) && !withheld(def, field2);
3111
+ let values = /* @__PURE__ */ Object.create(null);
3112
+ const record = (r) => r && typeof r === "object" && !Array.isArray(r) ? r : {};
3113
+ const from = record(base);
3114
+ const changes = record(patch);
3115
+ for (const [id, field2] of fields) {
3116
+ if (!keepable(field2)) continue;
3117
+ const changed = Object.prototype.hasOwnProperty.call(changes, id);
3118
+ const value = changed ? own(changes, id) : own(from, id);
3119
+ if (value === void 0 || value === null || isEmptyValue(value, field2)) continue;
3120
+ values[id] = value;
3121
+ }
3122
+ for (const id of Object.keys(values)) values = clearFilteredAnswers(def, values, id);
3123
+ const initial = {};
3124
+ for (const id of Object.keys(values)) if (values[id] !== void 0) put2(initial, id, values[id]);
3125
+ const state = createForm(def, { initialValues: initial, hidden: options.hidden, clock: () => void 0 }).getState();
3126
+ const keepOffPath = (options.offPath ?? (def.settings?.clearHiddenValues === false ? "keep" : "drop")) === "keep";
3127
+ const onPath = new Set(state.path);
3128
+ const out = {};
3129
+ for (const id of fields.keys()) {
3130
+ if (!keepOffPath && !onPath.has(id)) continue;
3131
+ const value = own(initial, id);
3132
+ if (value !== void 0) put2(out, id, value);
3133
+ }
3134
+ return out;
3135
+ }
3136
+
2912
3137
  // src/explain.ts
2913
3138
  function explain(source, blockId, options = {}) {
2914
3139
  const input = "getState" in source ? { definition: source.definition, values: source.getState().values, hidden: source.getState().hidden } : source;
@@ -2941,8 +3166,8 @@ function explain(source, blockId, options = {}) {
2941
3166
  const value = own(input.values, id);
2942
3167
  if (!visible) validation = { status: "not-checked" };
2943
3168
  else {
2944
- const message = validateField(block, value, { custom: options.validators?.[id] });
2945
- if (message) validation = { status: "invalid", message };
3169
+ const message2 = validateField(block, value, { custom: options.validators?.[id] });
3170
+ if (message2) validation = { status: "invalid", message: message2 };
2946
3171
  else validation = { status: isEmptyValue(value, block) ? "unanswered" : "valid" };
2947
3172
  }
2948
3173
  }
@@ -3061,6 +3286,588 @@ function carryAnswers(from, to, answers, extra = {}) {
3061
3286
  return out;
3062
3287
  }
3063
3288
 
3064
- export { CALCULATED_TYPE, CHOICE_FIELD_TYPES, COMPUTED_FIELD_TYPES, DEFAULT_PHOTO_FILES, DEFAULT_PHOTO_SIZE_MB, FORMULA_FUNCTIONS, HIGHLIGHT_NOTE_MESSAGE, MAX_CALCULATED_DECIMALS, MAX_FORMULA_ITEMS, MAX_FORMULA_LENGTH, MAX_FORM_FORMULA_LENGTH, MAX_HIDDEN_STRING_LENGTH, MAX_HIGHLIGHT_NOTE_LENGTH, MAX_HIGHLIGHT_QUOTE_LENGTH, MAX_PHOTO_FILES, MAX_PHOTO_ID_LENGTH, MAX_PHOTO_NAME_LENGTH, MAX_PHOTO_SIZE_MB, MAX_PREFILL_PARAM_LENGTH, MAX_SETTLE_ITEMS, MAX_TABLE_CELL_LENGTH, MAX_TABLE_COLUMNS, MAX_TIMESTAMP_SECONDS, MAX_UPLOAD_REQUEST_BYTES, OPTION_LIMITS, OTHER_MAX_LENGTH, PHOTO_FORMATS, PHOTO_TYPE, REQUIRED_MESSAGE, SEARCHABLE_OPTION_COUNT, STRUCTURED_FIELD_TYPES, UPLOAD_FIELD_TYPES, answerSnapshots, answersFromSnapshots, answersOnPath, calculatedProps, calculatedValue, canonicalJson, carryAnswers, choiceOptions, clearFilteredAnswers, coerceHidden, coerceHiddenValue, computePath, createForm, dateLimits, defineForm, describeCondition, evaluate, evaluateFormula, explain, f, filteredChoiceError, fingerprint, formProgress, formatCalculated, formatTimestamp, formulaInput, formulaNumber, formulaRefs, furthestReached, getFieldType, hasOptionFilters, hiddenByPrefill, isEmptyValue, isFormEvent, isHttpsUrl, isIsoDate, isJson, isOtherValue, isPhotoFormat, isPhotoRef, isPrefillParam, isSearchable, isSensitive, listFieldTypes, listLimits, normalizeValue, numberLimits, optionFilter, optionJson, optionMatches, optionsSource, otherOption, parseForm, parseFormula, parseTimestamp, photoCountMessage, photoFormatList, photoLimits, photoRefs, photoSizeMessage, photoTypeMessage, prefillFromLink, prefillSetting, ratingDisplay, readOption, recall, registerFieldType, renameFormulaKey, replay, respondentPath, ruleVisible, scaleLabels, searchOptions, steps, summarizeValue, tableColumns, tableLimits, validateField, visibleOptions, when, withHostOptions };
3289
+ // src/submit-protocol.ts
3290
+ var IDEMPOTENCY_KEY_PATTERN = /^[A-Za-z0-9_-]{16,128}$/;
3291
+ function isIdempotencyKey(value) {
3292
+ return typeof value === "string" && IDEMPOTENCY_KEY_PATTERN.test(value);
3293
+ }
3294
+ function submitOutcome(result) {
3295
+ if (!isSubmitResult(result)) return "retry";
3296
+ if (result.ok || result.reason === "duplicate") return "delivered";
3297
+ if (result.reason === "invalid" || result.reason === "conflict" || result.reason === "closed") return "rejected";
3298
+ return "retry";
3299
+ }
3300
+ function isSubmitResult(value) {
3301
+ if (!value || typeof value !== "object" || Array.isArray(value)) return false;
3302
+ const v = value;
3303
+ const stored = typeof v.id === "string" && v.id !== "" && typeof v.receivedAt === "string";
3304
+ if (v.ok === true) return stored;
3305
+ if (v.ok !== false) return false;
3306
+ switch (v.reason) {
3307
+ case "duplicate":
3308
+ return stored;
3309
+ case "invalid":
3310
+ return !!v.errors && typeof v.errors === "object" && !Array.isArray(v.errors);
3311
+ case "rate_limited":
3312
+ return v.retryAfterSeconds === void 0 || typeof v.retryAfterSeconds === "number" && Number.isFinite(v.retryAfterSeconds);
3313
+ case "conflict":
3314
+ case "closed":
3315
+ case "store_failed":
3316
+ return true;
3317
+ default:
3318
+ return false;
3319
+ }
3320
+ }
3321
+
3322
+ // src/storage.ts
3323
+ function memoryStorage() {
3324
+ const map = /* @__PURE__ */ new Map();
3325
+ return {
3326
+ getItem: (key) => map.get(key) ?? null,
3327
+ setItem: (key, value) => void map.set(key, value),
3328
+ removeItem: (key) => void map.delete(key),
3329
+ keys: () => [...map.keys()]
3330
+ };
3331
+ }
3332
+ function webStorage(storage) {
3333
+ const get = () => {
3334
+ const s = storage ? storage() : globalThis.localStorage;
3335
+ if (!s) throw new Error("Web storage isn't available here.");
3336
+ return s;
3337
+ };
3338
+ return {
3339
+ getItem: (key) => get().getItem(key),
3340
+ setItem: (key, value) => get().setItem(key, value),
3341
+ removeItem: (key) => get().removeItem(key)
3342
+ };
3343
+ }
3344
+ function isThenable(value) {
3345
+ return typeof value?.then === "function";
3346
+ }
3347
+ function andThen(value, next) {
3348
+ return isThenable(value) ? Promise.resolve(value).then(next) : next(value);
3349
+ }
3350
+
3351
+ // src/drafts.ts
3352
+ var DRAFT_KEY_PREFIX = "genesis-forms:draft:";
3353
+ function isRecord(value) {
3354
+ return !!value && typeof value === "object" && !Array.isArray(value);
3355
+ }
3356
+ function readDraft(raw, key) {
3357
+ if (typeof raw !== "string") return null;
3358
+ let value;
3359
+ try {
3360
+ value = JSON.parse(raw);
3361
+ } catch {
3362
+ return null;
3363
+ }
3364
+ if (!isRecord(value)) return null;
3365
+ const { formId, version, respondent, values, types, resumeAt, savedAt } = value;
3366
+ if (formId !== key.formId || respondent !== key.respondent || typeof version !== "string") return null;
3367
+ if (!isRecord(values) || !isRecord(types) || typeof savedAt !== "number") return null;
3368
+ if (resumeAt !== null && typeof resumeAt !== "string") return null;
3369
+ return value;
3370
+ }
3371
+ function createDraftStore(storage, options = {}) {
3372
+ const prefix = options.prefix ?? DRAFT_KEY_PREFIX;
3373
+ const storageKey = (key) => `${prefix}${encodeURIComponent(key.formId)}:${encodeURIComponent(key.respondent)}`;
3374
+ return {
3375
+ get: (key) => andThen(storage.getItem(storageKey(key)), (raw) => readDraft(raw, key)),
3376
+ set: (draft) => storage.setItem(storageKey(draft), JSON.stringify(draft)),
3377
+ remove: (key) => storage.removeItem(storageKey(key))
3378
+ };
3379
+ }
3380
+ function draftOf(engine, key, options = {}) {
3381
+ const definition = engine.definition;
3382
+ const state = engine.getState();
3383
+ const values = {};
3384
+ const types = {};
3385
+ for (const [id, field2] of fieldMap(definition)) {
3386
+ if (isCalculated(field2) || withheld(definition, field2)) continue;
3387
+ if (!options.keepSensitive && isSensitive(field2)) continue;
3388
+ const value = own(state.values, id);
3389
+ if (value === void 0 || isEmptyValue(value, field2)) continue;
3390
+ values[id] = value;
3391
+ types[id] = field2.type;
3392
+ }
3393
+ if (Object.keys(values).length === 0) return null;
3394
+ return {
3395
+ formId: key.formId,
3396
+ version: key.version,
3397
+ respondent: key.respondent,
3398
+ values,
3399
+ types,
3400
+ resumeAt: formProgress(engine).fieldId,
3401
+ savedAt: (options.now ?? Date.now)()
3402
+ };
3403
+ }
3404
+ var DRAFT_MAX_AGE_MS = 30 * 24 * 60 * 60 * 1e3;
3405
+ function restoreDraft(definition, draft, options) {
3406
+ const now = (options.now ?? Date.now)();
3407
+ if (now - draft.savedAt > (options.maxAgeMs ?? DRAFT_MAX_AGE_MS)) return { ok: false, reason: "expired" };
3408
+ const values = { ...draft.values };
3409
+ if (draft.version !== options.version) {
3410
+ const fields = fieldMap(definition);
3411
+ for (const [id, value] of Object.entries(values)) {
3412
+ const field2 = fields.get(id);
3413
+ if (!field2 || field2.type !== own(draft.types, id) || isCalculated(field2) || withheld(definition, field2)) {
3414
+ return { ok: false, reason: "version" };
3415
+ }
3416
+ if (validateField(field2, value, { requireAnswers: false }) !== null) return { ok: false, reason: "version" };
3417
+ }
3418
+ }
3419
+ const resumeAt = draft.resumeAt && fieldMap(definition).has(draft.resumeAt) ? draft.resumeAt : null;
3420
+ return { ok: true, values, resumeAt };
3421
+ }
3422
+ function createDraftAutosave(engine, store, key, options = {}) {
3423
+ const delay = options.delayMs ?? 800;
3424
+ const onError = options.onError ?? ((e) => console.warn("[forms] The draft could not be saved.", e));
3425
+ let timer = null;
3426
+ let dirty = false;
3427
+ let cleared = false;
3428
+ let listening = true;
3429
+ let stored = options.stored === true;
3430
+ let chain = Promise.resolve();
3431
+ const run = (step) => {
3432
+ chain = chain.then(async () => {
3433
+ try {
3434
+ const done = step();
3435
+ if (isThenable(done)) await done;
3436
+ } catch (e) {
3437
+ onError(e);
3438
+ }
3439
+ });
3440
+ return chain;
3441
+ };
3442
+ const write = () => {
3443
+ if (timer) clearTimeout(timer);
3444
+ timer = null;
3445
+ if (!dirty || cleared) return chain;
3446
+ dirty = false;
3447
+ const draft = draftOf(engine, key, options);
3448
+ return run(() => {
3449
+ if (cleared) return;
3450
+ if (!draft) {
3451
+ if (!stored) return;
3452
+ return andThen(store.remove(key), () => {
3453
+ stored = false;
3454
+ });
3455
+ }
3456
+ return andThen(store.set(draft), () => {
3457
+ stored = true;
3458
+ options.onSaved?.(draft);
3459
+ });
3460
+ });
3461
+ };
3462
+ const unsubscribe = engine.subscribe(() => {
3463
+ if (!listening) return;
3464
+ dirty = true;
3465
+ if (timer) clearTimeout(timer);
3466
+ timer = setTimeout(() => void write(), delay);
3467
+ });
3468
+ return {
3469
+ flush: () => write(),
3470
+ clear: () => {
3471
+ cleared = true;
3472
+ listening = false;
3473
+ dirty = false;
3474
+ if (timer) clearTimeout(timer);
3475
+ timer = null;
3476
+ unsubscribe();
3477
+ return run(() => store.remove(key));
3478
+ },
3479
+ dispose: () => {
3480
+ const done = write();
3481
+ listening = false;
3482
+ unsubscribe();
3483
+ return done;
3484
+ }
3485
+ };
3486
+ }
3487
+
3488
+ // src/outbox.ts
3489
+ var DEFAULT_PREFIX = "genesis-forms:outbox";
3490
+ function defaultKey() {
3491
+ const c = globalThis.crypto;
3492
+ if (c && typeof c.randomUUID === "function") return () => c.randomUUID();
3493
+ throw new Error("createSubmitOutbox: crypto.randomUUID isn't available here; pass createKey (expo-crypto's randomUUID, say).");
3494
+ }
3495
+ var MAX_TIMER_MS = 2 ** 31 - 1;
3496
+ var MAX_WAIT_MS = 24 * 60 * 60 * 1e3;
3497
+ var indexChains = /* @__PURE__ */ new WeakMap();
3498
+ function withIndexLock(storage, prefix, step) {
3499
+ let chains = indexChains.get(storage);
3500
+ if (!chains) indexChains.set(storage, chains = /* @__PURE__ */ new Map());
3501
+ const locks = globalThis.navigator?.locks;
3502
+ const locked = () => locks && typeof locks.request === "function" ? locks.request(`genesis-forms-outbox:${prefix}`, step) : step();
3503
+ const previous = chains.get(prefix) ?? Promise.resolve();
3504
+ const run = previous.then(locked, locked);
3505
+ chains.set(prefix, run.catch(() => void 0));
3506
+ return run;
3507
+ }
3508
+ var message = (e) => e instanceof Error ? e.message : String(e);
3509
+ function readEntry(raw, id) {
3510
+ if (typeof raw !== "string") return null;
3511
+ let v;
3512
+ try {
3513
+ v = JSON.parse(raw);
3514
+ } catch {
3515
+ return null;
3516
+ }
3517
+ if (!v || typeof v !== "object") return null;
3518
+ const e = v;
3519
+ if (e.id !== id || typeof e.formId !== "string" || !e.request || typeof e.request !== "object") return null;
3520
+ if (e.request.idempotencyKey !== id) return null;
3521
+ return {
3522
+ id,
3523
+ formId: e.formId,
3524
+ request: e.request,
3525
+ meta: e.meta ?? null,
3526
+ createdAt: typeof e.createdAt === "number" ? e.createdAt : 0,
3527
+ attempts: typeof e.attempts === "number" ? e.attempts : 0,
3528
+ nextAttemptAt: typeof e.nextAttemptAt === "number" ? e.nextAttemptAt : 0,
3529
+ state: e.state === "failed" ? "failed" : "pending",
3530
+ lastError: typeof e.lastError === "string" ? e.lastError : null,
3531
+ rejection: e.rejection ?? null
3532
+ };
3533
+ }
3534
+ function readIndex(raw) {
3535
+ if (typeof raw !== "string") return [];
3536
+ try {
3537
+ const v = JSON.parse(raw);
3538
+ return Array.isArray(v) ? v.filter((x) => typeof x === "string") : [];
3539
+ } catch {
3540
+ return [];
3541
+ }
3542
+ }
3543
+ function createSubmitOutbox(options) {
3544
+ const { storage, send } = options;
3545
+ const now = options.now ?? Date.now;
3546
+ const prefix = options.prefix ?? DEFAULT_PREFIX;
3547
+ const maxAttempts = Math.max(1, options.maxAttempts ?? 10);
3548
+ const baseMs = options.backoff?.baseMs ?? 2e3;
3549
+ const maxMs = options.backoff?.maxMs ?? 15 * 6e4;
3550
+ const timeoutMs = options.timeoutMs ?? 3e4;
3551
+ const createKey = options.createKey ?? defaultKey();
3552
+ const random = options.random ?? Math.random;
3553
+ const log = options.log ?? ((m) => console.warn(`[forms outbox] ${m}`));
3554
+ const indexKey = `${prefix}:index`;
3555
+ const itemKey = (id) => `${prefix}:item:${id}`;
3556
+ const entries = /* @__PURE__ */ new Map();
3557
+ const unreadable = /* @__PURE__ */ new Set();
3558
+ let sendingId = null;
3559
+ let online = options.online ?? true;
3560
+ let loaded = false;
3561
+ let storageError = null;
3562
+ let running = false;
3563
+ let timer = null;
3564
+ let flushing = null;
3565
+ let again = false;
3566
+ const listeners = /* @__PURE__ */ new Set();
3567
+ let snapshot = build2();
3568
+ function build2() {
3569
+ const items = [...entries.values()].sort((a, b) => a.createdAt - b.createdAt || (a.id < b.id ? -1 : 1)).map((e) => ({ ...e, sending: e.id === sendingId }));
3570
+ return {
3571
+ loaded,
3572
+ online,
3573
+ items,
3574
+ pending: items.filter((i) => i.state === "pending").length,
3575
+ failed: items.filter((i) => i.state === "failed").length,
3576
+ storageError,
3577
+ unreadable: [...unreadable]
3578
+ };
3579
+ }
3580
+ function changed() {
3581
+ snapshot = build2();
3582
+ for (const l of [...listeners]) l();
3583
+ }
3584
+ let writes = Promise.resolve();
3585
+ function serial(step) {
3586
+ const run = writes.then(step, step);
3587
+ writes = run.catch(() => void 0);
3588
+ return run;
3589
+ }
3590
+ function updateIndex(change) {
3591
+ return withIndexLock(storage, prefix, async () => {
3592
+ const ids = new Set(readIndex(await storage.getItem(indexKey)));
3593
+ for (const id of entries.keys()) ids.add(id);
3594
+ for (const id of unreadable) ids.add(id);
3595
+ change(ids);
3596
+ await storage.setItem(indexKey, JSON.stringify([...ids]));
3597
+ });
3598
+ }
3599
+ function storageOk() {
3600
+ if (storageError !== null) {
3601
+ storageError = null;
3602
+ changed();
3603
+ }
3604
+ }
3605
+ function storageFailed(what, e) {
3606
+ storageError = `${what}: ${message(e)}`;
3607
+ log(storageError);
3608
+ changed();
3609
+ }
3610
+ async function persist(entry) {
3611
+ try {
3612
+ await serial(() => Promise.resolve(storage.setItem(itemKey(entry.id), JSON.stringify(entry))));
3613
+ storageOk();
3614
+ } catch (e) {
3615
+ storageFailed("Couldn't save the outbox", e);
3616
+ }
3617
+ }
3618
+ async function forget(id) {
3619
+ await serial(async () => {
3620
+ await updateIndex((ids) => void ids.delete(id));
3621
+ await storage.removeItem(itemKey(id));
3622
+ });
3623
+ }
3624
+ async function load() {
3625
+ try {
3626
+ const ids = readIndex(await storage.getItem(indexKey));
3627
+ const gone = [];
3628
+ for (const id of ids) {
3629
+ if (entries.has(id)) continue;
3630
+ const raw = await storage.getItem(itemKey(id));
3631
+ if (raw === null || raw === void 0) {
3632
+ gone.push(id);
3633
+ continue;
3634
+ }
3635
+ const entry = readEntry(raw, id);
3636
+ if (entry) entries.set(id, entry);
3637
+ else if (!unreadable.has(id)) {
3638
+ unreadable.add(id);
3639
+ log(`A stored submission (${id}) couldn't be read; it was left in storage.`);
3640
+ }
3641
+ }
3642
+ if (gone.length) await serial(() => updateIndex((set) => gone.forEach((id) => set.delete(id))));
3643
+ loaded = true;
3644
+ storageError = null;
3645
+ } catch (e) {
3646
+ storageError = `Couldn't read the outbox: ${message(e)}`;
3647
+ log(storageError);
3648
+ }
3649
+ changed();
3650
+ }
3651
+ let ready = load();
3652
+ const isDue = (e, t) => e.state === "pending" && (e.nextAttemptAt <= t || e.nextAttemptAt - t > MAX_WAIT_MS);
3653
+ function revive() {
3654
+ const at = now();
3655
+ const revived = [];
3656
+ for (const [id, e] of entries) {
3657
+ if (e.state === "failed" && e.rejection === null) {
3658
+ const next = { ...e, state: "pending", attempts: 0, nextAttemptAt: at };
3659
+ entries.set(id, next);
3660
+ revived.push(next);
3661
+ }
3662
+ }
3663
+ return revived;
3664
+ }
3665
+ function delayFor(attempts) {
3666
+ const full = Math.min(maxMs, baseMs * 2 ** Math.max(0, attempts - 1));
3667
+ return Math.round(full / 2 + random() * full / 2);
3668
+ }
3669
+ function schedule() {
3670
+ if (timer) clearTimeout(timer);
3671
+ timer = null;
3672
+ if (!running || !online) return;
3673
+ let next = Infinity;
3674
+ const t = now();
3675
+ for (const e of entries.values()) if (e.state === "pending") next = Math.min(next, isDue(e, t) ? t : e.nextAttemptAt);
3676
+ if (next === Infinity) return;
3677
+ timer = setTimeout(() => void outbox.flush(), Math.min(MAX_TIMER_MS, Math.max(0, next - t)));
3678
+ }
3679
+ async function attempt(entry) {
3680
+ sendingId = entry.id;
3681
+ changed();
3682
+ const controller = new AbortController();
3683
+ let timeout = null;
3684
+ let result;
3685
+ let error = null;
3686
+ try {
3687
+ result = await Promise.race([
3688
+ send({ ...entry, sending: true }, { signal: controller.signal }),
3689
+ new Promise((_, reject) => {
3690
+ timeout = setTimeout(() => {
3691
+ controller.abort();
3692
+ reject(new Error(`No answer after ${timeoutMs} ms.`));
3693
+ }, timeoutMs);
3694
+ })
3695
+ ]);
3696
+ } catch (e) {
3697
+ error = message(e);
3698
+ } finally {
3699
+ if (timeout) clearTimeout(timeout);
3700
+ sendingId = null;
3701
+ }
3702
+ if (!entries.has(entry.id)) return changed();
3703
+ const outcome = error === null ? submitOutcome(result) : "retry";
3704
+ if (outcome === "delivered") {
3705
+ entries.delete(entry.id);
3706
+ changed();
3707
+ try {
3708
+ await forget(entry.id);
3709
+ storageOk();
3710
+ } catch (e) {
3711
+ storageFailed("Couldn't remove a delivered submission from the outbox", e);
3712
+ }
3713
+ options.onDelivered?.({ ...entry, sending: false }, result);
3714
+ return;
3715
+ }
3716
+ const next = { ...entry, attempts: entry.attempts + 1 };
3717
+ if (outcome === "rejected") {
3718
+ const r = result;
3719
+ next.state = "failed";
3720
+ next.rejection = r.reason === "invalid" ? { reason: r.reason, errors: r.errors } : { reason: r.reason };
3721
+ next.lastError = r.reason === "closed" ? "The form isn't taking responses." : r.reason === "conflict" ? "The server holds another response under this submission's key." : "The server refused the answers.";
3722
+ } else {
3723
+ next.lastError = error ?? (isSubmitResult(result) && !result.ok ? `The server answered ${result.reason}.` : "The server's answer wasn't understood.");
3724
+ if (next.attempts >= maxAttempts) {
3725
+ next.state = "failed";
3726
+ next.lastError = `Gave up after ${next.attempts} tries. Last: ${next.lastError}`;
3727
+ } else {
3728
+ let wait = delayFor(next.attempts);
3729
+ if (isSubmitResult(result) && !result.ok && result.reason === "rate_limited" && result.retryAfterSeconds !== void 0) {
3730
+ wait = Math.max(wait, Math.min(MAX_WAIT_MS, result.retryAfterSeconds * 1e3));
3731
+ }
3732
+ next.nextAttemptAt = now() + wait;
3733
+ }
3734
+ }
3735
+ entries.set(entry.id, next);
3736
+ changed();
3737
+ await persist(next);
3738
+ }
3739
+ async function drain() {
3740
+ do {
3741
+ again = false;
3742
+ await ready;
3743
+ if (!loaded) {
3744
+ ready = load();
3745
+ await ready;
3746
+ }
3747
+ while (online) {
3748
+ const t = now();
3749
+ const due = [...entries.values()].filter((e) => isDue(e, t)).sort((a, b) => a.createdAt - b.createdAt || (a.id < b.id ? -1 : 1))[0];
3750
+ if (!due) break;
3751
+ await attempt(due);
3752
+ }
3753
+ } while (again);
3754
+ }
3755
+ const outbox = {
3756
+ get ready() {
3757
+ return ready;
3758
+ },
3759
+ async enqueue(input) {
3760
+ await ready;
3761
+ const id = input.idempotencyKey ?? createKey();
3762
+ const existing = entries.get(id);
3763
+ if (existing) return { ...existing, sending: existing.id === sendingId };
3764
+ const submission = JSON.parse(JSON.stringify(input.submission ?? {}));
3765
+ const at = now();
3766
+ const entry = {
3767
+ id,
3768
+ formId: input.formId,
3769
+ request: {
3770
+ ...submission,
3771
+ idempotencyKey: id,
3772
+ clientSubmittedAt: input.clientSubmittedAt ?? at,
3773
+ ...input.attemptId !== void 0 ? { attemptId: input.attemptId } : {}
3774
+ },
3775
+ meta: input.meta ? JSON.parse(JSON.stringify(input.meta)) : null,
3776
+ createdAt: at,
3777
+ attempts: 0,
3778
+ nextAttemptAt: at,
3779
+ state: "pending",
3780
+ lastError: null,
3781
+ rejection: null
3782
+ };
3783
+ try {
3784
+ await serial(async () => {
3785
+ await storage.setItem(itemKey(id), JSON.stringify(entry));
3786
+ await updateIndex((ids) => void ids.add(id));
3787
+ });
3788
+ } catch (e) {
3789
+ storageFailed("Couldn't queue the submission", e);
3790
+ throw e;
3791
+ }
3792
+ storageOk();
3793
+ entries.set(id, entry);
3794
+ changed();
3795
+ if (running) void outbox.flush();
3796
+ return { ...entry, sending: false };
3797
+ },
3798
+ flush() {
3799
+ if (flushing) {
3800
+ again = true;
3801
+ return flushing;
3802
+ }
3803
+ flushing = drain().finally(() => {
3804
+ flushing = null;
3805
+ schedule();
3806
+ });
3807
+ return flushing;
3808
+ },
3809
+ async retry(id) {
3810
+ await ready;
3811
+ const entry = entries.get(id);
3812
+ if (!entry || entry.state !== "failed") return;
3813
+ const next = { ...entry, state: "pending", attempts: 0, nextAttemptAt: now(), lastError: null, rejection: null };
3814
+ entries.set(id, next);
3815
+ changed();
3816
+ await persist(next);
3817
+ await outbox.flush();
3818
+ },
3819
+ async discard(id) {
3820
+ await ready;
3821
+ const entry = entries.get(id);
3822
+ if (!entry || sendingId === id) return false;
3823
+ entries.delete(id);
3824
+ changed();
3825
+ schedule();
3826
+ try {
3827
+ await forget(id);
3828
+ storageOk();
3829
+ } catch (e) {
3830
+ entries.set(id, entry);
3831
+ storageFailed("Couldn't discard the submission", e);
3832
+ throw e;
3833
+ }
3834
+ return true;
3835
+ },
3836
+ setOnline(value) {
3837
+ if (online === value) return;
3838
+ online = value;
3839
+ if (online) {
3840
+ const at = now();
3841
+ for (const [id, e] of entries) if (e.state === "pending" && e.nextAttemptAt > at) entries.set(id, { ...e, nextAttemptAt: at });
3842
+ for (const e of revive()) void persist(e);
3843
+ }
3844
+ changed();
3845
+ if (online && running) void outbox.flush();
3846
+ else schedule();
3847
+ },
3848
+ start() {
3849
+ if (running) return;
3850
+ running = true;
3851
+ void ready.then(() => {
3852
+ const revived = revive();
3853
+ if (revived.length) changed();
3854
+ for (const e of revived) void persist(e);
3855
+ return outbox.flush();
3856
+ });
3857
+ },
3858
+ stop() {
3859
+ running = false;
3860
+ schedule();
3861
+ },
3862
+ getSnapshot: () => snapshot,
3863
+ subscribe(listener) {
3864
+ listeners.add(listener);
3865
+ return () => void listeners.delete(listener);
3866
+ }
3867
+ };
3868
+ return outbox;
3869
+ }
3870
+
3871
+ export { CALCULATED_TYPE, CHOICE_FIELD_TYPES, COMPUTED_FIELD_TYPES, DEFAULT_PHOTO_FILES, DEFAULT_PHOTO_SIZE_MB, DRAFT_KEY_PREFIX, DRAFT_MAX_AGE_MS, FORMULA_FUNCTIONS, HIGHLIGHT_NOTE_MESSAGE, IDEMPOTENCY_KEY_PATTERN, MAX_CALCULATED_DECIMALS, MAX_FORMULA_ITEMS, MAX_FORMULA_LENGTH, MAX_FORM_FORMULA_LENGTH, MAX_HIDDEN_STRING_LENGTH, MAX_HIGHLIGHT_NOTE_LENGTH, MAX_HIGHLIGHT_QUOTE_LENGTH, MAX_PHOTO_FILES, MAX_PHOTO_ID_LENGTH, MAX_PHOTO_NAME_LENGTH, MAX_PHOTO_SIZE_MB, MAX_PREFILL_PARAM_LENGTH, MAX_SETTLE_ITEMS, MAX_TABLE_CELL_LENGTH, MAX_TABLE_COLUMNS, MAX_TIMESTAMP_SECONDS, MAX_UPLOAD_REQUEST_BYTES, OPTION_LIMITS, OTHER_MAX_LENGTH, PHOTO_FORMATS, PHOTO_TYPE, REQUIRED_MESSAGE, SEARCHABLE_OPTION_COUNT, STRUCTURED_FIELD_TYPES, UPLOAD_FIELD_TYPES, VALIDATOR_FAILED_MESSAGE, answerSnapshots, answersFromSnapshots, answersOnPath, calculatedProps, calculatedValue, canonicalJson, carryAnswers, choiceKind, choiceOptions, clearFilteredAnswers, coerceHidden, coerceHiddenValue, computePath, contentText, createDraftAutosave, createDraftStore, createForm, createSubmitOutbox, dateLimits, defineForm, describeCondition, draftOf, evaluate, evaluateFormula, explain, f, filteredChoiceError, fingerprint, formProgress, formatCalculated, formatTimestamp, formulaInput, formulaNumber, formulaRefs, furthestReached, getContentType, getFieldType, hasOptionFilters, hiddenByPrefill, isBuiltInFieldType, isChoiceField, isEmptyValue, isFormEvent, isHttpsUrl, isIdempotencyKey, isIsoDate, isJson, isOtherValue, isPhotoFormat, isPhotoRef, isPrefillParam, isSearchable, isSensitive, isSubmitResult, listContentTypes, listFieldTypes, listLimits, memoryStorage, mergeResponses, normalizeValue, numberLimits, optionFilter, optionJson, optionMatches, optionsSource, otherOption, parseForm, parseFormula, parseTimestamp, photoCountMessage, photoFormatList, photoLimits, photoRefs, photoSizeMessage, photoTypeMessage, prefillFromLink, prefillSetting, ratingDisplay, readOption, recall, registerContentType, registerFieldType, renameFormulaKey, replay, respondentPath, restoreDraft, ruleVisible, scaleLabels, searchOptions, steps, submitOutcome, summarizeValue, tableColumns, tableLimits, validateField, visibleOptions, webStorage, when, withHostOptions };
3065
3872
  //# sourceMappingURL=index.js.map
3066
3873
  //# sourceMappingURL=index.js.map