attaform 0.19.0 → 0.20.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 (110) hide show
  1. package/dist/chunks/devtools.cjs +1 -1
  2. package/dist/chunks/devtools.mjs +1 -1
  3. package/dist/chunks/indexeddb.cjs +1 -1
  4. package/dist/chunks/indexeddb.mjs +1 -1
  5. package/dist/chunks/local-storage.cjs +1 -1
  6. package/dist/chunks/local-storage.mjs +1 -1
  7. package/dist/chunks/session-storage.cjs +1 -1
  8. package/dist/chunks/session-storage.mjs +1 -1
  9. package/dist/index.cjs +3 -6
  10. package/dist/index.cjs.map +1 -1
  11. package/dist/index.d.cts +14 -40
  12. package/dist/index.d.mts +14 -40
  13. package/dist/index.d.ts +14 -40
  14. package/dist/index.mjs +5 -5
  15. package/dist/nuxt.d.cts +1 -1
  16. package/dist/nuxt.d.mts +1 -1
  17. package/dist/nuxt.d.ts +1 -1
  18. package/dist/runtime/components/AttaformDevtoolsPanel.vue +2 -2
  19. package/dist/runtime/components/DevtoolsValueTree.d.vue.ts +1 -3
  20. package/dist/runtime/components/DevtoolsValueTree.vue.d.ts +1 -3
  21. package/dist/runtime/plugins/attaform.cjs +2 -2
  22. package/dist/runtime/plugins/attaform.mjs +2 -2
  23. package/dist/shared/{attaform.CrpjyXdO.mjs → attaform.BKozEdTr.mjs} +275 -266
  24. package/dist/shared/attaform.BKozEdTr.mjs.map +1 -0
  25. package/dist/shared/{attaform.Bubm_slq.cjs → attaform.BM6YD9kZ.cjs} +212 -269
  26. package/dist/shared/attaform.BM6YD9kZ.cjs.map +1 -0
  27. package/dist/shared/{attaform.CoxJ8Qm8.cjs → attaform.BPxsYtTe.cjs} +2 -26
  28. package/dist/shared/attaform.BPxsYtTe.cjs.map +1 -0
  29. package/dist/shared/{attaform.BqEfHpVB.cjs → attaform.BPy-4qRx.cjs} +275 -268
  30. package/dist/shared/attaform.BPy-4qRx.cjs.map +1 -0
  31. package/dist/shared/attaform.BWgAFnsj.mjs +770 -0
  32. package/dist/shared/attaform.BWgAFnsj.mjs.map +1 -0
  33. package/dist/shared/{attaform.BTpuvGec.d.ts → attaform.Bh3ACtts.d.ts} +109 -101
  34. package/dist/shared/{attaform.CXpzmj38.mjs → attaform.BupwXkj_.mjs} +213 -270
  35. package/dist/shared/attaform.BupwXkj_.mjs.map +1 -0
  36. package/dist/shared/{attaform.JBx8cfMA.cjs → attaform.CIn4bMsD.cjs} +263 -799
  37. package/dist/shared/attaform.CIn4bMsD.cjs.map +1 -0
  38. package/dist/shared/{attaform.BTi-PsHr.mjs → attaform.CKFbKFb6.mjs} +1818 -1472
  39. package/dist/shared/attaform.CKFbKFb6.mjs.map +1 -0
  40. package/dist/shared/{attaform.ePUcKxId.d.cts → attaform.D5-1XGQU.d.cts} +109 -101
  41. package/dist/shared/{attaform.a3uBo-gw.mjs → attaform.DEBvCjeH.mjs} +257 -793
  42. package/dist/shared/attaform.DEBvCjeH.mjs.map +1 -0
  43. package/dist/shared/{attaform.C1msmO2v.cjs → attaform.DL4CQ-oW.cjs} +1823 -1477
  44. package/dist/shared/attaform.DL4CQ-oW.cjs.map +1 -0
  45. package/dist/shared/{attaform.D4I63aBV.d.ts → attaform.DSD85fHb.d.cts} +1 -19
  46. package/dist/shared/{attaform.CBjmobqk.d.cts → attaform.DSD85fHb.d.mts} +1 -19
  47. package/dist/shared/{attaform.DXYHL99q.d.mts → attaform.DSD85fHb.d.ts} +1 -19
  48. package/dist/shared/{attaform.B7rzpK1U.d.cts → attaform.DkA5J8NW.d.cts} +1 -17
  49. package/dist/shared/{attaform.B7rzpK1U.d.mts → attaform.DkA5J8NW.d.mts} +1 -17
  50. package/dist/shared/{attaform.B7rzpK1U.d.ts → attaform.DkA5J8NW.d.ts} +1 -17
  51. package/dist/shared/{attaform.CJ-e9gYI.d.ts → attaform.Dl5kDY-A.d.ts} +1 -1
  52. package/dist/shared/attaform.Dmb6itxC.cjs +781 -0
  53. package/dist/shared/attaform.Dmb6itxC.cjs.map +1 -0
  54. package/dist/shared/{attaform.CRNA0vrd.d.mts → attaform.DoKXru-a.d.mts} +1 -1
  55. package/dist/shared/attaform.DvA-CJJW.mjs +1876 -0
  56. package/dist/shared/attaform.DvA-CJJW.mjs.map +1 -0
  57. package/dist/shared/{attaform.BtBmfLQN.d.mts → attaform.EMzJcQci.d.mts} +109 -101
  58. package/dist/shared/attaform.EZG6fOFb.mjs +35 -0
  59. package/dist/shared/attaform.EZG6fOFb.mjs.map +1 -0
  60. package/dist/shared/{attaform.QvygsFGh.d.cts → attaform.GbDo_lJi.d.cts} +1 -1
  61. package/dist/shared/{attaform.C0uGZQ4M.d.ts → attaform.SfhU0OEY.d.cts} +134 -30
  62. package/dist/shared/{attaform.C0uGZQ4M.d.cts → attaform.SfhU0OEY.d.mts} +134 -30
  63. package/dist/shared/{attaform.C0uGZQ4M.d.mts → attaform.SfhU0OEY.d.ts} +134 -30
  64. package/dist/shared/attaform.jgzuNZVC.cjs +1882 -0
  65. package/dist/shared/attaform.jgzuNZVC.cjs.map +1 -0
  66. package/dist/transforms.cjs +2 -2
  67. package/dist/transforms.d.cts +22 -13
  68. package/dist/transforms.d.mts +22 -13
  69. package/dist/transforms.d.ts +22 -13
  70. package/dist/transforms.mjs +1 -1
  71. package/dist/vite.cjs +8 -7
  72. package/dist/vite.cjs.map +1 -1
  73. package/dist/vite.mjs +8 -7
  74. package/dist/vite.mjs.map +1 -1
  75. package/dist/zod-v3.cjs +3 -3
  76. package/dist/zod-v3.d.cts +32 -6
  77. package/dist/zod-v3.d.mts +32 -6
  78. package/dist/zod-v3.d.ts +32 -6
  79. package/dist/zod-v3.mjs +3 -3
  80. package/dist/zod-v4.cjs +3 -3
  81. package/dist/zod-v4.d.cts +12 -8
  82. package/dist/zod-v4.d.mts +12 -8
  83. package/dist/zod-v4.d.ts +12 -8
  84. package/dist/zod-v4.mjs +3 -3
  85. package/dist/zod.cjs +8 -8
  86. package/dist/zod.cjs.map +1 -1
  87. package/dist/zod.d.cts +6 -6
  88. package/dist/zod.d.mts +6 -6
  89. package/dist/zod.d.ts +6 -6
  90. package/dist/zod.mjs +6 -6
  91. package/package.json +1 -1
  92. package/dist/shared/attaform.BTi-PsHr.mjs.map +0 -1
  93. package/dist/shared/attaform.BqEfHpVB.cjs.map +0 -1
  94. package/dist/shared/attaform.Bubm_slq.cjs.map +0 -1
  95. package/dist/shared/attaform.C1msmO2v.cjs.map +0 -1
  96. package/dist/shared/attaform.C8CyvYa_.cjs +0 -36
  97. package/dist/shared/attaform.C8CyvYa_.cjs.map +0 -1
  98. package/dist/shared/attaform.CXpzmj38.mjs.map +0 -1
  99. package/dist/shared/attaform.Cghpuav8.mjs +0 -57
  100. package/dist/shared/attaform.Cghpuav8.mjs.map +0 -1
  101. package/dist/shared/attaform.CiMqJHDm.mjs +0 -1594
  102. package/dist/shared/attaform.CiMqJHDm.mjs.map +0 -1
  103. package/dist/shared/attaform.CoxJ8Qm8.cjs.map +0 -1
  104. package/dist/shared/attaform.CrpjyXdO.mjs.map +0 -1
  105. package/dist/shared/attaform.D13GMFgK.mjs +0 -32
  106. package/dist/shared/attaform.D13GMFgK.mjs.map +0 -1
  107. package/dist/shared/attaform.JBx8cfMA.cjs.map +0 -1
  108. package/dist/shared/attaform.OznWyOPy.cjs +0 -1600
  109. package/dist/shared/attaform.OznWyOPy.cjs.map +0 -1
  110. package/dist/shared/attaform.a3uBo-gw.mjs.map +0 -1
@@ -1,7 +1,7 @@
1
1
  'use strict';
2
2
 
3
3
  const vue = require('vue');
4
- const paths = require('./attaform.BqEfHpVB.cjs');
4
+ const paths = require('./attaform.BPy-4qRx.cjs');
5
5
 
6
6
  const NOT_FOUND = Symbol("NOT_FOUND");
7
7
  function descendStep(value, segment) {
@@ -64,7 +64,7 @@ function setAtPathOffset(root, path, value, offset) {
64
64
  arr[head] = setAtPathOffset(arr[head], path, value, nextOffset);
65
65
  return arr;
66
66
  }
67
- const rec = isPlainRecord(root) ? { ...root } : {};
67
+ const rec = isPlainRecord(root) ? Object.assign(/* @__PURE__ */ Object.create(null), root) : /* @__PURE__ */ Object.create(null);
68
68
  rec[head] = setAtPathOffset(rec[head], path, value, nextOffset);
69
69
  return rec;
70
70
  }
@@ -165,7 +165,7 @@ function mergeStructuralImpl(schema, scratch, consumer, defaultValue) {
165
165
  return consumer;
166
166
  }
167
167
  let mutated = false;
168
- const out = { ...consumer };
168
+ const out = Object.assign(/* @__PURE__ */ Object.create(null), consumer);
169
169
  for (const key of Object.keys(defaultValue)) {
170
170
  if (!(key in consumer)) {
171
171
  const defAtKey = defaultValue[key];
@@ -429,7 +429,7 @@ function structuralSnapshot(value) {
429
429
  return out2;
430
430
  }
431
431
  const src = value;
432
- const out = {};
432
+ const out = /* @__PURE__ */ Object.create(null);
433
433
  for (const k of Object.keys(src)) {
434
434
  out[k] = structuralSnapshot(src[k]);
435
435
  }
@@ -490,7 +490,7 @@ function hashStableString(input, seed = 0) {
490
490
  const ANON_STEM = "atta";
491
491
  function readableFormKeyStem(formKey) {
492
492
  if (formKey === "" || formKey.startsWith(ANONYMOUS_FORM_KEY_PREFIX)) return ANON_STEM;
493
- const sanitized = formKey.replace(/[^A-Za-z0-9_-]+/g, "-").replace(/^-+|-+$/g, "");
493
+ const sanitized = formKey.replace(/[^A-Za-z0-9_-]+/g, "-").replace(/^-+|(?<!-)-+$/g, "");
494
494
  return sanitized === "" ? ANON_STEM : sanitized;
495
495
  }
496
496
  function fieldIdToken(formInstanceId, pathKey) {
@@ -524,6 +524,7 @@ function humanize(segment) {
524
524
  }).join(" ");
525
525
  }
526
526
 
527
+ const warnedDisplayStatePredicates = /* @__PURE__ */ new WeakSet();
527
528
  function isUnderStubAncestor(state, segments) {
528
529
  for (let i = 0; i < segments.length; i++) {
529
530
  const ancestorPath = segments.slice(0, i);
@@ -607,7 +608,7 @@ function buildLeafFieldState(state, segments, key, formInstanceId, getFormMetaBa
607
608
  function buildContainerFieldStateBase(state, segments, key, formInstanceId) {
608
609
  const formValue = state.form.value;
609
610
  const value = state.getValueAtPath(segments);
610
- const original = state.originals.get(paths.canonicalizePath(segments).key)?.value;
611
+ const original = state.originals.get(key)?.value;
611
612
  let pristine = true;
612
613
  let blank = true;
613
614
  let dirty = false;
@@ -620,13 +621,12 @@ function buildContainerFieldStateBase(state, segments, key, formInstanceId) {
620
621
  let validating = false;
621
622
  let updatedAt = null;
622
623
  let asyncPending = false;
623
- for (const [, entry] of state.originals) {
624
+ for (const [leafKey, entry] of state.originals) {
624
625
  if (!paths.isPathPrefix(segments, entry.segments)) continue;
625
626
  if (segments.length === entry.segments.length) continue;
626
627
  if (!hasAtPath(formValue, entry.segments)) continue;
627
- const leafKey = paths.canonicalizePath(entry.segments).key;
628
628
  const leafRecord = state.fields.get(leafKey);
629
- if (!state.isPristineAtPath(entry.segments)) {
629
+ if (!state.isPristineAtPathByKey(leafKey, entry.segments)) {
630
630
  pristine = false;
631
631
  dirty = true;
632
632
  }
@@ -638,7 +638,7 @@ function buildContainerFieldStateBase(state, segments, key, formInstanceId) {
638
638
  if (leafRecord?.blurredAfterInteraction === true) blurredAfterInteraction = true;
639
639
  if (leafRecord?.connected === true) connected = true;
640
640
  if ((state.fieldValidationCounts.get(leafKey) ?? 0) > 0) validating = true;
641
- if (state.pathHasAsyncValidation(entry.segments)) asyncPending = true;
641
+ if (state.pathHasAsyncValidationByKey(leafKey, entry.segments)) asyncPending = true;
642
642
  const ts = leafRecord?.updatedAt;
643
643
  if (ts !== void 0 && ts !== null) {
644
644
  if (updatedAt === null || ts > updatedAt) updatedAt = ts;
@@ -693,7 +693,14 @@ function decorateWithDerivedProps(base, state, getFormMetaBase, getDisplayState)
693
693
  let displayState;
694
694
  try {
695
695
  displayState = predicate(base, formMeta);
696
- } catch {
696
+ } catch (err) {
697
+ if (paths.__DEV__ && !warnedDisplayStatePredicates.has(predicate)) {
698
+ warnedDisplayStatePredicates.add(predicate);
699
+ console.warn(
700
+ "[attaform] custom getDisplayState threw \u2014 falling back to defaultDisplayState. Subsequent throws from the same predicate will not warn again.",
701
+ err
702
+ );
703
+ }
697
704
  displayState = defaultDisplayState(base, formMeta);
698
705
  }
699
706
  return {
@@ -730,6 +737,41 @@ function aggregateErrorsAt(state, prefix) {
730
737
  }
731
738
  const EMPTY_ELEMENTS = Object.freeze([]);
732
739
 
740
+ function liveKeysAtPath(state, segments) {
741
+ const value = getAtPath(state.form.value, segments);
742
+ if (value === null || value === void 0) return [];
743
+ if (Array.isArray(value)) {
744
+ const keys = new Array(value.length);
745
+ for (let i = 0; i < value.length; i += 1) keys[i] = String(i);
746
+ return keys;
747
+ }
748
+ if (typeof value === "object") return Object.keys(value);
749
+ return [];
750
+ }
751
+ function isArrayPath(state, segments) {
752
+ if (segments.length === 0) return false;
753
+ return Array.isArray(getAtPath(state.form.value, segments));
754
+ }
755
+
756
+ function warnReadOnly(surface, action, key) {
757
+ if (!paths.__DEV__) return;
758
+ const phrase = action === "write" ? `write to "${String(key)}"` : `${action} of "${String(key)}"`;
759
+ console.warn(
760
+ `[attaform] ${surface} is read-only \u2014 ${phrase} was ignored. Mutate the form via setValue / the directive / field-array helpers instead.`
761
+ );
762
+ }
763
+ function makeReadonlyCoercion(snapshot) {
764
+ const toString = () => JSON.stringify(snapshot());
765
+ return {
766
+ toString,
767
+ valueOf() {
768
+ return this;
769
+ },
770
+ toJSON: snapshot,
771
+ toPrimitive: (hint) => hint === "number" ? NaN : toString()
772
+ };
773
+ }
774
+
733
775
  const INTEGER_SEGMENT = /^(?:0|[1-9]\d*)$/;
734
776
  function keyToSegment(key) {
735
777
  return INTEGER_SEGMENT.test(key) ? Number(key) : key;
@@ -761,12 +803,12 @@ function buildSurfaceProxy(opts) {
761
803
  const existing = containerCache.get(cacheKey);
762
804
  if (existing !== void 0) return existing;
763
805
  const snapshotContainer = () => opts.materializeContainer === void 0 ? {} : opts.materializeContainer(segments);
764
- const containerToJSON = () => snapshotContainer();
765
- const containerToString = () => JSON.stringify(snapshotContainer());
766
- function containerValueOf() {
767
- return this;
768
- }
769
- const containerToPrimitive = (hint) => hint === "number" ? NaN : containerToString();
806
+ const {
807
+ toString: containerToString,
808
+ valueOf: containerValueOf,
809
+ toJSON: containerToJSON,
810
+ toPrimitive: containerToPrimitive
811
+ } = makeReadonlyCoercion(snapshotContainer);
770
812
  const target = isArrayLike ? [] : (() => {
771
813
  });
772
814
  const proxy = new Proxy(target, {
@@ -791,6 +833,9 @@ function buildSurfaceProxy(opts) {
791
833
  return opts.containerOwnKeys === void 0 ? 0 : opts.containerOwnKeys(segments).length;
792
834
  }
793
835
  const childSegs = [...segments, keyToSegment(key)];
836
+ if ((isArrayLike || opts.isArrayContainer?.(segments) === true) && typeof keyToSegment(key) === "string" && key in Array.prototype) {
837
+ return Reflect.get(Array.prototype, key);
838
+ }
794
839
  if (key === "toString" || key === "valueOf") {
795
840
  if (!schemaHasPath(childSegs)) {
796
841
  return key === "toString" ? containerToString : containerValueOf;
@@ -835,10 +880,25 @@ function buildSurfaceProxy(opts) {
835
880
  };
836
881
  },
837
882
  // Block writes at the proxy boundary. Mutations go through
838
- // `setValue`, the directive, or the field-array helpers.
839
- set: () => false,
840
- deleteProperty: () => false,
841
- defineProperty: () => false
883
+ // `setValue`, the directive, or the field-array helpers. Each
884
+ // trap returns `true` (warn-and-noop) returning `false` from a
885
+ // `set`/`delete`/`defineProperty` trap throws `TypeError` under
886
+ // strict mode (every ESM / `<script setup>`), which would surface
887
+ // a host-level exception in consumer code that the library
888
+ // documents as "writes are ignored." Aligns with the contract on
889
+ // `form.values` / `wizard.statuses`.
890
+ set: (_, key) => {
891
+ warnReadOnly("form.fields / form.errors", "write", key);
892
+ return true;
893
+ },
894
+ deleteProperty: (_, key) => {
895
+ warnReadOnly("form.fields / form.errors", "delete", key);
896
+ return true;
897
+ },
898
+ defineProperty: (_, key) => {
899
+ warnReadOnly("form.fields / form.errors", "define", key);
900
+ return true;
901
+ }
842
902
  });
843
903
  containerCache.set(cacheKey, proxy);
844
904
  return proxy;
@@ -860,11 +920,12 @@ function buildSurfaceProxy(opts) {
860
920
  }
861
921
  return snapshot;
862
922
  };
863
- const leafToString = () => JSON.stringify(snapshotLeaf());
864
- function leafValueOf() {
865
- return this;
866
- }
867
- const leafToPrimitive = (hint) => hint === "number" ? NaN : leafToString();
923
+ const {
924
+ toString: leafToString,
925
+ valueOf: leafValueOf,
926
+ toJSON: leafToJSONHandler,
927
+ toPrimitive: leafToPrimitive
928
+ } = makeReadonlyCoercion(snapshotLeaf);
868
929
  const target = (() => {
869
930
  });
870
931
  const proxy = new Proxy(target, {
@@ -882,7 +943,7 @@ function buildSurfaceProxy(opts) {
882
943
  if (typeof key !== "string") return void 0;
883
944
  if (key === "toString") return leafToString;
884
945
  if (key === "valueOf") return leafValueOf;
885
- if (key === "toJSON") return snapshotLeaf;
946
+ if (key === "toJSON") return leafToJSONHandler;
886
947
  if (leafKeys.has(key)) {
887
948
  const leaf = opts.resolveLeaf(segments);
888
949
  return readLeafKey(leaf, key);
@@ -909,9 +970,22 @@ function buildSurfaceProxy(opts) {
909
970
  writable: false
910
971
  };
911
972
  },
912
- set: () => false,
913
- deleteProperty: () => false,
914
- defineProperty: () => false
973
+ // Same warn-and-noop contract as the container traps above.
974
+ // Returning `true` keeps strict-mode callers from throwing on
975
+ // `form.fields.email.value = …`; the actual readonly guarantee
976
+ // is the absence of any mutation, not the host-level reject.
977
+ set: (_, key) => {
978
+ warnReadOnly("form.fields.<leaf>", "write", key);
979
+ return true;
980
+ },
981
+ deleteProperty: (_, key) => {
982
+ warnReadOnly("form.fields.<leaf>", "delete", key);
983
+ return true;
984
+ },
985
+ defineProperty: (_, key) => {
986
+ warnReadOnly("form.fields.<leaf>", "define", key);
987
+ return true;
988
+ }
915
989
  });
916
990
  leafViewCache.set(cacheKey, proxy);
917
991
  return proxy;
@@ -975,33 +1049,53 @@ function buildErrorsProxy(state) {
975
1049
  // working — the call-form just extends that semantic to
976
1050
  // containers and dynamic paths.
977
1051
  resolveCallTarget: (path) => aggregateErrorsAt(state, path),
978
- // Mirror `form.fields` enumeration: `Object.keys(form.errors.items)`
979
- // and `v-for="(errs, idx) in form.errors.items"` walk the live
980
- // array indices / object keys at the path. Iteration yields the
981
- // descended sub-proxies (one per live key), so consumers can
982
- // `form.errors.items[idx]` straight from the entry.
983
- containerOwnKeys: (segments) => liveKeysAtPath$1(state, segments),
984
- isArrayContainer: (segments) => isArrayPath$1(state, segments)
1052
+ // Enumeration unions the live form-data keys at this path with the
1053
+ // first-child segments drawn from every error store. Without the
1054
+ // union, `Object.keys(form.errors)` / `{...form.errors}` /
1055
+ // `v-for="(errs, k) in form.errors"` silently dropped two
1056
+ // important error classes that the dot / call / JSON.stringify
1057
+ // surfaces already exposed:
1058
+ //
1059
+ // - **Form-level** errors at the synthetic `['']` path (set via
1060
+ // `setFormErrors` or root cross-field refines).
1061
+ // - **Server-only** errors at a key the schema doesn't know
1062
+ // about (`['ghost']`, `['address', 'ghost']`).
1063
+ //
1064
+ // The union closes that gap so `ownKeys` agrees with the rest of
1065
+ // the surface. Active-path filter mirrors `resolveLeaf`:
1066
+ // library-produced verdicts (schema + derived-blank) at unreachable
1067
+ // paths stay hidden; user-supplied errors are unconditional.
1068
+ containerOwnKeys: (segments) => errorAwareContainerKeys(state, segments),
1069
+ isArrayContainer: (segments) => isArrayPath(state, segments)
985
1070
  });
986
1071
  }
987
- function liveKeysAtPath$1(state, segments) {
988
- const value = getAtPath(state.form.value, segments);
989
- if (value === null || value === void 0) return [];
990
- if (Array.isArray(value)) {
991
- const keys = new Array(value.length);
992
- for (let i = 0; i < value.length; i += 1) keys[i] = String(i);
993
- return keys;
994
- }
995
- if (typeof value === "object") return Object.keys(value);
996
- return [];
997
- }
998
- function isArrayPath$1(state, segments) {
999
- if (segments.length === 0) return false;
1000
- return Array.isArray(getAtPath(state.form.value, segments));
1072
+ function errorAwareContainerKeys(state, segments) {
1073
+ const keys = new Set(liveKeysAtPath(state, segments));
1074
+ const formValue = state.form.value;
1075
+ const walk = (store, applyActivePathFilter) => {
1076
+ for (const [pathKey, errors] of store) {
1077
+ if (errors.length === 0) continue;
1078
+ if (pathKey === paths.FORM_ERRORS_PATH_KEY) {
1079
+ if (segments.length === 0) keys.add("");
1080
+ continue;
1081
+ }
1082
+ const decoded = paths.segmentsForPathKey(pathKey);
1083
+ if (decoded === null) continue;
1084
+ if (decoded.length <= segments.length) continue;
1085
+ if (!paths.isPathPrefix(segments, decoded)) continue;
1086
+ if (applyActivePathFilter && !hasAtPath(formValue, decoded)) continue;
1087
+ const nextSeg = decoded[segments.length];
1088
+ keys.add(typeof nextSeg === "number" ? String(nextSeg) : nextSeg);
1089
+ }
1090
+ };
1091
+ walk(state.schemaErrors, true);
1092
+ walk(state.derivedBlankErrors.value, true);
1093
+ walk(state.userErrors, false);
1094
+ return [...keys];
1001
1095
  }
1002
1096
  function materializeErrors(state, containerSegments) {
1003
1097
  const liveContainer = getAtPath(state.form.value, containerSegments);
1004
- const tree = Array.isArray(liveContainer) ? [] : {};
1098
+ const tree = Array.isArray(liveContainer) ? [] : /* @__PURE__ */ Object.create(null);
1005
1099
  const collect = (store, applyActivePathFilter) => {
1006
1100
  entries: for (const [pathKey, errors] of store) {
1007
1101
  if (errors.length === 0) continue;
@@ -1051,7 +1145,7 @@ function placeAt(tree, path, errors) {
1051
1145
  const cursorRecord2 = cursor;
1052
1146
  let child = cursorRecord2[key];
1053
1147
  if (child === null || child === void 0 || typeof child !== "object") {
1054
- child = typeof nextSeg === "number" ? [] : {};
1148
+ child = typeof nextSeg === "number" ? [] : /* @__PURE__ */ Object.create(null);
1055
1149
  cursorRecord2[key] = child;
1056
1150
  }
1057
1151
  cursor = child;
@@ -1090,9 +1184,10 @@ function buildFieldArrayApi(state) {
1090
1184
  },
1091
1185
  insert(path, index, value) {
1092
1186
  const next = readArray(path);
1093
- next.splice(index, 0, value);
1094
- const clampedIndex = Math.max(0, Math.min(index, next.length));
1095
- return writeArray(path, next, { kind: "insert", index: clampedIndex });
1187
+ const preLen = next.length;
1188
+ const insertIndex = index < 0 ? Math.max(0, preLen + index) : Math.min(index, preLen);
1189
+ next.splice(insertIndex, 0, value);
1190
+ return writeArray(path, next, { kind: "insert", index: insertIndex });
1096
1191
  },
1097
1192
  remove(path, index) {
1098
1193
  const next = readArray(path);
@@ -1214,9 +1309,34 @@ function buildFieldStateProxy(state, formInstanceId, getFormMetaBase, options) {
1214
1309
  writable: false
1215
1310
  };
1216
1311
  },
1217
- set: () => false,
1218
- deleteProperty: () => false,
1219
- defineProperty: () => false
1312
+ // Warn-and-noop: returning `false` here throws `TypeError` under
1313
+ // strict mode (`form.fields('email').value = 1` from any ESM
1314
+ // module), which would violate the documented "writes are
1315
+ // ignored" contract. Mirrors `values-proxy` / `wizard-statuses-proxy`.
1316
+ set: (_, key) => {
1317
+ if (paths.__DEV__) {
1318
+ console.warn(
1319
+ `[attaform] form.fields(path) is read-only \u2014 write to "${String(key)}" was ignored. Use form.setValue / the directive / field-array helpers instead.`
1320
+ );
1321
+ }
1322
+ return true;
1323
+ },
1324
+ deleteProperty: (_, key) => {
1325
+ if (paths.__DEV__) {
1326
+ console.warn(
1327
+ `[attaform] form.fields(path) is read-only \u2014 delete of "${String(key)}" was ignored.`
1328
+ );
1329
+ }
1330
+ return true;
1331
+ },
1332
+ defineProperty: (_, key) => {
1333
+ if (paths.__DEV__) {
1334
+ console.warn(
1335
+ `[attaform] form.fields(path) is read-only \u2014 define of "${String(key)}" was ignored.`
1336
+ );
1337
+ }
1338
+ return true;
1339
+ }
1220
1340
  });
1221
1341
  terminalCache.set(cacheKey, proxy);
1222
1342
  return proxy;
@@ -1232,21 +1352,6 @@ function buildFieldStateProxy(state, formInstanceId, getFormMetaBase, options) {
1232
1352
  isArrayContainer: (segments) => isArrayPath(state, segments)
1233
1353
  });
1234
1354
  }
1235
- function liveKeysAtPath(state, segments) {
1236
- const value = getAtPath(state.form.value, segments);
1237
- if (value === null || value === void 0) return [];
1238
- if (Array.isArray(value)) {
1239
- const keys = new Array(value.length);
1240
- for (let i = 0; i < value.length; i += 1) keys[i] = String(i);
1241
- return keys;
1242
- }
1243
- if (typeof value === "object") return Object.keys(value);
1244
- return [];
1245
- }
1246
- function isArrayPath(state, segments) {
1247
- if (segments.length === 0) return false;
1248
- return Array.isArray(getAtPath(state.form.value, segments));
1249
- }
1250
1355
  function materializeFields(state, containerSegments, snapshotFieldStateAt) {
1251
1356
  const liveValue = getAtPath(state.form.value, containerSegments);
1252
1357
  return walk$2(liveValue, containerSegments, state.schema, snapshotFieldStateAt);
@@ -1290,7 +1395,7 @@ async function getStorageAdapter(storage) {
1290
1395
  }
1291
1396
  }
1292
1397
  }
1293
- const PERSISTED_ENVELOPE_VERSION = 5;
1398
+ const PERSISTED_ENVELOPE_VERSION = 6;
1294
1399
  function readPersistedPayload(value) {
1295
1400
  if (value === null || value === void 0 || typeof value !== "object") return null;
1296
1401
  const envelope = value;
@@ -1314,12 +1419,7 @@ function warnVersionMismatch(observedVersion) {
1314
1419
  function buildPersistedPayload(form, include, schemaErrors, userErrors, blankPaths) {
1315
1420
  let transientList;
1316
1421
  if (blankPaths !== void 0 && blankPaths.size > 0) {
1317
- const dotted = [];
1318
- for (const key of blankPaths) {
1319
- const d = paths.pathKeyToDotted(key);
1320
- if (d !== null) dotted.push(d);
1321
- }
1322
- transientList = dotted.length > 0 ? dotted : void 0;
1422
+ transientList = [...blankPaths];
1323
1423
  }
1324
1424
  if (include === "form") {
1325
1425
  if (transientList === void 0) return { v: PERSISTED_ENVELOPE_VERSION, data: { form } };
@@ -1341,30 +1441,35 @@ function buildPersistedPayload(form, include, schemaErrors, userErrors, blankPat
1341
1441
  function createDebouncedWriter(write, debounceMs) {
1342
1442
  let timer = null;
1343
1443
  let pending = null;
1444
+ let writeGeneration = 0;
1445
+ function runWrite() {
1446
+ const gen = ++writeGeneration;
1447
+ pending = write().finally(() => {
1448
+ if (writeGeneration === gen) pending = null;
1449
+ });
1450
+ }
1344
1451
  function schedule() {
1345
1452
  if (timer !== null) clearTimeout(timer);
1346
1453
  if (debounceMs === 0) {
1347
- pending = write().finally(() => {
1348
- pending = null;
1349
- });
1454
+ runWrite();
1350
1455
  return;
1351
1456
  }
1352
1457
  timer = setTimeout(() => {
1353
1458
  timer = null;
1354
- pending = write().finally(() => {
1355
- pending = null;
1356
- });
1459
+ runWrite();
1357
1460
  }, debounceMs);
1358
1461
  }
1359
1462
  async function flush() {
1360
1463
  if (timer !== null) {
1361
1464
  clearTimeout(timer);
1362
1465
  timer = null;
1363
- pending = write().finally(() => {
1364
- pending = null;
1365
- });
1466
+ runWrite();
1467
+ }
1468
+ while (pending !== null) {
1469
+ const awaited = pending;
1470
+ await awaited;
1471
+ if (pending === awaited) break;
1366
1472
  }
1367
- if (pending !== null) await pending;
1368
1473
  }
1369
1474
  function cancel() {
1370
1475
  if (timer !== null) {
@@ -1377,7 +1482,7 @@ function createDebouncedWriter(write, debounceMs) {
1377
1482
  function resolveStorageKeyBase(config, formKey) {
1378
1483
  return config.key ?? `${PERSISTENCE_KEY_PREFIX}${formKey}`;
1379
1484
  }
1380
- async function cleanupOrphanKeys(adapter, base, currentKey) {
1485
+ async function removeMatchingKeys(adapter, base, keepKey) {
1381
1486
  let keys;
1382
1487
  try {
1383
1488
  keys = await adapter.listKeys(base);
@@ -1385,12 +1490,15 @@ async function cleanupOrphanKeys(adapter, base, currentKey) {
1385
1490
  return;
1386
1491
  }
1387
1492
  for (const key of keys) {
1388
- if (key === currentKey) continue;
1493
+ if (key === keepKey) continue;
1389
1494
  if (key === base || key.startsWith(`${base}:`)) {
1390
1495
  void adapter.removeItem(key).catch(() => void 0);
1391
1496
  }
1392
1497
  }
1393
1498
  }
1499
+ async function cleanupOrphanKeys(adapter, base, currentKey) {
1500
+ await removeMatchingKeys(adapter, base, currentKey);
1501
+ }
1394
1502
  const STANDARD_STORAGE_KINDS = ["local", "session", "indexeddb"];
1395
1503
  function normalizePersistConfig(input) {
1396
1504
  if (typeof input === "string") return { storage: input };
@@ -1401,12 +1509,7 @@ async function sweepAllOrphansAcrossStandardStores(base) {
1401
1509
  for (const kind of STANDARD_STORAGE_KINDS) {
1402
1510
  try {
1403
1511
  const adapter = await getStorageAdapter(kind);
1404
- const keys = await adapter.listKeys(base);
1405
- for (const key of keys) {
1406
- if (key === base || key.startsWith(`${base}:`)) {
1407
- void adapter.removeItem(key).catch(() => void 0);
1408
- }
1409
- }
1512
+ await removeMatchingKeys(adapter, base);
1410
1513
  } catch {
1411
1514
  }
1412
1515
  }
@@ -1417,12 +1520,7 @@ async function sweepNonConfiguredStandardStoresForOrphans(configured, base) {
1417
1520
  if (kind === configuredKind) continue;
1418
1521
  try {
1419
1522
  const adapter = await getStorageAdapter(kind);
1420
- const keys = await adapter.listKeys(base);
1421
- for (const key of keys) {
1422
- if (key === base || key.startsWith(`${base}:`)) {
1423
- void adapter.removeItem(key).catch(() => void 0);
1424
- }
1425
- }
1523
+ await removeMatchingKeys(adapter, base);
1426
1524
  } catch {
1427
1525
  }
1428
1526
  }
@@ -1438,6 +1536,29 @@ function pluckPaths(form, pathKeys) {
1438
1536
  }
1439
1537
  return sparse ?? {};
1440
1538
  }
1539
+ function stripUnacknowledgedSensitiveLeaves(form, optedInPaths, isSensitivePath) {
1540
+ const acknowledgedSensitive = [];
1541
+ for (const key of optedInPaths) {
1542
+ const segs = paths.segmentsForPathKey(key);
1543
+ if (segs !== null && isSensitivePath(segs)) acknowledgedSensitive.push(segs);
1544
+ }
1545
+ const coveredByAcknowledged = (path) => acknowledgedSensitive.some((prefix) => paths.isPathPrefix(prefix, path));
1546
+ const walk = (path, value) => {
1547
+ if (path.length > 0 && isSensitivePath(path) && !coveredByAcknowledged(path)) {
1548
+ return void 0;
1549
+ }
1550
+ if (value === null || typeof value !== "object") return value;
1551
+ if (Array.isArray(value)) return value.map((item, i) => walk([...path, i], item));
1552
+ if (!isPlainRecord(value)) return value;
1553
+ const out = /* @__PURE__ */ Object.create(null);
1554
+ for (const key of Object.keys(value)) {
1555
+ const walked = walk([...path, key], value[key]);
1556
+ if (walked !== void 0) out[key] = walked;
1557
+ }
1558
+ return out;
1559
+ };
1560
+ return walk([], form);
1561
+ }
1441
1562
  function filterErrorsByPaths(errors, pathKeys) {
1442
1563
  const out = /* @__PURE__ */ new Map();
1443
1564
  for (const [key, value] of errors) {
@@ -1464,7 +1585,7 @@ function mergeDeep(target, source, path, schema) {
1464
1585
  if (sourceDisc !== void 0) {
1465
1586
  const variantDefault = du.getVariantDefault(sourceDisc);
1466
1587
  if (isPlainRecord(variantDefault)) {
1467
- const out2 = { ...variantDefault };
1588
+ const out2 = Object.assign(/* @__PURE__ */ Object.create(null), variantDefault);
1468
1589
  for (const key of Object.keys(sourceRecord)) {
1469
1590
  if (!(key in variantDefault) && key !== du.discriminatorKey) continue;
1470
1591
  out2[key] = mergeDeep(out2[key], sourceRecord[key], [...path, key], schema);
@@ -1476,7 +1597,7 @@ function mergeDeep(target, source, path, schema) {
1476
1597
  }
1477
1598
  }
1478
1599
  const mergeTarget = target;
1479
- const out = isPlainRecord(mergeTarget) ? { ...mergeTarget } : {};
1600
+ const out = isPlainRecord(mergeTarget) ? Object.assign(/* @__PURE__ */ Object.create(null), mergeTarget) : /* @__PURE__ */ Object.create(null);
1480
1601
  for (const key of Object.keys(source)) {
1481
1602
  out[key] = mergeDeep(out[key], source[key], [...path, key], schema);
1482
1603
  }
@@ -1579,39 +1700,44 @@ function buildProcessForm(state, formInstanceId, options = {}) {
1579
1700
  }
1580
1701
  return result;
1581
1702
  }
1582
- async function validateAsync(pathInput) {
1703
+ async function runImperativeValidation(pathInput, config) {
1583
1704
  const segments = pathInput === void 0 ? void 0 : toSegments(pathInput);
1584
1705
  const dataAtPath = segments === void 0 ? state.form.value : state.getValueAtPath(segments);
1585
1706
  try {
1586
1707
  state.activeValidations.value += 1;
1587
- state.cancelFieldValidation();
1708
+ if (config.cancelInFlight) state.cancelFieldValidation();
1588
1709
  const refinement = await runRefinementValidation(dataAtPath, segments);
1589
- const scopePath = segments ?? [];
1590
- const errors = refinement.success ? [] : refinement.errors;
1591
- const reStamped = segments === void 0 ? errors : errors.map((err) => ({
1592
- ...err,
1593
- path: [...segments, ...err.path]
1594
- }));
1595
- state.applySchemaErrorsForSubtree(scopePath, reStamped);
1596
- return stripData(composeWithDerivedBlank(refinement, segments));
1710
+ if (config.commitToSchemaErrors) {
1711
+ const scopePath = segments ?? [];
1712
+ const errors = refinement.success ? [] : refinement.errors;
1713
+ const reStamped = segments === void 0 ? errors : errors.map((err) => ({
1714
+ ...err,
1715
+ path: [...segments, ...err.path]
1716
+ }));
1717
+ state.applySchemaErrorsForSubtree(scopePath, reStamped);
1718
+ }
1719
+ return { ok: true, refinement, segments };
1597
1720
  } catch (err) {
1598
- return adapterThrowResponse(err);
1721
+ return { ok: false, error: adapterThrowResponse(err) };
1599
1722
  } finally {
1600
1723
  state.activeValidations.value = Math.max(0, state.activeValidations.value - 1);
1601
1724
  }
1602
1725
  }
1726
+ async function validateAsync(pathInput) {
1727
+ const result = await runImperativeValidation(pathInput, {
1728
+ cancelInFlight: true,
1729
+ commitToSchemaErrors: true
1730
+ });
1731
+ if (!result.ok) return result.error;
1732
+ return stripData(composeWithDerivedBlank(result.refinement, result.segments));
1733
+ }
1603
1734
  async function process(pathInput) {
1604
- const segments = pathInput === void 0 ? void 0 : toSegments(pathInput);
1605
- const dataAtPath = segments === void 0 ? state.form.value : state.getValueAtPath(segments);
1606
- try {
1607
- state.activeValidations.value += 1;
1608
- const refinement = await runRefinementValidation(dataAtPath, segments);
1609
- return composeWithDerivedBlank(refinement, segments);
1610
- } catch (err) {
1611
- return adapterThrowResponse(err);
1612
- } finally {
1613
- state.activeValidations.value = Math.max(0, state.activeValidations.value - 1);
1614
- }
1735
+ const result = await runImperativeValidation(pathInput, {
1736
+ cancelInFlight: false,
1737
+ commitToSchemaErrors: false
1738
+ });
1739
+ if (!result.ok) return result.error;
1740
+ return composeWithDerivedBlank(result.refinement, result.segments);
1615
1741
  }
1616
1742
  function adapterThrowResponse(err) {
1617
1743
  return {
@@ -2049,7 +2175,6 @@ function coerceValue(value, accepted, elementAccepted, index) {
2049
2175
  }
2050
2176
 
2051
2177
  const EMPTY_TRANSFORMS = Object.freeze([]);
2052
- const INTERACTIVE_TAG_NAMES = /* @__PURE__ */ new Set(["INPUT", "SELECT", "TEXTAREA"]);
2053
2178
  const attaformListenersSymbol = Symbol.for("attaform:focus-listeners");
2054
2179
  function attachFocusListeners(state, segments, element, instanceMeta) {
2055
2180
  const target = element;
@@ -2100,7 +2225,11 @@ function buildRegister(state, formInstanceId, instanceConfig) {
2100
2225
  if (typed !== null && typeof raw === "number" && parseFloat(typed) === raw) {
2101
2226
  return typed;
2102
2227
  }
2103
- return String(raw);
2228
+ try {
2229
+ return String(raw);
2230
+ } catch {
2231
+ return Object.prototype.toString.call(raw);
2232
+ }
2104
2233
  });
2105
2234
  const slimDefault = state.schema.getDefaultAtPath(segments);
2106
2235
  const slimTypes = state.schema.getSlimPrimitiveTypesAtPath(segments);
@@ -2155,7 +2284,7 @@ function buildRegister(state, formInstanceId, instanceConfig) {
2155
2284
  state.markInteracted(segments);
2156
2285
  },
2157
2286
  registerElement: (element) => {
2158
- if (!INTERACTIVE_TAG_NAMES.has(element.tagName)) return;
2287
+ if (!paths.INTERACTIVE_TAG_NAMES.has(element.tagName)) return;
2159
2288
  const added = state.registerElement(segments, element, formInstanceId);
2160
2289
  if (added) attachFocusListeners(state, segments, element, instanceMeta);
2161
2290
  },
@@ -2254,7 +2383,7 @@ function walk(input, segments, schema, paths) {
2254
2383
  if (slim !== null && slim !== void 0 && typeof slim === "object" && !Array.isArray(slim) && !(slim instanceof Date) && !(slim instanceof RegExp) && !(slim instanceof Map) && !(slim instanceof Set)) {
2255
2384
  for (const k of Object.keys(slim)) allKeys.add(k);
2256
2385
  }
2257
- const out = {};
2386
+ const out = /* @__PURE__ */ Object.create(null);
2258
2387
  let mutated = allKeys.size !== inputKeys.length;
2259
2388
  for (const key of allKeys) {
2260
2389
  const orig = input[key];
@@ -2283,7 +2412,7 @@ function walkUnspecified(slim, segments, paths$1) {
2283
2412
  }
2284
2413
  if (Array.isArray(slim)) return slim;
2285
2414
  if (slim !== null && typeof slim === "object") {
2286
- const out = {};
2415
+ const out = /* @__PURE__ */ Object.create(null);
2287
2416
  for (const key of Object.keys(slim)) {
2288
2417
  out[key] = walkUnspecified(slim[key], [...segments, key], paths$1);
2289
2418
  }
@@ -2316,7 +2445,7 @@ function substitute(input, segments, schema, paths) {
2316
2445
  }
2317
2446
  if (typeof input === "object") {
2318
2447
  let mutated = false;
2319
- const out = {};
2448
+ const out = /* @__PURE__ */ Object.create(null);
2320
2449
  for (const key of Object.keys(input)) {
2321
2450
  const orig = input[key];
2322
2451
  const walked = substitute(orig, [...segments, key], schema, paths);
@@ -2368,70 +2497,86 @@ function expandUnsetAt(segments, schema, paths$1) {
2368
2497
  return result;
2369
2498
  }
2370
2499
 
2371
- function buildValuesProxy(form) {
2372
- const inner = vue.computed(() => vue.readonly(form.value));
2500
+ function buildCallableReadonlySnapshotProxy(opts) {
2373
2501
  const target = (() => {
2374
2502
  });
2375
- const valuesToString = () => JSON.stringify(inner.value);
2376
- const valuesToPrimitive = (hint) => hint === "number" ? NaN : valuesToString();
2503
+ const { toString, valueOf, toJSON, toPrimitive } = makeReadonlyCoercion(opts.snapshot);
2504
+ const callResolve = opts.resolveCall ?? ((arg) => opts.resolveKey(String(arg)));
2377
2505
  return new Proxy(target, {
2378
2506
  apply(_, __, args) {
2379
2507
  const arg = args[0];
2380
- if (arg === void 0) return inner.value;
2381
- const { segments } = paths.canonicalizePath(arg);
2382
- let cursor = inner.value;
2383
- for (const seg of segments) {
2384
- if (cursor === null || cursor === void 0) return void 0;
2385
- cursor = cursor[seg];
2386
- }
2387
- return cursor;
2508
+ if (arg === void 0) return opts.snapshot();
2509
+ return callResolve(arg);
2388
2510
  },
2389
2511
  get(_, key) {
2390
2512
  if (typeof key === "symbol") {
2391
- if (key === Symbol.toPrimitive) return valuesToPrimitive;
2513
+ if (key === Symbol.toPrimitive) return toPrimitive;
2392
2514
  return Reflect.get(target, key);
2393
2515
  }
2394
- if (key === "toJSON") return () => inner.value;
2395
- if (key === "toString") return valuesToString;
2396
- if (key === "valueOf")
2397
- return function() {
2398
- return this;
2399
- };
2400
- return inner.value[key];
2516
+ if (key === "toJSON") return toJSON;
2517
+ if (key === "toString") return toString;
2518
+ if (key === "valueOf") return valueOf;
2519
+ return opts.resolveKey(key);
2401
2520
  },
2402
2521
  has(_, key) {
2403
2522
  if (typeof key === "symbol") return Reflect.has(target, key);
2404
- return Reflect.has(inner.value, key);
2405
- },
2406
- ownKeys() {
2407
- return Reflect.ownKeys(inner.value);
2523
+ return opts.hasKey(key);
2408
2524
  },
2525
+ ownKeys: () => opts.ownKeys(),
2409
2526
  getOwnPropertyDescriptor(_, key) {
2410
- const desc = Reflect.getOwnPropertyDescriptor(inner.value, key);
2411
- if (desc !== void 0) desc.configurable = true;
2412
- return desc;
2527
+ if (typeof key !== "string") return void 0;
2528
+ if (opts.describeKey !== void 0) return opts.describeKey(key);
2529
+ if (!opts.hasKey(key)) return void 0;
2530
+ return {
2531
+ configurable: true,
2532
+ enumerable: true,
2533
+ writable: false,
2534
+ value: opts.resolveKey(key)
2535
+ };
2413
2536
  },
2414
- // Match Vue's `readonly()` semantics: writes warn (in dev) and
2415
- // silently noop (return true). Returning false would throw
2416
- // TypeError in strict-mode consumers, surprising users who
2417
- // assigned through the proxy and expected it to be ignored.
2418
- set(_, key) {
2419
- if (paths.__DEV__) {
2420
- console.warn(
2421
- `[attaform] form.values is read-only \u2014 write to "${String(key)}" was ignored. Use form.setValue / the directive / field-array helpers instead.`
2422
- );
2423
- }
2537
+ set: (_, key) => {
2538
+ warnReadOnly(opts.surface, "write", key);
2424
2539
  return true;
2425
2540
  },
2426
- deleteProperty(_, key) {
2427
- if (paths.__DEV__) {
2428
- console.warn(
2429
- `[attaform] form.values is read-only \u2014 delete of "${String(key)}" was ignored.`
2430
- );
2431
- }
2541
+ deleteProperty: (_, key) => {
2542
+ warnReadOnly(opts.surface, "delete", key);
2543
+ return true;
2544
+ },
2545
+ defineProperty: (_, key) => {
2546
+ warnReadOnly(opts.surface, "define", key);
2432
2547
  return true;
2548
+ }
2549
+ });
2550
+ }
2551
+
2552
+ function buildValuesProxy(form) {
2553
+ const inner = vue.computed(() => vue.readonly(form.value));
2554
+ return buildCallableReadonlySnapshotProxy({
2555
+ surface: "form.values",
2556
+ snapshot: () => inner.value,
2557
+ // Read through the readonly proxy at access time so Vue's
2558
+ // dependency tracking lands inside the consumer's active effect
2559
+ // — `inner.value[key]` is what triggers per-key tracking.
2560
+ resolveKey: (key) => inner.value[key],
2561
+ // Dynamic path: walk segments through the readonly proxy. Each
2562
+ // step reads through the proxy's own get traps so dependency
2563
+ // tracking propagates at every level.
2564
+ resolveCall: (arg) => {
2565
+ const { segments } = paths.canonicalizePath(arg);
2566
+ let cursor = inner.value;
2567
+ for (const seg of segments) {
2568
+ if (cursor === null || cursor === void 0) return void 0;
2569
+ cursor = cursor[seg];
2570
+ }
2571
+ return cursor;
2433
2572
  },
2434
- defineProperty: () => true
2573
+ ownKeys: () => Reflect.ownKeys(inner.value),
2574
+ hasKey: (key) => Reflect.has(inner.value, key),
2575
+ describeKey: (key) => {
2576
+ const desc = Reflect.getOwnPropertyDescriptor(inner.value, key);
2577
+ if (desc !== void 0) desc.configurable = true;
2578
+ return desc;
2579
+ }
2435
2580
  });
2436
2581
  }
2437
2582
 
@@ -2497,10 +2642,18 @@ function buildFormApi(state, formInstanceId, options = {}) {
2497
2642
  next,
2498
2643
  state.schema
2499
2644
  );
2645
+ const ok2 = state.setValueAtPath([], walked2.cleanedValues, withInstanceMeta());
2646
+ if (!ok2) return false;
2500
2647
  for (const pathKey of walked2.paths) {
2501
- state.blankPaths.add(pathKey);
2648
+ const blankSegments = paths.segmentsForPathKey(pathKey);
2649
+ if (blankSegments === null) continue;
2650
+ state.setValueAtPath(
2651
+ blankSegments,
2652
+ state.getValueAtPath(blankSegments),
2653
+ withInstanceMeta({ blank: true })
2654
+ );
2502
2655
  }
2503
- return state.setValueAtPath([], walked2.cleanedValues, withInstanceMeta());
2656
+ return true;
2504
2657
  }
2505
2658
  const segments = paths.canonicalizePath(pathOrValue).segments;
2506
2659
  const writeUnsetAt = () => {
@@ -2649,23 +2802,45 @@ function buildFormApi(state, formInstanceId, options = {}) {
2649
2802
  const rootFieldState = getRootFieldStateAt([]);
2650
2803
  const formMeta = vue.readonly(
2651
2804
  vue.reactive({
2652
- // FieldState fields — read through one shared root computed. Each
2653
- // property accesses `rootFieldState.value[X]`, so any descendant
2654
- // change re-evaluates the root computed once (Vue's reactive
2655
- // graph dedupes the dependent re-renders).
2656
- value: vue.computed(() => rootFieldState.value.value),
2657
- original: vue.computed(() => rootFieldState.value.original),
2658
- pristine: vue.computed(() => rootFieldState.value.pristine),
2659
- dirty: vue.computed(() => rootFieldState.value.dirty),
2660
- focused: vue.computed(() => rootFieldState.value.focused),
2661
- blurred: vue.computed(() => rootFieldState.value.blurred),
2662
- touched: vue.computed(() => rootFieldState.value.touched),
2663
- interacted: vue.computed(() => rootFieldState.value.interacted),
2664
- blurredAfterInteraction: vue.computed(() => rootFieldState.value.blurredAfterInteraction),
2665
- connected: vue.computed(() => rootFieldState.value.connected),
2666
- element: vue.computed(() => rootFieldState.value.element),
2667
- elements: vue.computed(() => rootFieldState.value.elements),
2668
- updatedAt: vue.computed(() => rootFieldState.value.updatedAt),
2805
+ get value() {
2806
+ return rootFieldState.value.value;
2807
+ },
2808
+ get original() {
2809
+ return rootFieldState.value.original;
2810
+ },
2811
+ get pristine() {
2812
+ return rootFieldState.value.pristine;
2813
+ },
2814
+ get dirty() {
2815
+ return rootFieldState.value.dirty;
2816
+ },
2817
+ get focused() {
2818
+ return rootFieldState.value.focused;
2819
+ },
2820
+ get blurred() {
2821
+ return rootFieldState.value.blurred;
2822
+ },
2823
+ get touched() {
2824
+ return rootFieldState.value.touched;
2825
+ },
2826
+ get interacted() {
2827
+ return rootFieldState.value.interacted;
2828
+ },
2829
+ get blurredAfterInteraction() {
2830
+ return rootFieldState.value.blurredAfterInteraction;
2831
+ },
2832
+ get connected() {
2833
+ return rootFieldState.value.connected;
2834
+ },
2835
+ get element() {
2836
+ return rootFieldState.value.element;
2837
+ },
2838
+ get elements() {
2839
+ return rootFieldState.value.elements;
2840
+ },
2841
+ get updatedAt() {
2842
+ return rootFieldState.value.updatedAt;
2843
+ },
2669
2844
  // Whole-form validating mirrors the LIFECYCLE counter
2670
2845
  // (`state.activeValidations`) ORed with any per-leaf validation
2671
2846
  // in flight (via `rootFieldState.validating`). A submit-time
@@ -2688,21 +2863,51 @@ function buildFormApi(state, formInstanceId, options = {}) {
2688
2863
  // FieldState surface, so `form.meta.displayState` matches
2689
2864
  // `form.fields().displayState` exactly — the predicate runs once
2690
2865
  // at the root and the result is shared.
2691
- displayState: vue.computed(() => rootFieldState.value.displayState),
2692
- showErrors: vue.computed(() => rootFieldState.value.showErrors),
2693
- showPending: vue.computed(() => rootFieldState.value.showPending),
2694
- showSuccess: vue.computed(() => rootFieldState.value.showSuccess),
2695
- showIdle: vue.computed(() => rootFieldState.value.showIdle),
2696
- firstError: vue.computed(() => rootFieldState.value.firstError),
2697
- path: vue.computed(() => rootFieldState.value.path),
2698
- id: vue.computed(() => rootFieldState.value.id),
2699
- aria: vue.computed(() => rootFieldState.value.aria),
2700
- key: vue.computed(() => rootFieldState.value.key),
2701
- blank: vue.computed(() => rootFieldState.value.blank),
2702
- label: vue.computed(() => rootFieldState.value.label),
2703
- description: vue.computed(() => rootFieldState.value.description),
2704
- placeholder: vue.computed(() => rootFieldState.value.placeholder),
2705
- meta: vue.computed(() => rootFieldState.value.meta),
2866
+ get displayState() {
2867
+ return rootFieldState.value.displayState;
2868
+ },
2869
+ get showErrors() {
2870
+ return rootFieldState.value.showErrors;
2871
+ },
2872
+ get showPending() {
2873
+ return rootFieldState.value.showPending;
2874
+ },
2875
+ get showSuccess() {
2876
+ return rootFieldState.value.showSuccess;
2877
+ },
2878
+ get showIdle() {
2879
+ return rootFieldState.value.showIdle;
2880
+ },
2881
+ get firstError() {
2882
+ return rootFieldState.value.firstError;
2883
+ },
2884
+ get path() {
2885
+ return rootFieldState.value.path;
2886
+ },
2887
+ get id() {
2888
+ return rootFieldState.value.id;
2889
+ },
2890
+ get aria() {
2891
+ return rootFieldState.value.aria;
2892
+ },
2893
+ get key() {
2894
+ return rootFieldState.value.key;
2895
+ },
2896
+ get blank() {
2897
+ return rootFieldState.value.blank;
2898
+ },
2899
+ get label() {
2900
+ return rootFieldState.value.label;
2901
+ },
2902
+ get description() {
2903
+ return rootFieldState.value.description;
2904
+ },
2905
+ get placeholder() {
2906
+ return rootFieldState.value.placeholder;
2907
+ },
2908
+ get meta() {
2909
+ return rootFieldState.value.meta;
2910
+ },
2706
2911
  // Lifecycle (form-level only — not on FieldState).
2707
2912
  submitting,
2708
2913
  submissionAttempts,
@@ -2711,7 +2916,9 @@ function buildFormApi(state, formInstanceId, options = {}) {
2711
2916
  // Scalar mirror over the array — meta is a single sticky surface
2712
2917
  // for both templates and `useWizard`'s `FormStatus`, so the
2713
2918
  // projection lives here.
2714
- errorCount: vue.computed(() => metaErrors.value.length),
2919
+ get errorCount() {
2920
+ return metaErrors.value.length;
2921
+ },
2715
2922
  submitted,
2716
2923
  // Per-`useForm()`-call identity. Stable for one mount; new on
2717
2924
  // re-mount; orthogonal to `form.key` (which is the user-supplied
@@ -2748,12 +2955,20 @@ function buildFormApi(state, formInstanceId, options = {}) {
2748
2955
  }
2749
2956
  };
2750
2957
  function clear(pathInput) {
2751
- const segments = pathInput === void 0 ? paths.ROOT_PATH : paths.canonicalizePath(pathInput).segments;
2752
- return state.clear(segments);
2958
+ if (pathInput === void 0) {
2959
+ return setValueImpl(unset);
2960
+ }
2961
+ return setValueImpl(pathInput, unset);
2753
2962
  }
2754
2963
  const persist = async (pathInput, options2) => {
2755
2964
  const segments = paths.canonicalizePath(pathInput).segments;
2756
- paths.enforceSensitiveCheck(segments, options2?.acknowledgeSensitive === true, state.isSensitivePath);
2965
+ if (!paths.allowSensitivePersist(
2966
+ segments,
2967
+ options2?.acknowledgeSensitive === true,
2968
+ state.isSensitivePath
2969
+ )) {
2970
+ return;
2971
+ }
2757
2972
  if (persistence === void 0) return;
2758
2973
  await persistence.writePathImmediately(segments);
2759
2974
  };
@@ -2819,7 +3034,9 @@ function buildFormApi(state, formInstanceId, options = {}) {
2819
3034
  getFormMetaBase,
2820
3035
  fieldStateAccessorOptions
2821
3036
  );
3037
+ const needsLazyGate = state.defaultValuesFactory.value !== void 0 || state.hasSsrPrefetch;
2822
3038
  function gated(fn) {
3039
+ if (!needsLazyGate) return fn;
2823
3040
  return ((...args) => {
2824
3041
  void state.activate();
2825
3042
  return fn(...args);
@@ -2842,7 +3059,7 @@ function buildFormApi(state, formInstanceId, options = {}) {
2842
3059
  if (value === null || typeof value !== "object" || Array.isArray(value)) {
2843
3060
  return EMPTY_FIELD_RECORD;
2844
3061
  }
2845
- const out = {};
3062
+ const out = /* @__PURE__ */ Object.create(null);
2846
3063
  for (const key of Object.keys(value)) {
2847
3064
  out[key] = callTerminal(`${path}.${key}`);
2848
3065
  }
@@ -2938,19 +3155,142 @@ function buildFormApi(state, formInstanceId, options = {}) {
2938
3155
  };
2939
3156
  }
2940
3157
 
2941
- function createArrayIdentity(getArrayLength) {
2942
- const tokens = /* @__PURE__ */ new Map();
2943
- const baselines = /* @__PURE__ */ new Map();
2944
- let counter = 0;
2945
- const allocate = () => `k${(counter++).toString(36)}`;
2946
- function ensure(arrayKey, expectedLen) {
2947
- let ids = tokens.get(arrayKey);
2948
- const firstTrack = ids === void 0;
2949
- if (ids === void 0) {
2950
- ids = [];
2951
- tokens.set(arrayKey, ids);
2952
- }
2953
- while (ids.length < expectedLen) ids.push(allocate());
3158
+ function applyDuStubs(schema, data, options = {}) {
3159
+ const warned = options.warn === true ? /* @__PURE__ */ new Set() : void 0;
3160
+ return walkDuStubs(schema, data, options.basePath ?? [], warned);
3161
+ }
3162
+ function walkDuStubs(schema, value, path, warned) {
3163
+ if (value === null || value === void 0 || typeof value !== "object") return value;
3164
+ if (value instanceof Date || value instanceof RegExp || value instanceof Map || value instanceof Set || typeof value === "function") {
3165
+ return value;
3166
+ }
3167
+ if (Array.isArray(value)) {
3168
+ return value.map((item, i) => walkDuStubs(schema, item, [...path, i], warned));
3169
+ }
3170
+ const rec = value;
3171
+ const du = schema.getUnionDiscriminatorAtPath(path);
3172
+ if (du !== void 0) {
3173
+ const discValue = rec[du.discriminatorKey];
3174
+ if (discValue !== void 0 && !du.isVariantSelected(discValue)) {
3175
+ const isKindBlank = discValue === "" || discValue === 0 || discValue === 0n || discValue === false || discValue === null;
3176
+ if (!isKindBlank && warned !== void 0 && paths.__DEV__) {
3177
+ const dotted = path.map((s) => String(s)).join(".") || "(root)";
3178
+ const key = `${dotted}::${String(discValue)}`;
3179
+ if (!warned.has(key)) {
3180
+ warned.add(key);
3181
+ console.warn(
3182
+ `[attaform] defaultValues at '${dotted}' carries discriminator '${du.discriminatorKey}=${JSON.stringify(discValue)}' which isn't a known variant. Form mounts in a stub holding only the discriminator key. Validation will surface the mismatch.`
3183
+ );
3184
+ }
3185
+ }
3186
+ const stub = /* @__PURE__ */ Object.create(null);
3187
+ stub[du.discriminatorKey] = discValue;
3188
+ return stub;
3189
+ }
3190
+ }
3191
+ const out = /* @__PURE__ */ Object.create(null);
3192
+ for (const k of Object.keys(rec)) {
3193
+ out[k] = walkDuStubs(schema, rec[k], [...path, k], warned);
3194
+ }
3195
+ return out;
3196
+ }
3197
+
3198
+ function createVariantMemory() {
3199
+ const memory = /* @__PURE__ */ new Map();
3200
+ function clearAtArrayIndices(arrayPath, indexFilter) {
3201
+ for (const memKey of [...memory.keys()]) {
3202
+ const segs = paths.segmentsForPathKey(memKey);
3203
+ if (segs === null) continue;
3204
+ if (!paths.isPathPrefix(arrayPath, segs)) continue;
3205
+ if (segs.length <= arrayPath.length) continue;
3206
+ const idxSeg = segs[arrayPath.length];
3207
+ if (typeof idxSeg !== "number") continue;
3208
+ if (indexFilter(idxSeg)) memory.delete(memKey);
3209
+ }
3210
+ }
3211
+ return {
3212
+ clear() {
3213
+ memory.clear();
3214
+ },
3215
+ clearUnderPath(parentPath) {
3216
+ for (const memKey of [...memory.keys()]) {
3217
+ const segs = paths.segmentsForPathKey(memKey);
3218
+ if (segs === null) continue;
3219
+ if (paths.isPathPrefix(parentPath, segs)) memory.delete(memKey);
3220
+ }
3221
+ },
3222
+ applyArrayOp(arrayPath, op) {
3223
+ switch (op.kind) {
3224
+ case "insert":
3225
+ case "remove":
3226
+ clearAtArrayIndices(arrayPath, (i) => i >= op.index);
3227
+ return;
3228
+ case "move": {
3229
+ const lo = Math.min(op.from, op.to);
3230
+ const hi = Math.max(op.from, op.to);
3231
+ clearAtArrayIndices(arrayPath, (i) => i >= lo && i <= hi);
3232
+ return;
3233
+ }
3234
+ case "swap":
3235
+ clearAtArrayIndices(arrayPath, (i) => i === op.a || i === op.b);
3236
+ return;
3237
+ case "replace-at":
3238
+ clearAtArrayIndices(arrayPath, (i) => i === op.index);
3239
+ return;
3240
+ }
3241
+ },
3242
+ recordOutgoing(unionKey, discValue, snapshot) {
3243
+ let perUnion = memory.get(unionKey);
3244
+ if (perUnion === void 0) {
3245
+ perUnion = /* @__PURE__ */ new Map();
3246
+ memory.set(unionKey, perUnion);
3247
+ }
3248
+ perUnion.set(discValue, snapshot);
3249
+ },
3250
+ lookupIncoming(unionKey, discValue) {
3251
+ return memory.get(unionKey)?.get(discValue);
3252
+ }
3253
+ };
3254
+ }
3255
+ function cloneVariantSnapshot(value) {
3256
+ if (value === null || typeof value !== "object") return value;
3257
+ const raw = vue.toRaw(value);
3258
+ if (raw instanceof Date) return new Date(raw.getTime());
3259
+ if (raw instanceof Map) {
3260
+ const out2 = /* @__PURE__ */ new Map();
3261
+ for (const [k, v] of raw.entries()) out2.set(cloneVariantSnapshot(k), cloneVariantSnapshot(v));
3262
+ return out2;
3263
+ }
3264
+ if (raw instanceof Set) {
3265
+ const out2 = /* @__PURE__ */ new Set();
3266
+ for (const v of raw) out2.add(cloneVariantSnapshot(v));
3267
+ return out2;
3268
+ }
3269
+ if (raw instanceof RegExp) return new RegExp(raw.source, raw.flags);
3270
+ if (Array.isArray(raw)) {
3271
+ const out2 = new Array(raw.length);
3272
+ for (let i = 0; i < raw.length; i++) out2[i] = cloneVariantSnapshot(raw[i]);
3273
+ return out2;
3274
+ }
3275
+ const src = raw;
3276
+ const out = /* @__PURE__ */ Object.create(null);
3277
+ for (const k of Object.keys(src)) out[k] = cloneVariantSnapshot(src[k]);
3278
+ return out;
3279
+ }
3280
+
3281
+ function createArrayIdentity(getArrayLength) {
3282
+ const tokens = /* @__PURE__ */ new Map();
3283
+ const baselines = /* @__PURE__ */ new Map();
3284
+ let counter = 0;
3285
+ const allocate = () => `k${(counter++).toString(36)}`;
3286
+ function ensure(arrayKey, expectedLen) {
3287
+ let ids = tokens.get(arrayKey);
3288
+ const firstTrack = ids === void 0;
3289
+ if (ids === void 0) {
3290
+ ids = [];
3291
+ tokens.set(arrayKey, ids);
3292
+ }
3293
+ while (ids.length < expectedLen) ids.push(allocate());
2954
3294
  if (ids.length > expectedLen) ids.length = expectedLen;
2955
3295
  if (firstTrack) baselines.set(arrayKey, [...ids]);
2956
3296
  return ids;
@@ -3000,6 +3340,36 @@ function createArrayIdentity(getArrayLength) {
3000
3340
  return;
3001
3341
  }
3002
3342
  },
3343
+ applyRemap(arrayPath, remap) {
3344
+ if (remap.moved.size === 0 && remap.vacated.size === 0 && remap.fresh.size === 0) return;
3345
+ const relocate = (store) => {
3346
+ const idxPos = arrayPath.length;
3347
+ const snapshots = [];
3348
+ for (const [key, value] of store) {
3349
+ const segments = paths.segmentsForPathKey(key);
3350
+ if (segments === null) continue;
3351
+ if (!paths.isPathPrefix(arrayPath, segments)) continue;
3352
+ if (segments.length <= idxPos) continue;
3353
+ const idxSeg = segments[idxPos];
3354
+ if (typeof idxSeg !== "number") continue;
3355
+ if (!remap.moved.has(idxSeg) && !remap.vacated.has(idxSeg) && !remap.fresh.has(idxSeg)) {
3356
+ continue;
3357
+ }
3358
+ snapshots.push({ segments: [...segments], index: idxSeg, value });
3359
+ }
3360
+ if (snapshots.length === 0) return;
3361
+ for (const snap of snapshots) store.delete(paths.canonicalizePath(snap.segments).key);
3362
+ for (const snap of snapshots) {
3363
+ const target = remap.moved.get(snap.index);
3364
+ if (target === void 0) continue;
3365
+ const relocated = snap.segments.slice();
3366
+ relocated[idxPos] = target;
3367
+ store.set(paths.canonicalizePath(relocated).key, snap.value);
3368
+ }
3369
+ };
3370
+ relocate(tokens);
3371
+ relocate(baselines);
3372
+ },
3003
3373
  realign(arraySegs) {
3004
3374
  ensure(paths.canonicalizePath(arraySegs).key, getArrayLength(arraySegs));
3005
3375
  },
@@ -3114,6 +3484,94 @@ function migrateSetSubtree(set, arrayPath, remap) {
3114
3484
  }
3115
3485
  }
3116
3486
 
3487
+ function createArrayBookkeeping(deps) {
3488
+ const {
3489
+ form,
3490
+ fields,
3491
+ userErrors,
3492
+ originals,
3493
+ blankPaths,
3494
+ originalBlankPaths,
3495
+ fieldValidationCounts,
3496
+ fieldValidationState,
3497
+ schemaErrors,
3498
+ activeValidations,
3499
+ arrayIdentity,
3500
+ touchFieldRecord,
3501
+ decFieldValidation
3502
+ } = deps;
3503
+ function migrateElementState(arrayPath, remap) {
3504
+ if (remap.moved.size === 0 && remap.vacated.size === 0) return;
3505
+ migrateMapSubtree(fields, arrayPath, remap, (record, segments) => ({
3506
+ ...record,
3507
+ path: segments
3508
+ }));
3509
+ migrateMapSubtree(
3510
+ userErrors,
3511
+ arrayPath,
3512
+ remap,
3513
+ (errors, segments) => errors.map((error) => ({ ...error, path: [...segments] }))
3514
+ );
3515
+ migrateMapSubtree(originals, arrayPath, remap, (record, segments) => ({
3516
+ segments,
3517
+ value: record.value
3518
+ }));
3519
+ migrateSetSubtree(blankPaths, arrayPath, remap);
3520
+ migrateSetSubtree(originalBlankPaths, arrayPath, remap);
3521
+ migrateMapSubtree(fieldValidationCounts, arrayPath, remap, (count) => count);
3522
+ arrayIdentity.applyRemap(arrayPath, remap);
3523
+ }
3524
+ function seedFreshElement(arrayPath, freshIndex) {
3525
+ const elementPath = [...arrayPath, freshIndex];
3526
+ const now = (/* @__PURE__ */ new Date()).toISOString();
3527
+ diffAndApply(void 0, getAtPath(form.value, elementPath), elementPath, (patch) => {
3528
+ if (patch.kind !== "added") return;
3529
+ const { key } = paths.canonicalizePath(patch.path);
3530
+ if (!originals.has(key)) originals.set(key, { segments: patch.path, value: void 0 });
3531
+ touchFieldRecord(key, patch.path, { updatedAt: now });
3532
+ });
3533
+ }
3534
+ function dropSchemaErrorsAtChangedIndices(arrayPath, remap) {
3535
+ const changed = changedIndices(remap);
3536
+ if (changed.size === 0) return;
3537
+ const idxPos = arrayPath.length;
3538
+ for (const key of [...schemaErrors.keys()]) {
3539
+ const segs = paths.segmentsForPathKey(key);
3540
+ if (segs === null) continue;
3541
+ if (!paths.isPathPrefix(arrayPath, segs)) continue;
3542
+ if (segs.length <= idxPos) continue;
3543
+ const idx = segs[idxPos];
3544
+ if (typeof idx === "number" && changed.has(idx)) schemaErrors.delete(key);
3545
+ }
3546
+ }
3547
+ function abortValidationAtVacatedIndices(arrayPath, remap) {
3548
+ if (remap.vacated.size === 0) return;
3549
+ const idxPos = arrayPath.length;
3550
+ for (const [key, entry] of [...fieldValidationState]) {
3551
+ const segs = paths.segmentsForPathKey(key);
3552
+ if (segs === null) continue;
3553
+ if (!paths.isPathPrefix(arrayPath, segs)) continue;
3554
+ if (segs.length <= idxPos) continue;
3555
+ const idx = segs[idxPos];
3556
+ if (typeof idx !== "number" || !remap.vacated.has(idx)) continue;
3557
+ if (entry.timer !== null) {
3558
+ clearTimeout(entry.timer);
3559
+ } else if (!entry.settled) {
3560
+ activeValidations.value = Math.max(0, activeValidations.value - 1);
3561
+ decFieldValidation(key);
3562
+ }
3563
+ entry.controller.abort();
3564
+ fieldValidationState.delete(key);
3565
+ }
3566
+ }
3567
+ return {
3568
+ migrateElementState,
3569
+ seedFreshElement,
3570
+ dropSchemaErrorsAtChangedIndices,
3571
+ abortValidationAtVacatedIndices
3572
+ };
3573
+ }
3574
+
3117
3575
  function isHydratedFieldRecord(value) {
3118
3576
  if (typeof value !== "object" || value === null) return false;
3119
3577
  const r = value;
@@ -3137,43 +3595,6 @@ function warnMalformedHydration(formKey, kind, rawKey) {
3137
3595
  `[attaform] hydration: skipping malformed ${kind} entry at key '${rawKey}' on form '${formKey}'. This usually means the SSR bundle is on a different version than the client (rolling deploy / stale cache).`
3138
3596
  );
3139
3597
  }
3140
- function applyDuStubs(schema, data, options = {}) {
3141
- const warned = options.warn === true ? /* @__PURE__ */ new Set() : void 0;
3142
- return walkDuStubs(schema, data, options.basePath ?? [], warned);
3143
- }
3144
- function walkDuStubs(schema, value, path, warned) {
3145
- if (value === null || value === void 0 || typeof value !== "object") return value;
3146
- if (value instanceof Date || value instanceof RegExp || value instanceof Map || value instanceof Set || typeof value === "function") {
3147
- return value;
3148
- }
3149
- if (Array.isArray(value)) {
3150
- return value.map((item, i) => walkDuStubs(schema, item, [...path, i], warned));
3151
- }
3152
- const rec = value;
3153
- const du = schema.getUnionDiscriminatorAtPath(path);
3154
- if (du !== void 0) {
3155
- const discValue = rec[du.discriminatorKey];
3156
- if (discValue !== void 0 && !du.isVariantSelected(discValue)) {
3157
- const isKindBlank = discValue === "" || discValue === 0 || discValue === 0n || discValue === false || discValue === null;
3158
- if (!isKindBlank && warned !== void 0 && paths.__DEV__) {
3159
- const dotted = path.map((s) => String(s)).join(".") || "(root)";
3160
- const key = `${dotted}::${String(discValue)}`;
3161
- if (!warned.has(key)) {
3162
- warned.add(key);
3163
- console.warn(
3164
- `[attaform] defaultValues at '${dotted}' carries discriminator '${du.discriminatorKey}=${JSON.stringify(discValue)}' which isn't a known variant. Form mounts in a stub holding only the discriminator key. Validation will surface the mismatch.`
3165
- );
3166
- }
3167
- }
3168
- return { [du.discriminatorKey]: discValue };
3169
- }
3170
- }
3171
- const out = {};
3172
- for (const k of Object.keys(rec)) {
3173
- out[k] = walkDuStubs(schema, rec[k], [...path, k], warned);
3174
- }
3175
- return out;
3176
- }
3177
3598
  function isPathKeyUnder(existingKey, parentPath) {
3178
3599
  const parsed = paths.segmentsForPathKey(existingKey);
3179
3600
  if (parsed === null) return false;
@@ -3209,31 +3630,6 @@ function stripSymbolsDeep(value) {
3209
3630
  }
3210
3631
  return mutated ? out : value;
3211
3632
  }
3212
- function cloneVariantSnapshot(value) {
3213
- if (value === null || typeof value !== "object") return value;
3214
- const raw = vue.toRaw(value);
3215
- if (raw instanceof Date) return new Date(raw.getTime());
3216
- if (raw instanceof Map) {
3217
- const out2 = /* @__PURE__ */ new Map();
3218
- for (const [k, v] of raw.entries()) out2.set(cloneVariantSnapshot(k), cloneVariantSnapshot(v));
3219
- return out2;
3220
- }
3221
- if (raw instanceof Set) {
3222
- const out2 = /* @__PURE__ */ new Set();
3223
- for (const v of raw) out2.add(cloneVariantSnapshot(v));
3224
- return out2;
3225
- }
3226
- if (raw instanceof RegExp) return new RegExp(raw.source, raw.flags);
3227
- if (Array.isArray(raw)) {
3228
- const out2 = new Array(raw.length);
3229
- for (let i = 0; i < raw.length; i++) out2[i] = cloneVariantSnapshot(raw[i]);
3230
- return out2;
3231
- }
3232
- const src = raw;
3233
- const out = {};
3234
- for (const k of Object.keys(src)) out[k] = cloneVariantSnapshot(src[k]);
3235
- return out;
3236
- }
3237
3633
  function walkAuthoredFromConstraints(value, prefix, out) {
3238
3634
  if (prefix.length > 0) out.add(paths.canonicalizePath(prefix).key);
3239
3635
  if (isPlainRecord(value)) {
@@ -3306,7 +3702,6 @@ function createFormStore(options) {
3306
3702
  const coerceIndex = resolveCoercionIndex(options.coerce);
3307
3703
  const resolvedGetDisplayState = resolveGetDisplayState(options.getDisplayState);
3308
3704
  const resolvedIsSensitivePath = options.isSensitivePath ?? paths.isSensitivePath;
3309
- const resolvedSegmentMatchesSensitive = options.segmentMatchesSensitive ?? paths.segmentMatchesSensitive;
3310
3705
  const cleanupHooks = [];
3311
3706
  const modules = /* @__PURE__ */ new Map();
3312
3707
  const completedConstraints = defaultValues === void 0 ? void 0 : mergeStructural(schema, [], defaultValues);
@@ -3369,117 +3764,17 @@ function createFormStore(options) {
3369
3764
  blankPaths.add(key);
3370
3765
  originalBlankPaths.add(key);
3371
3766
  }
3372
- const variantMemory = /* @__PURE__ */ new Map();
3373
- function clearVariantMemoryUnderPath(arrayPath) {
3374
- for (const memKey of [...variantMemory.keys()]) {
3375
- const segs = paths.segmentsForPathKey(memKey);
3376
- if (segs === null) continue;
3377
- if (isPathPrefix(arrayPath, segs)) variantMemory.delete(memKey);
3767
+ const variantMemory = createVariantMemory();
3768
+ const pathOrdinals = /* @__PURE__ */ new Map();
3769
+ let nextOrdinal = 0;
3770
+ function ensurePathOrdinal(key) {
3771
+ let ordinal = pathOrdinals.get(key);
3772
+ if (ordinal === void 0) {
3773
+ ordinal = nextOrdinal;
3774
+ pathOrdinals.set(key, ordinal);
3775
+ nextOrdinal += 1;
3378
3776
  }
3379
- }
3380
- function clearVariantMemoryAtArrayIndices(arrayPath, indexFilter) {
3381
- for (const memKey of [...variantMemory.keys()]) {
3382
- const segs = paths.segmentsForPathKey(memKey);
3383
- if (segs === null) continue;
3384
- if (!isPathPrefix(arrayPath, segs)) continue;
3385
- if (segs.length <= arrayPath.length) continue;
3386
- const idxSeg = segs[arrayPath.length];
3387
- if (typeof idxSeg !== "number") continue;
3388
- if (indexFilter(idxSeg)) variantMemory.delete(memKey);
3389
- }
3390
- }
3391
- function applyArrayOpToMemory(arrayPath, op) {
3392
- switch (op.kind) {
3393
- case "insert":
3394
- case "remove":
3395
- clearVariantMemoryAtArrayIndices(arrayPath, (i) => i >= op.index);
3396
- return;
3397
- case "move": {
3398
- const lo = Math.min(op.from, op.to);
3399
- const hi = Math.max(op.from, op.to);
3400
- clearVariantMemoryAtArrayIndices(arrayPath, (i) => i >= lo && i <= hi);
3401
- return;
3402
- }
3403
- case "swap":
3404
- clearVariantMemoryAtArrayIndices(arrayPath, (i) => i === op.a || i === op.b);
3405
- return;
3406
- case "replace-at":
3407
- clearVariantMemoryAtArrayIndices(arrayPath, (i) => i === op.index);
3408
- return;
3409
- }
3410
- }
3411
- function migrateArrayElementState(arrayPath, remap) {
3412
- if (remap.moved.size === 0 && remap.vacated.size === 0) return;
3413
- migrateMapSubtree(fields, arrayPath, remap, (record, segments) => ({
3414
- ...record,
3415
- path: segments
3416
- }));
3417
- migrateMapSubtree(
3418
- userErrors,
3419
- arrayPath,
3420
- remap,
3421
- (errors, segments) => errors.map((error) => ({ ...error, path: [...segments] }))
3422
- );
3423
- migrateMapSubtree(originals, arrayPath, remap, (record, segments) => ({
3424
- segments,
3425
- value: record.value
3426
- }));
3427
- migrateSetSubtree(blankPaths, arrayPath, remap);
3428
- migrateSetSubtree(originalBlankPaths, arrayPath, remap);
3429
- }
3430
- function seedFreshElement(arrayPath, freshIndex) {
3431
- const elementPath = [...arrayPath, freshIndex];
3432
- const now = (/* @__PURE__ */ new Date()).toISOString();
3433
- diffAndApply(void 0, getAtPath(form.value, elementPath), elementPath, (patch) => {
3434
- if (patch.kind !== "added") return;
3435
- const { key } = paths.canonicalizePath(patch.path);
3436
- if (!originals.has(key)) originals.set(key, { segments: patch.path, value: void 0 });
3437
- touchFieldRecord(key, patch.path, { updatedAt: now });
3438
- });
3439
- }
3440
- function dropSchemaErrorsAtChangedIndices(arrayPath, remap) {
3441
- const changed = changedIndices(remap);
3442
- if (changed.size === 0) return;
3443
- const idxPos = arrayPath.length;
3444
- for (const key of [...schemaErrors.keys()]) {
3445
- const segs = paths.segmentsForPathKey(key);
3446
- if (segs === null) continue;
3447
- if (!isPathPrefix(arrayPath, segs)) continue;
3448
- if (segs.length <= idxPos) continue;
3449
- const idx = segs[idxPos];
3450
- if (typeof idx === "number" && changed.has(idx)) schemaErrors.delete(key);
3451
- }
3452
- }
3453
- function abortValidationAtVacatedIndices(arrayPath, remap) {
3454
- if (remap.vacated.size === 0) return;
3455
- const idxPos = arrayPath.length;
3456
- for (const [key, entry] of [...fieldValidationState]) {
3457
- const segs = paths.segmentsForPathKey(key);
3458
- if (segs === null) continue;
3459
- if (!isPathPrefix(arrayPath, segs)) continue;
3460
- if (segs.length <= idxPos) continue;
3461
- const idx = segs[idxPos];
3462
- if (typeof idx !== "number" || !remap.vacated.has(idx)) continue;
3463
- if (entry.timer !== null) {
3464
- clearTimeout(entry.timer);
3465
- } else if (!entry.settled) {
3466
- activeValidations.value = Math.max(0, activeValidations.value - 1);
3467
- decFieldValidation(key);
3468
- }
3469
- entry.controller.abort();
3470
- fieldValidationState.delete(key);
3471
- }
3472
- }
3473
- const pathOrdinals = /* @__PURE__ */ new Map();
3474
- let nextOrdinal = 0;
3475
- function ensurePathOrdinal(key) {
3476
- let ordinal = pathOrdinals.get(key);
3477
- if (ordinal === void 0) {
3478
- ordinal = nextOrdinal;
3479
- pathOrdinals.set(key, ordinal);
3480
- nextOrdinal += 1;
3481
- }
3482
- return ordinal;
3777
+ return ordinal;
3483
3778
  }
3484
3779
  const derivedBlankErrors = vue.computed(() => {
3485
3780
  const result = /* @__PURE__ */ new Map();
@@ -3507,7 +3802,9 @@ function createFormStore(options) {
3507
3802
  const departAttempts = vue.ref(0);
3508
3803
  const submissionGeneration = vue.ref(0);
3509
3804
  const activeValidations = vue.ref(0);
3510
- let lastValidatedSnapshot = null;
3805
+ const pathSnapshots = /* @__PURE__ */ new Map();
3806
+ let scheduleEpoch = 0;
3807
+ let lastCommittedEpoch = 0;
3511
3808
  const hydrating = vue.ref(false);
3512
3809
  const hydrateError = vue.ref(null);
3513
3810
  const defaultValuesFactory = vue.ref(void 0);
@@ -3523,9 +3820,12 @@ function createFormStore(options) {
3523
3820
  const pathAsyncCache = /* @__PURE__ */ new Map();
3524
3821
  function pathHasAsyncValidation(path) {
3525
3822
  const { key } = paths.canonicalizePath(path);
3823
+ return pathHasAsyncValidationByKey(key, path);
3824
+ }
3825
+ function pathHasAsyncValidationByKey(key, segments) {
3526
3826
  const cached = pathAsyncCache.get(key);
3527
3827
  if (cached !== void 0) return cached;
3528
- const candidates = schema.getSchemasAtPath(path);
3828
+ const candidates = schema.getSchemasAtPath(segments);
3529
3829
  const hasAsync = candidates.some((sub) => sub.needsAsyncValidation?.() === true);
3530
3830
  pathAsyncCache.set(key, hasAsync);
3531
3831
  return hasAsync;
@@ -3617,6 +3917,21 @@ function createFormStore(options) {
3617
3917
  blurredAfterInteraction: patch.blurredAfterInteraction ?? current?.blurredAfterInteraction ?? false
3618
3918
  });
3619
3919
  }
3920
+ const arrayBookkeeping = createArrayBookkeeping({
3921
+ form,
3922
+ fields,
3923
+ userErrors,
3924
+ originals,
3925
+ blankPaths,
3926
+ originalBlankPaths,
3927
+ fieldValidationCounts,
3928
+ fieldValidationState,
3929
+ schemaErrors,
3930
+ activeValidations,
3931
+ arrayIdentity,
3932
+ touchFieldRecord,
3933
+ decFieldValidation
3934
+ });
3620
3935
  function applyFormReplacement(next, meta) {
3621
3936
  const prev = form.value;
3622
3937
  if (Object.is(prev, next)) return;
@@ -3740,8 +4055,13 @@ function createFormStore(options) {
3740
4055
  const pathKey = paths.canonicalizePath(path).key;
3741
4056
  if (meta?.blank === true) {
3742
4057
  blankPaths.add(pathKey);
3743
- } else if (blankPaths.has(pathKey)) {
3744
- blankPaths.delete(pathKey);
4058
+ } else {
4059
+ if (blankPaths.has(pathKey)) blankPaths.delete(pathKey);
4060
+ if (meta?.arrayOp === void 0) {
4061
+ for (const existingKey of [...blankPaths]) {
4062
+ if (isPathKeyUnder(existingKey, path)) blankPaths.delete(existingKey);
4063
+ }
4064
+ }
3745
4065
  }
3746
4066
  const wasAuthoredBefore = authoredPaths.has(pathKey);
3747
4067
  walkAuthoredFromConstraints(value, path, authoredPaths);
@@ -3765,14 +4085,14 @@ function createFormStore(options) {
3765
4085
  applyFormReplacement(nextForm, meta);
3766
4086
  if (meta?.arrayOp !== void 0) {
3767
4087
  const remap = remapForOp(meta.arrayOp, oldArrayLength);
3768
- migrateArrayElementState(path, remap);
3769
- for (const freshIndex of remap.fresh) seedFreshElement(path, freshIndex);
3770
- dropSchemaErrorsAtChangedIndices(path, remap);
3771
- abortValidationAtVacatedIndices(path, remap);
3772
- applyArrayOpToMemory(path, meta.arrayOp);
4088
+ arrayBookkeeping.migrateElementState(path, remap);
4089
+ for (const freshIndex of remap.fresh) arrayBookkeeping.seedFreshElement(path, freshIndex);
4090
+ arrayBookkeeping.dropSchemaErrorsAtChangedIndices(path, remap);
4091
+ arrayBookkeeping.abortValidationAtVacatedIndices(path, remap);
4092
+ variantMemory.applyArrayOp(path, meta.arrayOp);
3773
4093
  arrayIdentity.applyOp(path, meta.arrayOp);
3774
4094
  } else if (Array.isArray(value) && Array.isArray(currentValue)) {
3775
- clearVariantMemoryUnderPath(path);
4095
+ variantMemory.clearUnderPath(path);
3776
4096
  arrayIdentity.realign(path);
3777
4097
  }
3778
4098
  const effectiveModeAfterWrite = meta?.instance?.validateOn ?? fieldValidationMode;
@@ -3797,18 +4117,12 @@ function createFormStore(options) {
3797
4117
  for (const k of blankPaths) {
3798
4118
  if (isPathKeyUnder(k, parentPath)) outgoingBlanks.push(k);
3799
4119
  }
3800
- let memoryForUnion2 = variantMemory.get(parentKey);
3801
- if (memoryForUnion2 === void 0) {
3802
- memoryForUnion2 = /* @__PURE__ */ new Map();
3803
- variantMemory.set(parentKey, memoryForUnion2);
3804
- }
3805
- memoryForUnion2.set(oldDiscValue, {
4120
+ variantMemory.recordOutgoing(parentKey, oldDiscValue, {
3806
4121
  value: currentValue2,
3807
4122
  blankPaths: outgoingBlanks
3808
4123
  });
3809
4124
  }
3810
- const memoryForUnion = variantMemory.get(parentKey);
3811
- const restored = memoryForUnion?.get(newDiscValue);
4125
+ const restored = variantMemory.lookupIncoming(parentKey, newDiscValue);
3812
4126
  if (restored !== void 0) {
3813
4127
  baseline = restored.value;
3814
4128
  restoredBlanks = [...restored.blankPaths];
@@ -3880,12 +4194,10 @@ function createFormStore(options) {
3880
4194
  const controller = new AbortController();
3881
4195
  const fresh = { controller, timer: null, settled: false };
3882
4196
  fieldValidationState.set(key, fresh);
4197
+ const myEpoch = ++scheduleEpoch;
3883
4198
  const run = () => {
3884
4199
  fresh.timer = null;
3885
4200
  if (controller.signal.aborted) return;
3886
- if (effectiveMode === "blur") {
3887
- lastValidatedSnapshot = { value: structuralSnapshot(form.value) };
3888
- }
3889
4201
  let activeIncremented = false;
3890
4202
  try {
3891
4203
  activeValidations.value += 1;
@@ -3897,11 +4209,25 @@ function createFormStore(options) {
3897
4209
  }
3898
4210
  throw err;
3899
4211
  }
3900
- void Promise.resolve().then(() => schema.validateAtPath(form.value, void 0)).then((response) => {
4212
+ const subtreeScope = path.length > 0 && schema.hasContainerOrRootRefine?.() === false;
4213
+ const scopePath = subtreeScope ? path : void 0;
4214
+ const dataAtScope = subtreeScope ? getAtPath(form.value, path) : form.value;
4215
+ const scopeKey = subtreeScope ? paths.canonicalizePath(path).key : paths.ROOT_PATH_KEY;
4216
+ void Promise.resolve().then(() => schema.validateAtPath(dataAtScope, scopePath)).then((response) => {
3901
4217
  if (controller.signal.aborted) return;
4218
+ if (myEpoch <= lastCommittedEpoch) return;
4219
+ lastCommittedEpoch = myEpoch;
4220
+ if (effectiveMode === "blur") {
4221
+ const snapshotSource = scopePath !== void 0 ? getAtPath(form.value, scopePath) : form.value;
4222
+ pathSnapshots.set(scopeKey, structuralSnapshot(snapshotSource));
4223
+ }
3902
4224
  const errors = response.success ? [] : response.errors;
3903
4225
  const filtered = filterAuthoredErrors(errors);
3904
- applySchemaErrorsForSubtree([], filtered);
4226
+ const restamped = subtreeScope ? filtered.map((err) => ({
4227
+ ...err,
4228
+ path: [...path, ...err.path]
4229
+ })) : filtered;
4230
+ applySchemaErrorsForSubtree(scopePath ?? [], restamped);
3905
4231
  }).catch(() => {
3906
4232
  }).finally(() => {
3907
4233
  activeValidations.value = Math.max(0, activeValidations.value - 1);
@@ -4144,11 +4470,24 @@ function createFormStore(options) {
4144
4470
  const focusMode = meta?.instance?.validateOn ?? fieldValidationMode;
4145
4471
  if (!focused && focusMode === "blur") {
4146
4472
  const firstInteractiveBlur = current?.interacted === true && current.blurredAfterInteraction !== true;
4147
- const snapshot = lastValidatedSnapshot;
4473
+ let snapshot = void 0;
4474
+ let snapshotScopeLength = 0;
4475
+ for (let i = path.length; i >= 0; i--) {
4476
+ const ancestorKey = paths.canonicalizePath(path.slice(0, i)).key;
4477
+ const entry = pathSnapshots.get(ancestorKey);
4478
+ if (entry !== void 0) {
4479
+ snapshot = entry;
4480
+ snapshotScopeLength = i;
4481
+ break;
4482
+ }
4483
+ }
4148
4484
  let changed = true;
4149
- if (!firstInteractiveBlur && snapshot !== null) {
4485
+ if (!firstInteractiveBlur && snapshot !== void 0) {
4486
+ const relPath = path.slice(snapshotScopeLength);
4487
+ const snapshotSubtree = getAtPath(snapshot, relPath);
4488
+ const liveSubtree = getAtPath(form.value, path);
4150
4489
  changed = false;
4151
- diffAndApply(snapshot.value, form.value, [], () => {
4490
+ diffAndApply(snapshotSubtree, liveSubtree, path, () => {
4152
4491
  changed = true;
4153
4492
  });
4154
4493
  }
@@ -4160,10 +4499,6 @@ function createFormStore(options) {
4160
4499
  }
4161
4500
  }
4162
4501
  }
4163
- function markTouched(path) {
4164
- const { key } = paths.canonicalizePath(path);
4165
- touchFieldRecord(key, path, { touched: true });
4166
- }
4167
4502
  function markInteracted(path) {
4168
4503
  const { key } = paths.canonicalizePath(path);
4169
4504
  if (fields.get(key)?.interacted === true) return;
@@ -4173,7 +4508,7 @@ function createFormStore(options) {
4173
4508
  const formValue = form.value;
4174
4509
  let touchedAny = false;
4175
4510
  for (const [, entry] of originals) {
4176
- if (!isPathPrefix(segments, entry.segments)) continue;
4511
+ if (!paths.isPathPrefix(segments, entry.segments)) continue;
4177
4512
  if (!hasAtPath(formValue, entry.segments)) continue;
4178
4513
  touchedAny = true;
4179
4514
  const leafKey = paths.canonicalizePath(entry.segments).key;
@@ -4187,9 +4522,6 @@ function createFormStore(options) {
4187
4522
  );
4188
4523
  }
4189
4524
  }
4190
- function clear(path) {
4191
- return setValueAtPath(path, schema.getEmptyValueAtPath(path));
4192
- }
4193
4525
  function rehydrate() {
4194
4526
  const factory = defaultValuesFactory.value;
4195
4527
  if (factory === void 0) {
@@ -4336,6 +4668,9 @@ function createFormStore(options) {
4336
4668
  submitError.value = null;
4337
4669
  departAttempts.value = 0;
4338
4670
  cancelFieldValidation();
4671
+ pathSnapshots.clear();
4672
+ scheduleEpoch = 0;
4673
+ lastCommittedEpoch = 0;
4339
4674
  variantMemory.clear();
4340
4675
  for (const listener of resetListeners) {
4341
4676
  try {
@@ -4347,13 +4682,7 @@ function createFormStore(options) {
4347
4682
  }
4348
4683
  function resetField(path) {
4349
4684
  const { key: targetKey, segments: targetSegments } = paths.canonicalizePath(path);
4350
- for (const memKey of [...variantMemory.keys()]) {
4351
- const memSegments = paths.segmentsForPathKey(memKey);
4352
- if (memSegments === null) continue;
4353
- if (isPathPrefix(targetSegments, memSegments)) {
4354
- variantMemory.delete(memKey);
4355
- }
4356
- }
4685
+ variantMemory.clearUnderPath(targetSegments);
4357
4686
  const leafEntry = originals.get(targetKey);
4358
4687
  if (leafEntry !== void 0) {
4359
4688
  const wrote = setValueAtPath(targetSegments, leafEntry.value);
@@ -4367,7 +4696,7 @@ function createFormStore(options) {
4367
4696
  let anyMatch = false;
4368
4697
  for (const [, entry] of originals) {
4369
4698
  const leafSegments = entry.segments;
4370
- if (!isPathPrefix(targetSegments, leafSegments)) continue;
4699
+ if (!paths.isPathPrefix(targetSegments, leafSegments)) continue;
4371
4700
  if (leafSegments.length === targetSegments.length) continue;
4372
4701
  anyMatch = true;
4373
4702
  const relative = leafSegments.slice(targetSegments.length);
@@ -4388,14 +4717,14 @@ function createFormStore(options) {
4388
4717
  deleteErrorsUnderPrefix(schemaErrors, targetSegments);
4389
4718
  deleteErrorsUnderPrefix(userErrors, targetSegments);
4390
4719
  for (const [fieldKey, record] of Array.from(fields.entries())) {
4391
- if (isPathPrefix(targetSegments, record.path)) clearFieldRecordFlags(fieldKey);
4720
+ if (paths.isPathPrefix(targetSegments, record.path)) clearFieldRecordFlags(fieldKey);
4392
4721
  }
4393
4722
  }
4394
4723
  function deleteErrorsUnderPrefix(map, prefix) {
4395
4724
  for (const [errorKey, errs] of Array.from(map.entries())) {
4396
4725
  const first = errs[0];
4397
4726
  if (first === void 0) continue;
4398
- if (isPathPrefix(prefix, first.path)) {
4727
+ if (paths.isPathPrefix(prefix, first.path)) {
4399
4728
  map.delete(errorKey);
4400
4729
  }
4401
4730
  }
@@ -4414,15 +4743,11 @@ function createFormStore(options) {
4414
4743
  blurredAfterInteraction: false
4415
4744
  });
4416
4745
  }
4417
- function isPathPrefix(prefix, candidate) {
4418
- if (prefix.length > candidate.length) return false;
4419
- for (let i = 0; i < prefix.length; i++) {
4420
- if (prefix[i] !== candidate[i]) return false;
4421
- }
4422
- return true;
4423
- }
4424
4746
  function isPristineAtPath(path) {
4425
4747
  const { key, segments } = paths.canonicalizePath(path);
4748
+ return isPristineAtPathByKey(key, segments);
4749
+ }
4750
+ function isPristineAtPathByKey(key, segments) {
4426
4751
  if (blankPaths.has(key) !== originalBlankPaths.has(key)) return false;
4427
4752
  const entry = originals.get(key);
4428
4753
  if (entry === void 0) return true;
@@ -4484,6 +4809,7 @@ function createFormStore(options) {
4484
4809
  hydrating,
4485
4810
  hydrateError,
4486
4811
  defaultValuesFactory,
4812
+ hasSsrPrefetch: ssrPrefetch !== void 0,
4487
4813
  defaultsResolved,
4488
4814
  activated,
4489
4815
  activationPromise,
@@ -4493,6 +4819,7 @@ function createFormStore(options) {
4493
4819
  activeValidations,
4494
4820
  firstValidationDone,
4495
4821
  pathHasAsyncValidation,
4822
+ pathHasAsyncValidationByKey,
4496
4823
  fieldValidationCounts,
4497
4824
  applyFormReplacement,
4498
4825
  setValueAtPath,
@@ -4500,7 +4827,6 @@ function createFormStore(options) {
4500
4827
  arrayElementKey,
4501
4828
  reset,
4502
4829
  resetField,
4503
- clear,
4504
4830
  setSchemaErrorsForPath,
4505
4831
  setAllSchemaErrors,
4506
4832
  clearSchemaErrors,
@@ -4513,11 +4839,11 @@ function createFormStore(options) {
4513
4839
  registerElement,
4514
4840
  deregisterElement,
4515
4841
  markFocused,
4516
- markTouched,
4517
4842
  markInteracted,
4518
4843
  touchAtPath,
4519
4844
  markConnectedOptimistically,
4520
4845
  isPristineAtPath,
4846
+ isPristineAtPathByKey,
4521
4847
  hasStructuralChangeUnder,
4522
4848
  getFieldRecord,
4523
4849
  getOriginalAtPath,
@@ -4534,7 +4860,6 @@ function createFormStore(options) {
4534
4860
  modules,
4535
4861
  persistOptIns,
4536
4862
  isSensitivePath: resolvedIsSensitivePath,
4537
- segmentMatchesSensitive: resolvedSegmentMatchesSensitive,
4538
4863
  noSyncPaths,
4539
4864
  incrementNoSyncOptOut,
4540
4865
  decrementNoSyncOptOut,
@@ -4735,931 +5060,988 @@ function createHistoryModule(state, config) {
4735
5060
  };
4736
5061
  }
4737
5062
 
4738
- const PROTOCOL_VERSION = 1;
4739
- const JOIN_COLLECTION_WINDOW_MS = 50;
4740
- const SNAPSHOT_TIMEOUT_MS = 200;
4741
- const MAX_LEADER_ATTEMPTS = 3;
4742
- function isFileLikeValue(value) {
4743
- if (typeof File !== "undefined" && value instanceof File) return true;
4744
- if (typeof Blob !== "undefined" && value instanceof Blob) return true;
4745
- return false;
4746
- }
4747
- function isDangerousSegment(s) {
4748
- return s === "__proto__" || s === "constructor" || s === "prototype";
4749
- }
4750
- function pathContainsDangerousSegment(path) {
4751
- for (let i = 0; i < path.length; i++) {
4752
- if (isDangerousSegment(path[i])) return true;
4753
- }
4754
- return false;
4755
- }
4756
- function isInboundShapeAcceptable(schema, path, value) {
4757
- if (isFileLikeValue(value)) return false;
4758
- let kind;
4759
- if (Array.isArray(value)) {
4760
- kind = "array";
4761
- } else if (value !== null && typeof value === "object" && isPlainRecord(value)) {
4762
- kind = "object";
4763
- } else {
4764
- kind = slimKindOf(value);
4765
- }
4766
- const accepted = schema.getSlimPrimitiveTypesAtPath(path);
4767
- if (!accepted.has(kind)) return false;
4768
- if (Array.isArray(value)) {
4769
- for (let i = 0; i < value.length; i++) {
4770
- if (!isInboundShapeAcceptable(schema, [...path, i], value[i])) return false;
5063
+ function wirePersistence(state, config) {
5064
+ let fingerprint;
5065
+ try {
5066
+ fingerprint = hashStableString(state.schema.fingerprint());
5067
+ } catch (err) {
5068
+ if (paths.__DEV__) {
5069
+ console.warn(
5070
+ `[attaform] Could not fingerprint the schema for form '${state.formKey}': ${err instanceof Error ? err.message : String(err)}. Persistence falls back to a fingerprint-free key, so a schema change won't auto-invalidate a saved draft.`
5071
+ );
4771
5072
  }
4772
- return true;
5073
+ fingerprint = "unfingerprinted";
4773
5074
  }
4774
- if (isPlainRecord(value)) {
4775
- for (const key of Object.keys(value)) {
4776
- if (!isInboundShapeAcceptable(schema, [...path, key], value[key])) return false;
5075
+ const base = resolveStorageKeyBase(config, state.formKey);
5076
+ const key = `${base}:${fingerprint}`;
5077
+ const debounceMs = normalizeNumericOption({
5078
+ value: config.debounceMs ?? DEFAULT_PERSISTENCE_DEBOUNCE_MS,
5079
+ source: "useForm.persist.debounceMs",
5080
+ allowInfinity: false,
5081
+ min: 0,
5082
+ defaultValue: DEFAULT_PERSISTENCE_DEBOUNCE_MS
5083
+ });
5084
+ const include = config.include ?? "form";
5085
+ const clearOnSubmitSuccess = config.clearOnSubmitSuccess ?? true;
5086
+ const adapterPromise = getStorageAdapter(config.storage);
5087
+ let disposed = false;
5088
+ const isDisposed = () => disposed;
5089
+ let inFlightFinalFlush = null;
5090
+ let pendingOptedInPaths = null;
5091
+ const writer = createDebouncedWriter(async () => {
5092
+ const optedInPaths = pendingOptedInPaths ?? new Set(state.persistOptIns.optedInPaths());
5093
+ pendingOptedInPaths = null;
5094
+ const adapter = await adapterPromise;
5095
+ if (isDisposed()) return;
5096
+ if (optedInPaths.size === 0) {
5097
+ await adapter.removeItem(key);
5098
+ return;
4777
5099
  }
4778
- return true;
4779
- }
4780
- return true;
4781
- }
4782
- function diffBlankPaths(prev, curr) {
4783
- const added = [];
4784
- const removed = [];
4785
- const prevSet = new Set(prev);
4786
- for (const k of curr) if (!prevSet.has(k)) added.push(k);
4787
- for (const k of prev) if (!curr.has(k)) removed.push(k);
4788
- return { added, removed };
4789
- }
4790
- function snapshotForm(form) {
4791
- return structuralSnapshot(form);
4792
- }
4793
- function stripSensitivePathsDeep(value, pathSoFar, isSensitivePath) {
4794
- if (isFileLikeValue(value)) return void 0;
4795
- if (value === null || typeof value !== "object") return value;
4796
- if (Array.isArray(value)) {
4797
- return value.map((item, i) => stripSensitivePathsDeep(item, [...pathSoFar, i], isSensitivePath));
4798
- }
4799
- const proto = Object.getPrototypeOf(value);
4800
- if (proto !== Object.prototype && proto !== null) return value;
4801
- const out = {};
4802
- const src = value;
4803
- for (const key of Object.keys(src)) {
4804
- const childPath = [...pathSoFar, key];
4805
- if (isSensitivePath(childPath)) {
4806
- out[key] = void 0;
4807
- continue;
5100
+ const rawForm = vue.toRaw(state.form.value);
5101
+ const filteredForm = stripUnacknowledgedSensitiveLeaves(
5102
+ pluckPaths(rawForm, optedInPaths),
5103
+ optedInPaths,
5104
+ state.isSensitivePath
5105
+ );
5106
+ const filteredSchemaErrors = filterErrorsByPaths(state.schemaErrors, optedInPaths);
5107
+ const filteredUserErrors = filterErrorsByPaths(state.userErrors, optedInPaths);
5108
+ const filteredTransientEmpty = /* @__PURE__ */ new Set();
5109
+ for (const tk of state.blankPaths) {
5110
+ if (optedInPaths.has(tk)) filteredTransientEmpty.add(tk);
4808
5111
  }
4809
- out[key] = stripSensitivePathsDeep(src[key], childPath, isSensitivePath);
4810
- }
4811
- return out;
4812
- }
4813
- function isValidSyncMessage(data) {
4814
- if (data === null || typeof data !== "object") return false;
4815
- const m = data;
4816
- if (m["v"] !== PROTOCOL_VERSION) return false;
4817
- if (typeof m["senderId"] !== "string") return false;
4818
- if (typeof m["kind"] !== "string") return false;
4819
- switch (m["kind"]) {
4820
- case "hello":
4821
- case "announce":
4822
- return true;
4823
- case "requestSnapshot":
4824
- return typeof m["targetId"] === "string";
4825
- case "snapshot":
4826
- return Array.isArray(m["blankPaths"]) && "form" in m;
4827
- case "patches":
4828
- return Array.isArray(m["formPatches"]) && Array.isArray(m["blankPathsAdded"]) && Array.isArray(m["blankPathsRemoved"]);
4829
- default:
4830
- return false;
5112
+ const payload = buildPersistedPayload(
5113
+ filteredForm,
5114
+ include,
5115
+ filteredSchemaErrors,
5116
+ filteredUserErrors,
5117
+ filteredTransientEmpty
5118
+ );
5119
+ await adapter.setItem(key, payload);
5120
+ }, debounceMs);
5121
+ const unsubscribeChange = state.onFormChange((_next, meta) => {
5122
+ if (isDisposed() || inFlightFinalFlush !== null) return;
5123
+ if (meta?.crossTab === true) return;
5124
+ if (meta?.persist !== true) return;
5125
+ pendingOptedInPaths = new Set(state.persistOptIns.optedInPaths());
5126
+ writer.schedule();
5127
+ });
5128
+ const unsubscribeSuccess = clearOnSubmitSuccess ? state.onSubmitSuccess(() => {
5129
+ if (isDisposed()) return;
5130
+ void (async () => {
5131
+ await writer.flush();
5132
+ if (isDisposed()) return;
5133
+ const adapter = await adapterPromise;
5134
+ if (isDisposed()) return;
5135
+ await adapter.removeItem(key);
5136
+ })();
5137
+ }) : () => void 0;
5138
+ const handlePageHide = () => {
5139
+ if (isDisposed()) return;
5140
+ void writer.flush();
5141
+ };
5142
+ if (typeof window !== "undefined") {
5143
+ window.addEventListener("pagehide", handlePageHide);
4831
5144
  }
4832
- }
4833
- function generateSenderId() {
4834
- try {
4835
- return globalThis.crypto.randomUUID();
4836
- } catch {
4837
- return `atta-${Math.random().toString(36).slice(2)}-${Date.now().toString(36)}`;
4838
- }
4839
- }
4840
- function createMultiTabSyncModule(state, channelName, options) {
4841
- if (typeof BroadcastChannel === "undefined") {
4842
- return {
4843
- dispose: () => void 0,
4844
- lifecycle: () => "established",
4845
- senderId: "",
4846
- channelName
4847
- };
4848
- }
4849
- let channel;
4850
- try {
4851
- channel = new BroadcastChannel(channelName);
4852
- } catch {
4853
- return {
4854
- dispose: () => void 0,
4855
- lifecycle: () => "established",
4856
- senderId: "",
4857
- channelName
4858
- };
4859
- }
4860
- const senderId = generateSenderId();
4861
- let lifecycle = "joining";
4862
- let disposed = false;
4863
- const peerIds = /* @__PURE__ */ new Set();
4864
- let joinCollectionTimer = null;
4865
- let snapshotTimeoutTimer = null;
4866
- let leaderAttempts = 0;
4867
- let prior = {
4868
- form: snapshotForm(state.form.value),
4869
- blankPathsSnapshot: [...state.blankPaths]
4870
- };
4871
- function safePost(msg) {
4872
- if (disposed) return;
5145
+ void (async () => {
5146
+ const adapter = await adapterPromise;
5147
+ if (isDisposed()) return;
5148
+ void cleanupOrphanKeys(adapter, base, key);
4873
5149
  try {
4874
- channel.postMessage(msg);
5150
+ const raw = await adapter.getItem(key);
5151
+ const payload = readPersistedPayload(raw);
5152
+ if (payload === null) {
5153
+ if (raw !== null && raw !== void 0) {
5154
+ await adapter.removeItem(key);
5155
+ }
5156
+ return;
5157
+ }
5158
+ if (isDisposed()) return;
5159
+ const merged = mergeSparseHydration(
5160
+ vue.toRaw(state.form.value),
5161
+ payload.data.form,
5162
+ state.schema
5163
+ );
5164
+ state.applyFormReplacement(merged, { hydration: true });
5165
+ const persistedLeafPaths = collectPersistedLeafPaths(payload.data.form);
5166
+ for (const k of persistedLeafPaths) {
5167
+ state.blankPaths.delete(k);
5168
+ state.originalBlankPaths.delete(k);
5169
+ }
5170
+ for (const k of payload.data.blankPaths ?? []) {
5171
+ state.blankPaths.add(k);
5172
+ state.originalBlankPaths.add(k);
5173
+ }
5174
+ if (include === "form+errors") {
5175
+ if (payload.data.schemaErrors !== void 0) {
5176
+ const flat = payload.data.schemaErrors.flatMap(([, errs]) => errs);
5177
+ state.setAllSchemaErrors(flat);
5178
+ }
5179
+ if (payload.data.userErrors !== void 0) {
5180
+ const flat = payload.data.userErrors.flatMap(([, errs]) => errs);
5181
+ state.setAllUserErrors(flat);
5182
+ }
5183
+ }
5184
+ state.scheduleFieldValidation(
5185
+ [],
5186
+ true
5187
+ /* immediate */
5188
+ );
4875
5189
  } catch {
4876
5190
  }
4877
- }
4878
- function refreshPrior() {
4879
- prior = {
4880
- form: snapshotForm(state.form.value),
4881
- blankPathsSnapshot: [...state.blankPaths]
4882
- };
4883
- }
4884
- function isPathLocallySuppressed(path) {
4885
- if (pathContainsDangerousSegment(path)) return true;
4886
- if (options.isSensitivePath(path)) return true;
4887
- const { key } = paths.canonicalizePath([...path]);
4888
- if (options.noSyncPaths.has(key)) return true;
4889
- return false;
4890
- }
4891
- function postPatches() {
4892
- if (lifecycle !== "established") return;
4893
- const next = snapshotForm(state.form.value);
4894
- const rawPatches = [];
4895
- diffAndApply(prior.form, next, [], (p) => rawPatches.push(p));
4896
- const safePatches = [];
4897
- for (const p of rawPatches) {
4898
- if (isPathLocallySuppressed(p.path)) continue;
4899
- if ("value" in p && isFileLikeValue(p.value)) continue;
4900
- safePatches.push(p);
4901
- }
4902
- const { added, removed } = diffBlankPaths(prior.blankPathsSnapshot, state.blankPaths);
4903
- if (safePatches.length === 0 && added.length === 0 && removed.length === 0) {
4904
- prior = { form: next, blankPathsSnapshot: [...state.blankPaths] };
4905
- return;
5191
+ })();
5192
+ async function writePathImmediately(path) {
5193
+ if (isDisposed()) return;
5194
+ await writer.flush();
5195
+ if (isDisposed()) return;
5196
+ const adapter = await adapterPromise;
5197
+ if (isDisposed()) return;
5198
+ const raw = await adapter.getItem(key);
5199
+ const existing = readPersistedPayload(raw);
5200
+ const baseForm = existing?.data.form ?? /* @__PURE__ */ Object.create(null);
5201
+ const value = getAtPath(vue.toRaw(state.form.value), path);
5202
+ const nextForm = setAtPath(baseForm, path, value);
5203
+ const { key: pathKey } = paths.canonicalizePath(path);
5204
+ const transientSet = new Set(
5205
+ (existing?.data.blankPaths ?? []).filter(
5206
+ (k) => k !== pathKey && !isDescendantPathKey(k, pathKey)
5207
+ )
5208
+ );
5209
+ for (const liveKey of state.blankPaths) {
5210
+ if (liveKey === pathKey || isDescendantPathKey(liveKey, pathKey)) {
5211
+ transientSet.add(liveKey);
5212
+ }
4906
5213
  }
4907
- safePost({
4908
- v: PROTOCOL_VERSION,
4909
- kind: "patches",
4910
- senderId,
4911
- formPatches: safePatches,
4912
- blankPathsAdded: added,
4913
- blankPathsRemoved: removed
4914
- });
4915
- prior = { form: next, blankPathsSnapshot: [...state.blankPaths] };
4916
- }
4917
- const unsubscribeChange = state.onFormChange((_next, meta) => {
4918
- if (disposed) return;
4919
- if (lifecycle !== "established") return;
4920
- if (meta?.crossTab === true) return;
4921
- if (meta?.hydration === true) {
4922
- refreshPrior();
5214
+ if (include === "form") {
5215
+ await adapter.setItem(
5216
+ key,
5217
+ buildPersistedPayload(nextForm, "form", /* @__PURE__ */ new Map(), /* @__PURE__ */ new Map(), transientSet)
5218
+ );
4923
5219
  return;
4924
5220
  }
4925
- postPatches();
4926
- });
4927
- function applyIncomingForm(form, blankPaths) {
4928
- state.blankPaths.clear();
4929
- for (const k of blankPaths) state.blankPaths.add(k);
4930
- state.applyFormReplacement(form, { crossTab: true, persist: false });
4931
- refreshPrior();
4932
- }
4933
- function handlePatches(msg) {
4934
- if (lifecycle !== "established") return;
4935
- const safePatches = [];
4936
- for (const p of msg.formPatches) {
4937
- if (!Array.isArray(p.path)) continue;
4938
- if (isPathLocallySuppressed(p.path)) continue;
4939
- if ("value" in p && !isInboundShapeAcceptable(state.schema, p.path, p.value)) {
4940
- continue;
4941
- }
4942
- safePatches.push(p);
4943
- }
4944
- const safeBlankAdded = [];
4945
- for (const k of msg.blankPathsAdded) {
4946
- const segs = paths.canonicalizePath(k).segments;
4947
- if (isPathLocallySuppressed(segs)) continue;
4948
- safeBlankAdded.push(k);
5221
+ const schemaMap = new Map(existing?.data.schemaErrors ?? []);
5222
+ const userMap = new Map(existing?.data.userErrors ?? []);
5223
+ const currentSchema = state.schemaErrors.get(pathKey);
5224
+ const currentUser = state.userErrors.get(pathKey);
5225
+ if (currentSchema !== void 0 && currentSchema.length > 0) {
5226
+ schemaMap.set(pathKey, [...currentSchema]);
5227
+ } else {
5228
+ schemaMap.delete(pathKey);
4949
5229
  }
4950
- const safeBlankRemoved = [];
4951
- for (const k of msg.blankPathsRemoved) {
4952
- const segs = paths.canonicalizePath(k).segments;
4953
- if (isPathLocallySuppressed(segs)) continue;
4954
- safeBlankRemoved.push(k);
5230
+ if (currentUser !== void 0 && currentUser.length > 0) {
5231
+ userMap.set(pathKey, [...currentUser]);
5232
+ } else {
5233
+ userMap.delete(pathKey);
4955
5234
  }
4956
- if (safePatches.length === 0 && safeBlankAdded.length === 0 && safeBlankRemoved.length === 0) {
5235
+ await adapter.setItem(
5236
+ key,
5237
+ buildPersistedPayload(nextForm, "form+errors", schemaMap, userMap, transientSet)
5238
+ );
5239
+ }
5240
+ async function clearPersistedDraft(path) {
5241
+ if (isDisposed()) return;
5242
+ await writer.flush();
5243
+ if (isDisposed()) return;
5244
+ const adapter = await adapterPromise;
5245
+ if (isDisposed()) return;
5246
+ if (path === void 0) {
5247
+ await adapter.removeItem(key);
4957
5248
  return;
4958
5249
  }
4959
- const candidate = applyPatchesForward(state.form.value, safePatches);
4960
- try {
4961
- options.validateForm(state.form.value);
4962
- try {
4963
- options.validateForm(candidate);
4964
- } catch {
4965
- return;
4966
- }
4967
- } catch {
4968
- }
4969
- const nextBlankPaths = new Set(state.blankPaths);
4970
- for (const k of safeBlankRemoved) nextBlankPaths.delete(k);
4971
- for (const k of safeBlankAdded) nextBlankPaths.add(k);
4972
- applyIncomingForm(candidate, [...nextBlankPaths]);
4973
- }
4974
- function handleSnapshot(msg) {
4975
- if (lifecycle !== "joining") return;
4976
- if (!isInboundShapeAcceptable(state.schema, [], msg.form)) return;
4977
- if (snapshotTimeoutTimer !== null) {
4978
- clearTimeout(snapshotTimeoutTimer);
4979
- snapshotTimeoutTimer = null;
5250
+ const raw = await adapter.getItem(key);
5251
+ const existing = readPersistedPayload(raw);
5252
+ if (existing === null) return;
5253
+ const nextForm = deleteAtPath(existing.data.form, path);
5254
+ if (isEmptyContainer(nextForm)) {
5255
+ await adapter.removeItem(key);
5256
+ return;
4980
5257
  }
4981
- if (joinCollectionTimer !== null) {
4982
- clearTimeout(joinCollectionTimer);
4983
- joinCollectionTimer = null;
5258
+ const { key: pathKey } = paths.canonicalizePath(path);
5259
+ const transientSet = new Set(
5260
+ (existing.data.blankPaths ?? []).filter(
5261
+ (k) => k !== pathKey && !isDescendantPathKey(k, pathKey)
5262
+ )
5263
+ );
5264
+ if (include === "form") {
5265
+ await adapter.setItem(
5266
+ key,
5267
+ buildPersistedPayload(nextForm, "form", /* @__PURE__ */ new Map(), /* @__PURE__ */ new Map(), transientSet)
5268
+ );
5269
+ return;
4984
5270
  }
4985
- applyIncomingForm(msg.form, msg.blankPaths);
4986
- lifecycle = "established";
4987
- peerIds.clear();
5271
+ const schemaErrors = (existing.data.schemaErrors ?? []).filter(([k]) => k !== pathKey);
5272
+ const userErrors = (existing.data.userErrors ?? []).filter(([k]) => k !== pathKey);
5273
+ const schemaMap = new Map(schemaErrors.map(([k, v]) => [k, [...v]]));
5274
+ const userMap = new Map(userErrors.map(([k, v]) => [k, [...v]]));
5275
+ await adapter.setItem(
5276
+ key,
5277
+ buildPersistedPayload(nextForm, "form+errors", schemaMap, userMap, transientSet)
5278
+ );
4988
5279
  }
4989
- function respondToHello() {
4990
- safePost({ v: PROTOCOL_VERSION, kind: "announce", senderId });
5280
+ function awaitPendingWrites() {
5281
+ if (inFlightFinalFlush !== null) return inFlightFinalFlush;
5282
+ if (isDisposed()) return Promise.resolve();
5283
+ return writer.flush().catch(() => void 0);
4991
5284
  }
4992
- function respondToSnapshotRequest() {
4993
- const scrubbedForm = stripSensitivePathsDeep(state.form.value, [], options.isSensitivePath);
4994
- safePost({
4995
- v: PROTOCOL_VERSION,
4996
- kind: "snapshot",
4997
- senderId,
4998
- form: scrubbedForm,
4999
- blankPaths: [...state.blankPaths]
5000
- });
5001
- }
5002
- channel.onmessage = (event) => {
5003
- if (disposed) return;
5004
- const data = event.data;
5005
- if (!isValidSyncMessage(data)) return;
5006
- const msg = data;
5007
- if (msg.senderId === senderId) return;
5008
- switch (msg.kind) {
5009
- case "hello":
5010
- if (lifecycle !== "established") return;
5011
- respondToHello();
5012
- break;
5013
- case "announce":
5014
- if (lifecycle === "joining") peerIds.add(msg.senderId);
5015
- break;
5016
- case "requestSnapshot":
5017
- if (lifecycle !== "established") return;
5018
- if (msg.targetId !== senderId) return;
5019
- respondToSnapshotRequest();
5020
- break;
5021
- case "snapshot":
5022
- handleSnapshot(msg);
5023
- break;
5024
- case "patches":
5025
- handlePatches(msg);
5026
- break;
5027
- }
5028
- };
5029
- function electLeaderAndRequest() {
5030
- if (disposed) return;
5031
- if (peerIds.size === 0) {
5032
- lifecycle = "established";
5033
- refreshPrior();
5034
- return;
5285
+ function dispose() {
5286
+ if (isDisposed() || inFlightFinalFlush !== null) return;
5287
+ unsubscribeChange();
5288
+ unsubscribeSuccess();
5289
+ if (typeof window !== "undefined") {
5290
+ window.removeEventListener("pagehide", handlePageHide);
5035
5291
  }
5036
- const sorted = [...peerIds].sort();
5037
- const leaderId = sorted[0];
5038
- peerIds.delete(leaderId);
5039
- leaderAttempts++;
5040
- safePost({
5041
- v: PROTOCOL_VERSION,
5042
- kind: "requestSnapshot",
5043
- senderId,
5044
- targetId: leaderId
5292
+ inFlightFinalFlush = writer.flush().catch(() => void 0).finally(() => {
5293
+ disposed = true;
5294
+ inFlightFinalFlush = null;
5045
5295
  });
5046
- snapshotTimeoutTimer = setTimeout(() => {
5047
- snapshotTimeoutTimer = null;
5048
- if (disposed) return;
5049
- if (lifecycle === "established") return;
5050
- if (leaderAttempts >= MAX_LEADER_ATTEMPTS || peerIds.size === 0) {
5051
- lifecycle = "established";
5052
- refreshPrior();
5053
- return;
5054
- }
5055
- electLeaderAndRequest();
5056
- }, SNAPSHOT_TIMEOUT_MS);
5057
- }
5058
- function joinFlow() {
5059
- safePost({ v: PROTOCOL_VERSION, kind: "hello", senderId });
5060
- joinCollectionTimer = setTimeout(() => {
5061
- joinCollectionTimer = null;
5062
- if (disposed) return;
5063
- if (lifecycle === "established") return;
5064
- electLeaderAndRequest();
5065
- }, JOIN_COLLECTION_WINDOW_MS);
5066
5296
  }
5067
- joinFlow();
5068
5297
  return {
5069
- dispose: () => {
5070
- if (disposed) return;
5071
- disposed = true;
5072
- if (joinCollectionTimer !== null) {
5073
- clearTimeout(joinCollectionTimer);
5074
- joinCollectionTimer = null;
5075
- }
5076
- if (snapshotTimeoutTimer !== null) {
5077
- clearTimeout(snapshotTimeoutTimer);
5078
- snapshotTimeoutTimer = null;
5079
- }
5080
- unsubscribeChange();
5081
- try {
5082
- channel.close();
5083
- } catch {
5084
- }
5085
- },
5086
- lifecycle: () => lifecycle,
5087
- senderId,
5088
- channelName
5298
+ wiredConfig: config,
5299
+ writePathImmediately,
5300
+ clearPersistedDraft,
5301
+ awaitPendingWrites,
5302
+ dispose
5089
5303
  };
5090
5304
  }
5091
- const MULTI_TAB_SYNC_MODULE_KEY = "multiTabSync";
5092
-
5093
- const warned = /* @__PURE__ */ new Set();
5094
- function warnOnceInsecureContext(feature) {
5095
- if (!paths.__DEV__) return;
5096
- if (warned.has(feature)) return;
5097
- warned.add(feature);
5098
- const message = featureMessage(feature);
5099
- console.warn(`[attaform] ${message}`);
5305
+ function isEmptyContainer(value) {
5306
+ if (value === void 0 || value === null) return true;
5307
+ if (Array.isArray(value)) return value.length === 0;
5308
+ if (isPlainRecord(value)) return Object.keys(value).length === 0;
5309
+ return false;
5100
5310
  }
5101
- function featureMessage(feature) {
5102
- switch (feature) {
5103
- case "multiTab":
5104
- return "Multi-tab sync requires a secure context (HTTPS or localhost). Plain HTTP on a real hostname is interceptable by network observers, so the sync module is disabled. Serve over HTTPS in production (or develop on `localhost`) to enable cross-tab synchronisation. Use `multiTab: false` on `useForm` to silence this warning.";
5105
- case "persist:local":
5106
- return "Built-in `persist: 'local'` storage requires a secure context (HTTPS or localhost). Plain HTTP on a real hostname is MITM-interceptable, so the persistence layer is disabled. Serve over HTTPS to enable localStorage persistence, or pass a custom storage adapter to opt out of the secure-context gate.";
5107
- case "persist:session":
5108
- return "Built-in `persist: 'session'` storage requires a secure context (HTTPS or localhost). Plain HTTP on a real hostname is MITM-interceptable, so the persistence layer is disabled. Serve over HTTPS to enable sessionStorage persistence, or pass a custom storage adapter to opt out of the secure-context gate.";
5311
+ function collectPersistedLeafPaths(form) {
5312
+ const out = [];
5313
+ walk(form, []);
5314
+ return out;
5315
+ function walk(node, prefix) {
5316
+ if (Array.isArray(node)) {
5317
+ for (let i = 0; i < node.length; i++) {
5318
+ walk(node[i], [...prefix, i]);
5319
+ }
5320
+ return;
5321
+ }
5322
+ if (isPlainRecord(node)) {
5323
+ for (const key of Object.keys(node)) {
5324
+ walk(node[key], [...prefix, key]);
5325
+ }
5326
+ return;
5327
+ }
5328
+ if (prefix.length === 0) return;
5329
+ out.push(paths.canonicalizePath(prefix).key);
5109
5330
  }
5110
5331
  }
5111
- function isSecureContext() {
5112
- return typeof window !== "undefined" && window.isSecureContext === true;
5332
+ function isDescendantPathKey(candidate, ancestor) {
5333
+ if (candidate.length <= ancestor.length) return false;
5334
+ if (!ancestor.endsWith("]")) return false;
5335
+ const childPrefix = `${ancestor.slice(0, -1)},`;
5336
+ return candidate.startsWith(childPrefix);
5113
5337
  }
5114
5338
 
5115
- function resolveTrichotomy(input) {
5116
- if (typeof input === "function") {
5117
- return { kind: "async", factory: input };
5118
- }
5119
- return { kind: "sync", value: input };
5339
+ const PROTOCOL_VERSION = 1;
5340
+ const JOIN_COLLECTION_WINDOW_MS = 50;
5341
+ const SNAPSHOT_TIMEOUT_MS = 200;
5342
+ const MAX_LEADER_ATTEMPTS = 3;
5343
+ const SNAPSHOT_RESPONSE_MIN_INTERVAL_MS = 500;
5344
+ function isFileLikeValue(value) {
5345
+ if (typeof File !== "undefined" && value instanceof File) return true;
5346
+ if (typeof Blob !== "undefined" && value instanceof Blob) return true;
5347
+ return false;
5120
5348
  }
5121
-
5122
- function useAbstractForm(configuration, options) {
5123
- if (configuration === void 0 || configuration === null || configuration.schema === void 0) {
5124
- throw new paths.InvalidUseFormConfigError();
5349
+ function isInboundShapeAcceptable(schema, path, value) {
5350
+ if (isFileLikeValue(value)) return false;
5351
+ let kind;
5352
+ if (Array.isArray(value)) {
5353
+ kind = "array";
5354
+ } else if (value !== null && typeof value === "object" && isPlainRecord(value)) {
5355
+ kind = "object";
5356
+ } else {
5357
+ kind = slimKindOf(value);
5125
5358
  }
5126
- const key = resolveFormKey(configuration.key);
5127
- const instance = vue.getCurrentInstance();
5128
- if (instance !== null) paths.ensureAttaformInstalled(instance.appContext.app);
5129
- const registry = options?.registry ?? paths.useRegistry();
5130
- const resolvedDefaults = resolveTrichotomy(configuration.defaultValues);
5131
- const materialisedDefaults = resolvedDefaults.kind === "sync" ? resolvedDefaults.value : void 0;
5132
- const { defaultValues: _droppedDefaults, ...configWithoutDefaults } = configuration;
5133
- const trichotomyOverride = materialisedDefaults === void 0 ? configWithoutDefaults : { ...configWithoutDefaults, defaultValues: materialisedDefaults };
5134
- const merged = mergeWithDefaults(registry.defaults, trichotomyOverride);
5135
- const maxRecursionDepth = normalizeNumericOption({
5136
- value: merged.maxRecursionDepth ?? DEFAULT_MAX_RECURSION_DEPTH,
5137
- source: "useForm.maxRecursionDepth",
5138
- allowInfinity: true,
5139
- min: 0,
5140
- defaultValue: DEFAULT_MAX_RECURSION_DEPTH
5141
- });
5142
- const resolvedSchema = getComputedSchema(key, configuration.schema, { maxRecursionDepth });
5143
- if (configuration.persist !== void 0 && configuration.key === void 0) {
5144
- throw new paths.AnonPersistError({
5145
- cause: "no-key",
5146
- schemaFields: extractSchemaFields(resolvedSchema),
5147
- callSite: paths.captureUserCallSite()
5148
- });
5359
+ const accepted = schema.getSlimPrimitiveTypesAtPath(path);
5360
+ if (!accepted.has(kind)) return false;
5361
+ if (Array.isArray(value)) {
5362
+ for (let i = 0; i < value.length; i++) {
5363
+ if (!isInboundShapeAcceptable(schema, [...path, i], value[i])) return false;
5364
+ }
5365
+ return true;
5149
5366
  }
5150
- const existing = registry.forms.get(key);
5151
- if (paths.__DEV__ && existing !== void 0) {
5152
- warnOnSchemaFingerprintMismatch(key, existing.schema, resolvedSchema);
5153
- warnOnPersistDivergence(key, existing, configuration.persist);
5367
+ if (isPlainRecord(value)) {
5368
+ for (const key of Object.keys(value)) {
5369
+ if (!isInboundShapeAcceptable(schema, [...path, key], value[key])) return false;
5370
+ }
5371
+ return true;
5154
5372
  }
5155
- const hadPendingHydration = registry.pendingHydration.has(key);
5156
- const state = existing ?? buildFreshState(key, resolvedSchema, merged, registry);
5157
- if (existing !== void 0) ; else if (resolvedDefaults.kind === "sync") {
5158
- state.defaultsResolved.value = true;
5373
+ return true;
5374
+ }
5375
+ function diffBlankPaths(prev, curr) {
5376
+ const added = [];
5377
+ const removed = [];
5378
+ const prevSet = new Set(prev);
5379
+ for (const k of curr) if (!prevSet.has(k)) added.push(k);
5380
+ for (const k of prev) if (!curr.has(k)) removed.push(k);
5381
+ return { added, removed };
5382
+ }
5383
+ function snapshotForm(form) {
5384
+ return structuralSnapshot(form);
5385
+ }
5386
+ function stripSensitivePathsDeep(value, pathSoFar, isSensitivePath) {
5387
+ if (isFileLikeValue(value)) return void 0;
5388
+ if (value === null || typeof value !== "object") return value;
5389
+ if (Array.isArray(value)) {
5390
+ return value.map((item, i) => stripSensitivePathsDeep(item, [...pathSoFar, i], isSensitivePath));
5159
5391
  }
5160
- if (existing === void 0 && resolvedDefaults.kind === "async") {
5161
- const factory = resolvedDefaults.factory;
5162
- state.defaultValuesFactory.value = factory;
5163
- if (hadPendingHydration) {
5164
- state.hydrating.value = false;
5165
- state.defaultsResolved.value = true;
5166
- } else if (registry.ssr) {
5167
- if (configuration.__ssrAccessed === true) {
5168
- registry.enqueuePrefetch(key);
5169
- }
5170
- vue.onServerPrefetch(() => {
5171
- if (!registry.shouldPrefetch(key)) return;
5172
- return state.activate();
5173
- });
5392
+ const proto = Object.getPrototypeOf(value);
5393
+ if (proto !== Object.prototype && proto !== null) return value;
5394
+ const out = /* @__PURE__ */ Object.create(null);
5395
+ const src = value;
5396
+ for (const key of Object.keys(src)) {
5397
+ const childPath = [...pathSoFar, key];
5398
+ if (isSensitivePath(childPath)) {
5399
+ out[key] = void 0;
5400
+ continue;
5174
5401
  }
5402
+ out[key] = stripSensitivePathsDeep(src[key], childPath, isSensitivePath);
5175
5403
  }
5176
- if (vue.getCurrentScope() !== void 0) {
5177
- const releaseConsumer = registry.trackConsumer(key);
5178
- vue.onScopeDispose(releaseConsumer);
5179
- }
5180
- const persistDisabledByAnonRule = merged.persist !== void 0 && enforceAnonPersistRule(state.formKey, registry.ssr);
5181
- if (existing === void 0 && !registry.ssr) {
5182
- if (merged.persist !== void 0 && !persistDisabledByAnonRule) {
5183
- const resolvedPersist = normalizePersistConfig(merged.persist);
5184
- const storageKind = resolvedPersist.storage;
5185
- const isBuiltinStorage = typeof storageKind === "string";
5186
- const secureContextOk = !isBuiltinStorage || isSecureContext();
5187
- if (!secureContextOk) {
5188
- const feature = storageKind === "session" ? "persist:session" : "persist:local";
5189
- warnOnceInsecureContext(feature);
5190
- void sweepAllOrphansAcrossStandardStores(`${PERSISTENCE_KEY_PREFIX}${state.formKey}`);
5191
- } else {
5192
- const persistenceBase = resolveStorageKeyBase(resolvedPersist, state.formKey);
5193
- void sweepNonConfiguredStandardStoresForOrphans(resolvedPersist.storage, persistenceBase);
5194
- const persistenceModule = wirePersistence(state, resolvedPersist);
5195
- state.modules.set(PERSISTENCE_MODULE_KEY, persistenceModule);
5196
- state.registerDrain(() => persistenceModule.awaitPendingWrites());
5197
- state.registerCleanup(() => persistenceModule.dispose());
5198
- }
5199
- } else {
5200
- void sweepAllOrphansAcrossStandardStores(`${PERSISTENCE_KEY_PREFIX}${state.formKey}`);
5201
- }
5404
+ return out;
5405
+ }
5406
+ function isStringArray(value) {
5407
+ return Array.isArray(value) && value.every((item) => typeof item === "string");
5408
+ }
5409
+ function isPatchArray(value) {
5410
+ return Array.isArray(value) && value.every(
5411
+ (p) => p !== null && typeof p === "object" && Array.isArray(p.path)
5412
+ );
5413
+ }
5414
+ function isValidSyncMessage(data) {
5415
+ if (data === null || typeof data !== "object") return false;
5416
+ const m = data;
5417
+ if (m["v"] !== PROTOCOL_VERSION) return false;
5418
+ if (typeof m["senderId"] !== "string") return false;
5419
+ if (typeof m["kind"] !== "string") return false;
5420
+ switch (m["kind"]) {
5421
+ case "hello":
5422
+ case "announce":
5423
+ return true;
5424
+ case "requestSnapshot":
5425
+ return typeof m["targetId"] === "string";
5426
+ case "snapshot":
5427
+ return isStringArray(m["blankPaths"]) && "form" in m;
5428
+ case "patches":
5429
+ return isPatchArray(m["formPatches"]) && isStringArray(m["blankPathsAdded"]) && isStringArray(m["blankPathsRemoved"]);
5430
+ default:
5431
+ return false;
5202
5432
  }
5203
- if (existing === void 0 && merged.multiTab === true && configuration.key !== void 0 && !registry.ssr) {
5204
- const hasBroadcastChannel = typeof BroadcastChannel !== "undefined";
5205
- const secureContext = isSecureContext();
5206
- if (hasBroadcastChannel && secureContext) {
5207
- let channelName;
5208
- try {
5209
- channelName = `attaform:sync:${state.formKey}:${hashStableString(state.schema.fingerprint())}`;
5210
- } catch {
5211
- channelName = null;
5212
- }
5213
- if (channelName !== null) {
5214
- const syncModule = createMultiTabSyncModule(state, channelName, {
5215
- isSensitivePath: state.isSensitivePath,
5216
- noSyncPaths: state.noSyncPaths,
5217
- validateForm: (form) => {
5218
- const result = state.schema.validateAtPath(form, void 0, { sync: true });
5219
- if (result instanceof Promise) return;
5220
- if (!result.success) {
5221
- throw new Error("attaform multi-tab sync: post-apply schema validation failed");
5222
- }
5223
- }
5224
- });
5225
- state.modules.set(MULTI_TAB_SYNC_MODULE_KEY, syncModule);
5226
- state.registerCleanup(() => syncModule.dispose());
5227
- }
5228
- } else if (hasBroadcastChannel && !secureContext) {
5229
- warnOnceInsecureContext("multiTab");
5230
- }
5433
+ }
5434
+ function generateSenderId() {
5435
+ try {
5436
+ return globalThis.crypto.randomUUID();
5437
+ } catch {
5438
+ return `atta-${Math.random().toString(36).slice(2)}-${Date.now().toString(36)}`;
5231
5439
  }
5232
- if (existing === void 0 && merged.history !== void 0) {
5233
- const historyModule = createHistoryModule(state, merged.history);
5234
- state.modules.set(HISTORY_MODULE_KEY, historyModule);
5235
- state.registerCleanup(() => historyModule.dispose());
5440
+ }
5441
+ function createMultiTabSyncModule(state, channelName, options) {
5442
+ if (typeof BroadcastChannel === "undefined") {
5443
+ return {
5444
+ dispose: () => void 0,
5445
+ lifecycle: () => "established",
5446
+ senderId: "",
5447
+ channelName
5448
+ };
5236
5449
  }
5237
- if (configuration.key === void 0) {
5238
- recordAmbientProvide(registry.ssr);
5239
- vue.provide(paths.kFormContext, state);
5450
+ let channel;
5451
+ try {
5452
+ channel = new BroadcastChannel(channelName);
5453
+ } catch {
5454
+ return {
5455
+ dispose: () => void 0,
5456
+ lifecycle: () => "established",
5457
+ senderId: "",
5458
+ channelName
5459
+ };
5240
5460
  }
5241
- const formInstanceId = vue.getCurrentInstance() !== null ? vue.useId() : `atta:form-instance:${formInstanceCounter++}`;
5242
- if (vue.getCurrentInstance() !== null) {
5243
- vue.provide(paths.kFormInstanceId, formInstanceId);
5461
+ const senderId = generateSenderId();
5462
+ let lifecycle = "joining";
5463
+ let disposed = false;
5464
+ const peerIds = /* @__PURE__ */ new Set();
5465
+ const lastSnapshotResponseAt = /* @__PURE__ */ new Map();
5466
+ let joinCollectionTimer = null;
5467
+ let snapshotTimeoutTimer = null;
5468
+ let leaderAttempts = 0;
5469
+ let prior = {
5470
+ form: snapshotForm(state.form.value),
5471
+ blankPathsSnapshot: [...state.blankPaths]
5472
+ };
5473
+ function safePost(msg) {
5474
+ if (disposed) return;
5475
+ try {
5476
+ channel.postMessage(msg);
5477
+ } catch {
5478
+ }
5244
5479
  }
5245
- const apiOptions = {};
5246
- if (merged.onInvalidSubmit !== void 0) {
5247
- apiOptions.onInvalidSubmit = merged.onInvalidSubmit;
5480
+ function refreshPrior() {
5481
+ prior = {
5482
+ form: snapshotForm(state.form.value),
5483
+ blankPathsSnapshot: [...state.blankPaths]
5484
+ };
5248
5485
  }
5249
- const history = state.modules.get(HISTORY_MODULE_KEY);
5250
- if (history !== void 0) {
5251
- apiOptions.history = history;
5486
+ function isPathLocallySuppressed(path) {
5487
+ if (options.isSensitivePath(path)) return true;
5488
+ const { key } = paths.canonicalizePath([...path]);
5489
+ if (options.noSyncPaths.has(key)) return true;
5490
+ return false;
5252
5491
  }
5253
- if (merged.validateOn !== void 0) {
5254
- apiOptions.validateOn = merged.validateOn;
5492
+ function postPatches() {
5493
+ if (lifecycle !== "established") return;
5494
+ const next = snapshotForm(state.form.value);
5495
+ const rawPatches = [];
5496
+ diffAndApply(prior.form, next, [], (p) => rawPatches.push(p));
5497
+ const safePatches = [];
5498
+ for (const p of rawPatches) {
5499
+ if (isPathLocallySuppressed(p.path)) continue;
5500
+ if ("value" in p && isFileLikeValue(p.value)) continue;
5501
+ safePatches.push(p);
5502
+ }
5503
+ const { added, removed } = diffBlankPaths(prior.blankPathsSnapshot, state.blankPaths);
5504
+ if (safePatches.length === 0 && added.length === 0 && removed.length === 0) {
5505
+ prior = { form: next, blankPathsSnapshot: [...state.blankPaths] };
5506
+ return;
5507
+ }
5508
+ safePost({
5509
+ v: PROTOCOL_VERSION,
5510
+ kind: "patches",
5511
+ senderId,
5512
+ formPatches: safePatches,
5513
+ blankPathsAdded: added,
5514
+ blankPathsRemoved: removed
5515
+ });
5516
+ prior = { form: next, blankPathsSnapshot: [...state.blankPaths] };
5255
5517
  }
5256
- const mergedDebounceMs = merged.debounceMs;
5257
- if (mergedDebounceMs !== void 0) {
5258
- apiOptions.debounceMs = mergedDebounceMs;
5518
+ const unsubscribeChange = state.onFormChange((_next, meta) => {
5519
+ if (disposed) return;
5520
+ if (lifecycle !== "established") return;
5521
+ if (meta?.crossTab === true) return;
5522
+ if (meta?.hydration === true) {
5523
+ refreshPrior();
5524
+ return;
5525
+ }
5526
+ postPatches();
5527
+ });
5528
+ function applyIncomingForm(form, blankPaths) {
5529
+ state.blankPaths.clear();
5530
+ for (const k of blankPaths) state.blankPaths.add(k);
5531
+ state.applyFormReplacement(form, { crossTab: true, persist: false });
5532
+ refreshPrior();
5259
5533
  }
5260
- if (merged.getDisplayState !== void 0) {
5261
- apiOptions.getDisplayState = merged.getDisplayState;
5534
+ function handlePatches(msg) {
5535
+ if (lifecycle !== "established") return;
5536
+ const safePatches = [];
5537
+ for (const p of msg.formPatches) {
5538
+ if (!Array.isArray(p.path)) continue;
5539
+ if (isPathLocallySuppressed(p.path)) continue;
5540
+ if ("value" in p && !isInboundShapeAcceptable(state.schema, p.path, p.value)) {
5541
+ continue;
5542
+ }
5543
+ safePatches.push(p);
5544
+ }
5545
+ const safeBlankAdded = [];
5546
+ for (const k of msg.blankPathsAdded) {
5547
+ const segs = paths.canonicalizePath(k).segments;
5548
+ if (isPathLocallySuppressed(segs)) continue;
5549
+ safeBlankAdded.push(k);
5550
+ }
5551
+ const safeBlankRemoved = [];
5552
+ for (const k of msg.blankPathsRemoved) {
5553
+ const segs = paths.canonicalizePath(k).segments;
5554
+ if (isPathLocallySuppressed(segs)) continue;
5555
+ safeBlankRemoved.push(k);
5556
+ }
5557
+ if (safePatches.length === 0 && safeBlankAdded.length === 0 && safeBlankRemoved.length === 0) {
5558
+ return;
5559
+ }
5560
+ const candidate = applyPatchesForward(state.form.value, safePatches);
5561
+ try {
5562
+ options.validateForm(state.form.value);
5563
+ try {
5564
+ options.validateForm(candidate);
5565
+ } catch {
5566
+ return;
5567
+ }
5568
+ } catch {
5569
+ }
5570
+ const nextBlankPaths = new Set(state.blankPaths);
5571
+ for (const k of safeBlankRemoved) nextBlankPaths.delete(k);
5572
+ for (const k of safeBlankAdded) nextBlankPaths.add(k);
5573
+ applyIncomingForm(candidate, [...nextBlankPaths]);
5262
5574
  }
5263
- if (merged.coerce !== void 0) {
5264
- apiOptions.coerce = merged.coerce;
5575
+ function handleSnapshot(msg) {
5576
+ if (lifecycle !== "joining") return;
5577
+ if (!isInboundShapeAcceptable(state.schema, [], msg.form)) return;
5578
+ if (snapshotTimeoutTimer !== null) {
5579
+ clearTimeout(snapshotTimeoutTimer);
5580
+ snapshotTimeoutTimer = null;
5581
+ }
5582
+ if (joinCollectionTimer !== null) {
5583
+ clearTimeout(joinCollectionTimer);
5584
+ joinCollectionTimer = null;
5585
+ }
5586
+ applyIncomingForm(msg.form, msg.blankPaths);
5587
+ lifecycle = "established";
5588
+ peerIds.clear();
5265
5589
  }
5266
- if (merged.rememberVariants !== void 0) {
5267
- apiOptions.rememberVariants = merged.rememberVariants;
5590
+ function respondToHello() {
5591
+ safePost({ v: PROTOCOL_VERSION, kind: "announce", senderId });
5268
5592
  }
5269
- if (merged.autoAria !== void 0) {
5270
- apiOptions.autoAria = merged.autoAria;
5593
+ function respondToSnapshotRequest(requesterId) {
5594
+ const now = Date.now();
5595
+ const last = lastSnapshotResponseAt.get(requesterId);
5596
+ if (last !== void 0 && now - last < SNAPSHOT_RESPONSE_MIN_INTERVAL_MS) return;
5597
+ lastSnapshotResponseAt.set(requesterId, now);
5598
+ const scrubbedForm = stripSensitivePathsDeep(state.form.value, [], options.isSensitivePath);
5599
+ safePost({
5600
+ v: PROTOCOL_VERSION,
5601
+ kind: "snapshot",
5602
+ senderId,
5603
+ form: scrubbedForm,
5604
+ blankPaths: [...state.blankPaths]
5605
+ });
5271
5606
  }
5272
- return buildFormApi(
5273
- state,
5274
- formInstanceId,
5275
- apiOptions
5276
- );
5277
- }
5278
- function mergeWithDefaults(defaults, configuration) {
5279
- const strict = configuration.strict ?? defaults.strict;
5280
- const onInvalidSubmit = configuration.onInvalidSubmit ?? defaults.onInvalidSubmit;
5281
- const history = configuration.history ?? defaults.history;
5282
- const rememberVariants = configuration.rememberVariants ?? defaults.rememberVariants;
5283
- const coerce = configuration.coerce ?? defaults.coerce;
5284
- const validateOn = configuration.validateOn ?? defaults.validateOn;
5285
- const debounceMs = configuration.debounceMs ?? defaults.debounceMs;
5286
- const getDisplayState = configuration.getDisplayState ?? defaults.getDisplayState;
5287
- const maxRecursionDepth = configuration.maxRecursionDepth ?? defaults.maxRecursionDepth;
5288
- const sensitiveNames = configuration.sensitiveNames ?? defaults.sensitiveNames;
5289
- const multiTab = configuration.multiTab ?? defaults.multiTab;
5290
- const autoAria = configuration.autoAria ?? defaults.autoAria;
5291
- return {
5292
- ...configuration,
5293
- ...strict === void 0 ? {} : { strict },
5294
- ...onInvalidSubmit === void 0 ? {} : { onInvalidSubmit },
5295
- ...history === void 0 ? {} : { history },
5296
- ...rememberVariants === void 0 ? {} : { rememberVariants },
5297
- ...coerce === void 0 ? {} : { coerce },
5298
- ...validateOn === void 0 ? {} : { validateOn },
5299
- ...debounceMs === void 0 ? {} : { debounceMs },
5300
- ...getDisplayState === void 0 ? {} : { getDisplayState },
5301
- ...maxRecursionDepth === void 0 ? {} : { maxRecursionDepth },
5302
- ...sensitiveNames === void 0 ? {} : { sensitiveNames },
5303
- ...multiTab === void 0 ? {} : { multiTab },
5304
- ...autoAria === void 0 ? {} : { autoAria }
5305
- };
5306
- }
5307
- const HISTORY_MODULE_KEY = "history";
5308
- function buildFreshState(key, schema, configuration, registry) {
5309
- const pending = registry.pendingHydration.get(key);
5310
- if (pending !== void 0) registry.pendingHydration.delete(key);
5311
- const walked = walkUnsetSentinels(
5312
- configuration.defaultValues,
5313
- schema
5314
- );
5315
- let initialBlankPaths;
5316
- if (pending === void 0) {
5317
- initialBlankPaths = walked.paths;
5318
- }
5319
- const resolvedSensitiveNames = configuration.sensitiveNames;
5320
- const resolvedIsSensitivePath = resolvedSensitiveNames === void 0 ? void 0 : paths.createIsSensitivePath(resolvedSensitiveNames);
5321
- const resolvedSegmentMatchesSensitive = resolvedSensitiveNames === void 0 ? void 0 : paths.createSegmentMatchesSensitive(resolvedSensitiveNames);
5322
- const createOptions = {
5323
- formKey: key,
5324
- schema,
5325
- defaultValues: walked.cleanedValues,
5326
- ...configuration.strict !== void 0 ? { strict: configuration.strict } : {},
5327
- hydration: pending,
5328
- ...configuration.validateOn !== void 0 ? { validateOn: configuration.validateOn } : {},
5329
- ...configuration.debounceMs !== void 0 ? { debounceMs: configuration.debounceMs } : {},
5330
- ssr: registry.ssr,
5331
- // Server-only: bind the SSR prefetch coordination handles. `enqueue`
5332
- // records intent on every `state.activate()` so a wizard skip-list
5333
- // override or a future transform mark has a consistent set to diff
5334
- // against; `shouldFire` lets the activate path bail when the
5335
- // wizard explicitly skipped this key — even an explicit
5336
- // `form.activate()` defers to the wizard's render-efficiency
5337
- // skip-list on the server.
5338
- ...registry.ssr ? {
5339
- ssrPrefetch: {
5340
- enqueue: () => {
5341
- registry.enqueuePrefetch(key);
5342
- },
5343
- shouldFire: () => registry.shouldPrefetch(key)
5607
+ channel.onmessage = (event) => {
5608
+ if (disposed) return;
5609
+ try {
5610
+ const data = event.data;
5611
+ if (!isValidSyncMessage(data)) return;
5612
+ const msg = data;
5613
+ if (msg.senderId === senderId) return;
5614
+ switch (msg.kind) {
5615
+ case "hello":
5616
+ if (lifecycle !== "established") return;
5617
+ respondToHello();
5618
+ break;
5619
+ case "announce":
5620
+ if (lifecycle === "joining") peerIds.add(msg.senderId);
5621
+ break;
5622
+ case "requestSnapshot":
5623
+ if (lifecycle !== "established") return;
5624
+ if (msg.targetId !== senderId) return;
5625
+ respondToSnapshotRequest(msg.senderId);
5626
+ break;
5627
+ case "snapshot":
5628
+ handleSnapshot(msg);
5629
+ break;
5630
+ case "patches":
5631
+ handlePatches(msg);
5632
+ break;
5344
5633
  }
5345
- } : {},
5346
- ...configuration.rememberVariants !== void 0 ? { rememberVariants: configuration.rememberVariants } : {},
5347
- ...configuration.coerce !== void 0 ? { coerce: configuration.coerce } : {},
5348
- ...configuration.getDisplayState !== void 0 ? { getDisplayState: configuration.getDisplayState } : {},
5349
- ...initialBlankPaths !== void 0 ? { initialBlankPaths } : {},
5350
- ...resolvedIsSensitivePath !== void 0 ? { isSensitivePath: resolvedIsSensitivePath } : {},
5351
- ...resolvedSegmentMatchesSensitive !== void 0 ? { segmentMatchesSensitive: resolvedSegmentMatchesSensitive } : {}
5352
- };
5353
- const state = createFormStore(createOptions);
5354
- registry.forms.set(
5355
- key,
5356
- state
5357
- );
5358
- return state;
5359
- }
5360
- let anonCounter = 0;
5361
- let formInstanceCounter = 0;
5362
- const ambientProvideHistory = paths.__DEV__ ? /* @__PURE__ */ new WeakMap() : null;
5363
- function recordAmbientProvide(ssr) {
5364
- if (!paths.__DEV__ || ssr || ambientProvideHistory === null) return;
5365
- const instance = vue.getCurrentInstance();
5366
- if (instance === null) return;
5367
- const instanceKey = instance;
5368
- const entry = {
5369
- source: paths.captureUserCallSite()
5634
+ } catch {
5635
+ }
5370
5636
  };
5371
- const existing = ambientProvideHistory.get(instanceKey);
5372
- if (existing === void 0) {
5373
- ambientProvideHistory.set(instanceKey, [entry]);
5374
- return;
5375
- }
5376
- existing.push(entry);
5377
- }
5378
- function resolveFormKey(key) {
5379
- if (key !== void 0 && key !== null && key !== "") {
5380
- if (key.startsWith(RESERVED_KEY_PREFIX)) {
5381
- throw new paths.ReservedFormKeyError(key);
5637
+ function electLeaderAndRequest() {
5638
+ if (disposed) return;
5639
+ if (peerIds.size === 0) {
5640
+ lifecycle = "established";
5641
+ refreshPrior();
5642
+ return;
5382
5643
  }
5383
- return key;
5644
+ const sorted = [...peerIds].sort();
5645
+ const leaderId = sorted[0];
5646
+ peerIds.delete(leaderId);
5647
+ leaderAttempts++;
5648
+ safePost({
5649
+ v: PROTOCOL_VERSION,
5650
+ kind: "requestSnapshot",
5651
+ senderId,
5652
+ targetId: leaderId
5653
+ });
5654
+ snapshotTimeoutTimer = setTimeout(() => {
5655
+ snapshotTimeoutTimer = null;
5656
+ if (disposed) return;
5657
+ if (lifecycle === "established") return;
5658
+ if (leaderAttempts >= MAX_LEADER_ATTEMPTS || peerIds.size === 0) {
5659
+ lifecycle = "established";
5660
+ refreshPrior();
5661
+ return;
5662
+ }
5663
+ electLeaderAndRequest();
5664
+ }, SNAPSHOT_TIMEOUT_MS);
5384
5665
  }
5385
- if (vue.getCurrentInstance() !== null) {
5386
- return `${ANONYMOUS_FORM_KEY_PREFIX}${vue.useId()}`;
5666
+ function joinFlow() {
5667
+ safePost({ v: PROTOCOL_VERSION, kind: "hello", senderId });
5668
+ joinCollectionTimer = setTimeout(() => {
5669
+ joinCollectionTimer = null;
5670
+ if (disposed) return;
5671
+ if (lifecycle === "established") return;
5672
+ electLeaderAndRequest();
5673
+ }, JOIN_COLLECTION_WINDOW_MS);
5387
5674
  }
5388
- return `${ANONYMOUS_FORM_KEY_PREFIX}${anonCounter++}`;
5675
+ joinFlow();
5676
+ return {
5677
+ dispose: () => {
5678
+ if (disposed) return;
5679
+ disposed = true;
5680
+ if (joinCollectionTimer !== null) {
5681
+ clearTimeout(joinCollectionTimer);
5682
+ joinCollectionTimer = null;
5683
+ }
5684
+ if (snapshotTimeoutTimer !== null) {
5685
+ clearTimeout(snapshotTimeoutTimer);
5686
+ snapshotTimeoutTimer = null;
5687
+ }
5688
+ unsubscribeChange();
5689
+ try {
5690
+ channel.close();
5691
+ } catch {
5692
+ }
5693
+ },
5694
+ lifecycle: () => lifecycle,
5695
+ senderId,
5696
+ channelName
5697
+ };
5389
5698
  }
5390
- function warnOnSchemaFingerprintMismatch(key, existing, incoming) {
5391
- let existingFp;
5392
- let incomingFp;
5393
- try {
5394
- existingFp = existing.fingerprint();
5395
- incomingFp = incoming.fingerprint();
5396
- } catch (error) {
5397
- console.error(
5398
- `[attaform] fingerprint() threw for key "${key}"; skipping mismatch check.`,
5399
- error
5400
- );
5401
- return;
5402
- }
5403
- if (existingFp === incomingFp) return;
5404
- console.warn(
5405
- `[attaform] useForm() calls with key "${key}" use different schemas; first wins, second is ignored. Use identical schemas or unique keys.
5406
- existing: ${existingFp}
5407
- incoming: ${incomingFp}`
5408
- );
5699
+ const MULTI_TAB_SYNC_MODULE_KEY = "multiTabSync";
5700
+
5701
+ const warned = /* @__PURE__ */ new Set();
5702
+ function warnOnceInsecureContext(feature) {
5703
+ if (!paths.__DEV__) return;
5704
+ if (warned.has(feature)) return;
5705
+ warned.add(feature);
5706
+ const message = featureMessage(feature);
5707
+ console.warn(`[attaform] ${message}`);
5409
5708
  }
5410
- function warnOnPersistDivergence(key, existing, incomingPersist) {
5411
- if (incomingPersist === void 0) return;
5412
- const wired = existing.modules.get(PERSISTENCE_MODULE_KEY);
5413
- const incomingNormalized = normalizePersistConfig(incomingPersist);
5414
- if (wired === void 0) {
5415
- console.warn(
5416
- `[attaform] useForm({ key: "${key}" }) passed a persist config but the first useForm({ key }) call didn't wire persistence; the new config is silently dropped. Pass persist on the first call, or remove persist here to make the inheritance explicit.`
5417
- );
5418
- return;
5709
+ function featureMessage(feature) {
5710
+ switch (feature) {
5711
+ case "multiTab":
5712
+ return "Multi-tab sync requires a secure context (HTTPS or localhost). Plain HTTP on a real hostname is interceptable by network observers, so the sync module is disabled. Serve over HTTPS in production (or develop on `localhost`) to enable cross-tab synchronisation. Use `multiTab: false` on `useForm` to silence this warning.";
5713
+ case "persist:local":
5714
+ return "Built-in `persist: 'local'` storage requires a secure context (HTTPS or localhost). Plain HTTP on a real hostname is MITM-interceptable, so the persistence layer is disabled. Serve over HTTPS to enable localStorage persistence, or pass a custom storage adapter to opt out of the secure-context gate.";
5715
+ case "persist:session":
5716
+ return "Built-in `persist: 'session'` storage requires a secure context (HTTPS or localhost). Plain HTTP on a real hostname is MITM-interceptable, so the persistence layer is disabled. Serve over HTTPS to enable sessionStorage persistence, or pass a custom storage adapter to opt out of the secure-context gate.";
5419
5717
  }
5420
- if (persistConfigsEquivalent(wired.wiredConfig, incomingNormalized)) return;
5421
- console.warn(
5422
- `[attaform] useForm({ key: "${key}" }) passed a persist config that differs from the first useForm({ key }) call's; first wins, this one is ignored.
5423
- wired: ${describePersist(wired.wiredConfig)}
5424
- incoming: ${describePersist(incomingNormalized)}`
5425
- );
5426
5718
  }
5427
- function persistConfigsEquivalent(a, b) {
5428
- if (a.storage !== b.storage) return false;
5429
- if ((a.key ?? void 0) !== (b.key ?? void 0)) return false;
5430
- if ((a.debounceMs ?? void 0) !== (b.debounceMs ?? void 0)) return false;
5431
- return true;
5719
+ function isSecureContext() {
5720
+ return typeof window !== "undefined" && window.isSecureContext === true;
5432
5721
  }
5433
- function describePersist(config) {
5434
- const storage = typeof config.storage === "string" ? config.storage : "custom-adapter";
5435
- const parts = [`storage=${storage}`];
5436
- if (config.key !== void 0) parts.push(`key=${config.key}`);
5437
- if (config.debounceMs !== void 0) parts.push(`debounceMs=${config.debounceMs}`);
5438
- return `{ ${parts.join(", ")} }`;
5722
+
5723
+ function resolveTrichotomy(input) {
5724
+ if (typeof input === "function") {
5725
+ return { kind: "async", factory: input };
5726
+ }
5727
+ return { kind: "sync", value: input };
5439
5728
  }
5440
- function wirePersistence(state, config) {
5441
- const fingerprint = hashStableString(state.schema.fingerprint());
5442
- const base = resolveStorageKeyBase(config, state.formKey);
5443
- const key = `${base}:${fingerprint}`;
5444
- const debounceMs = normalizeNumericOption({
5445
- value: config.debounceMs ?? DEFAULT_PERSISTENCE_DEBOUNCE_MS,
5446
- source: "useForm.persist.debounceMs",
5447
- allowInfinity: false,
5729
+
5730
+ function useAbstractForm(configuration, options) {
5731
+ if (configuration === void 0 || configuration === null || configuration.schema === void 0) {
5732
+ throw new paths.InvalidUseFormConfigError();
5733
+ }
5734
+ const key = resolveFormKey(configuration.key);
5735
+ const instance = vue.getCurrentInstance();
5736
+ if (instance !== null) paths.ensureAttaformInstalled(instance.appContext.app);
5737
+ const registry = options?.registry ?? paths.useRegistry();
5738
+ const resolvedDefaults = resolveTrichotomy(configuration.defaultValues);
5739
+ const materialisedDefaults = resolvedDefaults.kind === "sync" ? resolvedDefaults.value : void 0;
5740
+ const { defaultValues: _droppedDefaults, ...configWithoutDefaults } = configuration;
5741
+ const trichotomyOverride = materialisedDefaults === void 0 ? configWithoutDefaults : { ...configWithoutDefaults, defaultValues: materialisedDefaults };
5742
+ const merged = mergeWithDefaults(registry.defaults, trichotomyOverride);
5743
+ const maxRecursionDepth = normalizeNumericOption({
5744
+ value: merged.maxRecursionDepth ?? DEFAULT_MAX_RECURSION_DEPTH,
5745
+ source: "useForm.maxRecursionDepth",
5746
+ allowInfinity: true,
5448
5747
  min: 0,
5449
- defaultValue: DEFAULT_PERSISTENCE_DEBOUNCE_MS
5748
+ defaultValue: DEFAULT_MAX_RECURSION_DEPTH
5450
5749
  });
5451
- const include = config.include ?? "form";
5452
- const clearOnSubmitSuccess = config.clearOnSubmitSuccess ?? true;
5453
- const adapterPromise = getStorageAdapter(config.storage);
5454
- let disposed = false;
5455
- let inFlightFinalFlush = null;
5456
- let pendingOptedInPaths = null;
5457
- const writer = createDebouncedWriter(async () => {
5458
- const optedInPaths = pendingOptedInPaths ?? new Set(state.persistOptIns.optedInPaths());
5459
- pendingOptedInPaths = null;
5460
- const adapter = await adapterPromise;
5461
- if (disposed) return;
5462
- if (optedInPaths.size === 0) {
5463
- await adapter.removeItem(key);
5464
- return;
5465
- }
5466
- const rawForm = vue.toRaw(state.form.value);
5467
- const filteredForm = pluckPaths(rawForm, optedInPaths);
5468
- const filteredSchemaErrors = filterErrorsByPaths(state.schemaErrors, optedInPaths);
5469
- const filteredUserErrors = filterErrorsByPaths(state.userErrors, optedInPaths);
5470
- const filteredTransientEmpty = /* @__PURE__ */ new Set();
5471
- for (const tk of state.blankPaths) {
5472
- if (optedInPaths.has(tk)) filteredTransientEmpty.add(tk);
5473
- }
5474
- const payload = buildPersistedPayload(
5475
- filteredForm,
5476
- include,
5477
- filteredSchemaErrors,
5478
- filteredUserErrors,
5479
- filteredTransientEmpty
5480
- );
5481
- await adapter.setItem(key, payload);
5482
- }, debounceMs);
5483
- const unsubscribeChange = state.onFormChange((_next, meta) => {
5484
- if (disposed || inFlightFinalFlush !== null) return;
5485
- if (meta?.crossTab === true) return;
5486
- if (meta?.persist !== true) return;
5487
- pendingOptedInPaths = new Set(state.persistOptIns.optedInPaths());
5488
- writer.schedule();
5489
- });
5490
- const unsubscribeSuccess = clearOnSubmitSuccess ? state.onSubmitSuccess(() => {
5491
- if (disposed) return;
5492
- void (async () => {
5493
- await writer.flush();
5494
- if (disposed) return;
5495
- const adapter = await adapterPromise;
5496
- if (disposed) return;
5497
- await adapter.removeItem(key);
5498
- })();
5499
- }) : () => void 0;
5500
- void (async () => {
5501
- const adapter = await adapterPromise;
5502
- if (disposed) return;
5503
- void cleanupOrphanKeys(adapter, base, key);
5504
- try {
5505
- const raw = await adapter.getItem(key);
5506
- const payload = readPersistedPayload(raw);
5507
- if (payload === null) {
5508
- if (raw !== null && raw !== void 0) {
5509
- await adapter.removeItem(key);
5510
- }
5511
- return;
5512
- }
5513
- if (disposed) return;
5514
- const merged = mergeSparseHydration(
5515
- vue.toRaw(state.form.value),
5516
- payload.data.form,
5517
- state.schema
5518
- );
5519
- state.applyFormReplacement(merged, { hydration: true });
5520
- const persistedLeafPaths = collectPersistedLeafPaths(payload.data.form);
5521
- for (const k of persistedLeafPaths) {
5522
- state.blankPaths.delete(k);
5523
- state.originalBlankPaths.delete(k);
5524
- }
5525
- for (const k of payload.data.blankPaths ?? []) {
5526
- const key2 = paths.coerceToPathKey(k);
5527
- state.blankPaths.add(key2);
5528
- state.originalBlankPaths.add(key2);
5529
- }
5530
- if (include === "form+errors") {
5531
- if (payload.data.schemaErrors !== void 0) {
5532
- const flat = payload.data.schemaErrors.flatMap(([, errs]) => errs);
5533
- state.setAllSchemaErrors(flat);
5534
- }
5535
- if (payload.data.userErrors !== void 0) {
5536
- const flat = payload.data.userErrors.flatMap(([, errs]) => errs);
5537
- state.setAllUserErrors(flat);
5538
- }
5750
+ const resolvedSchema = getComputedSchema(key, configuration.schema, { maxRecursionDepth });
5751
+ if (configuration.persist !== void 0 && configuration.key === void 0) {
5752
+ throw new paths.AnonPersistError({
5753
+ cause: "no-key",
5754
+ schemaFields: extractSchemaFields(resolvedSchema),
5755
+ callSite: paths.captureUserCallSite()
5756
+ });
5757
+ }
5758
+ const existing = registry.forms.get(key);
5759
+ if (paths.__DEV__ && existing !== void 0) {
5760
+ warnOnSchemaFingerprintMismatch(key, existing.schema, resolvedSchema);
5761
+ warnOnPersistDivergence(key, existing, configuration.persist);
5762
+ }
5763
+ const hadPendingHydration = registry.pendingHydration.has(key);
5764
+ const state = existing ?? buildFreshState(key, resolvedSchema, merged, registry);
5765
+ if (existing !== void 0) ; else if (resolvedDefaults.kind === "sync") {
5766
+ state.defaultsResolved.value = true;
5767
+ }
5768
+ if (existing === void 0 && resolvedDefaults.kind === "async") {
5769
+ const factory = resolvedDefaults.factory;
5770
+ state.defaultValuesFactory.value = factory;
5771
+ if (hadPendingHydration) {
5772
+ state.hydrating.value = false;
5773
+ state.defaultsResolved.value = true;
5774
+ } else if (registry.ssr) {
5775
+ if (configuration.__ssrAccessed === true) {
5776
+ registry.enqueuePrefetch(key);
5539
5777
  }
5540
- state.scheduleFieldValidation(
5541
- [],
5542
- true
5543
- /* immediate */
5544
- );
5545
- } catch {
5778
+ vue.onServerPrefetch(() => {
5779
+ if (!registry.shouldPrefetch(key)) return;
5780
+ return state.activate();
5781
+ });
5546
5782
  }
5547
- })();
5548
- async function writePathImmediately(path) {
5549
- if (disposed) return;
5550
- await writer.flush();
5551
- if (disposed) return;
5552
- const adapter = await adapterPromise;
5553
- if (disposed) return;
5554
- const raw = await adapter.getItem(key);
5555
- const existing = readPersistedPayload(raw);
5556
- const baseForm = existing?.data.form ?? {};
5557
- const value = getAtPath(vue.toRaw(state.form.value), path);
5558
- const nextForm = setAtPath(baseForm, path, value);
5559
- const { key: pathKey } = paths.canonicalizePath(path);
5560
- const transientSet = new Set(
5561
- (existing?.data.blankPaths ?? []).filter(
5562
- (k) => k !== pathKey && !isDescendantPathKey(k, pathKey)
5563
- )
5564
- );
5565
- for (const liveKey of state.blankPaths) {
5566
- if (liveKey === pathKey || isDescendantPathKey(liveKey, pathKey)) {
5567
- transientSet.add(liveKey);
5783
+ }
5784
+ if (vue.getCurrentScope() !== void 0) {
5785
+ const releaseConsumer = registry.trackConsumer(key);
5786
+ vue.onScopeDispose(releaseConsumer);
5787
+ }
5788
+ const persistDisabledByAnonRule = merged.persist !== void 0 && enforceAnonPersistRule(state.formKey, registry.ssr);
5789
+ if (existing === void 0 && !registry.ssr) {
5790
+ if (merged.persist !== void 0 && !persistDisabledByAnonRule) {
5791
+ const resolvedPersist = normalizePersistConfig(merged.persist);
5792
+ const storageKind = resolvedPersist.storage;
5793
+ const isBuiltinStorage = typeof storageKind === "string";
5794
+ const secureContextOk = !isBuiltinStorage || isSecureContext();
5795
+ if (!secureContextOk) {
5796
+ const feature = storageKind === "session" ? "persist:session" : "persist:local";
5797
+ warnOnceInsecureContext(feature);
5798
+ void sweepAllOrphansAcrossStandardStores(`${PERSISTENCE_KEY_PREFIX}${state.formKey}`);
5799
+ } else {
5800
+ const persistenceBase = resolveStorageKeyBase(resolvedPersist, state.formKey);
5801
+ void sweepNonConfiguredStandardStoresForOrphans(resolvedPersist.storage, persistenceBase);
5802
+ const persistenceModule = wirePersistence(state, resolvedPersist);
5803
+ state.modules.set(PERSISTENCE_MODULE_KEY, persistenceModule);
5804
+ state.registerDrain(() => persistenceModule.awaitPendingWrites());
5805
+ state.registerCleanup(() => persistenceModule.dispose());
5568
5806
  }
5569
- }
5570
- if (include === "form") {
5571
- await adapter.setItem(
5572
- key,
5573
- buildPersistedPayload(nextForm, "form", /* @__PURE__ */ new Map(), /* @__PURE__ */ new Map(), transientSet)
5574
- );
5575
- return;
5576
- }
5577
- const schemaMap = new Map(existing?.data.schemaErrors ?? []);
5578
- const userMap = new Map(existing?.data.userErrors ?? []);
5579
- const currentSchema = state.schemaErrors.get(pathKey);
5580
- const currentUser = state.userErrors.get(pathKey);
5581
- if (currentSchema !== void 0 && currentSchema.length > 0) {
5582
- schemaMap.set(pathKey, [...currentSchema]);
5583
5807
  } else {
5584
- schemaMap.delete(pathKey);
5585
- }
5586
- if (currentUser !== void 0 && currentUser.length > 0) {
5587
- userMap.set(pathKey, [...currentUser]);
5588
- } else {
5589
- userMap.delete(pathKey);
5808
+ void sweepAllOrphansAcrossStandardStores(`${PERSISTENCE_KEY_PREFIX}${state.formKey}`);
5590
5809
  }
5591
- await adapter.setItem(
5592
- key,
5593
- buildPersistedPayload(nextForm, "form+errors", schemaMap, userMap, transientSet)
5594
- );
5595
5810
  }
5596
- async function clearPersistedDraft(path) {
5597
- if (disposed) return;
5598
- await writer.flush();
5599
- if (disposed) return;
5600
- const adapter = await adapterPromise;
5601
- if (disposed) return;
5602
- if (path === void 0) {
5603
- await adapter.removeItem(key);
5604
- return;
5605
- }
5606
- const raw = await adapter.getItem(key);
5607
- const existing = readPersistedPayload(raw);
5608
- if (existing === null) return;
5609
- const nextForm = deleteAtPath(existing.data.form, path);
5610
- if (isEmptyContainer(nextForm)) {
5611
- await adapter.removeItem(key);
5612
- return;
5811
+ if (existing === void 0 && merged.multiTab === true && configuration.key !== void 0 && !registry.ssr) {
5812
+ const hasBroadcastChannel = typeof BroadcastChannel !== "undefined";
5813
+ const secureContext = isSecureContext();
5814
+ if (hasBroadcastChannel && secureContext) {
5815
+ let channelName;
5816
+ try {
5817
+ channelName = `attaform:sync:${state.formKey}:${hashStableString(state.schema.fingerprint())}`;
5818
+ } catch {
5819
+ channelName = null;
5820
+ }
5821
+ if (channelName !== null) {
5822
+ const syncModule = createMultiTabSyncModule(state, channelName, {
5823
+ isSensitivePath: state.isSensitivePath,
5824
+ noSyncPaths: state.noSyncPaths,
5825
+ validateForm: (form) => {
5826
+ const result = state.schema.validateAtPath(form, void 0, { sync: true });
5827
+ if (result instanceof Promise) return;
5828
+ if (!result.success) {
5829
+ throw new Error("attaform multi-tab sync: post-apply schema validation failed");
5830
+ }
5831
+ }
5832
+ });
5833
+ state.modules.set(MULTI_TAB_SYNC_MODULE_KEY, syncModule);
5834
+ state.registerCleanup(() => syncModule.dispose());
5835
+ }
5836
+ } else if (hasBroadcastChannel && !secureContext) {
5837
+ warnOnceInsecureContext("multiTab");
5613
5838
  }
5614
- const { key: pathKey } = paths.canonicalizePath(path);
5615
- const transientSet = new Set(
5616
- (existing.data.blankPaths ?? []).filter(
5617
- (k) => k !== pathKey && !isDescendantPathKey(k, pathKey)
5618
- )
5619
- );
5620
- if (include === "form") {
5621
- await adapter.setItem(
5622
- key,
5623
- buildPersistedPayload(nextForm, "form", /* @__PURE__ */ new Map(), /* @__PURE__ */ new Map(), transientSet)
5624
- );
5625
- return;
5839
+ }
5840
+ if (existing === void 0 && merged.history !== void 0) {
5841
+ const historyModule = createHistoryModule(state, merged.history);
5842
+ state.modules.set(HISTORY_MODULE_KEY, historyModule);
5843
+ state.registerCleanup(() => historyModule.dispose());
5844
+ }
5845
+ if (configuration.key === void 0) {
5846
+ recordAmbientProvide(registry.ssr);
5847
+ vue.provide(paths.kFormContext, state);
5848
+ }
5849
+ const formInstanceId = vue.getCurrentInstance() !== null ? vue.useId() : `atta:form-instance:${formInstanceCounter++}`;
5850
+ if (vue.getCurrentInstance() !== null) {
5851
+ vue.provide(paths.kFormInstanceId, formInstanceId);
5852
+ }
5853
+ const apiOptions = {};
5854
+ if (merged.onInvalidSubmit !== void 0) {
5855
+ apiOptions.onInvalidSubmit = merged.onInvalidSubmit;
5856
+ }
5857
+ const history = state.modules.get(HISTORY_MODULE_KEY);
5858
+ if (history !== void 0) {
5859
+ apiOptions.history = history;
5860
+ }
5861
+ if (merged.validateOn !== void 0) {
5862
+ apiOptions.validateOn = merged.validateOn;
5863
+ }
5864
+ const mergedDebounceMs = merged.debounceMs;
5865
+ if (mergedDebounceMs !== void 0) {
5866
+ apiOptions.debounceMs = mergedDebounceMs;
5867
+ }
5868
+ if (merged.getDisplayState !== void 0) {
5869
+ apiOptions.getDisplayState = merged.getDisplayState;
5870
+ }
5871
+ if (merged.coerce !== void 0) {
5872
+ apiOptions.coerce = merged.coerce;
5873
+ }
5874
+ if (merged.rememberVariants !== void 0) {
5875
+ apiOptions.rememberVariants = merged.rememberVariants;
5876
+ }
5877
+ if (merged.autoAria !== void 0) {
5878
+ apiOptions.autoAria = merged.autoAria;
5879
+ }
5880
+ return buildFormApi(
5881
+ state,
5882
+ formInstanceId,
5883
+ apiOptions
5884
+ );
5885
+ }
5886
+ function mergeWithDefaults(defaults, configuration) {
5887
+ const strict = configuration.strict ?? defaults.strict;
5888
+ const onInvalidSubmit = configuration.onInvalidSubmit ?? defaults.onInvalidSubmit;
5889
+ const history = configuration.history ?? defaults.history;
5890
+ const rememberVariants = configuration.rememberVariants ?? defaults.rememberVariants;
5891
+ const coerce = configuration.coerce ?? defaults.coerce;
5892
+ const validateOn = configuration.validateOn ?? defaults.validateOn;
5893
+ const debounceMs = configuration.debounceMs ?? defaults.debounceMs;
5894
+ const getDisplayState = configuration.getDisplayState ?? defaults.getDisplayState;
5895
+ const maxRecursionDepth = configuration.maxRecursionDepth ?? defaults.maxRecursionDepth;
5896
+ const sensitiveNames = configuration.sensitiveNames ?? defaults.sensitiveNames;
5897
+ const multiTab = configuration.multiTab ?? defaults.multiTab;
5898
+ const autoAria = configuration.autoAria ?? defaults.autoAria;
5899
+ return {
5900
+ ...configuration,
5901
+ ...strict === void 0 ? {} : { strict },
5902
+ ...onInvalidSubmit === void 0 ? {} : { onInvalidSubmit },
5903
+ ...history === void 0 ? {} : { history },
5904
+ ...rememberVariants === void 0 ? {} : { rememberVariants },
5905
+ ...coerce === void 0 ? {} : { coerce },
5906
+ ...validateOn === void 0 ? {} : { validateOn },
5907
+ ...debounceMs === void 0 ? {} : { debounceMs },
5908
+ ...getDisplayState === void 0 ? {} : { getDisplayState },
5909
+ ...maxRecursionDepth === void 0 ? {} : { maxRecursionDepth },
5910
+ ...sensitiveNames === void 0 ? {} : { sensitiveNames },
5911
+ ...multiTab === void 0 ? {} : { multiTab },
5912
+ ...autoAria === void 0 ? {} : { autoAria }
5913
+ };
5914
+ }
5915
+ const HISTORY_MODULE_KEY = "history";
5916
+ function buildFreshState(key, schema, configuration, registry) {
5917
+ const pending = registry.pendingHydration.get(key);
5918
+ if (pending !== void 0) registry.pendingHydration.delete(key);
5919
+ const walked = walkUnsetSentinels(
5920
+ configuration.defaultValues,
5921
+ schema
5922
+ );
5923
+ let initialBlankPaths;
5924
+ if (pending === void 0) {
5925
+ initialBlankPaths = walked.paths;
5926
+ }
5927
+ const resolvedSensitiveNames = configuration.sensitiveNames;
5928
+ const resolvedIsSensitivePath = resolvedSensitiveNames === void 0 ? void 0 : paths.createIsSensitivePath(resolvedSensitiveNames);
5929
+ const createOptions = {
5930
+ formKey: key,
5931
+ schema,
5932
+ defaultValues: walked.cleanedValues,
5933
+ ...configuration.strict !== void 0 ? { strict: configuration.strict } : {},
5934
+ hydration: pending,
5935
+ ...configuration.validateOn !== void 0 ? { validateOn: configuration.validateOn } : {},
5936
+ ...configuration.debounceMs !== void 0 ? { debounceMs: configuration.debounceMs } : {},
5937
+ ssr: registry.ssr,
5938
+ // Server-only: bind the SSR prefetch coordination handles. `enqueue`
5939
+ // records intent on every `state.activate()` so a wizard skip-list
5940
+ // override or a future transform mark has a consistent set to diff
5941
+ // against; `shouldFire` lets the activate path bail when the
5942
+ // wizard explicitly skipped this key — even an explicit
5943
+ // `form.activate()` defers to the wizard's render-efficiency
5944
+ // skip-list on the server.
5945
+ ...registry.ssr ? {
5946
+ ssrPrefetch: {
5947
+ enqueue: () => {
5948
+ registry.enqueuePrefetch(key);
5949
+ },
5950
+ shouldFire: () => registry.shouldPrefetch(key)
5951
+ }
5952
+ } : {},
5953
+ ...configuration.rememberVariants !== void 0 ? { rememberVariants: configuration.rememberVariants } : {},
5954
+ ...configuration.coerce !== void 0 ? { coerce: configuration.coerce } : {},
5955
+ ...configuration.getDisplayState !== void 0 ? { getDisplayState: configuration.getDisplayState } : {},
5956
+ ...initialBlankPaths !== void 0 ? { initialBlankPaths } : {},
5957
+ ...resolvedIsSensitivePath !== void 0 ? { isSensitivePath: resolvedIsSensitivePath } : {}
5958
+ };
5959
+ const state = createFormStore(createOptions);
5960
+ registry.forms.set(
5961
+ key,
5962
+ state
5963
+ );
5964
+ return state;
5965
+ }
5966
+ let anonCounter = 0;
5967
+ let formInstanceCounter = 0;
5968
+ const ambientProvideHistory = paths.__DEV__ ? /* @__PURE__ */ new WeakMap() : null;
5969
+ function recordAmbientProvide(ssr) {
5970
+ if (!paths.__DEV__ || ssr || ambientProvideHistory === null) return;
5971
+ const instance = vue.getCurrentInstance();
5972
+ if (instance === null) return;
5973
+ const instanceKey = instance;
5974
+ const entry = {
5975
+ source: paths.captureUserCallSite()
5976
+ };
5977
+ const existing = ambientProvideHistory.get(instanceKey);
5978
+ if (existing === void 0) {
5979
+ ambientProvideHistory.set(instanceKey, [entry]);
5980
+ return;
5981
+ }
5982
+ existing.push(entry);
5983
+ }
5984
+ function resolveFormKey(key) {
5985
+ if (key !== void 0 && key !== null && key !== "") {
5986
+ if (key.startsWith(RESERVED_KEY_PREFIX)) {
5987
+ throw new paths.ReservedFormKeyError(key);
5626
5988
  }
5627
- const schemaErrors = (existing.data.schemaErrors ?? []).filter(([k]) => k !== pathKey);
5628
- const userErrors = (existing.data.userErrors ?? []).filter(([k]) => k !== pathKey);
5629
- const schemaMap = new Map(schemaErrors.map(([k, v]) => [k, [...v]]));
5630
- const userMap = new Map(userErrors.map(([k, v]) => [k, [...v]]));
5631
- await adapter.setItem(
5632
- key,
5633
- buildPersistedPayload(nextForm, "form+errors", schemaMap, userMap, transientSet)
5634
- );
5989
+ return key;
5635
5990
  }
5636
- function awaitPendingWrites() {
5637
- if (inFlightFinalFlush !== null) return inFlightFinalFlush;
5638
- if (disposed) return Promise.resolve();
5639
- return writer.flush().catch(() => void 0);
5991
+ if (vue.getCurrentInstance() !== null) {
5992
+ return `${ANONYMOUS_FORM_KEY_PREFIX}${vue.useId()}`;
5640
5993
  }
5641
- function dispose() {
5642
- if (disposed || inFlightFinalFlush !== null) return;
5643
- unsubscribeChange();
5644
- unsubscribeSuccess();
5645
- inFlightFinalFlush = writer.flush().catch(() => void 0).finally(() => {
5646
- disposed = true;
5647
- inFlightFinalFlush = null;
5648
- });
5994
+ return `${ANONYMOUS_FORM_KEY_PREFIX}${anonCounter++}`;
5995
+ }
5996
+ function warnOnSchemaFingerprintMismatch(key, existing, incoming) {
5997
+ let existingFp;
5998
+ let incomingFp;
5999
+ try {
6000
+ existingFp = existing.fingerprint();
6001
+ incomingFp = incoming.fingerprint();
6002
+ } catch (error) {
6003
+ console.error(
6004
+ `[attaform] fingerprint() threw for key "${key}"; skipping mismatch check.`,
6005
+ error
6006
+ );
6007
+ return;
5649
6008
  }
5650
- return {
5651
- wiredConfig: config,
5652
- writePathImmediately,
5653
- clearPersistedDraft,
5654
- awaitPendingWrites,
5655
- dispose
5656
- };
6009
+ if (existingFp === incomingFp) return;
6010
+ console.warn(
6011
+ `[attaform] useForm() calls with key "${key}" use different schemas; first wins, second is ignored. Use identical schemas or unique keys.
6012
+ existing: ${existingFp}
6013
+ incoming: ${incomingFp}`
6014
+ );
5657
6015
  }
5658
- function isEmptyContainer(value) {
5659
- if (value === void 0 || value === null) return true;
5660
- if (Array.isArray(value)) return value.length === 0;
5661
- if (isPlainRecord(value)) return Object.keys(value).length === 0;
5662
- return false;
6016
+ function warnOnPersistDivergence(key, existing, incomingPersist) {
6017
+ if (incomingPersist === void 0) return;
6018
+ const wired = existing.modules.get(PERSISTENCE_MODULE_KEY);
6019
+ const incomingNormalized = normalizePersistConfig(incomingPersist);
6020
+ if (wired === void 0) {
6021
+ console.warn(
6022
+ `[attaform] useForm({ key: "${key}" }) passed a persist config but the first useForm({ key }) call didn't wire persistence; the new config is silently dropped. Pass persist on the first call, or remove persist here to make the inheritance explicit.`
6023
+ );
6024
+ return;
6025
+ }
6026
+ if (persistConfigsEquivalent(wired.wiredConfig, incomingNormalized)) return;
6027
+ console.warn(
6028
+ `[attaform] useForm({ key: "${key}" }) passed a persist config that differs from the first useForm({ key }) call's; first wins, this one is ignored.
6029
+ wired: ${describePersist(wired.wiredConfig)}
6030
+ incoming: ${describePersist(incomingNormalized)}`
6031
+ );
6032
+ }
6033
+ function persistConfigsEquivalent(a, b) {
6034
+ if (a.storage !== b.storage) return false;
6035
+ if ((a.key ?? void 0) !== (b.key ?? void 0)) return false;
6036
+ if ((a.debounceMs ?? void 0) !== (b.debounceMs ?? void 0)) return false;
6037
+ return true;
6038
+ }
6039
+ function describePersist(config) {
6040
+ const storage = typeof config.storage === "string" ? config.storage : "custom-adapter";
6041
+ const parts = [`storage=${storage}`];
6042
+ if (config.key !== void 0) parts.push(`key=${config.key}`);
6043
+ if (config.debounceMs !== void 0) parts.push(`debounceMs=${config.debounceMs}`);
6044
+ return `{ ${parts.join(", ")} }`;
5663
6045
  }
5664
6046
  const warnedAnonPersistKeys = /* @__PURE__ */ new Set();
5665
6047
  function enforceAnonPersistRule(formKey, ssr) {
@@ -5677,33 +6059,6 @@ function enforceAnonPersistRule(formKey, ssr) {
5677
6059
  }
5678
6060
  return true;
5679
6061
  }
5680
- function collectPersistedLeafPaths(form) {
5681
- const out = [];
5682
- walk(form, []);
5683
- return out;
5684
- function walk(node, prefix) {
5685
- if (Array.isArray(node)) {
5686
- for (let i = 0; i < node.length; i++) {
5687
- walk(node[i], [...prefix, i]);
5688
- }
5689
- return;
5690
- }
5691
- if (isPlainRecord(node)) {
5692
- for (const key of Object.keys(node)) {
5693
- walk(node[key], [...prefix, key]);
5694
- }
5695
- return;
5696
- }
5697
- if (prefix.length === 0) return;
5698
- out.push(paths.canonicalizePath(prefix).key);
5699
- }
5700
- }
5701
- function isDescendantPathKey(candidate, ancestor) {
5702
- if (candidate.length <= ancestor.length) return false;
5703
- if (!ancestor.endsWith("]")) return false;
5704
- const childPrefix = `${ancestor.slice(0, -1)},`;
5705
- return candidate.startsWith(childPrefix);
5706
- }
5707
6062
 
5708
6063
  let injectedInstanceCounter = 0;
5709
6064
  function injectForm(input) {
@@ -5783,8 +6138,6 @@ function isLazyMarker(value) {
5783
6138
  }
5784
6139
 
5785
6140
  const NOOP_WIZARD_HISTORY = {
5786
- push() {
5787
- },
5788
6141
  replace() {
5789
6142
  },
5790
6143
  read() {
@@ -5811,21 +6164,16 @@ function createWizardHistory(param) {
5811
6164
  for (const subscriber of subscribers) subscriber(value);
5812
6165
  }
5813
6166
  window.addEventListener("popstate", handlePopstate);
5814
- function safeWriteState(key, op) {
6167
+ function safeReplaceState(key) {
5815
6168
  try {
5816
- const fn = op === "push" ? window.history.pushState : window.history.replaceState;
5817
- fn.call(window.history, {}, "", buildUrl(key));
6169
+ window.history.replaceState({}, "", buildUrl(key));
5818
6170
  } catch {
5819
6171
  }
5820
6172
  }
5821
6173
  return {
5822
- push(key) {
5823
- if (disposed) return;
5824
- safeWriteState(key, "push");
5825
- },
5826
6174
  replace(key) {
5827
6175
  if (disposed) return;
5828
- safeWriteState(key, "replace");
6176
+ safeReplaceState(key);
5829
6177
  },
5830
6178
  read() {
5831
6179
  const url = new URL(window.location.href);
@@ -5884,68 +6232,16 @@ function buildWizardStatusesProxy(statuses) {
5884
6232
  }
5885
6233
  return result;
5886
6234
  });
5887
- const target = (() => {
5888
- });
5889
- const proxyToString = () => JSON.stringify(snapshot.value);
5890
- const proxyToPrimitive = (hint) => hint === "number" ? NaN : proxyToString();
5891
- return new Proxy(target, {
5892
- apply(_, __, args) {
5893
- const key = args[0];
5894
- if (key === void 0) return snapshot.value;
5895
- const computedEntry = statuses[key];
5896
- if (computedEntry === void 0) return void 0;
5897
- return computedEntry.value;
5898
- },
5899
- get(_, key) {
5900
- if (typeof key === "symbol") {
5901
- if (key === Symbol.toPrimitive) return proxyToPrimitive;
5902
- return Reflect.get(target, key);
5903
- }
5904
- if (key === "toJSON") return () => snapshot.value;
5905
- if (key === "toString") return proxyToString;
5906
- if (key === "valueOf")
5907
- return function() {
5908
- return this;
5909
- };
5910
- const computedEntry = statuses[key];
5911
- if (computedEntry === void 0) return void 0;
5912
- return computedEntry.value;
5913
- },
5914
- has(_, key) {
5915
- if (typeof key === "symbol") return Reflect.has(target, key);
5916
- return Object.hasOwn(statuses, key);
5917
- },
5918
- ownKeys() {
5919
- return Object.keys(statuses);
5920
- },
5921
- getOwnPropertyDescriptor(_, key) {
5922
- if (typeof key === "symbol") return void 0;
5923
- const computedEntry = statuses[key];
5924
- if (computedEntry === void 0) return void 0;
5925
- return {
5926
- configurable: true,
5927
- enumerable: true,
5928
- writable: false,
5929
- value: computedEntry.value
5930
- };
5931
- },
5932
- set(_, key) {
5933
- if (paths.__DEV__) {
5934
- console.warn(
5935
- `[attaform] wizard.statuses is read-only \u2014 write to "${String(key)}" was ignored. Statuses derive from each form's meta; mutate the underlying form instead.`
5936
- );
5937
- }
5938
- return true;
5939
- },
5940
- deleteProperty(_, key) {
5941
- if (paths.__DEV__) {
5942
- console.warn(
5943
- `[attaform] wizard.statuses is read-only \u2014 delete of "${String(key)}" was ignored.`
5944
- );
5945
- }
5946
- return true;
5947
- },
5948
- defineProperty: () => true
6235
+ return buildCallableReadonlySnapshotProxy({
6236
+ surface: "wizard.statuses",
6237
+ snapshot: () => snapshot.value,
6238
+ resolveKey: (key) => statuses[key]?.value,
6239
+ // Single-key callable form. Strings stringify naturally; non-
6240
+ // string args coerce via `String(arg)` and miss the lookup, which
6241
+ // resolves to `undefined` (consistent with property-access).
6242
+ resolveCall: (arg) => statuses[String(arg)]?.value,
6243
+ ownKeys: () => Object.keys(statuses),
6244
+ hasKey: (key) => Object.hasOwn(statuses, key)
5949
6245
  });
5950
6246
  }
5951
6247
 
@@ -5962,6 +6258,12 @@ const NOOP_VALID_STATUS = {
5962
6258
  submitted: false,
5963
6259
  errorCount: 0
5964
6260
  };
6261
+ function asStatusSource(form) {
6262
+ return form;
6263
+ }
6264
+ function asSubmissionSource(form) {
6265
+ return form;
6266
+ }
5965
6267
  function useWizard(options) {
5966
6268
  const rawSteps = Array.isArray(options.steps) ? options.steps : [];
5967
6269
  if (rawSteps.length === 0 && paths.__DEV__) {
@@ -6049,10 +6351,12 @@ function useWizard(options) {
6049
6351
  return { configurable: true, enumerable: true, writable: false, value: form };
6050
6352
  }
6051
6353
  });
6052
- const slotCtx = vue.computed(() => ({
6354
+ const slotCtx = {
6053
6355
  forms: slotForms,
6054
- currentKey: activeKey.value === "" ? void 0 : activeKey.value
6055
- }));
6356
+ get currentKey() {
6357
+ return activeKey.value === "" ? void 0 : activeKey.value;
6358
+ }
6359
+ };
6056
6360
  function resolveSlot(slot, index, ctx) {
6057
6361
  if (typeof slot === "string") {
6058
6362
  return getOrBuildNoop(slot);
@@ -6075,12 +6379,6 @@ function useWizard(options) {
6075
6379
  }
6076
6380
  return result;
6077
6381
  }
6078
- const lazyCtx = {
6079
- forms: slotForms,
6080
- get currentKey() {
6081
- return activeKey.value === "" ? void 0 : activeKey.value;
6082
- }
6083
- };
6084
6382
  for (let i = 0; i < rawSteps.length; i++) {
6085
6383
  const slot = rawSteps[i];
6086
6384
  if (isLazyMarker(slot)) {
@@ -6090,18 +6388,17 @@ function useWizard(options) {
6090
6388
  idx,
6091
6389
  vue.computed(() => {
6092
6390
  void lazyEpoch.value;
6093
- return marker.resolve(lazyCtx);
6391
+ return marker.resolve(slotCtx);
6094
6392
  })
6095
6393
  );
6096
6394
  }
6097
6395
  }
6098
6396
  const compiledSteps = vue.computed(() => {
6099
- const ctx = slotCtx.value;
6100
6397
  const out = [];
6101
6398
  const seen = /* @__PURE__ */ new Set();
6102
6399
  for (let i = 0; i < rawSteps.length; i++) {
6103
6400
  const slot = rawSteps[i];
6104
- const form = resolveSlot(slot, i, ctx);
6401
+ const form = resolveSlot(slot, i, slotCtx);
6105
6402
  if (form === void 0) continue;
6106
6403
  if (seen.has(form.key)) {
6107
6404
  if (paths.__DEV__) {
@@ -6127,9 +6424,12 @@ function useWizard(options) {
6127
6424
  return -1;
6128
6425
  });
6129
6426
  const currentStep = vue.computed(() => {
6130
- const key = activeKey.value;
6131
- if (key !== "") return key;
6132
- const first = compiledSteps.value[0];
6427
+ const list = compiledSteps.value;
6428
+ const idx = activeIndex.value;
6429
+ if (idx >= 0 && idx < list.length) {
6430
+ return list[idx].key;
6431
+ }
6432
+ const first = list[0];
6133
6433
  return first === void 0 ? void 0 : first.key;
6134
6434
  });
6135
6435
  const activeForm = vue.computed(() => {
@@ -6152,36 +6452,94 @@ function useWizard(options) {
6152
6452
  for (const step of compiledSteps.value) out[step.key] = step.form;
6153
6453
  return out;
6154
6454
  });
6155
- const allValues = vue.computed(() => {
6156
- const out = {};
6157
- for (const step of compiledSteps.value) {
6158
- const source = step.form;
6159
- out[step.key] = source.values;
6455
+ function isFormReady(key) {
6456
+ const store = registry.forms.get(key);
6457
+ return store?.defaultsResolved.value === true;
6458
+ }
6459
+ function toWizardAggregateError(err, fallbackKey) {
6460
+ const entry = {
6461
+ formKey: err.formKey ?? fallbackKey,
6462
+ path: err.path,
6463
+ message: err.message
6464
+ };
6465
+ if (err.code !== void 0) entry.code = err.code;
6466
+ return entry;
6467
+ }
6468
+ const valuesCache = /* @__PURE__ */ new Map();
6469
+ function valuesFor(form) {
6470
+ const cached = valuesCache.get(form.key);
6471
+ if (cached !== void 0) return cached;
6472
+ const source = asStatusSource(form);
6473
+ const computedValues = vue.computed(() => source.values);
6474
+ valuesCache.set(form.key, computedValues);
6475
+ return computedValues;
6476
+ }
6477
+ const errorsCache = /* @__PURE__ */ new Map();
6478
+ function errorsFor(form) {
6479
+ const cached = errorsCache.get(form.key);
6480
+ if (cached !== void 0) return cached;
6481
+ const source = asStatusSource(form);
6482
+ const computedErrors = vue.computed(() => {
6483
+ if (!isFormReady(form.key)) return [];
6484
+ const errors = source.meta?.errors ?? [];
6485
+ const list = [];
6486
+ for (const err of errors) list.push(toWizardAggregateError(err, form.key));
6487
+ return list;
6488
+ });
6489
+ errorsCache.set(form.key, computedErrors);
6490
+ return computedErrors;
6491
+ }
6492
+ const allValues = new Proxy({}, {
6493
+ get(_, key) {
6494
+ if (typeof key !== "string") return void 0;
6495
+ const form = formsRecord.value[key];
6496
+ if (form === void 0) return void 0;
6497
+ return valuesFor(form).value;
6498
+ },
6499
+ has(_, key) {
6500
+ if (typeof key !== "string") return false;
6501
+ return formsRecord.value[key] !== void 0;
6502
+ },
6503
+ ownKeys() {
6504
+ return Object.keys(formsRecord.value);
6505
+ },
6506
+ getOwnPropertyDescriptor(_, key) {
6507
+ if (typeof key !== "string") return void 0;
6508
+ const form = formsRecord.value[key];
6509
+ if (form === void 0) return void 0;
6510
+ return {
6511
+ configurable: true,
6512
+ enumerable: true,
6513
+ writable: false,
6514
+ value: valuesFor(form).value
6515
+ };
6160
6516
  }
6161
- return out;
6162
6517
  });
6163
- const allErrors = vue.computed(() => {
6164
- const out = {};
6165
- for (const step of compiledSteps.value) {
6166
- const source = step.form;
6167
- const list = [];
6168
- const store = registry.forms.get(step.key);
6169
- const resolved = store?.defaultsResolved.value === true;
6170
- if (resolved) {
6171
- const errors = source.meta?.errors ?? [];
6172
- for (const err of errors) {
6173
- const entry = {
6174
- formKey: step.key,
6175
- path: err.path,
6176
- message: err.message
6177
- };
6178
- if (err.code !== void 0) entry.code = err.code;
6179
- list.push(entry);
6180
- }
6181
- }
6182
- out[step.key] = list;
6518
+ const allErrors = new Proxy({}, {
6519
+ get(_, key) {
6520
+ if (typeof key !== "string") return void 0;
6521
+ const form = formsRecord.value[key];
6522
+ if (form === void 0) return void 0;
6523
+ return errorsFor(form).value;
6524
+ },
6525
+ has(_, key) {
6526
+ if (typeof key !== "string") return false;
6527
+ return formsRecord.value[key] !== void 0;
6528
+ },
6529
+ ownKeys() {
6530
+ return Object.keys(formsRecord.value);
6531
+ },
6532
+ getOwnPropertyDescriptor(_, key) {
6533
+ if (typeof key !== "string") return void 0;
6534
+ const form = formsRecord.value[key];
6535
+ if (form === void 0) return void 0;
6536
+ return {
6537
+ configurable: true,
6538
+ enumerable: true,
6539
+ writable: false,
6540
+ value: errorsFor(form).value
6541
+ };
6183
6542
  }
6184
- return out;
6185
6543
  });
6186
6544
  const seedRef = vue.ref(void 0);
6187
6545
  const seedInput = options.defaultStatuses;
@@ -6204,11 +6562,9 @@ function useWizard(options) {
6204
6562
  function statusFor(form) {
6205
6563
  const cached = statusCache.get(form.key);
6206
6564
  if (cached !== void 0) return cached;
6207
- const source = form;
6565
+ const source = asStatusSource(form);
6208
6566
  const computedStatus = vue.computed(() => {
6209
- const store = registry.forms.get(form.key);
6210
- const resolved = store?.defaultsResolved.value === true;
6211
- if (resolved) {
6567
+ if (isFormReady(form.key)) {
6212
6568
  const meta = source.meta;
6213
6569
  if (meta !== void 0 && meta !== null) {
6214
6570
  return {
@@ -6351,7 +6707,7 @@ function useWizard(options) {
6351
6707
  }
6352
6708
  if (!registry.ssr) {
6353
6709
  for (const step of compiledSteps.value) {
6354
- const source = step.form;
6710
+ const source = asSubmissionSource(step.form);
6355
6711
  if (typeof source.activate === "function") void source.activate();
6356
6712
  }
6357
6713
  }
@@ -6395,7 +6751,7 @@ function useWizard(options) {
6395
6751
  const submissionAttempts = vue.ref(0);
6396
6752
  const done = vue.ref(false);
6397
6753
  function activateForm(form) {
6398
- const source = form;
6754
+ const source = asSubmissionSource(form);
6399
6755
  if (typeof source.activate === "function") {
6400
6756
  void source.activate();
6401
6757
  }
@@ -6495,7 +6851,7 @@ function useWizard(options) {
6495
6851
  };
6496
6852
  }
6497
6853
  async function processOne(form) {
6498
- const full = form;
6854
+ const full = asSubmissionSource(form);
6499
6855
  let activationFailure;
6500
6856
  try {
6501
6857
  if (typeof full.activate === "function") await full.activate();
@@ -6527,15 +6883,7 @@ function useWizard(options) {
6527
6883
  for (const step of compiledSteps.value) {
6528
6884
  const processed = results.get(step.key);
6529
6885
  if (processed === void 0 || processed.success === true) continue;
6530
- for (const err of processed.errors) {
6531
- const entry = {
6532
- formKey: err.formKey,
6533
- path: err.path,
6534
- message: err.message
6535
- };
6536
- if (err.code !== void 0) entry.code = err.code;
6537
- out.push(entry);
6538
- }
6886
+ for (const err of processed.errors) out.push(toWizardAggregateError(err, step.key));
6539
6887
  }
6540
6888
  return out;
6541
6889
  }
@@ -6589,8 +6937,7 @@ function useWizard(options) {
6589
6937
  if (processed !== void 0 && processed.success === true) {
6590
6938
  valuesMap[step.key] = processed.data;
6591
6939
  } else {
6592
- const source = step.form;
6593
- valuesMap[step.key] = source.values;
6940
+ valuesMap[step.key] = asStatusSource(step.form).values;
6594
6941
  }
6595
6942
  }
6596
6943
  const ctx = buildSubmitContext(valuesMap, currentKey, final);
@@ -6611,8 +6958,11 @@ function useWizard(options) {
6611
6958
  moveTo(firstFailedKey);
6612
6959
  await vue.nextTick();
6613
6960
  const failedForm = formsRecord.value[firstFailedKey];
6614
- if (failedForm !== void 0 && typeof failedForm.applyInvalidSubmitPolicy === "function") {
6615
- failedForm.applyInvalidSubmitPolicy();
6961
+ if (failedForm !== void 0) {
6962
+ const failedSource = asSubmissionSource(failedForm);
6963
+ if (typeof failedSource.applyInvalidSubmitPolicy === "function") {
6964
+ failedSource.applyInvalidSubmitPolicy();
6965
+ }
6616
6966
  }
6617
6967
  }
6618
6968
  }
@@ -6627,7 +6977,7 @@ function useWizard(options) {
6627
6977
  done.value = false;
6628
6978
  lazyEpoch.value += 1;
6629
6979
  for (const step of compiledSteps.value) {
6630
- const full = step.form;
6980
+ const full = asSubmissionSource(step.form);
6631
6981
  if (typeof full.reset === "function") full.reset();
6632
6982
  }
6633
6983
  const firstStep = compiledSteps.value[0];
@@ -6677,12 +7027,8 @@ function useWizard(options) {
6677
7027
  return count.value;
6678
7028
  },
6679
7029
  statuses,
6680
- get allValues() {
6681
- return allValues.value;
6682
- },
6683
- get allErrors() {
6684
- return allErrors.value;
6685
- },
7030
+ allValues,
7031
+ allErrors,
6686
7032
  get progress() {
6687
7033
  return progress.value;
6688
7034
  },
@@ -6830,4 +7176,4 @@ exports.slimKindOf = slimKindOf;
6830
7176
  exports.unset = unset;
6831
7177
  exports.useAbstractForm = useAbstractForm;
6832
7178
  exports.useWizard = useWizard;
6833
- //# sourceMappingURL=attaform.C1msmO2v.cjs.map
7179
+ //# sourceMappingURL=attaform.DL4CQ-oW.cjs.map