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