attaform 0.19.0 → 0.20.1

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 (111) hide show
  1. package/README.md +3 -0
  2. package/dist/chunks/devtools.cjs +1 -1
  3. package/dist/chunks/devtools.mjs +1 -1
  4. package/dist/chunks/indexeddb.cjs +1 -1
  5. package/dist/chunks/indexeddb.mjs +1 -1
  6. package/dist/chunks/local-storage.cjs +1 -1
  7. package/dist/chunks/local-storage.mjs +1 -1
  8. package/dist/chunks/session-storage.cjs +1 -1
  9. package/dist/chunks/session-storage.mjs +1 -1
  10. package/dist/index.cjs +3 -6
  11. package/dist/index.cjs.map +1 -1
  12. package/dist/index.d.cts +14 -40
  13. package/dist/index.d.mts +14 -40
  14. package/dist/index.d.ts +14 -40
  15. package/dist/index.mjs +5 -5
  16. package/dist/nuxt.d.cts +1 -1
  17. package/dist/nuxt.d.mts +1 -1
  18. package/dist/nuxt.d.ts +1 -1
  19. package/dist/runtime/components/AttaformDevtoolsPanel.vue +2 -2
  20. package/dist/runtime/components/DevtoolsValueTree.d.vue.ts +1 -3
  21. package/dist/runtime/components/DevtoolsValueTree.vue.d.ts +1 -3
  22. package/dist/runtime/plugins/attaform.cjs +2 -2
  23. package/dist/runtime/plugins/attaform.mjs +2 -2
  24. package/dist/shared/attaform.BCBxTyMC.cjs +1882 -0
  25. package/dist/shared/attaform.BCBxTyMC.cjs.map +1 -0
  26. package/dist/shared/{attaform.CrpjyXdO.mjs → attaform.BKozEdTr.mjs} +275 -266
  27. package/dist/shared/attaform.BKozEdTr.mjs.map +1 -0
  28. package/dist/shared/{attaform.Bubm_slq.cjs → attaform.BM6YD9kZ.cjs} +212 -269
  29. package/dist/shared/attaform.BM6YD9kZ.cjs.map +1 -0
  30. package/dist/shared/{attaform.CoxJ8Qm8.cjs → attaform.BPxsYtTe.cjs} +2 -26
  31. package/dist/shared/attaform.BPxsYtTe.cjs.map +1 -0
  32. package/dist/shared/{attaform.BqEfHpVB.cjs → attaform.BPy-4qRx.cjs} +275 -268
  33. package/dist/shared/attaform.BPy-4qRx.cjs.map +1 -0
  34. package/dist/shared/{attaform.BTpuvGec.d.ts → attaform.Bh3ACtts.d.ts} +109 -101
  35. package/dist/shared/{attaform.BTi-PsHr.mjs → attaform.BqZuwLTK.mjs} +1868 -1477
  36. package/dist/shared/attaform.BqZuwLTK.mjs.map +1 -0
  37. package/dist/shared/{attaform.JBx8cfMA.cjs → attaform.BrrXNmfK.cjs} +263 -799
  38. package/dist/shared/attaform.BrrXNmfK.cjs.map +1 -0
  39. package/dist/shared/{attaform.CXpzmj38.mjs → attaform.BupwXkj_.mjs} +213 -270
  40. package/dist/shared/attaform.BupwXkj_.mjs.map +1 -0
  41. package/dist/shared/{attaform.ePUcKxId.d.cts → attaform.D5-1XGQU.d.cts} +109 -101
  42. package/dist/shared/attaform.D6CwqkPx.mjs +1876 -0
  43. package/dist/shared/attaform.D6CwqkPx.mjs.map +1 -0
  44. package/dist/shared/attaform.DHRWn-cu.cjs +785 -0
  45. package/dist/shared/attaform.DHRWn-cu.cjs.map +1 -0
  46. package/dist/shared/{attaform.C1msmO2v.cjs → attaform.DLnE5bZa.cjs} +1798 -1405
  47. package/dist/shared/attaform.DLnE5bZa.cjs.map +1 -0
  48. package/dist/shared/{attaform.D4I63aBV.d.ts → attaform.DSD85fHb.d.cts} +1 -19
  49. package/dist/shared/{attaform.CBjmobqk.d.cts → attaform.DSD85fHb.d.mts} +1 -19
  50. package/dist/shared/{attaform.DXYHL99q.d.mts → attaform.DSD85fHb.d.ts} +1 -19
  51. package/dist/shared/{attaform.B7rzpK1U.d.cts → attaform.DkA5J8NW.d.cts} +1 -17
  52. package/dist/shared/{attaform.B7rzpK1U.d.mts → attaform.DkA5J8NW.d.mts} +1 -17
  53. package/dist/shared/{attaform.B7rzpK1U.d.ts → attaform.DkA5J8NW.d.ts} +1 -17
  54. package/dist/shared/{attaform.CJ-e9gYI.d.ts → attaform.Dl5kDY-A.d.ts} +1 -1
  55. package/dist/shared/{attaform.CRNA0vrd.d.mts → attaform.DoKXru-a.d.mts} +1 -1
  56. package/dist/shared/{attaform.BtBmfLQN.d.mts → attaform.EMzJcQci.d.mts} +109 -101
  57. package/dist/shared/attaform.EZG6fOFb.mjs +35 -0
  58. package/dist/shared/attaform.EZG6fOFb.mjs.map +1 -0
  59. package/dist/shared/{attaform.QvygsFGh.d.cts → attaform.GbDo_lJi.d.cts} +1 -1
  60. package/dist/shared/{attaform.C0uGZQ4M.d.ts → attaform.SfhU0OEY.d.cts} +134 -30
  61. package/dist/shared/{attaform.C0uGZQ4M.d.cts → attaform.SfhU0OEY.d.mts} +134 -30
  62. package/dist/shared/{attaform.C0uGZQ4M.d.mts → attaform.SfhU0OEY.d.ts} +134 -30
  63. package/dist/shared/{attaform.a3uBo-gw.mjs → attaform.iWo9soNX.mjs} +257 -793
  64. package/dist/shared/attaform.iWo9soNX.mjs.map +1 -0
  65. package/dist/shared/attaform.tVkmQh5w.mjs +774 -0
  66. package/dist/shared/attaform.tVkmQh5w.mjs.map +1 -0
  67. package/dist/transforms.cjs +2 -2
  68. package/dist/transforms.d.cts +22 -13
  69. package/dist/transforms.d.mts +22 -13
  70. package/dist/transforms.d.ts +22 -13
  71. package/dist/transforms.mjs +1 -1
  72. package/dist/vite.cjs +8 -7
  73. package/dist/vite.cjs.map +1 -1
  74. package/dist/vite.mjs +8 -7
  75. package/dist/vite.mjs.map +1 -1
  76. package/dist/zod-v3.cjs +3 -3
  77. package/dist/zod-v3.d.cts +32 -6
  78. package/dist/zod-v3.d.mts +32 -6
  79. package/dist/zod-v3.d.ts +32 -6
  80. package/dist/zod-v3.mjs +3 -3
  81. package/dist/zod-v4.cjs +3 -3
  82. package/dist/zod-v4.d.cts +12 -8
  83. package/dist/zod-v4.d.mts +12 -8
  84. package/dist/zod-v4.d.ts +12 -8
  85. package/dist/zod-v4.mjs +3 -3
  86. package/dist/zod.cjs +8 -8
  87. package/dist/zod.cjs.map +1 -1
  88. package/dist/zod.d.cts +6 -6
  89. package/dist/zod.d.mts +6 -6
  90. package/dist/zod.d.ts +6 -6
  91. package/dist/zod.mjs +6 -6
  92. package/package.json +2 -1
  93. package/dist/shared/attaform.BTi-PsHr.mjs.map +0 -1
  94. package/dist/shared/attaform.BqEfHpVB.cjs.map +0 -1
  95. package/dist/shared/attaform.Bubm_slq.cjs.map +0 -1
  96. package/dist/shared/attaform.C1msmO2v.cjs.map +0 -1
  97. package/dist/shared/attaform.C8CyvYa_.cjs +0 -36
  98. package/dist/shared/attaform.C8CyvYa_.cjs.map +0 -1
  99. package/dist/shared/attaform.CXpzmj38.mjs.map +0 -1
  100. package/dist/shared/attaform.Cghpuav8.mjs +0 -57
  101. package/dist/shared/attaform.Cghpuav8.mjs.map +0 -1
  102. package/dist/shared/attaform.CiMqJHDm.mjs +0 -1594
  103. package/dist/shared/attaform.CiMqJHDm.mjs.map +0 -1
  104. package/dist/shared/attaform.CoxJ8Qm8.cjs.map +0 -1
  105. package/dist/shared/attaform.CrpjyXdO.mjs.map +0 -1
  106. package/dist/shared/attaform.D13GMFgK.mjs +0 -32
  107. package/dist/shared/attaform.D13GMFgK.mjs.map +0 -1
  108. package/dist/shared/attaform.JBx8cfMA.cjs.map +0 -1
  109. package/dist/shared/attaform.OznWyOPy.cjs +0 -1600
  110. package/dist/shared/attaform.OznWyOPy.cjs.map +0 -1
  111. package/dist/shared/attaform.a3uBo-gw.mjs.map +0 -1
@@ -1,7 +1,30 @@
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
+
6
+ function safeAssign(target, key, value) {
7
+ if (key === "__proto__") {
8
+ Object.defineProperty(target, key, {
9
+ value,
10
+ writable: true,
11
+ enumerable: true,
12
+ configurable: true
13
+ });
14
+ return;
15
+ }
16
+ target[key] = value;
17
+ }
18
+ function safeOwnRead(target, key) {
19
+ if (key === "__proto__") {
20
+ const desc = Object.getOwnPropertyDescriptor(target, "__proto__");
21
+ return desc?.value;
22
+ }
23
+ return target[key];
24
+ }
25
+ function safeOwnHas(target, key) {
26
+ return Object.prototype.hasOwnProperty.call(target, key);
27
+ }
5
28
 
6
29
  const NOT_FOUND = Symbol("NOT_FOUND");
7
30
  function descendStep(value, segment) {
@@ -65,7 +88,7 @@ function setAtPathOffset(root, path, value, offset) {
65
88
  return arr;
66
89
  }
67
90
  const rec = isPlainRecord(root) ? { ...root } : {};
68
- rec[head] = setAtPathOffset(rec[head], path, value, nextOffset);
91
+ safeAssign(rec, head, setAtPathOffset(safeOwnRead(rec, head), path, value, nextOffset));
69
92
  return rec;
70
93
  }
71
94
  function deleteAtPath(root, path) {
@@ -94,9 +117,9 @@ function deleteAtPathOffset(root, path, offset) {
94
117
  delete rec2[head];
95
118
  return rec2;
96
119
  }
97
- if (!(head in root)) return root;
120
+ if (!safeOwnHas(root, head)) return root;
98
121
  const rec = { ...root };
99
- rec[head] = deleteAtPathOffset(rec[head], path, nextOffset);
122
+ safeAssign(rec, head, deleteAtPathOffset(safeOwnRead(rec, head), path, nextOffset));
100
123
  return rec;
101
124
  }
102
125
  function resolveArrayShape(schema, scratch) {
@@ -167,25 +190,25 @@ function mergeStructuralImpl(schema, scratch, consumer, defaultValue) {
167
190
  let mutated = false;
168
191
  const out = { ...consumer };
169
192
  for (const key of Object.keys(defaultValue)) {
170
- if (!(key in consumer)) {
171
- const defAtKey = defaultValue[key];
193
+ if (!safeOwnHas(consumer, key)) {
194
+ const defAtKey = safeOwnRead(defaultValue, key);
172
195
  scratch.push(key);
173
196
  const filled = mergeStructuralImpl(schema, scratch, void 0, defAtKey);
174
197
  scratch.pop();
175
198
  if (filled !== void 0) {
176
- out[key] = filled;
199
+ safeAssign(out, key, filled);
177
200
  mutated = true;
178
201
  }
179
202
  }
180
203
  }
181
204
  for (const key of Object.keys(consumer)) {
182
- const cVal = consumer[key];
205
+ const cVal = safeOwnRead(consumer, key);
183
206
  if (cVal === void 0) continue;
184
207
  scratch.push(key);
185
- const merged = mergeStructuralImpl(schema, scratch, cVal, defaultValue[key]);
208
+ const merged = mergeStructuralImpl(schema, scratch, cVal, safeOwnRead(defaultValue, key));
186
209
  scratch.pop();
187
210
  if (merged !== cVal) {
188
- out[key] = merged;
211
+ safeAssign(out, key, merged);
189
212
  mutated = true;
190
213
  }
191
214
  }
@@ -379,7 +402,8 @@ function applyChangedKeys(target, source) {
379
402
  }
380
403
  for (const k of changedFirstSegments) {
381
404
  if (typeof k === "symbol") continue;
382
- t[String(k)] = s[String(k)];
405
+ const key = String(k);
406
+ safeAssign(t, key, safeOwnRead(s, key));
383
407
  }
384
408
  }
385
409
  return true;
@@ -431,7 +455,7 @@ function structuralSnapshot(value) {
431
455
  const src = value;
432
456
  const out = {};
433
457
  for (const k of Object.keys(src)) {
434
- out[k] = structuralSnapshot(src[k]);
458
+ safeAssign(out, k, structuralSnapshot(safeOwnRead(src, k)));
435
459
  }
436
460
  return out;
437
461
  }
@@ -490,7 +514,7 @@ function hashStableString(input, seed = 0) {
490
514
  const ANON_STEM = "atta";
491
515
  function readableFormKeyStem(formKey) {
492
516
  if (formKey === "" || formKey.startsWith(ANONYMOUS_FORM_KEY_PREFIX)) return ANON_STEM;
493
- const sanitized = formKey.replace(/[^A-Za-z0-9_-]+/g, "-").replace(/^-+|-+$/g, "");
517
+ const sanitized = formKey.replace(/[^A-Za-z0-9_-]+/g, "-").replace(/^-+|(?<!-)-+$/g, "");
494
518
  return sanitized === "" ? ANON_STEM : sanitized;
495
519
  }
496
520
  function fieldIdToken(formInstanceId, pathKey) {
@@ -524,6 +548,7 @@ function humanize(segment) {
524
548
  }).join(" ");
525
549
  }
526
550
 
551
+ const warnedDisplayStatePredicates = /* @__PURE__ */ new WeakSet();
527
552
  function isUnderStubAncestor(state, segments) {
528
553
  for (let i = 0; i < segments.length; i++) {
529
554
  const ancestorPath = segments.slice(0, i);
@@ -607,7 +632,7 @@ function buildLeafFieldState(state, segments, key, formInstanceId, getFormMetaBa
607
632
  function buildContainerFieldStateBase(state, segments, key, formInstanceId) {
608
633
  const formValue = state.form.value;
609
634
  const value = state.getValueAtPath(segments);
610
- const original = state.originals.get(paths.canonicalizePath(segments).key)?.value;
635
+ const original = state.originals.get(key)?.value;
611
636
  let pristine = true;
612
637
  let blank = true;
613
638
  let dirty = false;
@@ -620,13 +645,12 @@ function buildContainerFieldStateBase(state, segments, key, formInstanceId) {
620
645
  let validating = false;
621
646
  let updatedAt = null;
622
647
  let asyncPending = false;
623
- for (const [, entry] of state.originals) {
648
+ for (const [leafKey, entry] of state.originals) {
624
649
  if (!paths.isPathPrefix(segments, entry.segments)) continue;
625
650
  if (segments.length === entry.segments.length) continue;
626
651
  if (!hasAtPath(formValue, entry.segments)) continue;
627
- const leafKey = paths.canonicalizePath(entry.segments).key;
628
652
  const leafRecord = state.fields.get(leafKey);
629
- if (!state.isPristineAtPath(entry.segments)) {
653
+ if (!state.isPristineAtPathByKey(leafKey, entry.segments)) {
630
654
  pristine = false;
631
655
  dirty = true;
632
656
  }
@@ -638,7 +662,7 @@ function buildContainerFieldStateBase(state, segments, key, formInstanceId) {
638
662
  if (leafRecord?.blurredAfterInteraction === true) blurredAfterInteraction = true;
639
663
  if (leafRecord?.connected === true) connected = true;
640
664
  if ((state.fieldValidationCounts.get(leafKey) ?? 0) > 0) validating = true;
641
- if (state.pathHasAsyncValidation(entry.segments)) asyncPending = true;
665
+ if (state.pathHasAsyncValidationByKey(leafKey, entry.segments)) asyncPending = true;
642
666
  const ts = leafRecord?.updatedAt;
643
667
  if (ts !== void 0 && ts !== null) {
644
668
  if (updatedAt === null || ts > updatedAt) updatedAt = ts;
@@ -693,7 +717,14 @@ function decorateWithDerivedProps(base, state, getFormMetaBase, getDisplayState)
693
717
  let displayState;
694
718
  try {
695
719
  displayState = predicate(base, formMeta);
696
- } catch {
720
+ } catch (err) {
721
+ if (paths.__DEV__ && !warnedDisplayStatePredicates.has(predicate)) {
722
+ warnedDisplayStatePredicates.add(predicate);
723
+ console.warn(
724
+ "[attaform] custom getDisplayState threw \u2014 falling back to defaultDisplayState. Subsequent throws from the same predicate will not warn again.",
725
+ err
726
+ );
727
+ }
697
728
  displayState = defaultDisplayState(base, formMeta);
698
729
  }
699
730
  return {
@@ -730,6 +761,41 @@ function aggregateErrorsAt(state, prefix) {
730
761
  }
731
762
  const EMPTY_ELEMENTS = Object.freeze([]);
732
763
 
764
+ function liveKeysAtPath(state, segments) {
765
+ const value = getAtPath(state.form.value, segments);
766
+ if (value === null || value === void 0) return [];
767
+ if (Array.isArray(value)) {
768
+ const keys = new Array(value.length);
769
+ for (let i = 0; i < value.length; i += 1) keys[i] = String(i);
770
+ return keys;
771
+ }
772
+ if (typeof value === "object") return Object.keys(value);
773
+ return [];
774
+ }
775
+ function isArrayPath(state, segments) {
776
+ if (segments.length === 0) return false;
777
+ return Array.isArray(getAtPath(state.form.value, segments));
778
+ }
779
+
780
+ function warnReadOnly(surface, action, key) {
781
+ if (!paths.__DEV__) return;
782
+ const phrase = action === "write" ? `write to "${String(key)}"` : `${action} of "${String(key)}"`;
783
+ console.warn(
784
+ `[attaform] ${surface} is read-only \u2014 ${phrase} was ignored. Mutate the form via setValue / the directive / field-array helpers instead.`
785
+ );
786
+ }
787
+ function makeReadonlyCoercion(snapshot) {
788
+ const toString = () => JSON.stringify(snapshot());
789
+ return {
790
+ toString,
791
+ valueOf() {
792
+ return this;
793
+ },
794
+ toJSON: snapshot,
795
+ toPrimitive: (hint) => hint === "number" ? NaN : toString()
796
+ };
797
+ }
798
+
733
799
  const INTEGER_SEGMENT = /^(?:0|[1-9]\d*)$/;
734
800
  function keyToSegment(key) {
735
801
  return INTEGER_SEGMENT.test(key) ? Number(key) : key;
@@ -761,12 +827,12 @@ function buildSurfaceProxy(opts) {
761
827
  const existing = containerCache.get(cacheKey);
762
828
  if (existing !== void 0) return existing;
763
829
  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();
830
+ const {
831
+ toString: containerToString,
832
+ valueOf: containerValueOf,
833
+ toJSON: containerToJSON,
834
+ toPrimitive: containerToPrimitive
835
+ } = makeReadonlyCoercion(snapshotContainer);
770
836
  const target = isArrayLike ? [] : (() => {
771
837
  });
772
838
  const proxy = new Proxy(target, {
@@ -791,6 +857,9 @@ function buildSurfaceProxy(opts) {
791
857
  return opts.containerOwnKeys === void 0 ? 0 : opts.containerOwnKeys(segments).length;
792
858
  }
793
859
  const childSegs = [...segments, keyToSegment(key)];
860
+ if ((isArrayLike || opts.isArrayContainer?.(segments) === true) && typeof keyToSegment(key) === "string" && key in Array.prototype) {
861
+ return Reflect.get(Array.prototype, key);
862
+ }
794
863
  if (key === "toString" || key === "valueOf") {
795
864
  if (!schemaHasPath(childSegs)) {
796
865
  return key === "toString" ? containerToString : containerValueOf;
@@ -835,10 +904,25 @@ function buildSurfaceProxy(opts) {
835
904
  };
836
905
  },
837
906
  // 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
907
+ // `setValue`, the directive, or the field-array helpers. Each
908
+ // trap returns `true` (warn-and-noop) returning `false` from a
909
+ // `set`/`delete`/`defineProperty` trap throws `TypeError` under
910
+ // strict mode (every ESM / `<script setup>`), which would surface
911
+ // a host-level exception in consumer code that the library
912
+ // documents as "writes are ignored." Aligns with the contract on
913
+ // `form.values` / `wizard.statuses`.
914
+ set: (_, key) => {
915
+ warnReadOnly("form.fields / form.errors", "write", key);
916
+ return true;
917
+ },
918
+ deleteProperty: (_, key) => {
919
+ warnReadOnly("form.fields / form.errors", "delete", key);
920
+ return true;
921
+ },
922
+ defineProperty: (_, key) => {
923
+ warnReadOnly("form.fields / form.errors", "define", key);
924
+ return true;
925
+ }
842
926
  });
843
927
  containerCache.set(cacheKey, proxy);
844
928
  return proxy;
@@ -860,11 +944,12 @@ function buildSurfaceProxy(opts) {
860
944
  }
861
945
  return snapshot;
862
946
  };
863
- const leafToString = () => JSON.stringify(snapshotLeaf());
864
- function leafValueOf() {
865
- return this;
866
- }
867
- const leafToPrimitive = (hint) => hint === "number" ? NaN : leafToString();
947
+ const {
948
+ toString: leafToString,
949
+ valueOf: leafValueOf,
950
+ toJSON: leafToJSONHandler,
951
+ toPrimitive: leafToPrimitive
952
+ } = makeReadonlyCoercion(snapshotLeaf);
868
953
  const target = (() => {
869
954
  });
870
955
  const proxy = new Proxy(target, {
@@ -882,7 +967,7 @@ function buildSurfaceProxy(opts) {
882
967
  if (typeof key !== "string") return void 0;
883
968
  if (key === "toString") return leafToString;
884
969
  if (key === "valueOf") return leafValueOf;
885
- if (key === "toJSON") return snapshotLeaf;
970
+ if (key === "toJSON") return leafToJSONHandler;
886
971
  if (leafKeys.has(key)) {
887
972
  const leaf = opts.resolveLeaf(segments);
888
973
  return readLeafKey(leaf, key);
@@ -909,9 +994,22 @@ function buildSurfaceProxy(opts) {
909
994
  writable: false
910
995
  };
911
996
  },
912
- set: () => false,
913
- deleteProperty: () => false,
914
- defineProperty: () => false
997
+ // Same warn-and-noop contract as the container traps above.
998
+ // Returning `true` keeps strict-mode callers from throwing on
999
+ // `form.fields.email.value = …`; the actual readonly guarantee
1000
+ // is the absence of any mutation, not the host-level reject.
1001
+ set: (_, key) => {
1002
+ warnReadOnly("form.fields.<leaf>", "write", key);
1003
+ return true;
1004
+ },
1005
+ deleteProperty: (_, key) => {
1006
+ warnReadOnly("form.fields.<leaf>", "delete", key);
1007
+ return true;
1008
+ },
1009
+ defineProperty: (_, key) => {
1010
+ warnReadOnly("form.fields.<leaf>", "define", key);
1011
+ return true;
1012
+ }
915
1013
  });
916
1014
  leafViewCache.set(cacheKey, proxy);
917
1015
  return proxy;
@@ -975,29 +1073,49 @@ function buildErrorsProxy(state) {
975
1073
  // working — the call-form just extends that semantic to
976
1074
  // containers and dynamic paths.
977
1075
  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)
1076
+ // Enumeration unions the live form-data keys at this path with the
1077
+ // first-child segments drawn from every error store. Without the
1078
+ // union, `Object.keys(form.errors)` / `{...form.errors}` /
1079
+ // `v-for="(errs, k) in form.errors"` silently dropped two
1080
+ // important error classes that the dot / call / JSON.stringify
1081
+ // surfaces already exposed:
1082
+ //
1083
+ // - **Form-level** errors at the synthetic `['']` path (set via
1084
+ // `setFormErrors` or root cross-field refines).
1085
+ // - **Server-only** errors at a key the schema doesn't know
1086
+ // about (`['ghost']`, `['address', 'ghost']`).
1087
+ //
1088
+ // The union closes that gap so `ownKeys` agrees with the rest of
1089
+ // the surface. Active-path filter mirrors `resolveLeaf`:
1090
+ // library-produced verdicts (schema + derived-blank) at unreachable
1091
+ // paths stay hidden; user-supplied errors are unconditional.
1092
+ containerOwnKeys: (segments) => errorAwareContainerKeys(state, segments),
1093
+ isArrayContainer: (segments) => isArrayPath(state, segments)
985
1094
  });
986
1095
  }
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));
1096
+ function errorAwareContainerKeys(state, segments) {
1097
+ const keys = new Set(liveKeysAtPath(state, segments));
1098
+ const formValue = state.form.value;
1099
+ const walk = (store, applyActivePathFilter) => {
1100
+ for (const [pathKey, errors] of store) {
1101
+ if (errors.length === 0) continue;
1102
+ if (pathKey === paths.FORM_ERRORS_PATH_KEY) {
1103
+ if (segments.length === 0) keys.add("");
1104
+ continue;
1105
+ }
1106
+ const decoded = paths.segmentsForPathKey(pathKey);
1107
+ if (decoded === null) continue;
1108
+ if (decoded.length <= segments.length) continue;
1109
+ if (!paths.isPathPrefix(segments, decoded)) continue;
1110
+ if (applyActivePathFilter && !hasAtPath(formValue, decoded)) continue;
1111
+ const nextSeg = decoded[segments.length];
1112
+ keys.add(typeof nextSeg === "number" ? String(nextSeg) : nextSeg);
1113
+ }
1114
+ };
1115
+ walk(state.schemaErrors, true);
1116
+ walk(state.derivedBlankErrors.value, true);
1117
+ walk(state.userErrors, false);
1118
+ return [...keys];
1001
1119
  }
1002
1120
  function materializeErrors(state, containerSegments) {
1003
1121
  const liveContainer = getAtPath(state.form.value, containerSegments);
@@ -1049,18 +1167,18 @@ function placeAt(tree, path, errors) {
1049
1167
  const nextSeg = path[i + 1];
1050
1168
  const key = typeof seg === "number" ? String(seg) : seg;
1051
1169
  const cursorRecord2 = cursor;
1052
- let child = cursorRecord2[key];
1170
+ let child = safeOwnRead(cursorRecord2, key);
1053
1171
  if (child === null || child === void 0 || typeof child !== "object") {
1054
1172
  child = typeof nextSeg === "number" ? [] : {};
1055
- cursorRecord2[key] = child;
1173
+ safeAssign(cursorRecord2, key, child);
1056
1174
  }
1057
1175
  cursor = child;
1058
1176
  }
1059
1177
  const lastSeg = path[path.length - 1];
1060
1178
  const lastKey = typeof lastSeg === "number" ? String(lastSeg) : lastSeg;
1061
1179
  const cursorRecord = cursor;
1062
- const existing = cursorRecord[lastKey];
1063
- cursorRecord[lastKey] = Array.isArray(existing) ? [...existing, ...errors] : errors;
1180
+ const existing = safeOwnRead(cursorRecord, lastKey);
1181
+ safeAssign(cursorRecord, lastKey, Array.isArray(existing) ? [...existing, ...errors] : errors);
1064
1182
  }
1065
1183
 
1066
1184
  function buildFieldArrayApi(state) {
@@ -1090,9 +1208,10 @@ function buildFieldArrayApi(state) {
1090
1208
  },
1091
1209
  insert(path, index, value) {
1092
1210
  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 });
1211
+ const preLen = next.length;
1212
+ const insertIndex = index < 0 ? Math.max(0, preLen + index) : Math.min(index, preLen);
1213
+ next.splice(insertIndex, 0, value);
1214
+ return writeArray(path, next, { kind: "insert", index: insertIndex });
1096
1215
  },
1097
1216
  remove(path, index) {
1098
1217
  const next = readArray(path);
@@ -1214,9 +1333,34 @@ function buildFieldStateProxy(state, formInstanceId, getFormMetaBase, options) {
1214
1333
  writable: false
1215
1334
  };
1216
1335
  },
1217
- set: () => false,
1218
- deleteProperty: () => false,
1219
- defineProperty: () => false
1336
+ // Warn-and-noop: returning `false` here throws `TypeError` under
1337
+ // strict mode (`form.fields('email').value = 1` from any ESM
1338
+ // module), which would violate the documented "writes are
1339
+ // ignored" contract. Mirrors `values-proxy` / `wizard-statuses-proxy`.
1340
+ set: (_, key) => {
1341
+ if (paths.__DEV__) {
1342
+ console.warn(
1343
+ `[attaform] form.fields(path) is read-only \u2014 write to "${String(key)}" was ignored. Use form.setValue / the directive / field-array helpers instead.`
1344
+ );
1345
+ }
1346
+ return true;
1347
+ },
1348
+ deleteProperty: (_, key) => {
1349
+ if (paths.__DEV__) {
1350
+ console.warn(
1351
+ `[attaform] form.fields(path) is read-only \u2014 delete of "${String(key)}" was ignored.`
1352
+ );
1353
+ }
1354
+ return true;
1355
+ },
1356
+ defineProperty: (_, key) => {
1357
+ if (paths.__DEV__) {
1358
+ console.warn(
1359
+ `[attaform] form.fields(path) is read-only \u2014 define of "${String(key)}" was ignored.`
1360
+ );
1361
+ }
1362
+ return true;
1363
+ }
1220
1364
  });
1221
1365
  terminalCache.set(cacheKey, proxy);
1222
1366
  return proxy;
@@ -1232,21 +1376,6 @@ function buildFieldStateProxy(state, formInstanceId, getFormMetaBase, options) {
1232
1376
  isArrayContainer: (segments) => isArrayPath(state, segments)
1233
1377
  });
1234
1378
  }
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
1379
  function materializeFields(state, containerSegments, snapshotFieldStateAt) {
1251
1380
  const liveValue = getAtPath(state.form.value, containerSegments);
1252
1381
  return walk$2(liveValue, containerSegments, state.schema, snapshotFieldStateAt);
@@ -1290,7 +1419,7 @@ async function getStorageAdapter(storage) {
1290
1419
  }
1291
1420
  }
1292
1421
  }
1293
- const PERSISTED_ENVELOPE_VERSION = 5;
1422
+ const PERSISTED_ENVELOPE_VERSION = 6;
1294
1423
  function readPersistedPayload(value) {
1295
1424
  if (value === null || value === void 0 || typeof value !== "object") return null;
1296
1425
  const envelope = value;
@@ -1314,12 +1443,7 @@ function warnVersionMismatch(observedVersion) {
1314
1443
  function buildPersistedPayload(form, include, schemaErrors, userErrors, blankPaths) {
1315
1444
  let transientList;
1316
1445
  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;
1446
+ transientList = [...blankPaths];
1323
1447
  }
1324
1448
  if (include === "form") {
1325
1449
  if (transientList === void 0) return { v: PERSISTED_ENVELOPE_VERSION, data: { form } };
@@ -1341,30 +1465,35 @@ function buildPersistedPayload(form, include, schemaErrors, userErrors, blankPat
1341
1465
  function createDebouncedWriter(write, debounceMs) {
1342
1466
  let timer = null;
1343
1467
  let pending = null;
1468
+ let writeGeneration = 0;
1469
+ function runWrite() {
1470
+ const gen = ++writeGeneration;
1471
+ pending = write().finally(() => {
1472
+ if (writeGeneration === gen) pending = null;
1473
+ });
1474
+ }
1344
1475
  function schedule() {
1345
1476
  if (timer !== null) clearTimeout(timer);
1346
1477
  if (debounceMs === 0) {
1347
- pending = write().finally(() => {
1348
- pending = null;
1349
- });
1478
+ runWrite();
1350
1479
  return;
1351
1480
  }
1352
1481
  timer = setTimeout(() => {
1353
1482
  timer = null;
1354
- pending = write().finally(() => {
1355
- pending = null;
1356
- });
1483
+ runWrite();
1357
1484
  }, debounceMs);
1358
1485
  }
1359
1486
  async function flush() {
1360
1487
  if (timer !== null) {
1361
1488
  clearTimeout(timer);
1362
1489
  timer = null;
1363
- pending = write().finally(() => {
1364
- pending = null;
1365
- });
1490
+ runWrite();
1491
+ }
1492
+ while (pending !== null) {
1493
+ const awaited = pending;
1494
+ await awaited;
1495
+ if (pending === awaited) break;
1366
1496
  }
1367
- if (pending !== null) await pending;
1368
1497
  }
1369
1498
  function cancel() {
1370
1499
  if (timer !== null) {
@@ -1377,7 +1506,7 @@ function createDebouncedWriter(write, debounceMs) {
1377
1506
  function resolveStorageKeyBase(config, formKey) {
1378
1507
  return config.key ?? `${PERSISTENCE_KEY_PREFIX}${formKey}`;
1379
1508
  }
1380
- async function cleanupOrphanKeys(adapter, base, currentKey) {
1509
+ async function removeMatchingKeys(adapter, base, keepKey) {
1381
1510
  let keys;
1382
1511
  try {
1383
1512
  keys = await adapter.listKeys(base);
@@ -1385,12 +1514,15 @@ async function cleanupOrphanKeys(adapter, base, currentKey) {
1385
1514
  return;
1386
1515
  }
1387
1516
  for (const key of keys) {
1388
- if (key === currentKey) continue;
1517
+ if (key === keepKey) continue;
1389
1518
  if (key === base || key.startsWith(`${base}:`)) {
1390
1519
  void adapter.removeItem(key).catch(() => void 0);
1391
1520
  }
1392
1521
  }
1393
1522
  }
1523
+ async function cleanupOrphanKeys(adapter, base, currentKey) {
1524
+ await removeMatchingKeys(adapter, base, currentKey);
1525
+ }
1394
1526
  const STANDARD_STORAGE_KINDS = ["local", "session", "indexeddb"];
1395
1527
  function normalizePersistConfig(input) {
1396
1528
  if (typeof input === "string") return { storage: input };
@@ -1401,12 +1533,7 @@ async function sweepAllOrphansAcrossStandardStores(base) {
1401
1533
  for (const kind of STANDARD_STORAGE_KINDS) {
1402
1534
  try {
1403
1535
  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
- }
1536
+ await removeMatchingKeys(adapter, base);
1410
1537
  } catch {
1411
1538
  }
1412
1539
  }
@@ -1417,12 +1544,7 @@ async function sweepNonConfiguredStandardStoresForOrphans(configured, base) {
1417
1544
  if (kind === configuredKind) continue;
1418
1545
  try {
1419
1546
  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
- }
1547
+ await removeMatchingKeys(adapter, base);
1426
1548
  } catch {
1427
1549
  }
1428
1550
  }
@@ -1438,6 +1560,29 @@ function pluckPaths(form, pathKeys) {
1438
1560
  }
1439
1561
  return sparse ?? {};
1440
1562
  }
1563
+ function stripUnacknowledgedSensitiveLeaves(form, optedInPaths, isSensitivePath) {
1564
+ const acknowledgedSensitive = [];
1565
+ for (const key of optedInPaths) {
1566
+ const segs = paths.segmentsForPathKey(key);
1567
+ if (segs !== null && isSensitivePath(segs)) acknowledgedSensitive.push(segs);
1568
+ }
1569
+ const coveredByAcknowledged = (path) => acknowledgedSensitive.some((prefix) => paths.isPathPrefix(prefix, path));
1570
+ const walk = (path, value) => {
1571
+ if (path.length > 0 && isSensitivePath(path) && !coveredByAcknowledged(path)) {
1572
+ return void 0;
1573
+ }
1574
+ if (value === null || typeof value !== "object") return value;
1575
+ if (Array.isArray(value)) return value.map((item, i) => walk([...path, i], item));
1576
+ if (!isPlainRecord(value)) return value;
1577
+ const out = {};
1578
+ for (const key of Object.keys(value)) {
1579
+ const walked = walk([...path, key], value[key]);
1580
+ if (walked !== void 0) safeAssign(out, key, walked);
1581
+ }
1582
+ return out;
1583
+ };
1584
+ return walk([], form);
1585
+ }
1441
1586
  function filterErrorsByPaths(errors, pathKeys) {
1442
1587
  const out = /* @__PURE__ */ new Map();
1443
1588
  for (const [key, value] of errors) {
@@ -1466,8 +1611,17 @@ function mergeDeep(target, source, path, schema) {
1466
1611
  if (isPlainRecord(variantDefault)) {
1467
1612
  const out2 = { ...variantDefault };
1468
1613
  for (const key of Object.keys(sourceRecord)) {
1469
- if (!(key in variantDefault) && key !== du.discriminatorKey) continue;
1470
- out2[key] = mergeDeep(out2[key], sourceRecord[key], [...path, key], schema);
1614
+ if (!safeOwnHas(variantDefault, key) && key !== du.discriminatorKey) continue;
1615
+ safeAssign(
1616
+ out2,
1617
+ key,
1618
+ mergeDeep(
1619
+ safeOwnRead(out2, key),
1620
+ safeOwnRead(sourceRecord, key),
1621
+ [...path, key],
1622
+ schema
1623
+ )
1624
+ );
1471
1625
  }
1472
1626
  return out2;
1473
1627
  }
@@ -1478,7 +1632,16 @@ function mergeDeep(target, source, path, schema) {
1478
1632
  const mergeTarget = target;
1479
1633
  const out = isPlainRecord(mergeTarget) ? { ...mergeTarget } : {};
1480
1634
  for (const key of Object.keys(source)) {
1481
- out[key] = mergeDeep(out[key], source[key], [...path, key], schema);
1635
+ safeAssign(
1636
+ out,
1637
+ key,
1638
+ mergeDeep(
1639
+ safeOwnRead(out, key),
1640
+ safeOwnRead(source, key),
1641
+ [...path, key],
1642
+ schema
1643
+ )
1644
+ );
1482
1645
  }
1483
1646
  return out;
1484
1647
  }
@@ -1579,39 +1742,44 @@ function buildProcessForm(state, formInstanceId, options = {}) {
1579
1742
  }
1580
1743
  return result;
1581
1744
  }
1582
- async function validateAsync(pathInput) {
1745
+ async function runImperativeValidation(pathInput, config) {
1583
1746
  const segments = pathInput === void 0 ? void 0 : toSegments(pathInput);
1584
1747
  const dataAtPath = segments === void 0 ? state.form.value : state.getValueAtPath(segments);
1585
1748
  try {
1586
1749
  state.activeValidations.value += 1;
1587
- state.cancelFieldValidation();
1750
+ if (config.cancelInFlight) state.cancelFieldValidation();
1588
1751
  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));
1752
+ if (config.commitToSchemaErrors) {
1753
+ const scopePath = segments ?? [];
1754
+ const errors = refinement.success ? [] : refinement.errors;
1755
+ const reStamped = segments === void 0 ? errors : errors.map((err) => ({
1756
+ ...err,
1757
+ path: [...segments, ...err.path]
1758
+ }));
1759
+ state.applySchemaErrorsForSubtree(scopePath, reStamped);
1760
+ }
1761
+ return { ok: true, refinement, segments };
1597
1762
  } catch (err) {
1598
- return adapterThrowResponse(err);
1763
+ return { ok: false, error: adapterThrowResponse(err) };
1599
1764
  } finally {
1600
1765
  state.activeValidations.value = Math.max(0, state.activeValidations.value - 1);
1601
1766
  }
1602
1767
  }
1768
+ async function validateAsync(pathInput) {
1769
+ const result = await runImperativeValidation(pathInput, {
1770
+ cancelInFlight: true,
1771
+ commitToSchemaErrors: true
1772
+ });
1773
+ if (!result.ok) return result.error;
1774
+ return stripData(composeWithDerivedBlank(result.refinement, result.segments));
1775
+ }
1603
1776
  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
- }
1777
+ const result = await runImperativeValidation(pathInput, {
1778
+ cancelInFlight: false,
1779
+ commitToSchemaErrors: false
1780
+ });
1781
+ if (!result.ok) return result.error;
1782
+ return composeWithDerivedBlank(result.refinement, result.segments);
1615
1783
  }
1616
1784
  function adapterThrowResponse(err) {
1617
1785
  return {
@@ -2049,7 +2217,6 @@ function coerceValue(value, accepted, elementAccepted, index) {
2049
2217
  }
2050
2218
 
2051
2219
  const EMPTY_TRANSFORMS = Object.freeze([]);
2052
- const INTERACTIVE_TAG_NAMES = /* @__PURE__ */ new Set(["INPUT", "SELECT", "TEXTAREA"]);
2053
2220
  const attaformListenersSymbol = Symbol.for("attaform:focus-listeners");
2054
2221
  function attachFocusListeners(state, segments, element, instanceMeta) {
2055
2222
  const target = element;
@@ -2100,7 +2267,11 @@ function buildRegister(state, formInstanceId, instanceConfig) {
2100
2267
  if (typed !== null && typeof raw === "number" && parseFloat(typed) === raw) {
2101
2268
  return typed;
2102
2269
  }
2103
- return String(raw);
2270
+ try {
2271
+ return String(raw);
2272
+ } catch {
2273
+ return Object.prototype.toString.call(raw);
2274
+ }
2104
2275
  });
2105
2276
  const slimDefault = state.schema.getDefaultAtPath(segments);
2106
2277
  const slimTypes = state.schema.getSlimPrimitiveTypesAtPath(segments);
@@ -2155,7 +2326,7 @@ function buildRegister(state, formInstanceId, instanceConfig) {
2155
2326
  state.markInteracted(segments);
2156
2327
  },
2157
2328
  registerElement: (element) => {
2158
- if (!INTERACTIVE_TAG_NAMES.has(element.tagName)) return;
2329
+ if (!paths.INTERACTIVE_TAG_NAMES.has(element.tagName)) return;
2159
2330
  const added = state.registerElement(segments, element, formInstanceId);
2160
2331
  if (added) attachFocusListeners(state, segments, element, instanceMeta);
2161
2332
  },
@@ -2259,12 +2430,12 @@ function walk(input, segments, schema, paths) {
2259
2430
  for (const key of allKeys) {
2260
2431
  const orig = input[key];
2261
2432
  if (orig === void 0 && inputKeysSet.has(key)) {
2262
- out[key] = void 0;
2433
+ safeAssign(out, key, void 0);
2263
2434
  mutated = true;
2264
2435
  continue;
2265
2436
  }
2266
2437
  const walked = walk(orig, [...segments, key], schema, paths);
2267
- out[key] = walked;
2438
+ safeAssign(out, key, walked);
2268
2439
  if (walked !== orig) mutated = true;
2269
2440
  }
2270
2441
  return mutated ? out : input;
@@ -2285,7 +2456,11 @@ function walkUnspecified(slim, segments, paths$1) {
2285
2456
  if (slim !== null && typeof slim === "object") {
2286
2457
  const out = {};
2287
2458
  for (const key of Object.keys(slim)) {
2288
- out[key] = walkUnspecified(slim[key], [...segments, key], paths$1);
2459
+ safeAssign(
2460
+ out,
2461
+ key,
2462
+ walkUnspecified(slim[key], [...segments, key], paths$1)
2463
+ );
2289
2464
  }
2290
2465
  return out;
2291
2466
  }
@@ -2320,7 +2495,7 @@ function substitute(input, segments, schema, paths) {
2320
2495
  for (const key of Object.keys(input)) {
2321
2496
  const orig = input[key];
2322
2497
  const walked = substitute(orig, [...segments, key], schema, paths);
2323
- out[key] = walked;
2498
+ safeAssign(out, key, walked);
2324
2499
  if (walked !== orig) mutated = true;
2325
2500
  }
2326
2501
  return mutated ? out : input;
@@ -2368,70 +2543,86 @@ function expandUnsetAt(segments, schema, paths$1) {
2368
2543
  return result;
2369
2544
  }
2370
2545
 
2371
- function buildValuesProxy(form) {
2372
- const inner = vue.computed(() => vue.readonly(form.value));
2546
+ function buildCallableReadonlySnapshotProxy(opts) {
2373
2547
  const target = (() => {
2374
2548
  });
2375
- const valuesToString = () => JSON.stringify(inner.value);
2376
- const valuesToPrimitive = (hint) => hint === "number" ? NaN : valuesToString();
2549
+ const { toString, valueOf, toJSON, toPrimitive } = makeReadonlyCoercion(opts.snapshot);
2550
+ const callResolve = opts.resolveCall ?? ((arg) => opts.resolveKey(String(arg)));
2377
2551
  return new Proxy(target, {
2378
2552
  apply(_, __, args) {
2379
2553
  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;
2554
+ if (arg === void 0) return opts.snapshot();
2555
+ return callResolve(arg);
2388
2556
  },
2389
2557
  get(_, key) {
2390
2558
  if (typeof key === "symbol") {
2391
- if (key === Symbol.toPrimitive) return valuesToPrimitive;
2559
+ if (key === Symbol.toPrimitive) return toPrimitive;
2392
2560
  return Reflect.get(target, key);
2393
2561
  }
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];
2562
+ if (key === "toJSON") return toJSON;
2563
+ if (key === "toString") return toString;
2564
+ if (key === "valueOf") return valueOf;
2565
+ return opts.resolveKey(key);
2401
2566
  },
2402
2567
  has(_, key) {
2403
2568
  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);
2569
+ return opts.hasKey(key);
2408
2570
  },
2571
+ ownKeys: () => opts.ownKeys(),
2409
2572
  getOwnPropertyDescriptor(_, key) {
2410
- const desc = Reflect.getOwnPropertyDescriptor(inner.value, key);
2411
- if (desc !== void 0) desc.configurable = true;
2412
- return desc;
2573
+ if (typeof key !== "string") return void 0;
2574
+ if (opts.describeKey !== void 0) return opts.describeKey(key);
2575
+ if (!opts.hasKey(key)) return void 0;
2576
+ return {
2577
+ configurable: true,
2578
+ enumerable: true,
2579
+ writable: false,
2580
+ value: opts.resolveKey(key)
2581
+ };
2413
2582
  },
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
- }
2583
+ set: (_, key) => {
2584
+ warnReadOnly(opts.surface, "write", key);
2424
2585
  return true;
2425
2586
  },
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
- }
2587
+ deleteProperty: (_, key) => {
2588
+ warnReadOnly(opts.surface, "delete", key);
2589
+ return true;
2590
+ },
2591
+ defineProperty: (_, key) => {
2592
+ warnReadOnly(opts.surface, "define", key);
2432
2593
  return true;
2594
+ }
2595
+ });
2596
+ }
2597
+
2598
+ function buildValuesProxy(form) {
2599
+ const inner = vue.computed(() => vue.readonly(form.value));
2600
+ return buildCallableReadonlySnapshotProxy({
2601
+ surface: "form.values",
2602
+ snapshot: () => inner.value,
2603
+ // Read through the readonly proxy at access time so Vue's
2604
+ // dependency tracking lands inside the consumer's active effect
2605
+ // — `inner.value[key]` is what triggers per-key tracking.
2606
+ resolveKey: (key) => inner.value[key],
2607
+ // Dynamic path: walk segments through the readonly proxy. Each
2608
+ // step reads through the proxy's own get traps so dependency
2609
+ // tracking propagates at every level.
2610
+ resolveCall: (arg) => {
2611
+ const { segments } = paths.canonicalizePath(arg);
2612
+ let cursor = inner.value;
2613
+ for (const seg of segments) {
2614
+ if (cursor === null || cursor === void 0) return void 0;
2615
+ cursor = cursor[seg];
2616
+ }
2617
+ return cursor;
2433
2618
  },
2434
- defineProperty: () => true
2619
+ ownKeys: () => Reflect.ownKeys(inner.value),
2620
+ hasKey: (key) => Reflect.has(inner.value, key),
2621
+ describeKey: (key) => {
2622
+ const desc = Reflect.getOwnPropertyDescriptor(inner.value, key);
2623
+ if (desc !== void 0) desc.configurable = true;
2624
+ return desc;
2625
+ }
2435
2626
  });
2436
2627
  }
2437
2628
 
@@ -2497,10 +2688,18 @@ function buildFormApi(state, formInstanceId, options = {}) {
2497
2688
  next,
2498
2689
  state.schema
2499
2690
  );
2691
+ const ok2 = state.setValueAtPath([], walked2.cleanedValues, withInstanceMeta());
2692
+ if (!ok2) return false;
2500
2693
  for (const pathKey of walked2.paths) {
2501
- state.blankPaths.add(pathKey);
2694
+ const blankSegments = paths.segmentsForPathKey(pathKey);
2695
+ if (blankSegments === null) continue;
2696
+ state.setValueAtPath(
2697
+ blankSegments,
2698
+ state.getValueAtPath(blankSegments),
2699
+ withInstanceMeta({ blank: true })
2700
+ );
2502
2701
  }
2503
- return state.setValueAtPath([], walked2.cleanedValues, withInstanceMeta());
2702
+ return true;
2504
2703
  }
2505
2704
  const segments = paths.canonicalizePath(pathOrValue).segments;
2506
2705
  const writeUnsetAt = () => {
@@ -2649,23 +2848,45 @@ function buildFormApi(state, formInstanceId, options = {}) {
2649
2848
  const rootFieldState = getRootFieldStateAt([]);
2650
2849
  const formMeta = vue.readonly(
2651
2850
  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),
2851
+ get value() {
2852
+ return rootFieldState.value.value;
2853
+ },
2854
+ get original() {
2855
+ return rootFieldState.value.original;
2856
+ },
2857
+ get pristine() {
2858
+ return rootFieldState.value.pristine;
2859
+ },
2860
+ get dirty() {
2861
+ return rootFieldState.value.dirty;
2862
+ },
2863
+ get focused() {
2864
+ return rootFieldState.value.focused;
2865
+ },
2866
+ get blurred() {
2867
+ return rootFieldState.value.blurred;
2868
+ },
2869
+ get touched() {
2870
+ return rootFieldState.value.touched;
2871
+ },
2872
+ get interacted() {
2873
+ return rootFieldState.value.interacted;
2874
+ },
2875
+ get blurredAfterInteraction() {
2876
+ return rootFieldState.value.blurredAfterInteraction;
2877
+ },
2878
+ get connected() {
2879
+ return rootFieldState.value.connected;
2880
+ },
2881
+ get element() {
2882
+ return rootFieldState.value.element;
2883
+ },
2884
+ get elements() {
2885
+ return rootFieldState.value.elements;
2886
+ },
2887
+ get updatedAt() {
2888
+ return rootFieldState.value.updatedAt;
2889
+ },
2669
2890
  // Whole-form validating mirrors the LIFECYCLE counter
2670
2891
  // (`state.activeValidations`) ORed with any per-leaf validation
2671
2892
  // in flight (via `rootFieldState.validating`). A submit-time
@@ -2688,21 +2909,51 @@ function buildFormApi(state, formInstanceId, options = {}) {
2688
2909
  // FieldState surface, so `form.meta.displayState` matches
2689
2910
  // `form.fields().displayState` exactly — the predicate runs once
2690
2911
  // 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),
2912
+ get displayState() {
2913
+ return rootFieldState.value.displayState;
2914
+ },
2915
+ get showErrors() {
2916
+ return rootFieldState.value.showErrors;
2917
+ },
2918
+ get showPending() {
2919
+ return rootFieldState.value.showPending;
2920
+ },
2921
+ get showSuccess() {
2922
+ return rootFieldState.value.showSuccess;
2923
+ },
2924
+ get showIdle() {
2925
+ return rootFieldState.value.showIdle;
2926
+ },
2927
+ get firstError() {
2928
+ return rootFieldState.value.firstError;
2929
+ },
2930
+ get path() {
2931
+ return rootFieldState.value.path;
2932
+ },
2933
+ get id() {
2934
+ return rootFieldState.value.id;
2935
+ },
2936
+ get aria() {
2937
+ return rootFieldState.value.aria;
2938
+ },
2939
+ get key() {
2940
+ return rootFieldState.value.key;
2941
+ },
2942
+ get blank() {
2943
+ return rootFieldState.value.blank;
2944
+ },
2945
+ get label() {
2946
+ return rootFieldState.value.label;
2947
+ },
2948
+ get description() {
2949
+ return rootFieldState.value.description;
2950
+ },
2951
+ get placeholder() {
2952
+ return rootFieldState.value.placeholder;
2953
+ },
2954
+ get meta() {
2955
+ return rootFieldState.value.meta;
2956
+ },
2706
2957
  // Lifecycle (form-level only — not on FieldState).
2707
2958
  submitting,
2708
2959
  submissionAttempts,
@@ -2711,7 +2962,9 @@ function buildFormApi(state, formInstanceId, options = {}) {
2711
2962
  // Scalar mirror over the array — meta is a single sticky surface
2712
2963
  // for both templates and `useWizard`'s `FormStatus`, so the
2713
2964
  // projection lives here.
2714
- errorCount: vue.computed(() => metaErrors.value.length),
2965
+ get errorCount() {
2966
+ return metaErrors.value.length;
2967
+ },
2715
2968
  submitted,
2716
2969
  // Per-`useForm()`-call identity. Stable for one mount; new on
2717
2970
  // re-mount; orthogonal to `form.key` (which is the user-supplied
@@ -2748,12 +3001,20 @@ function buildFormApi(state, formInstanceId, options = {}) {
2748
3001
  }
2749
3002
  };
2750
3003
  function clear(pathInput) {
2751
- const segments = pathInput === void 0 ? paths.ROOT_PATH : paths.canonicalizePath(pathInput).segments;
2752
- return state.clear(segments);
3004
+ if (pathInput === void 0) {
3005
+ return setValueImpl(unset);
3006
+ }
3007
+ return setValueImpl(pathInput, unset);
2753
3008
  }
2754
3009
  const persist = async (pathInput, options2) => {
2755
3010
  const segments = paths.canonicalizePath(pathInput).segments;
2756
- paths.enforceSensitiveCheck(segments, options2?.acknowledgeSensitive === true, state.isSensitivePath);
3011
+ if (!paths.allowSensitivePersist(
3012
+ segments,
3013
+ options2?.acknowledgeSensitive === true,
3014
+ state.isSensitivePath
3015
+ )) {
3016
+ return;
3017
+ }
2757
3018
  if (persistence === void 0) return;
2758
3019
  await persistence.writePathImmediately(segments);
2759
3020
  };
@@ -2819,7 +3080,9 @@ function buildFormApi(state, formInstanceId, options = {}) {
2819
3080
  getFormMetaBase,
2820
3081
  fieldStateAccessorOptions
2821
3082
  );
3083
+ const needsLazyGate = state.defaultValuesFactory.value !== void 0 || state.hasSsrPrefetch;
2822
3084
  function gated(fn) {
3085
+ if (!needsLazyGate) return fn;
2823
3086
  return ((...args) => {
2824
3087
  void state.activate();
2825
3088
  return fn(...args);
@@ -2844,7 +3107,7 @@ function buildFormApi(state, formInstanceId, options = {}) {
2844
3107
  }
2845
3108
  const out = {};
2846
3109
  for (const key of Object.keys(value)) {
2847
- out[key] = callTerminal(`${path}.${key}`);
3110
+ safeAssign(out, key, callTerminal(`${path}.${key}`));
2848
3111
  }
2849
3112
  return Object.freeze(out);
2850
3113
  }
@@ -2938,9 +3201,132 @@ function buildFormApi(state, formInstanceId, options = {}) {
2938
3201
  };
2939
3202
  }
2940
3203
 
2941
- function createArrayIdentity(getArrayLength) {
2942
- const tokens = /* @__PURE__ */ new Map();
2943
- const baselines = /* @__PURE__ */ new Map();
3204
+ function applyDuStubs(schema, data, options = {}) {
3205
+ const warned = options.warn === true ? /* @__PURE__ */ new Set() : void 0;
3206
+ return walkDuStubs(schema, data, options.basePath ?? [], warned);
3207
+ }
3208
+ function walkDuStubs(schema, value, path, warned) {
3209
+ if (value === null || value === void 0 || typeof value !== "object") return value;
3210
+ if (value instanceof Date || value instanceof RegExp || value instanceof Map || value instanceof Set || typeof value === "function") {
3211
+ return value;
3212
+ }
3213
+ if (Array.isArray(value)) {
3214
+ return value.map((item, i) => walkDuStubs(schema, item, [...path, i], warned));
3215
+ }
3216
+ const rec = value;
3217
+ const du = schema.getUnionDiscriminatorAtPath(path);
3218
+ if (du !== void 0) {
3219
+ const discValue = rec[du.discriminatorKey];
3220
+ if (discValue !== void 0 && !du.isVariantSelected(discValue)) {
3221
+ const isKindBlank = discValue === "" || discValue === 0 || discValue === 0n || discValue === false || discValue === null;
3222
+ if (!isKindBlank && warned !== void 0 && paths.__DEV__) {
3223
+ const dotted = path.map((s) => String(s)).join(".") || "(root)";
3224
+ const key = `${dotted}::${String(discValue)}`;
3225
+ if (!warned.has(key)) {
3226
+ warned.add(key);
3227
+ console.warn(
3228
+ `[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.`
3229
+ );
3230
+ }
3231
+ }
3232
+ const stub = {};
3233
+ safeAssign(stub, du.discriminatorKey, discValue);
3234
+ return stub;
3235
+ }
3236
+ }
3237
+ const out = {};
3238
+ for (const k of Object.keys(rec)) {
3239
+ safeAssign(out, k, walkDuStubs(schema, rec[k], [...path, k], warned));
3240
+ }
3241
+ return out;
3242
+ }
3243
+
3244
+ function createVariantMemory() {
3245
+ const memory = /* @__PURE__ */ new Map();
3246
+ function clearAtArrayIndices(arrayPath, indexFilter) {
3247
+ for (const memKey of [...memory.keys()]) {
3248
+ const segs = paths.segmentsForPathKey(memKey);
3249
+ if (segs === null) continue;
3250
+ if (!paths.isPathPrefix(arrayPath, segs)) continue;
3251
+ if (segs.length <= arrayPath.length) continue;
3252
+ const idxSeg = segs[arrayPath.length];
3253
+ if (typeof idxSeg !== "number") continue;
3254
+ if (indexFilter(idxSeg)) memory.delete(memKey);
3255
+ }
3256
+ }
3257
+ return {
3258
+ clear() {
3259
+ memory.clear();
3260
+ },
3261
+ clearUnderPath(parentPath) {
3262
+ for (const memKey of [...memory.keys()]) {
3263
+ const segs = paths.segmentsForPathKey(memKey);
3264
+ if (segs === null) continue;
3265
+ if (paths.isPathPrefix(parentPath, segs)) memory.delete(memKey);
3266
+ }
3267
+ },
3268
+ applyArrayOp(arrayPath, op) {
3269
+ switch (op.kind) {
3270
+ case "insert":
3271
+ case "remove":
3272
+ clearAtArrayIndices(arrayPath, (i) => i >= op.index);
3273
+ return;
3274
+ case "move": {
3275
+ const lo = Math.min(op.from, op.to);
3276
+ const hi = Math.max(op.from, op.to);
3277
+ clearAtArrayIndices(arrayPath, (i) => i >= lo && i <= hi);
3278
+ return;
3279
+ }
3280
+ case "swap":
3281
+ clearAtArrayIndices(arrayPath, (i) => i === op.a || i === op.b);
3282
+ return;
3283
+ case "replace-at":
3284
+ clearAtArrayIndices(arrayPath, (i) => i === op.index);
3285
+ return;
3286
+ }
3287
+ },
3288
+ recordOutgoing(unionKey, discValue, snapshot) {
3289
+ let perUnion = memory.get(unionKey);
3290
+ if (perUnion === void 0) {
3291
+ perUnion = /* @__PURE__ */ new Map();
3292
+ memory.set(unionKey, perUnion);
3293
+ }
3294
+ perUnion.set(discValue, snapshot);
3295
+ },
3296
+ lookupIncoming(unionKey, discValue) {
3297
+ return memory.get(unionKey)?.get(discValue);
3298
+ }
3299
+ };
3300
+ }
3301
+ function cloneVariantSnapshot(value) {
3302
+ if (value === null || typeof value !== "object") return value;
3303
+ const raw = vue.toRaw(value);
3304
+ if (raw instanceof Date) return new Date(raw.getTime());
3305
+ if (raw instanceof Map) {
3306
+ const out2 = /* @__PURE__ */ new Map();
3307
+ for (const [k, v] of raw.entries()) out2.set(cloneVariantSnapshot(k), cloneVariantSnapshot(v));
3308
+ return out2;
3309
+ }
3310
+ if (raw instanceof Set) {
3311
+ const out2 = /* @__PURE__ */ new Set();
3312
+ for (const v of raw) out2.add(cloneVariantSnapshot(v));
3313
+ return out2;
3314
+ }
3315
+ if (raw instanceof RegExp) return new RegExp(raw.source, raw.flags);
3316
+ if (Array.isArray(raw)) {
3317
+ const out2 = new Array(raw.length);
3318
+ for (let i = 0; i < raw.length; i++) out2[i] = cloneVariantSnapshot(raw[i]);
3319
+ return out2;
3320
+ }
3321
+ const src = raw;
3322
+ const out = {};
3323
+ for (const k of Object.keys(src)) safeAssign(out, k, cloneVariantSnapshot(src[k]));
3324
+ return out;
3325
+ }
3326
+
3327
+ function createArrayIdentity(getArrayLength) {
3328
+ const tokens = /* @__PURE__ */ new Map();
3329
+ const baselines = /* @__PURE__ */ new Map();
2944
3330
  let counter = 0;
2945
3331
  const allocate = () => `k${(counter++).toString(36)}`;
2946
3332
  function ensure(arrayKey, expectedLen) {
@@ -3000,6 +3386,36 @@ function createArrayIdentity(getArrayLength) {
3000
3386
  return;
3001
3387
  }
3002
3388
  },
3389
+ applyRemap(arrayPath, remap) {
3390
+ if (remap.moved.size === 0 && remap.vacated.size === 0 && remap.fresh.size === 0) return;
3391
+ const relocate = (store) => {
3392
+ const idxPos = arrayPath.length;
3393
+ const snapshots = [];
3394
+ for (const [key, value] of store) {
3395
+ const segments = paths.segmentsForPathKey(key);
3396
+ if (segments === null) continue;
3397
+ if (!paths.isPathPrefix(arrayPath, segments)) continue;
3398
+ if (segments.length <= idxPos) continue;
3399
+ const idxSeg = segments[idxPos];
3400
+ if (typeof idxSeg !== "number") continue;
3401
+ if (!remap.moved.has(idxSeg) && !remap.vacated.has(idxSeg) && !remap.fresh.has(idxSeg)) {
3402
+ continue;
3403
+ }
3404
+ snapshots.push({ segments: [...segments], index: idxSeg, value });
3405
+ }
3406
+ if (snapshots.length === 0) return;
3407
+ for (const snap of snapshots) store.delete(paths.canonicalizePath(snap.segments).key);
3408
+ for (const snap of snapshots) {
3409
+ const target = remap.moved.get(snap.index);
3410
+ if (target === void 0) continue;
3411
+ const relocated = snap.segments.slice();
3412
+ relocated[idxPos] = target;
3413
+ store.set(paths.canonicalizePath(relocated).key, snap.value);
3414
+ }
3415
+ };
3416
+ relocate(tokens);
3417
+ relocate(baselines);
3418
+ },
3003
3419
  realign(arraySegs) {
3004
3420
  ensure(paths.canonicalizePath(arraySegs).key, getArrayLength(arraySegs));
3005
3421
  },
@@ -3114,6 +3530,94 @@ function migrateSetSubtree(set, arrayPath, remap) {
3114
3530
  }
3115
3531
  }
3116
3532
 
3533
+ function createArrayBookkeeping(deps) {
3534
+ const {
3535
+ form,
3536
+ fields,
3537
+ userErrors,
3538
+ originals,
3539
+ blankPaths,
3540
+ originalBlankPaths,
3541
+ fieldValidationCounts,
3542
+ fieldValidationState,
3543
+ schemaErrors,
3544
+ activeValidations,
3545
+ arrayIdentity,
3546
+ touchFieldRecord,
3547
+ decFieldValidation
3548
+ } = deps;
3549
+ function migrateElementState(arrayPath, remap) {
3550
+ if (remap.moved.size === 0 && remap.vacated.size === 0) return;
3551
+ migrateMapSubtree(fields, arrayPath, remap, (record, segments) => ({
3552
+ ...record,
3553
+ path: segments
3554
+ }));
3555
+ migrateMapSubtree(
3556
+ userErrors,
3557
+ arrayPath,
3558
+ remap,
3559
+ (errors, segments) => errors.map((error) => ({ ...error, path: [...segments] }))
3560
+ );
3561
+ migrateMapSubtree(originals, arrayPath, remap, (record, segments) => ({
3562
+ segments,
3563
+ value: record.value
3564
+ }));
3565
+ migrateSetSubtree(blankPaths, arrayPath, remap);
3566
+ migrateSetSubtree(originalBlankPaths, arrayPath, remap);
3567
+ migrateMapSubtree(fieldValidationCounts, arrayPath, remap, (count) => count);
3568
+ arrayIdentity.applyRemap(arrayPath, remap);
3569
+ }
3570
+ function seedFreshElement(arrayPath, freshIndex) {
3571
+ const elementPath = [...arrayPath, freshIndex];
3572
+ const now = (/* @__PURE__ */ new Date()).toISOString();
3573
+ diffAndApply(void 0, getAtPath(form.value, elementPath), elementPath, (patch) => {
3574
+ if (patch.kind !== "added") return;
3575
+ const { key } = paths.canonicalizePath(patch.path);
3576
+ if (!originals.has(key)) originals.set(key, { segments: patch.path, value: void 0 });
3577
+ touchFieldRecord(key, patch.path, { updatedAt: now });
3578
+ });
3579
+ }
3580
+ function dropSchemaErrorsAtChangedIndices(arrayPath, remap) {
3581
+ const changed = changedIndices(remap);
3582
+ if (changed.size === 0) return;
3583
+ const idxPos = arrayPath.length;
3584
+ for (const key of [...schemaErrors.keys()]) {
3585
+ const segs = paths.segmentsForPathKey(key);
3586
+ if (segs === null) continue;
3587
+ if (!paths.isPathPrefix(arrayPath, segs)) continue;
3588
+ if (segs.length <= idxPos) continue;
3589
+ const idx = segs[idxPos];
3590
+ if (typeof idx === "number" && changed.has(idx)) schemaErrors.delete(key);
3591
+ }
3592
+ }
3593
+ function abortValidationAtVacatedIndices(arrayPath, remap) {
3594
+ if (remap.vacated.size === 0) return;
3595
+ const idxPos = arrayPath.length;
3596
+ for (const [key, entry] of [...fieldValidationState]) {
3597
+ const segs = paths.segmentsForPathKey(key);
3598
+ if (segs === null) continue;
3599
+ if (!paths.isPathPrefix(arrayPath, segs)) continue;
3600
+ if (segs.length <= idxPos) continue;
3601
+ const idx = segs[idxPos];
3602
+ if (typeof idx !== "number" || !remap.vacated.has(idx)) continue;
3603
+ if (entry.timer !== null) {
3604
+ clearTimeout(entry.timer);
3605
+ } else if (!entry.settled) {
3606
+ activeValidations.value = Math.max(0, activeValidations.value - 1);
3607
+ decFieldValidation(key);
3608
+ }
3609
+ entry.controller.abort();
3610
+ fieldValidationState.delete(key);
3611
+ }
3612
+ }
3613
+ return {
3614
+ migrateElementState,
3615
+ seedFreshElement,
3616
+ dropSchemaErrorsAtChangedIndices,
3617
+ abortValidationAtVacatedIndices
3618
+ };
3619
+ }
3620
+
3117
3621
  function isHydratedFieldRecord(value) {
3118
3622
  if (typeof value !== "object" || value === null) return false;
3119
3623
  const r = value;
@@ -3137,43 +3641,6 @@ function warnMalformedHydration(formKey, kind, rawKey) {
3137
3641
  `[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
3642
  );
3139
3643
  }
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
3644
  function isPathKeyUnder(existingKey, parentPath) {
3178
3645
  const parsed = paths.segmentsForPathKey(existingKey);
3179
3646
  if (parsed === null) return false;
@@ -3209,31 +3676,6 @@ function stripSymbolsDeep(value) {
3209
3676
  }
3210
3677
  return mutated ? out : value;
3211
3678
  }
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
3679
  function walkAuthoredFromConstraints(value, prefix, out) {
3238
3680
  if (prefix.length > 0) out.add(paths.canonicalizePath(prefix).key);
3239
3681
  if (isPlainRecord(value)) {
@@ -3306,7 +3748,6 @@ function createFormStore(options) {
3306
3748
  const coerceIndex = resolveCoercionIndex(options.coerce);
3307
3749
  const resolvedGetDisplayState = resolveGetDisplayState(options.getDisplayState);
3308
3750
  const resolvedIsSensitivePath = options.isSensitivePath ?? paths.isSensitivePath;
3309
- const resolvedSegmentMatchesSensitive = options.segmentMatchesSensitive ?? paths.segmentMatchesSensitive;
3310
3751
  const cleanupHooks = [];
3311
3752
  const modules = /* @__PURE__ */ new Map();
3312
3753
  const completedConstraints = defaultValues === void 0 ? void 0 : mergeStructural(schema, [], defaultValues);
@@ -3369,117 +3810,17 @@ function createFormStore(options) {
3369
3810
  blankPaths.add(key);
3370
3811
  originalBlankPaths.add(key);
3371
3812
  }
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);
3378
- }
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;
3813
+ const variantMemory = createVariantMemory();
3814
+ const pathOrdinals = /* @__PURE__ */ new Map();
3815
+ let nextOrdinal = 0;
3816
+ function ensurePathOrdinal(key) {
3817
+ let ordinal = pathOrdinals.get(key);
3818
+ if (ordinal === void 0) {
3819
+ ordinal = nextOrdinal;
3820
+ pathOrdinals.set(key, ordinal);
3821
+ nextOrdinal += 1;
3409
3822
  }
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;
3823
+ return ordinal;
3483
3824
  }
3484
3825
  const derivedBlankErrors = vue.computed(() => {
3485
3826
  const result = /* @__PURE__ */ new Map();
@@ -3507,7 +3848,9 @@ function createFormStore(options) {
3507
3848
  const departAttempts = vue.ref(0);
3508
3849
  const submissionGeneration = vue.ref(0);
3509
3850
  const activeValidations = vue.ref(0);
3510
- let lastValidatedSnapshot = null;
3851
+ const pathSnapshots = /* @__PURE__ */ new Map();
3852
+ let scheduleEpoch = 0;
3853
+ let lastCommittedEpoch = 0;
3511
3854
  const hydrating = vue.ref(false);
3512
3855
  const hydrateError = vue.ref(null);
3513
3856
  const defaultValuesFactory = vue.ref(void 0);
@@ -3523,9 +3866,12 @@ function createFormStore(options) {
3523
3866
  const pathAsyncCache = /* @__PURE__ */ new Map();
3524
3867
  function pathHasAsyncValidation(path) {
3525
3868
  const { key } = paths.canonicalizePath(path);
3869
+ return pathHasAsyncValidationByKey(key, path);
3870
+ }
3871
+ function pathHasAsyncValidationByKey(key, segments) {
3526
3872
  const cached = pathAsyncCache.get(key);
3527
3873
  if (cached !== void 0) return cached;
3528
- const candidates = schema.getSchemasAtPath(path);
3874
+ const candidates = schema.getSchemasAtPath(segments);
3529
3875
  const hasAsync = candidates.some((sub) => sub.needsAsyncValidation?.() === true);
3530
3876
  pathAsyncCache.set(key, hasAsync);
3531
3877
  return hasAsync;
@@ -3617,6 +3963,21 @@ function createFormStore(options) {
3617
3963
  blurredAfterInteraction: patch.blurredAfterInteraction ?? current?.blurredAfterInteraction ?? false
3618
3964
  });
3619
3965
  }
3966
+ const arrayBookkeeping = createArrayBookkeeping({
3967
+ form,
3968
+ fields,
3969
+ userErrors,
3970
+ originals,
3971
+ blankPaths,
3972
+ originalBlankPaths,
3973
+ fieldValidationCounts,
3974
+ fieldValidationState,
3975
+ schemaErrors,
3976
+ activeValidations,
3977
+ arrayIdentity,
3978
+ touchFieldRecord,
3979
+ decFieldValidation
3980
+ });
3620
3981
  function applyFormReplacement(next, meta) {
3621
3982
  const prev = form.value;
3622
3983
  if (Object.is(prev, next)) return;
@@ -3740,8 +4101,13 @@ function createFormStore(options) {
3740
4101
  const pathKey = paths.canonicalizePath(path).key;
3741
4102
  if (meta?.blank === true) {
3742
4103
  blankPaths.add(pathKey);
3743
- } else if (blankPaths.has(pathKey)) {
3744
- blankPaths.delete(pathKey);
4104
+ } else {
4105
+ if (blankPaths.has(pathKey)) blankPaths.delete(pathKey);
4106
+ if (meta?.arrayOp === void 0) {
4107
+ for (const existingKey of [...blankPaths]) {
4108
+ if (isPathKeyUnder(existingKey, path)) blankPaths.delete(existingKey);
4109
+ }
4110
+ }
3745
4111
  }
3746
4112
  const wasAuthoredBefore = authoredPaths.has(pathKey);
3747
4113
  walkAuthoredFromConstraints(value, path, authoredPaths);
@@ -3765,14 +4131,14 @@ function createFormStore(options) {
3765
4131
  applyFormReplacement(nextForm, meta);
3766
4132
  if (meta?.arrayOp !== void 0) {
3767
4133
  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);
4134
+ arrayBookkeeping.migrateElementState(path, remap);
4135
+ for (const freshIndex of remap.fresh) arrayBookkeeping.seedFreshElement(path, freshIndex);
4136
+ arrayBookkeeping.dropSchemaErrorsAtChangedIndices(path, remap);
4137
+ arrayBookkeeping.abortValidationAtVacatedIndices(path, remap);
4138
+ variantMemory.applyArrayOp(path, meta.arrayOp);
3773
4139
  arrayIdentity.applyOp(path, meta.arrayOp);
3774
4140
  } else if (Array.isArray(value) && Array.isArray(currentValue)) {
3775
- clearVariantMemoryUnderPath(path);
4141
+ variantMemory.clearUnderPath(path);
3776
4142
  arrayIdentity.realign(path);
3777
4143
  }
3778
4144
  const effectiveModeAfterWrite = meta?.instance?.validateOn ?? fieldValidationMode;
@@ -3797,18 +4163,12 @@ function createFormStore(options) {
3797
4163
  for (const k of blankPaths) {
3798
4164
  if (isPathKeyUnder(k, parentPath)) outgoingBlanks.push(k);
3799
4165
  }
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, {
4166
+ variantMemory.recordOutgoing(parentKey, oldDiscValue, {
3806
4167
  value: currentValue2,
3807
4168
  blankPaths: outgoingBlanks
3808
4169
  });
3809
4170
  }
3810
- const memoryForUnion = variantMemory.get(parentKey);
3811
- const restored = memoryForUnion?.get(newDiscValue);
4171
+ const restored = variantMemory.lookupIncoming(parentKey, newDiscValue);
3812
4172
  if (restored !== void 0) {
3813
4173
  baseline = restored.value;
3814
4174
  restoredBlanks = [...restored.blankPaths];
@@ -3880,12 +4240,10 @@ function createFormStore(options) {
3880
4240
  const controller = new AbortController();
3881
4241
  const fresh = { controller, timer: null, settled: false };
3882
4242
  fieldValidationState.set(key, fresh);
4243
+ const myEpoch = ++scheduleEpoch;
3883
4244
  const run = () => {
3884
4245
  fresh.timer = null;
3885
4246
  if (controller.signal.aborted) return;
3886
- if (effectiveMode === "blur") {
3887
- lastValidatedSnapshot = { value: structuralSnapshot(form.value) };
3888
- }
3889
4247
  let activeIncremented = false;
3890
4248
  try {
3891
4249
  activeValidations.value += 1;
@@ -3897,11 +4255,25 @@ function createFormStore(options) {
3897
4255
  }
3898
4256
  throw err;
3899
4257
  }
3900
- void Promise.resolve().then(() => schema.validateAtPath(form.value, void 0)).then((response) => {
4258
+ const subtreeScope = path.length > 0 && schema.hasContainerOrRootRefine?.() === false;
4259
+ const scopePath = subtreeScope ? path : void 0;
4260
+ const dataAtScope = subtreeScope ? getAtPath(form.value, path) : form.value;
4261
+ const scopeKey = subtreeScope ? paths.canonicalizePath(path).key : paths.ROOT_PATH_KEY;
4262
+ void Promise.resolve().then(() => schema.validateAtPath(dataAtScope, scopePath)).then((response) => {
3901
4263
  if (controller.signal.aborted) return;
4264
+ if (myEpoch <= lastCommittedEpoch) return;
4265
+ lastCommittedEpoch = myEpoch;
4266
+ if (effectiveMode === "blur") {
4267
+ const snapshotSource = scopePath !== void 0 ? getAtPath(form.value, scopePath) : form.value;
4268
+ pathSnapshots.set(scopeKey, structuralSnapshot(snapshotSource));
4269
+ }
3902
4270
  const errors = response.success ? [] : response.errors;
3903
4271
  const filtered = filterAuthoredErrors(errors);
3904
- applySchemaErrorsForSubtree([], filtered);
4272
+ const restamped = subtreeScope ? filtered.map((err) => ({
4273
+ ...err,
4274
+ path: [...path, ...err.path]
4275
+ })) : filtered;
4276
+ applySchemaErrorsForSubtree(scopePath ?? [], restamped);
3905
4277
  }).catch(() => {
3906
4278
  }).finally(() => {
3907
4279
  activeValidations.value = Math.max(0, activeValidations.value - 1);
@@ -4144,11 +4516,24 @@ function createFormStore(options) {
4144
4516
  const focusMode = meta?.instance?.validateOn ?? fieldValidationMode;
4145
4517
  if (!focused && focusMode === "blur") {
4146
4518
  const firstInteractiveBlur = current?.interacted === true && current.blurredAfterInteraction !== true;
4147
- const snapshot = lastValidatedSnapshot;
4519
+ let snapshot = void 0;
4520
+ let snapshotScopeLength = 0;
4521
+ for (let i = path.length; i >= 0; i--) {
4522
+ const ancestorKey = paths.canonicalizePath(path.slice(0, i)).key;
4523
+ const entry = pathSnapshots.get(ancestorKey);
4524
+ if (entry !== void 0) {
4525
+ snapshot = entry;
4526
+ snapshotScopeLength = i;
4527
+ break;
4528
+ }
4529
+ }
4148
4530
  let changed = true;
4149
- if (!firstInteractiveBlur && snapshot !== null) {
4531
+ if (!firstInteractiveBlur && snapshot !== void 0) {
4532
+ const relPath = path.slice(snapshotScopeLength);
4533
+ const snapshotSubtree = getAtPath(snapshot, relPath);
4534
+ const liveSubtree = getAtPath(form.value, path);
4150
4535
  changed = false;
4151
- diffAndApply(snapshot.value, form.value, [], () => {
4536
+ diffAndApply(snapshotSubtree, liveSubtree, path, () => {
4152
4537
  changed = true;
4153
4538
  });
4154
4539
  }
@@ -4160,10 +4545,6 @@ function createFormStore(options) {
4160
4545
  }
4161
4546
  }
4162
4547
  }
4163
- function markTouched(path) {
4164
- const { key } = paths.canonicalizePath(path);
4165
- touchFieldRecord(key, path, { touched: true });
4166
- }
4167
4548
  function markInteracted(path) {
4168
4549
  const { key } = paths.canonicalizePath(path);
4169
4550
  if (fields.get(key)?.interacted === true) return;
@@ -4173,7 +4554,7 @@ function createFormStore(options) {
4173
4554
  const formValue = form.value;
4174
4555
  let touchedAny = false;
4175
4556
  for (const [, entry] of originals) {
4176
- if (!isPathPrefix(segments, entry.segments)) continue;
4557
+ if (!paths.isPathPrefix(segments, entry.segments)) continue;
4177
4558
  if (!hasAtPath(formValue, entry.segments)) continue;
4178
4559
  touchedAny = true;
4179
4560
  const leafKey = paths.canonicalizePath(entry.segments).key;
@@ -4187,9 +4568,6 @@ function createFormStore(options) {
4187
4568
  );
4188
4569
  }
4189
4570
  }
4190
- function clear(path) {
4191
- return setValueAtPath(path, schema.getEmptyValueAtPath(path));
4192
- }
4193
4571
  function rehydrate() {
4194
4572
  const factory = defaultValuesFactory.value;
4195
4573
  if (factory === void 0) {
@@ -4336,6 +4714,9 @@ function createFormStore(options) {
4336
4714
  submitError.value = null;
4337
4715
  departAttempts.value = 0;
4338
4716
  cancelFieldValidation();
4717
+ pathSnapshots.clear();
4718
+ scheduleEpoch = 0;
4719
+ lastCommittedEpoch = 0;
4339
4720
  variantMemory.clear();
4340
4721
  for (const listener of resetListeners) {
4341
4722
  try {
@@ -4347,13 +4728,7 @@ function createFormStore(options) {
4347
4728
  }
4348
4729
  function resetField(path) {
4349
4730
  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
- }
4731
+ variantMemory.clearUnderPath(targetSegments);
4357
4732
  const leafEntry = originals.get(targetKey);
4358
4733
  if (leafEntry !== void 0) {
4359
4734
  const wrote = setValueAtPath(targetSegments, leafEntry.value);
@@ -4367,7 +4742,7 @@ function createFormStore(options) {
4367
4742
  let anyMatch = false;
4368
4743
  for (const [, entry] of originals) {
4369
4744
  const leafSegments = entry.segments;
4370
- if (!isPathPrefix(targetSegments, leafSegments)) continue;
4745
+ if (!paths.isPathPrefix(targetSegments, leafSegments)) continue;
4371
4746
  if (leafSegments.length === targetSegments.length) continue;
4372
4747
  anyMatch = true;
4373
4748
  const relative = leafSegments.slice(targetSegments.length);
@@ -4388,14 +4763,14 @@ function createFormStore(options) {
4388
4763
  deleteErrorsUnderPrefix(schemaErrors, targetSegments);
4389
4764
  deleteErrorsUnderPrefix(userErrors, targetSegments);
4390
4765
  for (const [fieldKey, record] of Array.from(fields.entries())) {
4391
- if (isPathPrefix(targetSegments, record.path)) clearFieldRecordFlags(fieldKey);
4766
+ if (paths.isPathPrefix(targetSegments, record.path)) clearFieldRecordFlags(fieldKey);
4392
4767
  }
4393
4768
  }
4394
4769
  function deleteErrorsUnderPrefix(map, prefix) {
4395
4770
  for (const [errorKey, errs] of Array.from(map.entries())) {
4396
4771
  const first = errs[0];
4397
4772
  if (first === void 0) continue;
4398
- if (isPathPrefix(prefix, first.path)) {
4773
+ if (paths.isPathPrefix(prefix, first.path)) {
4399
4774
  map.delete(errorKey);
4400
4775
  }
4401
4776
  }
@@ -4414,15 +4789,11 @@ function createFormStore(options) {
4414
4789
  blurredAfterInteraction: false
4415
4790
  });
4416
4791
  }
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
4792
  function isPristineAtPath(path) {
4425
4793
  const { key, segments } = paths.canonicalizePath(path);
4794
+ return isPristineAtPathByKey(key, segments);
4795
+ }
4796
+ function isPristineAtPathByKey(key, segments) {
4426
4797
  if (blankPaths.has(key) !== originalBlankPaths.has(key)) return false;
4427
4798
  const entry = originals.get(key);
4428
4799
  if (entry === void 0) return true;
@@ -4484,6 +4855,7 @@ function createFormStore(options) {
4484
4855
  hydrating,
4485
4856
  hydrateError,
4486
4857
  defaultValuesFactory,
4858
+ hasSsrPrefetch: ssrPrefetch !== void 0,
4487
4859
  defaultsResolved,
4488
4860
  activated,
4489
4861
  activationPromise,
@@ -4493,6 +4865,7 @@ function createFormStore(options) {
4493
4865
  activeValidations,
4494
4866
  firstValidationDone,
4495
4867
  pathHasAsyncValidation,
4868
+ pathHasAsyncValidationByKey,
4496
4869
  fieldValidationCounts,
4497
4870
  applyFormReplacement,
4498
4871
  setValueAtPath,
@@ -4500,7 +4873,6 @@ function createFormStore(options) {
4500
4873
  arrayElementKey,
4501
4874
  reset,
4502
4875
  resetField,
4503
- clear,
4504
4876
  setSchemaErrorsForPath,
4505
4877
  setAllSchemaErrors,
4506
4878
  clearSchemaErrors,
@@ -4513,11 +4885,11 @@ function createFormStore(options) {
4513
4885
  registerElement,
4514
4886
  deregisterElement,
4515
4887
  markFocused,
4516
- markTouched,
4517
4888
  markInteracted,
4518
4889
  touchAtPath,
4519
4890
  markConnectedOptimistically,
4520
4891
  isPristineAtPath,
4892
+ isPristineAtPathByKey,
4521
4893
  hasStructuralChangeUnder,
4522
4894
  getFieldRecord,
4523
4895
  getOriginalAtPath,
@@ -4534,7 +4906,6 @@ function createFormStore(options) {
4534
4906
  modules,
4535
4907
  persistOptIns,
4536
4908
  isSensitivePath: resolvedIsSensitivePath,
4537
- segmentMatchesSensitive: resolvedSegmentMatchesSensitive,
4538
4909
  noSyncPaths,
4539
4910
  incrementNoSyncOptOut,
4540
4911
  decrementNoSyncOptOut,
@@ -4735,532 +5106,814 @@ function createHistoryModule(state, config) {
4735
5106
  };
4736
5107
  }
4737
5108
 
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;
5109
+ function wirePersistence(state, config) {
5110
+ let fingerprint;
5111
+ try {
5112
+ fingerprint = hashStableString(state.schema.fingerprint());
5113
+ } catch (err) {
5114
+ if (paths.__DEV__) {
5115
+ console.warn(
5116
+ `[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.`
5117
+ );
4771
5118
  }
4772
- return true;
5119
+ fingerprint = "unfingerprinted";
4773
5120
  }
4774
- if (isPlainRecord(value)) {
4775
- for (const key of Object.keys(value)) {
4776
- if (!isInboundShapeAcceptable(schema, [...path, key], value[key])) return false;
5121
+ const base = resolveStorageKeyBase(config, state.formKey);
5122
+ const key = `${base}:${fingerprint}`;
5123
+ const debounceMs = normalizeNumericOption({
5124
+ value: config.debounceMs ?? DEFAULT_PERSISTENCE_DEBOUNCE_MS,
5125
+ source: "useForm.persist.debounceMs",
5126
+ allowInfinity: false,
5127
+ min: 0,
5128
+ defaultValue: DEFAULT_PERSISTENCE_DEBOUNCE_MS
5129
+ });
5130
+ const include = config.include ?? "form";
5131
+ const clearOnSubmitSuccess = config.clearOnSubmitSuccess ?? true;
5132
+ const adapterPromise = getStorageAdapter(config.storage);
5133
+ let disposed = false;
5134
+ const isDisposed = () => disposed;
5135
+ let inFlightFinalFlush = null;
5136
+ let pendingOptedInPaths = null;
5137
+ const writer = createDebouncedWriter(async () => {
5138
+ const optedInPaths = pendingOptedInPaths ?? new Set(state.persistOptIns.optedInPaths());
5139
+ pendingOptedInPaths = null;
5140
+ const adapter = await adapterPromise;
5141
+ if (isDisposed()) return;
5142
+ if (optedInPaths.size === 0) {
5143
+ await adapter.removeItem(key);
5144
+ return;
4777
5145
  }
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;
5146
+ const rawForm = vue.toRaw(state.form.value);
5147
+ const filteredForm = stripUnacknowledgedSensitiveLeaves(
5148
+ pluckPaths(rawForm, optedInPaths),
5149
+ optedInPaths,
5150
+ state.isSensitivePath
5151
+ );
5152
+ const filteredSchemaErrors = filterErrorsByPaths(state.schemaErrors, optedInPaths);
5153
+ const filteredUserErrors = filterErrorsByPaths(state.userErrors, optedInPaths);
5154
+ const filteredTransientEmpty = /* @__PURE__ */ new Set();
5155
+ for (const tk of state.blankPaths) {
5156
+ if (optedInPaths.has(tk)) filteredTransientEmpty.add(tk);
4808
5157
  }
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;
4831
- }
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
- };
5158
+ const payload = buildPersistedPayload(
5159
+ filteredForm,
5160
+ include,
5161
+ filteredSchemaErrors,
5162
+ filteredUserErrors,
5163
+ filteredTransientEmpty
5164
+ );
5165
+ await adapter.setItem(key, payload);
5166
+ }, debounceMs);
5167
+ const unsubscribeChange = state.onFormChange((_next, meta) => {
5168
+ if (isDisposed() || inFlightFinalFlush !== null) return;
5169
+ if (meta?.crossTab === true) return;
5170
+ if (meta?.persist !== true) return;
5171
+ pendingOptedInPaths = new Set(state.persistOptIns.optedInPaths());
5172
+ writer.schedule();
5173
+ });
5174
+ const unsubscribeSuccess = clearOnSubmitSuccess ? state.onSubmitSuccess(() => {
5175
+ if (isDisposed()) return;
5176
+ void (async () => {
5177
+ await writer.flush();
5178
+ if (isDisposed()) return;
5179
+ const adapter = await adapterPromise;
5180
+ if (isDisposed()) return;
5181
+ await adapter.removeItem(key);
5182
+ })();
5183
+ }) : () => void 0;
5184
+ const handlePageHide = () => {
5185
+ if (isDisposed()) return;
5186
+ void writer.flush();
5187
+ };
5188
+ if (typeof window !== "undefined") {
5189
+ window.addEventListener("pagehide", handlePageHide);
4848
5190
  }
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;
5191
+ void (async () => {
5192
+ const adapter = await adapterPromise;
5193
+ if (isDisposed()) return;
5194
+ void cleanupOrphanKeys(adapter, base, key);
4873
5195
  try {
4874
- channel.postMessage(msg);
5196
+ const raw = await adapter.getItem(key);
5197
+ const payload = readPersistedPayload(raw);
5198
+ if (payload === null) {
5199
+ if (raw !== null && raw !== void 0) {
5200
+ await adapter.removeItem(key);
5201
+ }
5202
+ return;
5203
+ }
5204
+ if (isDisposed()) return;
5205
+ const merged = mergeSparseHydration(
5206
+ vue.toRaw(state.form.value),
5207
+ payload.data.form,
5208
+ state.schema
5209
+ );
5210
+ state.applyFormReplacement(merged, { hydration: true });
5211
+ const persistedLeafPaths = collectPersistedLeafPaths(payload.data.form);
5212
+ for (const k of persistedLeafPaths) {
5213
+ state.blankPaths.delete(k);
5214
+ state.originalBlankPaths.delete(k);
5215
+ }
5216
+ for (const k of payload.data.blankPaths ?? []) {
5217
+ state.blankPaths.add(k);
5218
+ state.originalBlankPaths.add(k);
5219
+ }
5220
+ if (include === "form+errors") {
5221
+ if (payload.data.schemaErrors !== void 0) {
5222
+ const flat = payload.data.schemaErrors.flatMap(([, errs]) => errs);
5223
+ state.setAllSchemaErrors(flat);
5224
+ }
5225
+ if (payload.data.userErrors !== void 0) {
5226
+ const flat = payload.data.userErrors.flatMap(([, errs]) => errs);
5227
+ state.setAllUserErrors(flat);
5228
+ }
5229
+ }
5230
+ state.scheduleFieldValidation(
5231
+ [],
5232
+ true
5233
+ /* immediate */
5234
+ );
4875
5235
  } catch {
4876
5236
  }
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;
5237
+ })();
5238
+ async function writePathImmediately(path) {
5239
+ if (isDisposed()) return;
5240
+ await writer.flush();
5241
+ if (isDisposed()) return;
5242
+ const adapter = await adapterPromise;
5243
+ if (isDisposed()) return;
5244
+ const raw = await adapter.getItem(key);
5245
+ const existing = readPersistedPayload(raw);
5246
+ const value = getAtPath(vue.toRaw(state.form.value), path);
5247
+ const nextForm = setAtPath(existing?.data.form ?? {}, path, value);
5248
+ const { key: pathKey } = paths.canonicalizePath(path);
5249
+ const transientSet = new Set(
5250
+ (existing?.data.blankPaths ?? []).filter(
5251
+ (k) => k !== pathKey && !isDescendantPathKey(k, pathKey)
5252
+ )
5253
+ );
5254
+ for (const liveKey of state.blankPaths) {
5255
+ if (liveKey === pathKey || isDescendantPathKey(liveKey, pathKey)) {
5256
+ transientSet.add(liveKey);
5257
+ }
4906
5258
  }
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();
5259
+ if (include === "form") {
5260
+ await adapter.setItem(
5261
+ key,
5262
+ buildPersistedPayload(nextForm, "form", /* @__PURE__ */ new Map(), /* @__PURE__ */ new Map(), transientSet)
5263
+ );
4923
5264
  return;
4924
5265
  }
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);
5266
+ const schemaMap = new Map(existing?.data.schemaErrors ?? []);
5267
+ const userMap = new Map(existing?.data.userErrors ?? []);
5268
+ const currentSchema = state.schemaErrors.get(pathKey);
5269
+ const currentUser = state.userErrors.get(pathKey);
5270
+ if (currentSchema !== void 0 && currentSchema.length > 0) {
5271
+ schemaMap.set(pathKey, [...currentSchema]);
5272
+ } else {
5273
+ schemaMap.delete(pathKey);
4949
5274
  }
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);
5275
+ if (currentUser !== void 0 && currentUser.length > 0) {
5276
+ userMap.set(pathKey, [...currentUser]);
5277
+ } else {
5278
+ userMap.delete(pathKey);
4955
5279
  }
4956
- if (safePatches.length === 0 && safeBlankAdded.length === 0 && safeBlankRemoved.length === 0) {
5280
+ await adapter.setItem(
5281
+ key,
5282
+ buildPersistedPayload(nextForm, "form+errors", schemaMap, userMap, transientSet)
5283
+ );
5284
+ }
5285
+ async function clearPersistedDraft(path) {
5286
+ if (isDisposed()) return;
5287
+ await writer.flush();
5288
+ if (isDisposed()) return;
5289
+ const adapter = await adapterPromise;
5290
+ if (isDisposed()) return;
5291
+ if (path === void 0) {
5292
+ await adapter.removeItem(key);
4957
5293
  return;
4958
5294
  }
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;
5295
+ const raw = await adapter.getItem(key);
5296
+ const existing = readPersistedPayload(raw);
5297
+ if (existing === null) return;
5298
+ const nextForm = deleteAtPath(existing.data.form, path);
5299
+ if (isEmptyContainer(nextForm)) {
5300
+ await adapter.removeItem(key);
5301
+ return;
4980
5302
  }
4981
- if (joinCollectionTimer !== null) {
4982
- clearTimeout(joinCollectionTimer);
4983
- joinCollectionTimer = null;
5303
+ const { key: pathKey } = paths.canonicalizePath(path);
5304
+ const transientSet = new Set(
5305
+ (existing.data.blankPaths ?? []).filter(
5306
+ (k) => k !== pathKey && !isDescendantPathKey(k, pathKey)
5307
+ )
5308
+ );
5309
+ if (include === "form") {
5310
+ await adapter.setItem(
5311
+ key,
5312
+ buildPersistedPayload(nextForm, "form", /* @__PURE__ */ new Map(), /* @__PURE__ */ new Map(), transientSet)
5313
+ );
5314
+ return;
4984
5315
  }
4985
- applyIncomingForm(msg.form, msg.blankPaths);
4986
- lifecycle = "established";
4987
- peerIds.clear();
5316
+ const schemaErrors = (existing.data.schemaErrors ?? []).filter(([k]) => k !== pathKey);
5317
+ const userErrors = (existing.data.userErrors ?? []).filter(([k]) => k !== pathKey);
5318
+ const schemaMap = new Map(schemaErrors.map(([k, v]) => [k, [...v]]));
5319
+ const userMap = new Map(userErrors.map(([k, v]) => [k, [...v]]));
5320
+ await adapter.setItem(
5321
+ key,
5322
+ buildPersistedPayload(nextForm, "form+errors", schemaMap, userMap, transientSet)
5323
+ );
4988
5324
  }
4989
- function respondToHello() {
4990
- safePost({ v: PROTOCOL_VERSION, kind: "announce", senderId });
5325
+ function awaitPendingWrites() {
5326
+ if (inFlightFinalFlush !== null) return inFlightFinalFlush;
5327
+ if (isDisposed()) return Promise.resolve();
5328
+ return writer.flush().catch(() => void 0);
4991
5329
  }
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]
5330
+ function dispose() {
5331
+ if (isDisposed() || inFlightFinalFlush !== null) return;
5332
+ unsubscribeChange();
5333
+ unsubscribeSuccess();
5334
+ if (typeof window !== "undefined") {
5335
+ window.removeEventListener("pagehide", handlePageHide);
5336
+ }
5337
+ inFlightFinalFlush = writer.flush().catch(() => void 0).finally(() => {
5338
+ disposed = true;
5339
+ inFlightFinalFlush = null;
5000
5340
  });
5001
5341
  }
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
- }
5342
+ return {
5343
+ wiredConfig: config,
5344
+ writePathImmediately,
5345
+ clearPersistedDraft,
5346
+ awaitPendingWrites,
5347
+ dispose
5028
5348
  };
5029
- function electLeaderAndRequest() {
5030
- if (disposed) return;
5031
- if (peerIds.size === 0) {
5032
- lifecycle = "established";
5033
- refreshPrior();
5349
+ }
5350
+ function isEmptyContainer(value) {
5351
+ if (value === void 0 || value === null) return true;
5352
+ if (Array.isArray(value)) return value.length === 0;
5353
+ if (isPlainRecord(value)) return Object.keys(value).length === 0;
5354
+ return false;
5355
+ }
5356
+ function collectPersistedLeafPaths(form) {
5357
+ const out = [];
5358
+ walk(form, []);
5359
+ return out;
5360
+ function walk(node, prefix) {
5361
+ if (Array.isArray(node)) {
5362
+ for (let i = 0; i < node.length; i++) {
5363
+ walk(node[i], [...prefix, i]);
5364
+ }
5034
5365
  return;
5035
5366
  }
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
5045
- });
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;
5367
+ if (isPlainRecord(node)) {
5368
+ for (const key of Object.keys(node)) {
5369
+ walk(node[key], [...prefix, key]);
5054
5370
  }
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);
5371
+ return;
5372
+ }
5373
+ if (prefix.length === 0) return;
5374
+ out.push(paths.canonicalizePath(prefix).key);
5066
5375
  }
5067
- joinFlow();
5068
- 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
5089
- };
5090
5376
  }
5091
- const MULTI_TAB_SYNC_MODULE_KEY = "multiTabSync";
5377
+ function isDescendantPathKey(candidate, ancestor) {
5378
+ if (candidate.length <= ancestor.length) return false;
5379
+ if (!ancestor.endsWith("]")) return false;
5380
+ const childPrefix = `${ancestor.slice(0, -1)},`;
5381
+ return candidate.startsWith(childPrefix);
5382
+ }
5092
5383
 
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}`);
5384
+ const PROTOCOL_VERSION = 1;
5385
+ const JOIN_COLLECTION_WINDOW_MS = 50;
5386
+ const SNAPSHOT_TIMEOUT_MS = 200;
5387
+ const MAX_LEADER_ATTEMPTS = 3;
5388
+ const SNAPSHOT_RESPONSE_MIN_INTERVAL_MS = 500;
5389
+ function isFileLikeValue(value) {
5390
+ if (typeof File !== "undefined" && value instanceof File) return true;
5391
+ if (typeof Blob !== "undefined" && value instanceof Blob) return true;
5392
+ return false;
5100
5393
  }
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.";
5394
+ function isInboundShapeAcceptable(schema, path, value) {
5395
+ if (isFileLikeValue(value)) return false;
5396
+ let kind;
5397
+ if (Array.isArray(value)) {
5398
+ kind = "array";
5399
+ } else if (value !== null && typeof value === "object" && isPlainRecord(value)) {
5400
+ kind = "object";
5401
+ } else {
5402
+ kind = slimKindOf(value);
5403
+ }
5404
+ const accepted = schema.getSlimPrimitiveTypesAtPath(path);
5405
+ if (!accepted.has(kind)) return false;
5406
+ if (Array.isArray(value)) {
5407
+ for (let i = 0; i < value.length; i++) {
5408
+ if (!isInboundShapeAcceptable(schema, [...path, i], value[i])) return false;
5409
+ }
5410
+ return true;
5411
+ }
5412
+ if (isPlainRecord(value)) {
5413
+ for (const key of Object.keys(value)) {
5414
+ if (!isInboundShapeAcceptable(schema, [...path, key], value[key])) return false;
5415
+ }
5416
+ return true;
5109
5417
  }
5418
+ return true;
5110
5419
  }
5111
- function isSecureContext() {
5112
- return typeof window !== "undefined" && window.isSecureContext === true;
5420
+ function diffBlankPaths(prev, curr) {
5421
+ const added = [];
5422
+ const removed = [];
5423
+ const prevSet = new Set(prev);
5424
+ for (const k of curr) if (!prevSet.has(k)) added.push(k);
5425
+ for (const k of prev) if (!curr.has(k)) removed.push(k);
5426
+ return { added, removed };
5113
5427
  }
5114
-
5115
- function resolveTrichotomy(input) {
5116
- if (typeof input === "function") {
5117
- return { kind: "async", factory: input };
5428
+ function snapshotForm(form) {
5429
+ return structuralSnapshot(form);
5430
+ }
5431
+ function stripSensitivePathsDeep(value, pathSoFar, isSensitivePath) {
5432
+ if (isFileLikeValue(value)) return void 0;
5433
+ if (value === null || typeof value !== "object") return value;
5434
+ if (Array.isArray(value)) {
5435
+ return value.map((item, i) => stripSensitivePathsDeep(item, [...pathSoFar, i], isSensitivePath));
5118
5436
  }
5119
- return { kind: "sync", value: input };
5437
+ const proto = Object.getPrototypeOf(value);
5438
+ if (proto !== Object.prototype && proto !== null) return value;
5439
+ const out = {};
5440
+ const src = value;
5441
+ for (const key of Object.keys(src)) {
5442
+ const childPath = [...pathSoFar, key];
5443
+ if (isSensitivePath(childPath)) {
5444
+ safeAssign(out, key, void 0);
5445
+ continue;
5446
+ }
5447
+ safeAssign(out, key, stripSensitivePathsDeep(src[key], childPath, isSensitivePath));
5448
+ }
5449
+ return out;
5120
5450
  }
5121
-
5122
- function useAbstractForm(configuration, options) {
5123
- if (configuration === void 0 || configuration === null || configuration.schema === void 0) {
5124
- throw new paths.InvalidUseFormConfigError();
5451
+ function isStringArray(value) {
5452
+ return Array.isArray(value) && value.every((item) => typeof item === "string");
5453
+ }
5454
+ function isPatchArray(value) {
5455
+ return Array.isArray(value) && value.every(
5456
+ (p) => p !== null && typeof p === "object" && Array.isArray(p.path)
5457
+ );
5458
+ }
5459
+ function isValidSyncMessage(data) {
5460
+ if (data === null || typeof data !== "object") return false;
5461
+ const m = data;
5462
+ if (m["v"] !== PROTOCOL_VERSION) return false;
5463
+ if (typeof m["senderId"] !== "string") return false;
5464
+ if (typeof m["kind"] !== "string") return false;
5465
+ switch (m["kind"]) {
5466
+ case "hello":
5467
+ case "announce":
5468
+ return true;
5469
+ case "requestSnapshot":
5470
+ return typeof m["targetId"] === "string";
5471
+ case "snapshot":
5472
+ return isStringArray(m["blankPaths"]) && "form" in m;
5473
+ case "patches":
5474
+ return isPatchArray(m["formPatches"]) && isStringArray(m["blankPathsAdded"]) && isStringArray(m["blankPathsRemoved"]);
5475
+ default:
5476
+ return false;
5125
5477
  }
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
- });
5478
+ }
5479
+ function generateSenderId() {
5480
+ try {
5481
+ return globalThis.crypto.randomUUID();
5482
+ } catch {
5483
+ return `atta-${Math.random().toString(36).slice(2)}-${Date.now().toString(36)}`;
5149
5484
  }
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);
5485
+ }
5486
+ function createMultiTabSyncModule(state, channelName, options) {
5487
+ if (typeof BroadcastChannel === "undefined") {
5488
+ return {
5489
+ dispose: () => void 0,
5490
+ lifecycle: () => "established",
5491
+ senderId: "",
5492
+ channelName
5493
+ };
5154
5494
  }
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;
5495
+ let channel;
5496
+ try {
5497
+ channel = new BroadcastChannel(channelName);
5498
+ } catch {
5499
+ return {
5500
+ dispose: () => void 0,
5501
+ lifecycle: () => "established",
5502
+ senderId: "",
5503
+ channelName
5504
+ };
5159
5505
  }
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
- });
5506
+ const senderId = generateSenderId();
5507
+ let lifecycle = "joining";
5508
+ let disposed = false;
5509
+ const peerIds = /* @__PURE__ */ new Set();
5510
+ const lastSnapshotResponseAt = /* @__PURE__ */ new Map();
5511
+ let joinCollectionTimer = null;
5512
+ let snapshotTimeoutTimer = null;
5513
+ let leaderAttempts = 0;
5514
+ let prior = {
5515
+ form: snapshotForm(state.form.value),
5516
+ blankPathsSnapshot: [...state.blankPaths]
5517
+ };
5518
+ function safePost(msg) {
5519
+ if (disposed) return;
5520
+ try {
5521
+ channel.postMessage(msg);
5522
+ } catch {
5174
5523
  }
5175
5524
  }
5176
- if (vue.getCurrentScope() !== void 0) {
5177
- const releaseConsumer = registry.trackConsumer(key);
5178
- vue.onScopeDispose(releaseConsumer);
5525
+ function refreshPrior() {
5526
+ prior = {
5527
+ form: snapshotForm(state.form.value),
5528
+ blankPathsSnapshot: [...state.blankPaths]
5529
+ };
5179
5530
  }
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}`);
5531
+ function isPathLocallySuppressed(path) {
5532
+ if (options.isSensitivePath(path)) return true;
5533
+ const { key } = paths.canonicalizePath([...path]);
5534
+ if (options.noSyncPaths.has(key)) return true;
5535
+ return false;
5536
+ }
5537
+ function postPatches() {
5538
+ if (lifecycle !== "established") return;
5539
+ const next = snapshotForm(state.form.value);
5540
+ const rawPatches = [];
5541
+ diffAndApply(prior.form, next, [], (p) => rawPatches.push(p));
5542
+ const safePatches = [];
5543
+ for (const p of rawPatches) {
5544
+ if (isPathLocallySuppressed(p.path)) continue;
5545
+ if ("value" in p && isFileLikeValue(p.value)) continue;
5546
+ safePatches.push(p);
5201
5547
  }
5548
+ const { added, removed } = diffBlankPaths(prior.blankPathsSnapshot, state.blankPaths);
5549
+ if (safePatches.length === 0 && added.length === 0 && removed.length === 0) {
5550
+ prior = { form: next, blankPathsSnapshot: [...state.blankPaths] };
5551
+ return;
5552
+ }
5553
+ safePost({
5554
+ v: PROTOCOL_VERSION,
5555
+ kind: "patches",
5556
+ senderId,
5557
+ formPatches: safePatches,
5558
+ blankPathsAdded: added,
5559
+ blankPathsRemoved: removed
5560
+ });
5561
+ prior = { form: next, blankPathsSnapshot: [...state.blankPaths] };
5202
5562
  }
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;
5563
+ const unsubscribeChange = state.onFormChange((_next, meta) => {
5564
+ if (disposed) return;
5565
+ if (lifecycle !== "established") return;
5566
+ if (meta?.crossTab === true) return;
5567
+ if (meta?.hydration === true) {
5568
+ refreshPrior();
5569
+ return;
5570
+ }
5571
+ postPatches();
5572
+ });
5573
+ function applyIncomingForm(form, blankPaths) {
5574
+ state.blankPaths.clear();
5575
+ for (const k of blankPaths) state.blankPaths.add(k);
5576
+ state.applyFormReplacement(form, { crossTab: true, persist: false });
5577
+ refreshPrior();
5578
+ }
5579
+ function handlePatches(msg) {
5580
+ if (lifecycle !== "established") return;
5581
+ const safePatches = [];
5582
+ for (const p of msg.formPatches) {
5583
+ if (!Array.isArray(p.path)) continue;
5584
+ if (isPathLocallySuppressed(p.path)) continue;
5585
+ if ("value" in p && !isInboundShapeAcceptable(state.schema, p.path, p.value)) {
5586
+ continue;
5587
+ }
5588
+ safePatches.push(p);
5589
+ }
5590
+ const safeBlankAdded = [];
5591
+ for (const k of msg.blankPathsAdded) {
5592
+ const segs = paths.canonicalizePath(k).segments;
5593
+ if (isPathLocallySuppressed(segs)) continue;
5594
+ safeBlankAdded.push(k);
5595
+ }
5596
+ const safeBlankRemoved = [];
5597
+ for (const k of msg.blankPathsRemoved) {
5598
+ const segs = paths.canonicalizePath(k).segments;
5599
+ if (isPathLocallySuppressed(segs)) continue;
5600
+ safeBlankRemoved.push(k);
5601
+ }
5602
+ if (safePatches.length === 0 && safeBlankAdded.length === 0 && safeBlankRemoved.length === 0) {
5603
+ return;
5604
+ }
5605
+ const candidate = applyPatchesForward(state.form.value, safePatches);
5606
+ try {
5607
+ options.validateForm(state.form.value);
5208
5608
  try {
5209
- channelName = `attaform:sync:${state.formKey}:${hashStableString(state.schema.fingerprint())}`;
5609
+ options.validateForm(candidate);
5210
5610
  } 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());
5611
+ return;
5227
5612
  }
5228
- } else if (hasBroadcastChannel && !secureContext) {
5229
- warnOnceInsecureContext("multiTab");
5613
+ } catch {
5230
5614
  }
5615
+ const nextBlankPaths = new Set(state.blankPaths);
5616
+ for (const k of safeBlankRemoved) nextBlankPaths.delete(k);
5617
+ for (const k of safeBlankAdded) nextBlankPaths.add(k);
5618
+ applyIncomingForm(candidate, [...nextBlankPaths]);
5231
5619
  }
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());
5236
- }
5237
- if (configuration.key === void 0) {
5238
- recordAmbientProvide(registry.ssr);
5239
- vue.provide(paths.kFormContext, state);
5240
- }
5241
- const formInstanceId = vue.getCurrentInstance() !== null ? vue.useId() : `atta:form-instance:${formInstanceCounter++}`;
5242
- if (vue.getCurrentInstance() !== null) {
5243
- vue.provide(paths.kFormInstanceId, formInstanceId);
5244
- }
5245
- const apiOptions = {};
5246
- if (merged.onInvalidSubmit !== void 0) {
5247
- apiOptions.onInvalidSubmit = merged.onInvalidSubmit;
5620
+ function handleSnapshot(msg) {
5621
+ if (lifecycle !== "joining") return;
5622
+ if (!isInboundShapeAcceptable(state.schema, [], msg.form)) return;
5623
+ if (snapshotTimeoutTimer !== null) {
5624
+ clearTimeout(snapshotTimeoutTimer);
5625
+ snapshotTimeoutTimer = null;
5626
+ }
5627
+ if (joinCollectionTimer !== null) {
5628
+ clearTimeout(joinCollectionTimer);
5629
+ joinCollectionTimer = null;
5630
+ }
5631
+ applyIncomingForm(msg.form, msg.blankPaths);
5632
+ lifecycle = "established";
5633
+ peerIds.clear();
5248
5634
  }
5249
- const history = state.modules.get(HISTORY_MODULE_KEY);
5250
- if (history !== void 0) {
5251
- apiOptions.history = history;
5635
+ function respondToHello() {
5636
+ safePost({ v: PROTOCOL_VERSION, kind: "announce", senderId });
5252
5637
  }
5253
- if (merged.validateOn !== void 0) {
5254
- apiOptions.validateOn = merged.validateOn;
5638
+ function respondToSnapshotRequest(requesterId) {
5639
+ const now = Date.now();
5640
+ const last = lastSnapshotResponseAt.get(requesterId);
5641
+ if (last !== void 0 && now - last < SNAPSHOT_RESPONSE_MIN_INTERVAL_MS) return;
5642
+ lastSnapshotResponseAt.set(requesterId, now);
5643
+ const scrubbedForm = stripSensitivePathsDeep(state.form.value, [], options.isSensitivePath);
5644
+ safePost({
5645
+ v: PROTOCOL_VERSION,
5646
+ kind: "snapshot",
5647
+ senderId,
5648
+ form: scrubbedForm,
5649
+ blankPaths: [...state.blankPaths]
5650
+ });
5255
5651
  }
5256
- const mergedDebounceMs = merged.debounceMs;
5257
- if (mergedDebounceMs !== void 0) {
5258
- apiOptions.debounceMs = mergedDebounceMs;
5652
+ channel.onmessage = (event) => {
5653
+ if (disposed) return;
5654
+ try {
5655
+ const data = event.data;
5656
+ if (!isValidSyncMessage(data)) return;
5657
+ const msg = data;
5658
+ if (msg.senderId === senderId) return;
5659
+ switch (msg.kind) {
5660
+ case "hello":
5661
+ if (lifecycle !== "established") return;
5662
+ respondToHello();
5663
+ break;
5664
+ case "announce":
5665
+ if (lifecycle === "joining") peerIds.add(msg.senderId);
5666
+ break;
5667
+ case "requestSnapshot":
5668
+ if (lifecycle !== "established") return;
5669
+ if (msg.targetId !== senderId) return;
5670
+ respondToSnapshotRequest(msg.senderId);
5671
+ break;
5672
+ case "snapshot":
5673
+ handleSnapshot(msg);
5674
+ break;
5675
+ case "patches":
5676
+ handlePatches(msg);
5677
+ break;
5678
+ }
5679
+ } catch {
5680
+ }
5681
+ };
5682
+ function electLeaderAndRequest() {
5683
+ if (disposed) return;
5684
+ if (peerIds.size === 0) {
5685
+ lifecycle = "established";
5686
+ refreshPrior();
5687
+ return;
5688
+ }
5689
+ const sorted = [...peerIds].sort();
5690
+ const leaderId = sorted[0];
5691
+ peerIds.delete(leaderId);
5692
+ leaderAttempts++;
5693
+ safePost({
5694
+ v: PROTOCOL_VERSION,
5695
+ kind: "requestSnapshot",
5696
+ senderId,
5697
+ targetId: leaderId
5698
+ });
5699
+ snapshotTimeoutTimer = setTimeout(() => {
5700
+ snapshotTimeoutTimer = null;
5701
+ if (disposed) return;
5702
+ if (lifecycle === "established") return;
5703
+ if (leaderAttempts >= MAX_LEADER_ATTEMPTS || peerIds.size === 0) {
5704
+ lifecycle = "established";
5705
+ refreshPrior();
5706
+ return;
5707
+ }
5708
+ electLeaderAndRequest();
5709
+ }, SNAPSHOT_TIMEOUT_MS);
5259
5710
  }
5260
- if (merged.getDisplayState !== void 0) {
5261
- apiOptions.getDisplayState = merged.getDisplayState;
5711
+ function joinFlow() {
5712
+ safePost({ v: PROTOCOL_VERSION, kind: "hello", senderId });
5713
+ joinCollectionTimer = setTimeout(() => {
5714
+ joinCollectionTimer = null;
5715
+ if (disposed) return;
5716
+ if (lifecycle === "established") return;
5717
+ electLeaderAndRequest();
5718
+ }, JOIN_COLLECTION_WINDOW_MS);
5262
5719
  }
5263
- if (merged.coerce !== void 0) {
5720
+ joinFlow();
5721
+ return {
5722
+ dispose: () => {
5723
+ if (disposed) return;
5724
+ disposed = true;
5725
+ if (joinCollectionTimer !== null) {
5726
+ clearTimeout(joinCollectionTimer);
5727
+ joinCollectionTimer = null;
5728
+ }
5729
+ if (snapshotTimeoutTimer !== null) {
5730
+ clearTimeout(snapshotTimeoutTimer);
5731
+ snapshotTimeoutTimer = null;
5732
+ }
5733
+ unsubscribeChange();
5734
+ try {
5735
+ channel.close();
5736
+ } catch {
5737
+ }
5738
+ },
5739
+ lifecycle: () => lifecycle,
5740
+ senderId,
5741
+ channelName
5742
+ };
5743
+ }
5744
+ const MULTI_TAB_SYNC_MODULE_KEY = "multiTabSync";
5745
+
5746
+ const warned = /* @__PURE__ */ new Set();
5747
+ function warnOnceInsecureContext(feature) {
5748
+ if (!paths.__DEV__) return;
5749
+ if (warned.has(feature)) return;
5750
+ warned.add(feature);
5751
+ const message = featureMessage(feature);
5752
+ console.warn(`[attaform] ${message}`);
5753
+ }
5754
+ function featureMessage(feature) {
5755
+ switch (feature) {
5756
+ case "multiTab":
5757
+ 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.";
5758
+ case "persist:local":
5759
+ 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.";
5760
+ case "persist:session":
5761
+ 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.";
5762
+ }
5763
+ }
5764
+ function isSecureContext() {
5765
+ return typeof window !== "undefined" && window.isSecureContext === true;
5766
+ }
5767
+
5768
+ function resolveTrichotomy(input) {
5769
+ if (typeof input === "function") {
5770
+ return { kind: "async", factory: input };
5771
+ }
5772
+ return { kind: "sync", value: input };
5773
+ }
5774
+
5775
+ function useAbstractForm(configuration, options) {
5776
+ if (configuration === void 0 || configuration === null || configuration.schema === void 0) {
5777
+ throw new paths.InvalidUseFormConfigError();
5778
+ }
5779
+ const key = resolveFormKey(configuration.key);
5780
+ const instance = vue.getCurrentInstance();
5781
+ if (instance !== null) paths.ensureAttaformInstalled(instance.appContext.app);
5782
+ const registry = options?.registry ?? paths.useRegistry();
5783
+ const resolvedDefaults = resolveTrichotomy(configuration.defaultValues);
5784
+ const materialisedDefaults = resolvedDefaults.kind === "sync" ? resolvedDefaults.value : void 0;
5785
+ const { defaultValues: _droppedDefaults, ...configWithoutDefaults } = configuration;
5786
+ const trichotomyOverride = materialisedDefaults === void 0 ? configWithoutDefaults : { ...configWithoutDefaults, defaultValues: materialisedDefaults };
5787
+ const merged = mergeWithDefaults(registry.defaults, trichotomyOverride);
5788
+ const maxRecursionDepth = normalizeNumericOption({
5789
+ value: merged.maxRecursionDepth ?? DEFAULT_MAX_RECURSION_DEPTH,
5790
+ source: "useForm.maxRecursionDepth",
5791
+ allowInfinity: true,
5792
+ min: 0,
5793
+ defaultValue: DEFAULT_MAX_RECURSION_DEPTH
5794
+ });
5795
+ const resolvedSchema = getComputedSchema(key, configuration.schema, { maxRecursionDepth });
5796
+ if (configuration.persist !== void 0 && configuration.key === void 0) {
5797
+ throw new paths.AnonPersistError({
5798
+ cause: "no-key",
5799
+ schemaFields: extractSchemaFields(resolvedSchema),
5800
+ callSite: paths.captureUserCallSite()
5801
+ });
5802
+ }
5803
+ const existing = registry.forms.get(key);
5804
+ if (paths.__DEV__ && existing !== void 0) {
5805
+ warnOnSchemaFingerprintMismatch(key, existing.schema, resolvedSchema);
5806
+ warnOnPersistDivergence(key, existing, configuration.persist);
5807
+ }
5808
+ const hadPendingHydration = registry.pendingHydration.has(key);
5809
+ const state = existing ?? buildFreshState(key, resolvedSchema, merged, registry);
5810
+ if (existing !== void 0) ; else if (resolvedDefaults.kind === "sync") {
5811
+ state.defaultsResolved.value = true;
5812
+ }
5813
+ if (existing === void 0 && resolvedDefaults.kind === "async") {
5814
+ const factory = resolvedDefaults.factory;
5815
+ state.defaultValuesFactory.value = factory;
5816
+ if (hadPendingHydration) {
5817
+ state.hydrating.value = false;
5818
+ state.defaultsResolved.value = true;
5819
+ } else if (registry.ssr) {
5820
+ if (configuration.__ssrAccessed === true) {
5821
+ registry.enqueuePrefetch(key);
5822
+ }
5823
+ vue.onServerPrefetch(() => {
5824
+ if (!registry.shouldPrefetch(key)) return;
5825
+ return state.activate();
5826
+ });
5827
+ }
5828
+ }
5829
+ if (vue.getCurrentScope() !== void 0) {
5830
+ const releaseConsumer = registry.trackConsumer(key);
5831
+ vue.onScopeDispose(releaseConsumer);
5832
+ }
5833
+ const persistDisabledByAnonRule = merged.persist !== void 0 && enforceAnonPersistRule(state.formKey, registry.ssr);
5834
+ if (existing === void 0 && !registry.ssr) {
5835
+ if (merged.persist !== void 0 && !persistDisabledByAnonRule) {
5836
+ const resolvedPersist = normalizePersistConfig(merged.persist);
5837
+ const storageKind = resolvedPersist.storage;
5838
+ const isBuiltinStorage = typeof storageKind === "string";
5839
+ const secureContextOk = !isBuiltinStorage || isSecureContext();
5840
+ if (!secureContextOk) {
5841
+ const feature = storageKind === "session" ? "persist:session" : "persist:local";
5842
+ warnOnceInsecureContext(feature);
5843
+ void sweepAllOrphansAcrossStandardStores(`${PERSISTENCE_KEY_PREFIX}${state.formKey}`);
5844
+ } else {
5845
+ const persistenceBase = resolveStorageKeyBase(resolvedPersist, state.formKey);
5846
+ void sweepNonConfiguredStandardStoresForOrphans(resolvedPersist.storage, persistenceBase);
5847
+ const persistenceModule = wirePersistence(state, resolvedPersist);
5848
+ state.modules.set(PERSISTENCE_MODULE_KEY, persistenceModule);
5849
+ state.registerDrain(() => persistenceModule.awaitPendingWrites());
5850
+ state.registerCleanup(() => persistenceModule.dispose());
5851
+ }
5852
+ } else {
5853
+ void sweepAllOrphansAcrossStandardStores(`${PERSISTENCE_KEY_PREFIX}${state.formKey}`);
5854
+ }
5855
+ }
5856
+ if (existing === void 0 && merged.multiTab === true && configuration.key !== void 0 && !registry.ssr) {
5857
+ const hasBroadcastChannel = typeof BroadcastChannel !== "undefined";
5858
+ const secureContext = isSecureContext();
5859
+ if (hasBroadcastChannel && secureContext) {
5860
+ let channelName;
5861
+ try {
5862
+ channelName = `attaform:sync:${state.formKey}:${hashStableString(state.schema.fingerprint())}`;
5863
+ } catch {
5864
+ channelName = null;
5865
+ }
5866
+ if (channelName !== null) {
5867
+ const syncModule = createMultiTabSyncModule(state, channelName, {
5868
+ isSensitivePath: state.isSensitivePath,
5869
+ noSyncPaths: state.noSyncPaths,
5870
+ validateForm: (form) => {
5871
+ const result = state.schema.validateAtPath(form, void 0, { sync: true });
5872
+ if (result instanceof Promise) return;
5873
+ if (!result.success) {
5874
+ throw new Error("attaform multi-tab sync: post-apply schema validation failed");
5875
+ }
5876
+ }
5877
+ });
5878
+ state.modules.set(MULTI_TAB_SYNC_MODULE_KEY, syncModule);
5879
+ state.registerCleanup(() => syncModule.dispose());
5880
+ }
5881
+ } else if (hasBroadcastChannel && !secureContext) {
5882
+ warnOnceInsecureContext("multiTab");
5883
+ }
5884
+ }
5885
+ if (existing === void 0 && merged.history !== void 0) {
5886
+ const historyModule = createHistoryModule(state, merged.history);
5887
+ state.modules.set(HISTORY_MODULE_KEY, historyModule);
5888
+ state.registerCleanup(() => historyModule.dispose());
5889
+ }
5890
+ if (configuration.key === void 0) {
5891
+ recordAmbientProvide(registry.ssr);
5892
+ vue.provide(paths.kFormContext, state);
5893
+ }
5894
+ const formInstanceId = vue.getCurrentInstance() !== null ? vue.useId() : `atta:form-instance:${formInstanceCounter++}`;
5895
+ if (vue.getCurrentInstance() !== null) {
5896
+ vue.provide(paths.kFormInstanceId, formInstanceId);
5897
+ }
5898
+ const apiOptions = {};
5899
+ if (merged.onInvalidSubmit !== void 0) {
5900
+ apiOptions.onInvalidSubmit = merged.onInvalidSubmit;
5901
+ }
5902
+ const history = state.modules.get(HISTORY_MODULE_KEY);
5903
+ if (history !== void 0) {
5904
+ apiOptions.history = history;
5905
+ }
5906
+ if (merged.validateOn !== void 0) {
5907
+ apiOptions.validateOn = merged.validateOn;
5908
+ }
5909
+ const mergedDebounceMs = merged.debounceMs;
5910
+ if (mergedDebounceMs !== void 0) {
5911
+ apiOptions.debounceMs = mergedDebounceMs;
5912
+ }
5913
+ if (merged.getDisplayState !== void 0) {
5914
+ apiOptions.getDisplayState = merged.getDisplayState;
5915
+ }
5916
+ if (merged.coerce !== void 0) {
5264
5917
  apiOptions.coerce = merged.coerce;
5265
5918
  }
5266
5919
  if (merged.rememberVariants !== void 0) {
@@ -5318,7 +5971,6 @@ function buildFreshState(key, schema, configuration, registry) {
5318
5971
  }
5319
5972
  const resolvedSensitiveNames = configuration.sensitiveNames;
5320
5973
  const resolvedIsSensitivePath = resolvedSensitiveNames === void 0 ? void 0 : paths.createIsSensitivePath(resolvedSensitiveNames);
5321
- const resolvedSegmentMatchesSensitive = resolvedSensitiveNames === void 0 ? void 0 : paths.createSegmentMatchesSensitive(resolvedSensitiveNames);
5322
5974
  const createOptions = {
5323
5975
  formKey: key,
5324
5976
  schema,
@@ -5347,319 +5999,94 @@ function buildFreshState(key, schema, configuration, registry) {
5347
5999
  ...configuration.coerce !== void 0 ? { coerce: configuration.coerce } : {},
5348
6000
  ...configuration.getDisplayState !== void 0 ? { getDisplayState: configuration.getDisplayState } : {},
5349
6001
  ...initialBlankPaths !== void 0 ? { initialBlankPaths } : {},
5350
- ...resolvedIsSensitivePath !== void 0 ? { isSensitivePath: resolvedIsSensitivePath } : {},
5351
- ...resolvedSegmentMatchesSensitive !== void 0 ? { segmentMatchesSensitive: resolvedSegmentMatchesSensitive } : {}
6002
+ ...resolvedIsSensitivePath !== void 0 ? { isSensitivePath: resolvedIsSensitivePath } : {}
5352
6003
  };
5353
6004
  const state = createFormStore(createOptions);
5354
6005
  registry.forms.set(
5355
6006
  key,
5356
6007
  state
5357
6008
  );
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()
5370
- };
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);
5382
- }
5383
- return key;
5384
- }
5385
- if (vue.getCurrentInstance() !== null) {
5386
- return `${ANONYMOUS_FORM_KEY_PREFIX}${vue.useId()}`;
5387
- }
5388
- return `${ANONYMOUS_FORM_KEY_PREFIX}${anonCounter++}`;
5389
- }
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
- );
5409
- }
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;
5419
- }
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
- }
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;
5432
- }
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(", ")} }`;
5439
- }
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,
5448
- min: 0,
5449
- defaultValue: DEFAULT_PERSISTENCE_DEBOUNCE_MS
5450
- });
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
- }
5539
- }
5540
- state.scheduleFieldValidation(
5541
- [],
5542
- true
5543
- /* immediate */
5544
- );
5545
- } catch {
5546
- }
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);
5568
- }
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
- } 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);
5590
- }
5591
- await adapter.setItem(
5592
- key,
5593
- buildPersistedPayload(nextForm, "form+errors", schemaMap, userMap, transientSet)
5594
- );
5595
- }
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;
5613
- }
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;
6009
+ return state;
6010
+ }
6011
+ let anonCounter = 0;
6012
+ let formInstanceCounter = 0;
6013
+ const ambientProvideHistory = paths.__DEV__ ? /* @__PURE__ */ new WeakMap() : null;
6014
+ function recordAmbientProvide(ssr) {
6015
+ if (!paths.__DEV__ || ssr || ambientProvideHistory === null) return;
6016
+ const instance = vue.getCurrentInstance();
6017
+ if (instance === null) return;
6018
+ const instanceKey = instance;
6019
+ const entry = {
6020
+ source: paths.captureUserCallSite()
6021
+ };
6022
+ const existing = ambientProvideHistory.get(instanceKey);
6023
+ if (existing === void 0) {
6024
+ ambientProvideHistory.set(instanceKey, [entry]);
6025
+ return;
6026
+ }
6027
+ existing.push(entry);
6028
+ }
6029
+ function resolveFormKey(key) {
6030
+ if (key !== void 0 && key !== null && key !== "") {
6031
+ if (key.startsWith(RESERVED_KEY_PREFIX)) {
6032
+ throw new paths.ReservedFormKeyError(key);
5626
6033
  }
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
- );
6034
+ return key;
5635
6035
  }
5636
- function awaitPendingWrites() {
5637
- if (inFlightFinalFlush !== null) return inFlightFinalFlush;
5638
- if (disposed) return Promise.resolve();
5639
- return writer.flush().catch(() => void 0);
6036
+ if (vue.getCurrentInstance() !== null) {
6037
+ return `${ANONYMOUS_FORM_KEY_PREFIX}${vue.useId()}`;
5640
6038
  }
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
- });
6039
+ return `${ANONYMOUS_FORM_KEY_PREFIX}${anonCounter++}`;
6040
+ }
6041
+ function warnOnSchemaFingerprintMismatch(key, existing, incoming) {
6042
+ let existingFp;
6043
+ let incomingFp;
6044
+ try {
6045
+ existingFp = existing.fingerprint();
6046
+ incomingFp = incoming.fingerprint();
6047
+ } catch (error) {
6048
+ console.error(
6049
+ `[attaform] fingerprint() threw for key "${key}"; skipping mismatch check.`,
6050
+ error
6051
+ );
6052
+ return;
5649
6053
  }
5650
- return {
5651
- wiredConfig: config,
5652
- writePathImmediately,
5653
- clearPersistedDraft,
5654
- awaitPendingWrites,
5655
- dispose
5656
- };
6054
+ if (existingFp === incomingFp) return;
6055
+ console.warn(
6056
+ `[attaform] useForm() calls with key "${key}" use different schemas; first wins, second is ignored. Use identical schemas or unique keys.
6057
+ existing: ${existingFp}
6058
+ incoming: ${incomingFp}`
6059
+ );
5657
6060
  }
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;
6061
+ function warnOnPersistDivergence(key, existing, incomingPersist) {
6062
+ if (incomingPersist === void 0) return;
6063
+ const wired = existing.modules.get(PERSISTENCE_MODULE_KEY);
6064
+ const incomingNormalized = normalizePersistConfig(incomingPersist);
6065
+ if (wired === void 0) {
6066
+ console.warn(
6067
+ `[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.`
6068
+ );
6069
+ return;
6070
+ }
6071
+ if (persistConfigsEquivalent(wired.wiredConfig, incomingNormalized)) return;
6072
+ console.warn(
6073
+ `[attaform] useForm({ key: "${key}" }) passed a persist config that differs from the first useForm({ key }) call's; first wins, this one is ignored.
6074
+ wired: ${describePersist(wired.wiredConfig)}
6075
+ incoming: ${describePersist(incomingNormalized)}`
6076
+ );
6077
+ }
6078
+ function persistConfigsEquivalent(a, b) {
6079
+ if (a.storage !== b.storage) return false;
6080
+ if ((a.key ?? void 0) !== (b.key ?? void 0)) return false;
6081
+ if ((a.debounceMs ?? void 0) !== (b.debounceMs ?? void 0)) return false;
6082
+ return true;
6083
+ }
6084
+ function describePersist(config) {
6085
+ const storage = typeof config.storage === "string" ? config.storage : "custom-adapter";
6086
+ const parts = [`storage=${storage}`];
6087
+ if (config.key !== void 0) parts.push(`key=${config.key}`);
6088
+ if (config.debounceMs !== void 0) parts.push(`debounceMs=${config.debounceMs}`);
6089
+ return `{ ${parts.join(", ")} }`;
5663
6090
  }
5664
6091
  const warnedAnonPersistKeys = /* @__PURE__ */ new Set();
5665
6092
  function enforceAnonPersistRule(formKey, ssr) {
@@ -5677,33 +6104,6 @@ function enforceAnonPersistRule(formKey, ssr) {
5677
6104
  }
5678
6105
  return true;
5679
6106
  }
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
6107
 
5708
6108
  let injectedInstanceCounter = 0;
5709
6109
  function injectForm(input) {
@@ -5783,8 +6183,6 @@ function isLazyMarker(value) {
5783
6183
  }
5784
6184
 
5785
6185
  const NOOP_WIZARD_HISTORY = {
5786
- push() {
5787
- },
5788
6186
  replace() {
5789
6187
  },
5790
6188
  read() {
@@ -5811,21 +6209,16 @@ function createWizardHistory(param) {
5811
6209
  for (const subscriber of subscribers) subscriber(value);
5812
6210
  }
5813
6211
  window.addEventListener("popstate", handlePopstate);
5814
- function safeWriteState(key, op) {
6212
+ function safeReplaceState(key) {
5815
6213
  try {
5816
- const fn = op === "push" ? window.history.pushState : window.history.replaceState;
5817
- fn.call(window.history, {}, "", buildUrl(key));
6214
+ window.history.replaceState({}, "", buildUrl(key));
5818
6215
  } catch {
5819
6216
  }
5820
6217
  }
5821
6218
  return {
5822
- push(key) {
5823
- if (disposed) return;
5824
- safeWriteState(key, "push");
5825
- },
5826
6219
  replace(key) {
5827
6220
  if (disposed) return;
5828
- safeWriteState(key, "replace");
6221
+ safeReplaceState(key);
5829
6222
  },
5830
6223
  read() {
5831
6224
  const url = new URL(window.location.href);
@@ -5884,68 +6277,16 @@ function buildWizardStatusesProxy(statuses) {
5884
6277
  }
5885
6278
  return result;
5886
6279
  });
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
6280
+ return buildCallableReadonlySnapshotProxy({
6281
+ surface: "wizard.statuses",
6282
+ snapshot: () => snapshot.value,
6283
+ resolveKey: (key) => statuses[key]?.value,
6284
+ // Single-key callable form. Strings stringify naturally; non-
6285
+ // string args coerce via `String(arg)` and miss the lookup, which
6286
+ // resolves to `undefined` (consistent with property-access).
6287
+ resolveCall: (arg) => statuses[String(arg)]?.value,
6288
+ ownKeys: () => Object.keys(statuses),
6289
+ hasKey: (key) => Object.hasOwn(statuses, key)
5949
6290
  });
5950
6291
  }
5951
6292
 
@@ -5962,6 +6303,12 @@ const NOOP_VALID_STATUS = {
5962
6303
  submitted: false,
5963
6304
  errorCount: 0
5964
6305
  };
6306
+ function asStatusSource(form) {
6307
+ return form;
6308
+ }
6309
+ function asSubmissionSource(form) {
6310
+ return form;
6311
+ }
5965
6312
  function useWizard(options) {
5966
6313
  const rawSteps = Array.isArray(options.steps) ? options.steps : [];
5967
6314
  if (rawSteps.length === 0 && paths.__DEV__) {
@@ -6049,10 +6396,12 @@ function useWizard(options) {
6049
6396
  return { configurable: true, enumerable: true, writable: false, value: form };
6050
6397
  }
6051
6398
  });
6052
- const slotCtx = vue.computed(() => ({
6399
+ const slotCtx = {
6053
6400
  forms: slotForms,
6054
- currentKey: activeKey.value === "" ? void 0 : activeKey.value
6055
- }));
6401
+ get currentKey() {
6402
+ return activeKey.value === "" ? void 0 : activeKey.value;
6403
+ }
6404
+ };
6056
6405
  function resolveSlot(slot, index, ctx) {
6057
6406
  if (typeof slot === "string") {
6058
6407
  return getOrBuildNoop(slot);
@@ -6075,12 +6424,6 @@ function useWizard(options) {
6075
6424
  }
6076
6425
  return result;
6077
6426
  }
6078
- const lazyCtx = {
6079
- forms: slotForms,
6080
- get currentKey() {
6081
- return activeKey.value === "" ? void 0 : activeKey.value;
6082
- }
6083
- };
6084
6427
  for (let i = 0; i < rawSteps.length; i++) {
6085
6428
  const slot = rawSteps[i];
6086
6429
  if (isLazyMarker(slot)) {
@@ -6090,18 +6433,17 @@ function useWizard(options) {
6090
6433
  idx,
6091
6434
  vue.computed(() => {
6092
6435
  void lazyEpoch.value;
6093
- return marker.resolve(lazyCtx);
6436
+ return marker.resolve(slotCtx);
6094
6437
  })
6095
6438
  );
6096
6439
  }
6097
6440
  }
6098
6441
  const compiledSteps = vue.computed(() => {
6099
- const ctx = slotCtx.value;
6100
6442
  const out = [];
6101
6443
  const seen = /* @__PURE__ */ new Set();
6102
6444
  for (let i = 0; i < rawSteps.length; i++) {
6103
6445
  const slot = rawSteps[i];
6104
- const form = resolveSlot(slot, i, ctx);
6446
+ const form = resolveSlot(slot, i, slotCtx);
6105
6447
  if (form === void 0) continue;
6106
6448
  if (seen.has(form.key)) {
6107
6449
  if (paths.__DEV__) {
@@ -6127,9 +6469,12 @@ function useWizard(options) {
6127
6469
  return -1;
6128
6470
  });
6129
6471
  const currentStep = vue.computed(() => {
6130
- const key = activeKey.value;
6131
- if (key !== "") return key;
6132
- const first = compiledSteps.value[0];
6472
+ const list = compiledSteps.value;
6473
+ const idx = activeIndex.value;
6474
+ if (idx >= 0 && idx < list.length) {
6475
+ return list[idx].key;
6476
+ }
6477
+ const first = list[0];
6133
6478
  return first === void 0 ? void 0 : first.key;
6134
6479
  });
6135
6480
  const activeForm = vue.computed(() => {
@@ -6152,36 +6497,94 @@ function useWizard(options) {
6152
6497
  for (const step of compiledSteps.value) out[step.key] = step.form;
6153
6498
  return out;
6154
6499
  });
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;
6500
+ function isFormReady(key) {
6501
+ const store = registry.forms.get(key);
6502
+ return store?.defaultsResolved.value === true;
6503
+ }
6504
+ function toWizardAggregateError(err, fallbackKey) {
6505
+ const entry = {
6506
+ formKey: err.formKey ?? fallbackKey,
6507
+ path: err.path,
6508
+ message: err.message
6509
+ };
6510
+ if (err.code !== void 0) entry.code = err.code;
6511
+ return entry;
6512
+ }
6513
+ const valuesCache = /* @__PURE__ */ new Map();
6514
+ function valuesFor(form) {
6515
+ const cached = valuesCache.get(form.key);
6516
+ if (cached !== void 0) return cached;
6517
+ const source = asStatusSource(form);
6518
+ const computedValues = vue.computed(() => source.values);
6519
+ valuesCache.set(form.key, computedValues);
6520
+ return computedValues;
6521
+ }
6522
+ const errorsCache = /* @__PURE__ */ new Map();
6523
+ function errorsFor(form) {
6524
+ const cached = errorsCache.get(form.key);
6525
+ if (cached !== void 0) return cached;
6526
+ const source = asStatusSource(form);
6527
+ const computedErrors = vue.computed(() => {
6528
+ if (!isFormReady(form.key)) return [];
6529
+ const errors = source.meta?.errors ?? [];
6530
+ const list = [];
6531
+ for (const err of errors) list.push(toWizardAggregateError(err, form.key));
6532
+ return list;
6533
+ });
6534
+ errorsCache.set(form.key, computedErrors);
6535
+ return computedErrors;
6536
+ }
6537
+ const allValues = new Proxy({}, {
6538
+ get(_, key) {
6539
+ if (typeof key !== "string") return void 0;
6540
+ const form = formsRecord.value[key];
6541
+ if (form === void 0) return void 0;
6542
+ return valuesFor(form).value;
6543
+ },
6544
+ has(_, key) {
6545
+ if (typeof key !== "string") return false;
6546
+ return formsRecord.value[key] !== void 0;
6547
+ },
6548
+ ownKeys() {
6549
+ return Object.keys(formsRecord.value);
6550
+ },
6551
+ getOwnPropertyDescriptor(_, key) {
6552
+ if (typeof key !== "string") return void 0;
6553
+ const form = formsRecord.value[key];
6554
+ if (form === void 0) return void 0;
6555
+ return {
6556
+ configurable: true,
6557
+ enumerable: true,
6558
+ writable: false,
6559
+ value: valuesFor(form).value
6560
+ };
6160
6561
  }
6161
- return out;
6162
6562
  });
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;
6563
+ const allErrors = new Proxy({}, {
6564
+ get(_, key) {
6565
+ if (typeof key !== "string") return void 0;
6566
+ const form = formsRecord.value[key];
6567
+ if (form === void 0) return void 0;
6568
+ return errorsFor(form).value;
6569
+ },
6570
+ has(_, key) {
6571
+ if (typeof key !== "string") return false;
6572
+ return formsRecord.value[key] !== void 0;
6573
+ },
6574
+ ownKeys() {
6575
+ return Object.keys(formsRecord.value);
6576
+ },
6577
+ getOwnPropertyDescriptor(_, key) {
6578
+ if (typeof key !== "string") return void 0;
6579
+ const form = formsRecord.value[key];
6580
+ if (form === void 0) return void 0;
6581
+ return {
6582
+ configurable: true,
6583
+ enumerable: true,
6584
+ writable: false,
6585
+ value: errorsFor(form).value
6586
+ };
6183
6587
  }
6184
- return out;
6185
6588
  });
6186
6589
  const seedRef = vue.ref(void 0);
6187
6590
  const seedInput = options.defaultStatuses;
@@ -6204,11 +6607,9 @@ function useWizard(options) {
6204
6607
  function statusFor(form) {
6205
6608
  const cached = statusCache.get(form.key);
6206
6609
  if (cached !== void 0) return cached;
6207
- const source = form;
6610
+ const source = asStatusSource(form);
6208
6611
  const computedStatus = vue.computed(() => {
6209
- const store = registry.forms.get(form.key);
6210
- const resolved = store?.defaultsResolved.value === true;
6211
- if (resolved) {
6612
+ if (isFormReady(form.key)) {
6212
6613
  const meta = source.meta;
6213
6614
  if (meta !== void 0 && meta !== null) {
6214
6615
  return {
@@ -6351,7 +6752,7 @@ function useWizard(options) {
6351
6752
  }
6352
6753
  if (!registry.ssr) {
6353
6754
  for (const step of compiledSteps.value) {
6354
- const source = step.form;
6755
+ const source = asSubmissionSource(step.form);
6355
6756
  if (typeof source.activate === "function") void source.activate();
6356
6757
  }
6357
6758
  }
@@ -6395,7 +6796,7 @@ function useWizard(options) {
6395
6796
  const submissionAttempts = vue.ref(0);
6396
6797
  const done = vue.ref(false);
6397
6798
  function activateForm(form) {
6398
- const source = form;
6799
+ const source = asSubmissionSource(form);
6399
6800
  if (typeof source.activate === "function") {
6400
6801
  void source.activate();
6401
6802
  }
@@ -6495,7 +6896,7 @@ function useWizard(options) {
6495
6896
  };
6496
6897
  }
6497
6898
  async function processOne(form) {
6498
- const full = form;
6899
+ const full = asSubmissionSource(form);
6499
6900
  let activationFailure;
6500
6901
  try {
6501
6902
  if (typeof full.activate === "function") await full.activate();
@@ -6527,15 +6928,7 @@ function useWizard(options) {
6527
6928
  for (const step of compiledSteps.value) {
6528
6929
  const processed = results.get(step.key);
6529
6930
  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
- }
6931
+ for (const err of processed.errors) out.push(toWizardAggregateError(err, step.key));
6539
6932
  }
6540
6933
  return out;
6541
6934
  }
@@ -6589,8 +6982,7 @@ function useWizard(options) {
6589
6982
  if (processed !== void 0 && processed.success === true) {
6590
6983
  valuesMap[step.key] = processed.data;
6591
6984
  } else {
6592
- const source = step.form;
6593
- valuesMap[step.key] = source.values;
6985
+ valuesMap[step.key] = asStatusSource(step.form).values;
6594
6986
  }
6595
6987
  }
6596
6988
  const ctx = buildSubmitContext(valuesMap, currentKey, final);
@@ -6611,8 +7003,11 @@ function useWizard(options) {
6611
7003
  moveTo(firstFailedKey);
6612
7004
  await vue.nextTick();
6613
7005
  const failedForm = formsRecord.value[firstFailedKey];
6614
- if (failedForm !== void 0 && typeof failedForm.applyInvalidSubmitPolicy === "function") {
6615
- failedForm.applyInvalidSubmitPolicy();
7006
+ if (failedForm !== void 0) {
7007
+ const failedSource = asSubmissionSource(failedForm);
7008
+ if (typeof failedSource.applyInvalidSubmitPolicy === "function") {
7009
+ failedSource.applyInvalidSubmitPolicy();
7010
+ }
6616
7011
  }
6617
7012
  }
6618
7013
  }
@@ -6627,7 +7022,7 @@ function useWizard(options) {
6627
7022
  done.value = false;
6628
7023
  lazyEpoch.value += 1;
6629
7024
  for (const step of compiledSteps.value) {
6630
- const full = step.form;
7025
+ const full = asSubmissionSource(step.form);
6631
7026
  if (typeof full.reset === "function") full.reset();
6632
7027
  }
6633
7028
  const firstStep = compiledSteps.value[0];
@@ -6677,12 +7072,8 @@ function useWizard(options) {
6677
7072
  return count.value;
6678
7073
  },
6679
7074
  statuses,
6680
- get allValues() {
6681
- return allValues.value;
6682
- },
6683
- get allErrors() {
6684
- return allErrors.value;
6685
- },
7075
+ allValues,
7076
+ allErrors,
6686
7077
  get progress() {
6687
7078
  return progress.value;
6688
7079
  },
@@ -6825,9 +7216,11 @@ exports.isPlainRecord = isPlainRecord;
6825
7216
  exports.isUnset = isUnset;
6826
7217
  exports.lazy = lazy;
6827
7218
  exports.normalizeNumericOption = normalizeNumericOption;
7219
+ exports.safeAssign = safeAssign;
7220
+ exports.safeOwnRead = safeOwnRead;
6828
7221
  exports.setAtPath = setAtPath;
6829
7222
  exports.slimKindOf = slimKindOf;
6830
7223
  exports.unset = unset;
6831
7224
  exports.useAbstractForm = useAbstractForm;
6832
7225
  exports.useWizard = useWizard;
6833
- //# sourceMappingURL=attaform.C1msmO2v.cjs.map
7226
+ //# sourceMappingURL=attaform.DLnE5bZa.cjs.map