attaform 0.19.0 → 0.20.0

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