attaform 0.19.0 → 0.20.1

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