attaform 0.18.2 → 0.20.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (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 +4 -7
  11. package/dist/index.cjs.map +1 -1
  12. package/dist/index.d.cts +77 -110
  13. package/dist/index.d.mts +77 -110
  14. package/dist/index.d.ts +77 -110
  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.CDmaxrt2.mjs → attaform.BKozEdTr.mjs} +305 -178
  25. package/dist/shared/attaform.BKozEdTr.mjs.map +1 -0
  26. package/dist/shared/{attaform.Bubm_slq.cjs → attaform.BM6YD9kZ.cjs} +212 -269
  27. package/dist/shared/attaform.BM6YD9kZ.cjs.map +1 -0
  28. package/dist/shared/{attaform.5UhpSVFI.cjs → attaform.BPxsYtTe.cjs} +2 -26
  29. package/dist/shared/attaform.BPxsYtTe.cjs.map +1 -0
  30. package/dist/shared/{attaform.BqK_L4gK.cjs → attaform.BPy-4qRx.cjs} +305 -180
  31. package/dist/shared/attaform.BPy-4qRx.cjs.map +1 -0
  32. package/dist/shared/attaform.BWgAFnsj.mjs +770 -0
  33. package/dist/shared/attaform.BWgAFnsj.mjs.map +1 -0
  34. package/dist/shared/{attaform.CGX1CNpz.d.ts → attaform.Bh3ACtts.d.ts} +152 -111
  35. package/dist/shared/{attaform.CXpzmj38.mjs → attaform.BupwXkj_.mjs} +213 -270
  36. package/dist/shared/attaform.BupwXkj_.mjs.map +1 -0
  37. package/dist/shared/{attaform.Dlk1jMuv.cjs → attaform.CIn4bMsD.cjs} +263 -799
  38. package/dist/shared/attaform.CIn4bMsD.cjs.map +1 -0
  39. package/dist/shared/{attaform.CZ-XtZt_.mjs → attaform.CKFbKFb6.mjs} +2265 -1509
  40. package/dist/shared/attaform.CKFbKFb6.mjs.map +1 -0
  41. package/dist/shared/{attaform.CuN7ZhBy.d.cts → attaform.D5-1XGQU.d.cts} +152 -111
  42. package/dist/shared/{attaform.-1GQTX2T.mjs → attaform.DEBvCjeH.mjs} +257 -793
  43. package/dist/shared/attaform.DEBvCjeH.mjs.map +1 -0
  44. package/dist/shared/{attaform.II89Pcf4.cjs → attaform.DL4CQ-oW.cjs} +2270 -1514
  45. package/dist/shared/attaform.DL4CQ-oW.cjs.map +1 -0
  46. package/dist/shared/{attaform.FnEwjhvX.d.ts → attaform.DSD85fHb.d.cts} +1 -19
  47. package/dist/shared/{attaform.CRmmNAYp.d.cts → attaform.DSD85fHb.d.mts} +1 -19
  48. package/dist/shared/{attaform.D9wuTGu9.d.mts → attaform.DSD85fHb.d.ts} +1 -19
  49. package/dist/shared/{attaform.B7rzpK1U.d.cts → attaform.DkA5J8NW.d.cts} +1 -17
  50. package/dist/shared/{attaform.B7rzpK1U.d.mts → attaform.DkA5J8NW.d.mts} +1 -17
  51. package/dist/shared/{attaform.B7rzpK1U.d.ts → attaform.DkA5J8NW.d.ts} +1 -17
  52. package/dist/shared/{attaform.B957T6NU.d.ts → attaform.Dl5kDY-A.d.ts} +1 -1
  53. package/dist/shared/attaform.Dmb6itxC.cjs +781 -0
  54. package/dist/shared/attaform.Dmb6itxC.cjs.map +1 -0
  55. package/dist/shared/{attaform.M-RanbyV.d.mts → attaform.DoKXru-a.d.mts} +1 -1
  56. package/dist/shared/attaform.DvA-CJJW.mjs +1876 -0
  57. package/dist/shared/attaform.DvA-CJJW.mjs.map +1 -0
  58. package/dist/shared/{attaform.D1gzu2GL.d.mts → attaform.EMzJcQci.d.mts} +152 -111
  59. package/dist/shared/attaform.EZG6fOFb.mjs +35 -0
  60. package/dist/shared/attaform.EZG6fOFb.mjs.map +1 -0
  61. package/dist/shared/{attaform.XDjA7sRz.d.cts → attaform.GbDo_lJi.d.cts} +1 -1
  62. package/dist/shared/{attaform.Ca5_6Ky-.d.mts → attaform.SfhU0OEY.d.cts} +499 -116
  63. package/dist/shared/{attaform.Ca5_6Ky-.d.cts → attaform.SfhU0OEY.d.mts} +499 -116
  64. package/dist/shared/{attaform.Ca5_6Ky-.d.ts → attaform.SfhU0OEY.d.ts} +499 -116
  65. package/dist/shared/attaform.jgzuNZVC.cjs +1882 -0
  66. package/dist/shared/attaform.jgzuNZVC.cjs.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 -2
  93. package/dist/shared/attaform.-1GQTX2T.mjs.map +0 -1
  94. package/dist/shared/attaform.5UhpSVFI.cjs.map +0 -1
  95. package/dist/shared/attaform.BqK_L4gK.cjs.map +0 -1
  96. package/dist/shared/attaform.Bubm_slq.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.CDmaxrt2.mjs.map +0 -1
  100. package/dist/shared/attaform.CXpzmj38.mjs.map +0 -1
  101. package/dist/shared/attaform.CZ-XtZt_.mjs.map +0 -1
  102. package/dist/shared/attaform.D13GMFgK.mjs +0 -32
  103. package/dist/shared/attaform.D13GMFgK.mjs.map +0 -1
  104. package/dist/shared/attaform.DUHru0OF.cjs +0 -1600
  105. package/dist/shared/attaform.DUHru0OF.cjs.map +0 -1
  106. package/dist/shared/attaform.Df0tU0Ut.mjs +0 -1594
  107. package/dist/shared/attaform.Df0tU0Ut.mjs.map +0 -1
  108. package/dist/shared/attaform.Dl161U6E.mjs +0 -57
  109. package/dist/shared/attaform.Dl161U6E.mjs.map +0 -1
  110. package/dist/shared/attaform.Dlk1jMuv.cjs.map +0 -1
  111. package/dist/shared/attaform.II89Pcf4.cjs.map +0 -1
@@ -1,5 +1,5 @@
1
- import { computed, ref, watchEffect, getCurrentScope, onScopeDispose, shallowReadonly, readonly, reactive, watch, markRaw, toRaw, shallowRef, getCurrentInstance, onServerPrefetch, provide, useId, inject, effectScope, nextTick } from 'vue';
2
- import { i as canonicalizePath, H as segmentsForPathKey, t as isPathPrefix, b as FORM_ERRORS_PATH_KEY, _ as __DEV__, E as pathKeyToDotted, g as SubmitErrorHandlerError, A as AnonPersistError, j as captureUserCallSite, 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.CDmaxrt2.mjs';
1
+ import { computed, ref, watchEffect, getCurrentScope, onScopeDispose, shallowReadonly, readonly, reactive, toRaw, watch, markRaw, shallowRef, getCurrentInstance, onServerPrefetch, provide, useId, inject, effectScope, nextTick } from 'vue';
2
+ import { _ as __DEV__, j as canonicalizePath, E as segmentsForPathKey, s as isPathPrefix, b as FORM_ERRORS_PATH_KEY, S as SubmitErrorHandlerError, A as AnonPersistError, k as captureUserCallSite, I as INTERACTIVE_TAG_NAMES, e as ROOT_PATH_KEY, R as ROOT_PATH, h as allowSensitivePersist, F as FORM_ERRORS_PATH, l as coerceToPathKey, u as isSensitivePath, o as createPersistOptInRegistry, d as InvalidUseFormConfigError, q as ensureAttaformInstalled, H as useRegistry, y as kFormContext, z as kFormInstanceId, g as ReservedFormKeyError, n as createIsSensitivePath, x as kAttaformWizardActiveStepResolver, v as kAttaformAncestorWizard } from './attaform.BKozEdTr.mjs';
3
3
 
4
4
  const NOT_FOUND = Symbol("NOT_FOUND");
5
5
  function descendStep(value, segment) {
@@ -62,7 +62,7 @@ function setAtPathOffset(root, path, value, offset) {
62
62
  arr[head] = setAtPathOffset(arr[head], path, value, nextOffset);
63
63
  return arr;
64
64
  }
65
- const rec = isPlainRecord(root) ? { ...root } : {};
65
+ const rec = isPlainRecord(root) ? Object.assign(/* @__PURE__ */ Object.create(null), root) : /* @__PURE__ */ Object.create(null);
66
66
  rec[head] = setAtPathOffset(rec[head], path, value, nextOffset);
67
67
  return rec;
68
68
  }
@@ -163,7 +163,7 @@ function mergeStructuralImpl(schema, scratch, consumer, defaultValue) {
163
163
  return consumer;
164
164
  }
165
165
  let mutated = false;
166
- const out = { ...consumer };
166
+ const out = Object.assign(/* @__PURE__ */ Object.create(null), consumer);
167
167
  for (const key of Object.keys(defaultValue)) {
168
168
  if (!(key in consumer)) {
169
169
  const defAtKey = defaultValue[key];
@@ -427,13 +427,81 @@ function structuralSnapshot(value) {
427
427
  return out2;
428
428
  }
429
429
  const src = value;
430
- const out = {};
430
+ const out = /* @__PURE__ */ Object.create(null);
431
431
  for (const k of Object.keys(src)) {
432
432
  out[k] = structuralSnapshot(src[k]);
433
433
  }
434
434
  return out;
435
435
  }
436
436
 
437
+ const defaultDisplayState = (field, formMeta) => {
438
+ const gateOpen = formMeta.submissionAttempts > 0 || field.blurredAfterInteraction === true;
439
+ if (!gateOpen) return "idle";
440
+ if (field.validating === true) return "pending";
441
+ const hasOwnError = field.errors.some(
442
+ (e) => e.path.length === field.path.length && e.path.every((s, i) => s === field.path[i])
443
+ );
444
+ if (hasOwnError) return "error";
445
+ if (field.valid === true && field.blank !== true && field.dirty === true) return "success";
446
+ return "idle";
447
+ };
448
+ function resolveGetDisplayState(config) {
449
+ return config ?? defaultDisplayState;
450
+ }
451
+
452
+ const DEFAULT_FIELD_VALIDATION_DEBOUNCE_MS = 0;
453
+ const DEFAULT_PERSISTENCE_DEBOUNCE_MS = 300;
454
+ const DEFAULT_HISTORY_MAX_SNAPSHOTS = 128;
455
+ const PERSISTENCE_KEY_PREFIX = "attaform:";
456
+ const RESERVED_KEY_PREFIX = "__atta:";
457
+ const ANONYMOUS_FORM_KEY_PREFIX = `${RESERVED_KEY_PREFIX}anon:`;
458
+ const ANONYMOUS_WIZARD_KEY_PREFIX = `${RESERVED_KEY_PREFIX}anon-wizard:`;
459
+ const DEFAULT_MAX_RECURSION_DEPTH = 64;
460
+ function normalizeNumericOption(config) {
461
+ const { value, source, allowInfinity, min, defaultValue } = config;
462
+ if (allowInfinity && value === Infinity) return Infinity;
463
+ if (typeof value !== "number" || Number.isNaN(value) || value === Infinity || value === -Infinity) {
464
+ if (__DEV__) {
465
+ const acceptedDescription = allowInfinity ? "a non-negative integer or Infinity" : "a non-negative finite integer";
466
+ console.warn(
467
+ `[attaform] ${source} must be ${acceptedDescription}; got ${String(value)}. Falling back to ${String(defaultValue)}.`
468
+ );
469
+ }
470
+ return defaultValue;
471
+ }
472
+ return Math.max(min, Math.floor(value));
473
+ }
474
+
475
+ function hashStableString(input, seed = 0) {
476
+ let h1 = 3735928559 ^ seed;
477
+ let h2 = 1103547991 ^ seed;
478
+ for (let i = 0; i < input.length; i++) {
479
+ const ch = input.charCodeAt(i);
480
+ h1 = Math.imul(h1 ^ ch, 2654435761);
481
+ h2 = Math.imul(h2 ^ ch, 1597334677);
482
+ }
483
+ h1 = Math.imul(h1 ^ h1 >>> 16, 2246822507) ^ Math.imul(h2 ^ h2 >>> 13, 3266489909);
484
+ h2 = Math.imul(h2 ^ h2 >>> 16, 2246822507) ^ Math.imul(h1 ^ h1 >>> 13, 3266489909);
485
+ return (4294967296 * (2097151 & h2) + (h1 >>> 0)).toString(36).padStart(11, "0");
486
+ }
487
+
488
+ const ANON_STEM = "atta";
489
+ function readableFormKeyStem(formKey) {
490
+ if (formKey === "" || formKey.startsWith(ANONYMOUS_FORM_KEY_PREFIX)) return ANON_STEM;
491
+ const sanitized = formKey.replace(/[^A-Za-z0-9_-]+/g, "-").replace(/^-+|(?<!-)-+$/g, "");
492
+ return sanitized === "" ? ANON_STEM : sanitized;
493
+ }
494
+ function fieldIdToken(formInstanceId, pathKey) {
495
+ return hashStableString(`${formInstanceId}:${pathKey}`).slice(-7);
496
+ }
497
+ function computeFieldIdentity(formInstanceId, formKey, pathKey) {
498
+ const id = `${readableFormKeyStem(formKey)}-${fieldIdToken(formInstanceId, pathKey)}`;
499
+ return {
500
+ id,
501
+ aria: Object.freeze({ errorId: `${id}-error`, descriptionId: `${id}-description` })
502
+ };
503
+ }
504
+
437
505
  const EMPTY_RESOLVED_FIELD_META = Object.freeze({
438
506
  label: "",
439
507
  description: void 0,
@@ -454,6 +522,7 @@ function humanize(segment) {
454
522
  }).join(" ");
455
523
  }
456
524
 
525
+ const warnedDisplayStatePredicates = /* @__PURE__ */ new WeakSet();
457
526
  function isUnderStubAncestor(state, segments) {
458
527
  for (let i = 0; i < segments.length; i++) {
459
528
  const ancestorPath = segments.slice(0, i);
@@ -467,21 +536,21 @@ function isUnderStubAncestor(state, segments) {
467
536
  }
468
537
  return false;
469
538
  }
470
- function buildFieldStateAccessor(state, getFormMetaBase, options) {
539
+ function buildFieldStateAccessor(state, formInstanceId, getFormMetaBase, options) {
471
540
  const cache = /* @__PURE__ */ new Map();
472
- const predicate = options?.shouldShowErrors;
541
+ const predicate = options?.getDisplayState;
473
542
  return function getFieldState(pathInput) {
474
543
  const { segments, key } = canonicalizePath(pathInput);
475
544
  const cached = cache.get(key);
476
545
  if (cached !== void 0) return cached;
477
546
  const c = computed(
478
- () => state.schema.isLeafAtPath(segments) ? buildLeafFieldState(state, segments, key, getFormMetaBase, predicate) : buildContainerFieldState(state, segments, key, getFormMetaBase, predicate)
547
+ () => state.schema.isLeafAtPath(segments) ? buildLeafFieldState(state, segments, key, formInstanceId, getFormMetaBase, predicate) : buildContainerFieldState(state, segments, key, formInstanceId, getFormMetaBase, predicate)
479
548
  );
480
549
  cache.set(key, c);
481
550
  return c;
482
551
  };
483
552
  }
484
- function buildLeafFieldStateBase(state, segments, key) {
553
+ function buildLeafFieldStateBase(state, segments, key, formInstanceId) {
485
554
  const record = state.fields.get(key);
486
555
  const value = state.getValueAtPath(segments);
487
556
  const original = state.originals.get(key)?.value;
@@ -511,6 +580,8 @@ function buildLeafFieldStateBase(state, segments, key) {
511
580
  focused: record?.focused ?? null,
512
581
  blurred: record?.blurred ?? null,
513
582
  touched: record?.touched ?? false,
583
+ interacted: record?.interacted ?? false,
584
+ blurredAfterInteraction: record?.blurredAfterInteraction ?? false,
514
585
  connected: record?.connected ?? false,
515
586
  element: firstElement,
516
587
  elements: elementsArr,
@@ -519,6 +590,8 @@ function buildLeafFieldStateBase(state, segments, key) {
519
590
  validating,
520
591
  valid,
521
592
  path: segments,
593
+ ...computeFieldIdentity(formInstanceId, state.formKey, key),
594
+ key: state.arrayElementKey(segments),
522
595
  blank: state.blankPaths.has(key),
523
596
  label,
524
597
  description: resolved.description,
@@ -526,31 +599,32 @@ function buildLeafFieldStateBase(state, segments, key) {
526
599
  meta: resolved.meta
527
600
  };
528
601
  }
529
- function buildLeafFieldState(state, segments, key, getFormMetaBase, shouldShowErrors) {
530
- const base = buildLeafFieldStateBase(state, segments, key);
531
- return decorateWithDerivedProps(base, state, getFormMetaBase, shouldShowErrors);
602
+ function buildLeafFieldState(state, segments, key, formInstanceId, getFormMetaBase, getDisplayState) {
603
+ const base = buildLeafFieldStateBase(state, segments, key, formInstanceId);
604
+ return decorateWithDerivedProps(base, state, getFormMetaBase, getDisplayState);
532
605
  }
533
- function buildContainerFieldStateBase(state, segments, _key) {
606
+ function buildContainerFieldStateBase(state, segments, key, formInstanceId) {
534
607
  const formValue = state.form.value;
535
608
  const value = state.getValueAtPath(segments);
536
- const original = state.originals.get(canonicalizePath(segments).key)?.value;
609
+ const original = state.originals.get(key)?.value;
537
610
  let pristine = true;
538
611
  let blank = true;
539
612
  let dirty = false;
540
613
  let focused = false;
541
614
  let blurred = false;
542
615
  let touched = false;
616
+ let interacted = false;
617
+ let blurredAfterInteraction = false;
543
618
  let connected = false;
544
619
  let validating = false;
545
620
  let updatedAt = null;
546
621
  let asyncPending = false;
547
- for (const [, entry] of state.originals) {
622
+ for (const [leafKey, entry] of state.originals) {
548
623
  if (!isPathPrefix(segments, entry.segments)) continue;
549
624
  if (segments.length === entry.segments.length) continue;
550
625
  if (!hasAtPath(formValue, entry.segments)) continue;
551
- const leafKey = canonicalizePath(entry.segments).key;
552
626
  const leafRecord = state.fields.get(leafKey);
553
- if (!state.isPristineAtPath(entry.segments)) {
627
+ if (!state.isPristineAtPathByKey(leafKey, entry.segments)) {
554
628
  pristine = false;
555
629
  dirty = true;
556
630
  }
@@ -558,14 +632,20 @@ function buildContainerFieldStateBase(state, segments, _key) {
558
632
  if (leafRecord?.focused === true) focused = true;
559
633
  if (leafRecord?.blurred === true) blurred = true;
560
634
  if (leafRecord?.touched === true) touched = true;
635
+ if (leafRecord?.interacted === true) interacted = true;
636
+ if (leafRecord?.blurredAfterInteraction === true) blurredAfterInteraction = true;
561
637
  if (leafRecord?.connected === true) connected = true;
562
638
  if ((state.fieldValidationCounts.get(leafKey) ?? 0) > 0) validating = true;
563
- if (state.pathHasAsyncValidation(entry.segments)) asyncPending = true;
639
+ if (state.pathHasAsyncValidationByKey(leafKey, entry.segments)) asyncPending = true;
564
640
  const ts = leafRecord?.updatedAt;
565
641
  if (ts !== void 0 && ts !== null) {
566
642
  if (updatedAt === null || ts > updatedAt) updatedAt = ts;
567
643
  }
568
644
  }
645
+ if (!dirty && state.hasStructuralChangeUnder(segments)) {
646
+ pristine = false;
647
+ dirty = true;
648
+ }
569
649
  const errors = aggregateErrorsAt(state, segments);
570
650
  if (!asyncPending && state.pathHasAsyncValidation(segments)) asyncPending = true;
571
651
  const gated = asyncPending && !state.firstValidationDone.value;
@@ -581,6 +661,8 @@ function buildContainerFieldStateBase(state, segments, _key) {
581
661
  focused,
582
662
  blurred,
583
663
  touched,
664
+ interacted,
665
+ blurredAfterInteraction,
584
666
  connected,
585
667
  element: null,
586
668
  elements: EMPTY_ELEMENTS,
@@ -589,6 +671,8 @@ function buildContainerFieldStateBase(state, segments, _key) {
589
671
  validating,
590
672
  valid,
591
673
  path: segments,
674
+ ...computeFieldIdentity(formInstanceId, state.formKey, key),
675
+ key: state.arrayElementKey(segments),
592
676
  blank,
593
677
  label,
594
678
  description: resolved.description,
@@ -596,15 +680,36 @@ function buildContainerFieldStateBase(state, segments, _key) {
596
680
  meta: resolved.meta
597
681
  };
598
682
  }
599
- function buildContainerFieldState(state, segments, key, getFormMetaBase, shouldShowErrors) {
600
- const base = buildContainerFieldStateBase(state, segments);
601
- return decorateWithDerivedProps(base, state, getFormMetaBase, shouldShowErrors);
683
+ function buildContainerFieldState(state, segments, key, formInstanceId, getFormMetaBase, getDisplayState) {
684
+ const base = buildContainerFieldStateBase(state, segments, key, formInstanceId);
685
+ return decorateWithDerivedProps(base, state, getFormMetaBase, getDisplayState);
602
686
  }
603
- function decorateWithDerivedProps(base, state, getFormMetaBase, shouldShowErrors) {
687
+ function decorateWithDerivedProps(base, state, getFormMetaBase, getDisplayState) {
604
688
  const firstError = base.errors[0];
605
- const predicate = shouldShowErrors ?? state.shouldShowErrors;
606
- const showErrors = base.errors.length > 0 && predicate(base, getFormMetaBase());
607
- return { ...base, showErrors, firstError };
689
+ const predicate = getDisplayState ?? state.getDisplayState;
690
+ const formMeta = getFormMetaBase();
691
+ let displayState;
692
+ try {
693
+ displayState = predicate(base, formMeta);
694
+ } catch (err) {
695
+ if (__DEV__ && !warnedDisplayStatePredicates.has(predicate)) {
696
+ warnedDisplayStatePredicates.add(predicate);
697
+ console.warn(
698
+ "[attaform] custom getDisplayState threw \u2014 falling back to defaultDisplayState. Subsequent throws from the same predicate will not warn again.",
699
+ err
700
+ );
701
+ }
702
+ displayState = defaultDisplayState(base, formMeta);
703
+ }
704
+ return {
705
+ ...base,
706
+ displayState,
707
+ showErrors: displayState === "error",
708
+ showPending: displayState === "pending",
709
+ showSuccess: displayState === "success",
710
+ showIdle: displayState === "idle",
711
+ firstError
712
+ };
608
713
  }
609
714
  function aggregateErrorsAt(state, prefix) {
610
715
  const formValue = state.form.value;
@@ -630,6 +735,41 @@ function aggregateErrorsAt(state, prefix) {
630
735
  }
631
736
  const EMPTY_ELEMENTS = Object.freeze([]);
632
737
 
738
+ function liveKeysAtPath(state, segments) {
739
+ const value = getAtPath(state.form.value, segments);
740
+ if (value === null || value === void 0) return [];
741
+ if (Array.isArray(value)) {
742
+ const keys = new Array(value.length);
743
+ for (let i = 0; i < value.length; i += 1) keys[i] = String(i);
744
+ return keys;
745
+ }
746
+ if (typeof value === "object") return Object.keys(value);
747
+ return [];
748
+ }
749
+ function isArrayPath(state, segments) {
750
+ if (segments.length === 0) return false;
751
+ return Array.isArray(getAtPath(state.form.value, segments));
752
+ }
753
+
754
+ function warnReadOnly(surface, action, key) {
755
+ if (!__DEV__) return;
756
+ const phrase = action === "write" ? `write to "${String(key)}"` : `${action} of "${String(key)}"`;
757
+ console.warn(
758
+ `[attaform] ${surface} is read-only \u2014 ${phrase} was ignored. Mutate the form via setValue / the directive / field-array helpers instead.`
759
+ );
760
+ }
761
+ function makeReadonlyCoercion(snapshot) {
762
+ const toString = () => JSON.stringify(snapshot());
763
+ return {
764
+ toString,
765
+ valueOf() {
766
+ return this;
767
+ },
768
+ toJSON: snapshot,
769
+ toPrimitive: (hint) => hint === "number" ? NaN : toString()
770
+ };
771
+ }
772
+
633
773
  const INTEGER_SEGMENT = /^(?:0|[1-9]\d*)$/;
634
774
  function keyToSegment(key) {
635
775
  return INTEGER_SEGMENT.test(key) ? Number(key) : key;
@@ -661,12 +801,12 @@ function buildSurfaceProxy(opts) {
661
801
  const existing = containerCache.get(cacheKey);
662
802
  if (existing !== void 0) return existing;
663
803
  const snapshotContainer = () => opts.materializeContainer === void 0 ? {} : opts.materializeContainer(segments);
664
- const containerToJSON = () => snapshotContainer();
665
- const containerToString = () => JSON.stringify(snapshotContainer());
666
- function containerValueOf() {
667
- return this;
668
- }
669
- const containerToPrimitive = (hint) => hint === "number" ? NaN : containerToString();
804
+ const {
805
+ toString: containerToString,
806
+ valueOf: containerValueOf,
807
+ toJSON: containerToJSON,
808
+ toPrimitive: containerToPrimitive
809
+ } = makeReadonlyCoercion(snapshotContainer);
670
810
  const target = isArrayLike ? [] : (() => {
671
811
  });
672
812
  const proxy = new Proxy(target, {
@@ -691,6 +831,9 @@ function buildSurfaceProxy(opts) {
691
831
  return opts.containerOwnKeys === void 0 ? 0 : opts.containerOwnKeys(segments).length;
692
832
  }
693
833
  const childSegs = [...segments, keyToSegment(key)];
834
+ if ((isArrayLike || opts.isArrayContainer?.(segments) === true) && typeof keyToSegment(key) === "string" && key in Array.prototype) {
835
+ return Reflect.get(Array.prototype, key);
836
+ }
694
837
  if (key === "toString" || key === "valueOf") {
695
838
  if (!schemaHasPath(childSegs)) {
696
839
  return key === "toString" ? containerToString : containerValueOf;
@@ -735,10 +878,25 @@ function buildSurfaceProxy(opts) {
735
878
  };
736
879
  },
737
880
  // Block writes at the proxy boundary. Mutations go through
738
- // `setValue`, the directive, or the field-array helpers.
739
- set: () => false,
740
- deleteProperty: () => false,
741
- defineProperty: () => false
881
+ // `setValue`, the directive, or the field-array helpers. Each
882
+ // trap returns `true` (warn-and-noop) returning `false` from a
883
+ // `set`/`delete`/`defineProperty` trap throws `TypeError` under
884
+ // strict mode (every ESM / `<script setup>`), which would surface
885
+ // a host-level exception in consumer code that the library
886
+ // documents as "writes are ignored." Aligns with the contract on
887
+ // `form.values` / `wizard.statuses`.
888
+ set: (_, key) => {
889
+ warnReadOnly("form.fields / form.errors", "write", key);
890
+ return true;
891
+ },
892
+ deleteProperty: (_, key) => {
893
+ warnReadOnly("form.fields / form.errors", "delete", key);
894
+ return true;
895
+ },
896
+ defineProperty: (_, key) => {
897
+ warnReadOnly("form.fields / form.errors", "define", key);
898
+ return true;
899
+ }
742
900
  });
743
901
  containerCache.set(cacheKey, proxy);
744
902
  return proxy;
@@ -760,11 +918,12 @@ function buildSurfaceProxy(opts) {
760
918
  }
761
919
  return snapshot;
762
920
  };
763
- const leafToString = () => JSON.stringify(snapshotLeaf());
764
- function leafValueOf() {
765
- return this;
766
- }
767
- const leafToPrimitive = (hint) => hint === "number" ? NaN : leafToString();
921
+ const {
922
+ toString: leafToString,
923
+ valueOf: leafValueOf,
924
+ toJSON: leafToJSONHandler,
925
+ toPrimitive: leafToPrimitive
926
+ } = makeReadonlyCoercion(snapshotLeaf);
768
927
  const target = (() => {
769
928
  });
770
929
  const proxy = new Proxy(target, {
@@ -782,7 +941,7 @@ function buildSurfaceProxy(opts) {
782
941
  if (typeof key !== "string") return void 0;
783
942
  if (key === "toString") return leafToString;
784
943
  if (key === "valueOf") return leafValueOf;
785
- if (key === "toJSON") return snapshotLeaf;
944
+ if (key === "toJSON") return leafToJSONHandler;
786
945
  if (leafKeys.has(key)) {
787
946
  const leaf = opts.resolveLeaf(segments);
788
947
  return readLeafKey(leaf, key);
@@ -809,9 +968,22 @@ function buildSurfaceProxy(opts) {
809
968
  writable: false
810
969
  };
811
970
  },
812
- set: () => false,
813
- deleteProperty: () => false,
814
- defineProperty: () => false
971
+ // Same warn-and-noop contract as the container traps above.
972
+ // Returning `true` keeps strict-mode callers from throwing on
973
+ // `form.fields.email.value = …`; the actual readonly guarantee
974
+ // is the absence of any mutation, not the host-level reject.
975
+ set: (_, key) => {
976
+ warnReadOnly("form.fields.<leaf>", "write", key);
977
+ return true;
978
+ },
979
+ deleteProperty: (_, key) => {
980
+ warnReadOnly("form.fields.<leaf>", "delete", key);
981
+ return true;
982
+ },
983
+ defineProperty: (_, key) => {
984
+ warnReadOnly("form.fields.<leaf>", "define", key);
985
+ return true;
986
+ }
815
987
  });
816
988
  leafViewCache.set(cacheKey, proxy);
817
989
  return proxy;
@@ -875,33 +1047,53 @@ function buildErrorsProxy(state) {
875
1047
  // working — the call-form just extends that semantic to
876
1048
  // containers and dynamic paths.
877
1049
  resolveCallTarget: (path) => aggregateErrorsAt(state, path),
878
- // Mirror `form.fields` enumeration: `Object.keys(form.errors.items)`
879
- // and `v-for="(errs, idx) in form.errors.items"` walk the live
880
- // array indices / object keys at the path. Iteration yields the
881
- // descended sub-proxies (one per live key), so consumers can
882
- // `form.errors.items[idx]` straight from the entry.
883
- containerOwnKeys: (segments) => liveKeysAtPath$1(state, segments),
884
- isArrayContainer: (segments) => isArrayPath$1(state, segments)
1050
+ // Enumeration unions the live form-data keys at this path with the
1051
+ // first-child segments drawn from every error store. Without the
1052
+ // union, `Object.keys(form.errors)` / `{...form.errors}` /
1053
+ // `v-for="(errs, k) in form.errors"` silently dropped two
1054
+ // important error classes that the dot / call / JSON.stringify
1055
+ // surfaces already exposed:
1056
+ //
1057
+ // - **Form-level** errors at the synthetic `['']` path (set via
1058
+ // `setFormErrors` or root cross-field refines).
1059
+ // - **Server-only** errors at a key the schema doesn't know
1060
+ // about (`['ghost']`, `['address', 'ghost']`).
1061
+ //
1062
+ // The union closes that gap so `ownKeys` agrees with the rest of
1063
+ // the surface. Active-path filter mirrors `resolveLeaf`:
1064
+ // library-produced verdicts (schema + derived-blank) at unreachable
1065
+ // paths stay hidden; user-supplied errors are unconditional.
1066
+ containerOwnKeys: (segments) => errorAwareContainerKeys(state, segments),
1067
+ isArrayContainer: (segments) => isArrayPath(state, segments)
885
1068
  });
886
1069
  }
887
- function liveKeysAtPath$1(state, segments) {
888
- const value = getAtPath(state.form.value, segments);
889
- if (value === null || value === void 0) return [];
890
- if (Array.isArray(value)) {
891
- const keys = new Array(value.length);
892
- for (let i = 0; i < value.length; i += 1) keys[i] = String(i);
893
- return keys;
894
- }
895
- if (typeof value === "object") return Object.keys(value);
896
- return [];
897
- }
898
- function isArrayPath$1(state, segments) {
899
- if (segments.length === 0) return false;
900
- return Array.isArray(getAtPath(state.form.value, segments));
1070
+ function errorAwareContainerKeys(state, segments) {
1071
+ const keys = new Set(liveKeysAtPath(state, segments));
1072
+ const formValue = state.form.value;
1073
+ const walk = (store, applyActivePathFilter) => {
1074
+ for (const [pathKey, errors] of store) {
1075
+ if (errors.length === 0) continue;
1076
+ if (pathKey === FORM_ERRORS_PATH_KEY) {
1077
+ if (segments.length === 0) keys.add("");
1078
+ continue;
1079
+ }
1080
+ const decoded = segmentsForPathKey(pathKey);
1081
+ if (decoded === null) continue;
1082
+ if (decoded.length <= segments.length) continue;
1083
+ if (!isPathPrefix(segments, decoded)) continue;
1084
+ if (applyActivePathFilter && !hasAtPath(formValue, decoded)) continue;
1085
+ const nextSeg = decoded[segments.length];
1086
+ keys.add(typeof nextSeg === "number" ? String(nextSeg) : nextSeg);
1087
+ }
1088
+ };
1089
+ walk(state.schemaErrors, true);
1090
+ walk(state.derivedBlankErrors.value, true);
1091
+ walk(state.userErrors, false);
1092
+ return [...keys];
901
1093
  }
902
1094
  function materializeErrors(state, containerSegments) {
903
1095
  const liveContainer = getAtPath(state.form.value, containerSegments);
904
- const tree = Array.isArray(liveContainer) ? [] : {};
1096
+ const tree = Array.isArray(liveContainer) ? [] : /* @__PURE__ */ Object.create(null);
905
1097
  const collect = (store, applyActivePathFilter) => {
906
1098
  entries: for (const [pathKey, errors] of store) {
907
1099
  if (errors.length === 0) continue;
@@ -951,7 +1143,7 @@ function placeAt(tree, path, errors) {
951
1143
  const cursorRecord2 = cursor;
952
1144
  let child = cursorRecord2[key];
953
1145
  if (child === null || child === void 0 || typeof child !== "object") {
954
- child = typeof nextSeg === "number" ? [] : {};
1146
+ child = typeof nextSeg === "number" ? [] : /* @__PURE__ */ Object.create(null);
955
1147
  cursorRecord2[key] = child;
956
1148
  }
957
1149
  cursor = child;
@@ -986,19 +1178,20 @@ function buildFieldArrayApi(state) {
986
1178
  prepend(path, value) {
987
1179
  const next = readArray(path);
988
1180
  next.unshift(value);
989
- return writeArray(path, next, { kind: "shift-from", index: 0 });
1181
+ return writeArray(path, next, { kind: "insert", index: 0 });
990
1182
  },
991
1183
  insert(path, index, value) {
992
1184
  const next = readArray(path);
993
- next.splice(index, 0, value);
994
- const clampedIndex = Math.max(0, Math.min(index, next.length));
995
- return writeArray(path, next, { kind: "shift-from", index: clampedIndex });
1185
+ const preLen = next.length;
1186
+ const insertIndex = index < 0 ? Math.max(0, preLen + index) : Math.min(index, preLen);
1187
+ next.splice(insertIndex, 0, value);
1188
+ return writeArray(path, next, { kind: "insert", index: insertIndex });
996
1189
  },
997
1190
  remove(path, index) {
998
1191
  const next = readArray(path);
999
1192
  if (index < 0 || index >= next.length) return false;
1000
1193
  next.splice(index, 1);
1001
- return writeArray(path, next, { kind: "shift-from", index });
1194
+ return writeArray(path, next, { kind: "remove", index });
1002
1195
  },
1003
1196
  swap(path, a, b) {
1004
1197
  const next = readArray(path);
@@ -1016,11 +1209,7 @@ function buildFieldArrayApi(state) {
1016
1209
  const [item] = next.splice(from, 1);
1017
1210
  const clampedTo = Math.max(0, Math.min(to, next.length));
1018
1211
  next.splice(clampedTo, 0, item);
1019
- return writeArray(path, next, {
1020
- kind: "shift-range",
1021
- fromIndex: Math.min(from, clampedTo),
1022
- toIndex: Math.max(from, clampedTo)
1023
- });
1212
+ return writeArray(path, next, { kind: "move", from, to: clampedTo });
1024
1213
  },
1025
1214
  replace(path, index, value) {
1026
1215
  const next = readArray(path);
@@ -1039,6 +1228,8 @@ const FIELD_STATE_KEYS = /* @__PURE__ */ new Set([
1039
1228
  "focused",
1040
1229
  "blurred",
1041
1230
  "touched",
1231
+ "interacted",
1232
+ "blurredAfterInteraction",
1042
1233
  "connected",
1043
1234
  "element",
1044
1235
  "elements",
@@ -1046,20 +1237,28 @@ const FIELD_STATE_KEYS = /* @__PURE__ */ new Set([
1046
1237
  "errors",
1047
1238
  "validating",
1048
1239
  "valid",
1240
+ "displayState",
1049
1241
  "showErrors",
1242
+ "showPending",
1243
+ "showSuccess",
1244
+ "showIdle",
1050
1245
  "firstError",
1051
1246
  "path",
1247
+ "id",
1248
+ "aria",
1249
+ "key",
1052
1250
  "blank",
1053
1251
  "label",
1054
1252
  "description",
1055
1253
  "placeholder",
1056
1254
  "meta"
1057
1255
  ]);
1058
- function buildFieldStateProxy(state, getFormMetaBase, options) {
1256
+ function buildFieldStateProxy(state, formInstanceId, getFormMetaBase, options) {
1059
1257
  const getFieldStateAt = buildFieldStateAccessor(
1060
1258
  state,
1259
+ formInstanceId,
1061
1260
  getFormMetaBase,
1062
- options?.shouldShowErrors !== void 0 ? { shouldShowErrors: options.shouldShowErrors } : void 0
1261
+ options?.getDisplayState !== void 0 ? { getDisplayState: options.getDisplayState } : void 0
1063
1262
  );
1064
1263
  const snapshotFieldStateAt = (path) => {
1065
1264
  const view = getFieldStateAt(path).value;
@@ -1108,9 +1307,34 @@ function buildFieldStateProxy(state, getFormMetaBase, options) {
1108
1307
  writable: false
1109
1308
  };
1110
1309
  },
1111
- set: () => false,
1112
- deleteProperty: () => false,
1113
- defineProperty: () => false
1310
+ // Warn-and-noop: returning `false` here throws `TypeError` under
1311
+ // strict mode (`form.fields('email').value = 1` from any ESM
1312
+ // module), which would violate the documented "writes are
1313
+ // ignored" contract. Mirrors `values-proxy` / `wizard-statuses-proxy`.
1314
+ set: (_, key) => {
1315
+ if (__DEV__) {
1316
+ console.warn(
1317
+ `[attaform] form.fields(path) is read-only \u2014 write to "${String(key)}" was ignored. Use form.setValue / the directive / field-array helpers instead.`
1318
+ );
1319
+ }
1320
+ return true;
1321
+ },
1322
+ deleteProperty: (_, key) => {
1323
+ if (__DEV__) {
1324
+ console.warn(
1325
+ `[attaform] form.fields(path) is read-only \u2014 delete of "${String(key)}" was ignored.`
1326
+ );
1327
+ }
1328
+ return true;
1329
+ },
1330
+ defineProperty: (_, key) => {
1331
+ if (__DEV__) {
1332
+ console.warn(
1333
+ `[attaform] form.fields(path) is read-only \u2014 define of "${String(key)}" was ignored.`
1334
+ );
1335
+ }
1336
+ return true;
1337
+ }
1114
1338
  });
1115
1339
  terminalCache.set(cacheKey, proxy);
1116
1340
  return proxy;
@@ -1126,21 +1350,6 @@ function buildFieldStateProxy(state, getFormMetaBase, options) {
1126
1350
  isArrayContainer: (segments) => isArrayPath(state, segments)
1127
1351
  });
1128
1352
  }
1129
- function liveKeysAtPath(state, segments) {
1130
- const value = getAtPath(state.form.value, segments);
1131
- if (value === null || value === void 0) return [];
1132
- if (Array.isArray(value)) {
1133
- const keys = new Array(value.length);
1134
- for (let i = 0; i < value.length; i += 1) keys[i] = String(i);
1135
- return keys;
1136
- }
1137
- if (typeof value === "object") return Object.keys(value);
1138
- return [];
1139
- }
1140
- function isArrayPath(state, segments) {
1141
- if (segments.length === 0) return false;
1142
- return Array.isArray(getAtPath(state.form.value, segments));
1143
- }
1144
1353
  function materializeFields(state, containerSegments, snapshotFieldStateAt) {
1145
1354
  const liveValue = getAtPath(state.form.value, containerSegments);
1146
1355
  return walk$2(liveValue, containerSegments, state.schema, snapshotFieldStateAt);
@@ -1166,29 +1375,6 @@ function walk$2(value, basePath, schema, snapshotFieldStateAt) {
1166
1375
  return result;
1167
1376
  }
1168
1377
 
1169
- const DEFAULT_FIELD_VALIDATION_DEBOUNCE_MS = 0;
1170
- const DEFAULT_PERSISTENCE_DEBOUNCE_MS = 300;
1171
- const DEFAULT_HISTORY_MAX_SNAPSHOTS = 128;
1172
- const PERSISTENCE_KEY_PREFIX = "attaform:";
1173
- const RESERVED_KEY_PREFIX = "__atta:";
1174
- const ANONYMOUS_FORM_KEY_PREFIX = `${RESERVED_KEY_PREFIX}anon:`;
1175
- const ANONYMOUS_WIZARD_KEY_PREFIX = `${RESERVED_KEY_PREFIX}anon-wizard:`;
1176
- const DEFAULT_MAX_RECURSION_DEPTH = 64;
1177
- function normalizeNumericOption(config) {
1178
- const { value, source, allowInfinity, min, defaultValue } = config;
1179
- if (allowInfinity && value === Infinity) return Infinity;
1180
- if (typeof value !== "number" || Number.isNaN(value) || value === Infinity || value === -Infinity) {
1181
- if (__DEV__) {
1182
- const acceptedDescription = allowInfinity ? "a non-negative integer or Infinity" : "a non-negative finite integer";
1183
- console.warn(
1184
- `[attaform] ${source} must be ${acceptedDescription}; got ${String(value)}. Falling back to ${String(defaultValue)}.`
1185
- );
1186
- }
1187
- return defaultValue;
1188
- }
1189
- return Math.max(min, Math.floor(value));
1190
- }
1191
-
1192
1378
  const PERSISTENCE_MODULE_KEY = "persistence";
1193
1379
  async function getStorageAdapter(storage) {
1194
1380
  if (typeof storage === "object") return storage;
@@ -1207,7 +1393,7 @@ async function getStorageAdapter(storage) {
1207
1393
  }
1208
1394
  }
1209
1395
  }
1210
- const PERSISTED_ENVELOPE_VERSION = 5;
1396
+ const PERSISTED_ENVELOPE_VERSION = 6;
1211
1397
  function readPersistedPayload(value) {
1212
1398
  if (value === null || value === void 0 || typeof value !== "object") return null;
1213
1399
  const envelope = value;
@@ -1231,12 +1417,7 @@ function warnVersionMismatch(observedVersion) {
1231
1417
  function buildPersistedPayload(form, include, schemaErrors, userErrors, blankPaths) {
1232
1418
  let transientList;
1233
1419
  if (blankPaths !== void 0 && blankPaths.size > 0) {
1234
- const dotted = [];
1235
- for (const key of blankPaths) {
1236
- const d = pathKeyToDotted(key);
1237
- if (d !== null) dotted.push(d);
1238
- }
1239
- transientList = dotted.length > 0 ? dotted : void 0;
1420
+ transientList = [...blankPaths];
1240
1421
  }
1241
1422
  if (include === "form") {
1242
1423
  if (transientList === void 0) return { v: PERSISTED_ENVELOPE_VERSION, data: { form } };
@@ -1258,30 +1439,35 @@ function buildPersistedPayload(form, include, schemaErrors, userErrors, blankPat
1258
1439
  function createDebouncedWriter(write, debounceMs) {
1259
1440
  let timer = null;
1260
1441
  let pending = null;
1442
+ let writeGeneration = 0;
1443
+ function runWrite() {
1444
+ const gen = ++writeGeneration;
1445
+ pending = write().finally(() => {
1446
+ if (writeGeneration === gen) pending = null;
1447
+ });
1448
+ }
1261
1449
  function schedule() {
1262
1450
  if (timer !== null) clearTimeout(timer);
1263
1451
  if (debounceMs === 0) {
1264
- pending = write().finally(() => {
1265
- pending = null;
1266
- });
1452
+ runWrite();
1267
1453
  return;
1268
1454
  }
1269
1455
  timer = setTimeout(() => {
1270
1456
  timer = null;
1271
- pending = write().finally(() => {
1272
- pending = null;
1273
- });
1457
+ runWrite();
1274
1458
  }, debounceMs);
1275
1459
  }
1276
1460
  async function flush() {
1277
1461
  if (timer !== null) {
1278
1462
  clearTimeout(timer);
1279
1463
  timer = null;
1280
- pending = write().finally(() => {
1281
- pending = null;
1282
- });
1464
+ runWrite();
1465
+ }
1466
+ while (pending !== null) {
1467
+ const awaited = pending;
1468
+ await awaited;
1469
+ if (pending === awaited) break;
1283
1470
  }
1284
- if (pending !== null) await pending;
1285
1471
  }
1286
1472
  function cancel() {
1287
1473
  if (timer !== null) {
@@ -1294,7 +1480,7 @@ function createDebouncedWriter(write, debounceMs) {
1294
1480
  function resolveStorageKeyBase(config, formKey) {
1295
1481
  return config.key ?? `${PERSISTENCE_KEY_PREFIX}${formKey}`;
1296
1482
  }
1297
- async function cleanupOrphanKeys(adapter, base, currentKey) {
1483
+ async function removeMatchingKeys(adapter, base, keepKey) {
1298
1484
  let keys;
1299
1485
  try {
1300
1486
  keys = await adapter.listKeys(base);
@@ -1302,12 +1488,15 @@ async function cleanupOrphanKeys(adapter, base, currentKey) {
1302
1488
  return;
1303
1489
  }
1304
1490
  for (const key of keys) {
1305
- if (key === currentKey) continue;
1491
+ if (key === keepKey) continue;
1306
1492
  if (key === base || key.startsWith(`${base}:`)) {
1307
1493
  void adapter.removeItem(key).catch(() => void 0);
1308
1494
  }
1309
1495
  }
1310
1496
  }
1497
+ async function cleanupOrphanKeys(adapter, base, currentKey) {
1498
+ await removeMatchingKeys(adapter, base, currentKey);
1499
+ }
1311
1500
  const STANDARD_STORAGE_KINDS = ["local", "session", "indexeddb"];
1312
1501
  function normalizePersistConfig(input) {
1313
1502
  if (typeof input === "string") return { storage: input };
@@ -1318,12 +1507,7 @@ async function sweepAllOrphansAcrossStandardStores(base) {
1318
1507
  for (const kind of STANDARD_STORAGE_KINDS) {
1319
1508
  try {
1320
1509
  const adapter = await getStorageAdapter(kind);
1321
- const keys = await adapter.listKeys(base);
1322
- for (const key of keys) {
1323
- if (key === base || key.startsWith(`${base}:`)) {
1324
- void adapter.removeItem(key).catch(() => void 0);
1325
- }
1326
- }
1510
+ await removeMatchingKeys(adapter, base);
1327
1511
  } catch {
1328
1512
  }
1329
1513
  }
@@ -1334,12 +1518,7 @@ async function sweepNonConfiguredStandardStoresForOrphans(configured, base) {
1334
1518
  if (kind === configuredKind) continue;
1335
1519
  try {
1336
1520
  const adapter = await getStorageAdapter(kind);
1337
- const keys = await adapter.listKeys(base);
1338
- for (const key of keys) {
1339
- if (key === base || key.startsWith(`${base}:`)) {
1340
- void adapter.removeItem(key).catch(() => void 0);
1341
- }
1342
- }
1521
+ await removeMatchingKeys(adapter, base);
1343
1522
  } catch {
1344
1523
  }
1345
1524
  }
@@ -1355,6 +1534,29 @@ function pluckPaths(form, pathKeys) {
1355
1534
  }
1356
1535
  return sparse ?? {};
1357
1536
  }
1537
+ function stripUnacknowledgedSensitiveLeaves(form, optedInPaths, isSensitivePath) {
1538
+ const acknowledgedSensitive = [];
1539
+ for (const key of optedInPaths) {
1540
+ const segs = segmentsForPathKey(key);
1541
+ if (segs !== null && isSensitivePath(segs)) acknowledgedSensitive.push(segs);
1542
+ }
1543
+ const coveredByAcknowledged = (path) => acknowledgedSensitive.some((prefix) => isPathPrefix(prefix, path));
1544
+ const walk = (path, value) => {
1545
+ if (path.length > 0 && isSensitivePath(path) && !coveredByAcknowledged(path)) {
1546
+ return void 0;
1547
+ }
1548
+ if (value === null || typeof value !== "object") return value;
1549
+ if (Array.isArray(value)) return value.map((item, i) => walk([...path, i], item));
1550
+ if (!isPlainRecord(value)) return value;
1551
+ const out = /* @__PURE__ */ Object.create(null);
1552
+ for (const key of Object.keys(value)) {
1553
+ const walked = walk([...path, key], value[key]);
1554
+ if (walked !== void 0) out[key] = walked;
1555
+ }
1556
+ return out;
1557
+ };
1558
+ return walk([], form);
1559
+ }
1358
1560
  function filterErrorsByPaths(errors, pathKeys) {
1359
1561
  const out = /* @__PURE__ */ new Map();
1360
1562
  for (const [key, value] of errors) {
@@ -1381,7 +1583,7 @@ function mergeDeep(target, source, path, schema) {
1381
1583
  if (sourceDisc !== void 0) {
1382
1584
  const variantDefault = du.getVariantDefault(sourceDisc);
1383
1585
  if (isPlainRecord(variantDefault)) {
1384
- const out2 = { ...variantDefault };
1586
+ const out2 = Object.assign(/* @__PURE__ */ Object.create(null), variantDefault);
1385
1587
  for (const key of Object.keys(sourceRecord)) {
1386
1588
  if (!(key in variantDefault) && key !== du.discriminatorKey) continue;
1387
1589
  out2[key] = mergeDeep(out2[key], sourceRecord[key], [...path, key], schema);
@@ -1393,7 +1595,7 @@ function mergeDeep(target, source, path, schema) {
1393
1595
  }
1394
1596
  }
1395
1597
  const mergeTarget = target;
1396
- const out = isPlainRecord(mergeTarget) ? { ...mergeTarget } : {};
1598
+ const out = isPlainRecord(mergeTarget) ? Object.assign(/* @__PURE__ */ Object.create(null), mergeTarget) : /* @__PURE__ */ Object.create(null);
1397
1599
  for (const key of Object.keys(source)) {
1398
1600
  out[key] = mergeDeep(out[key], source[key], [...path, key], schema);
1399
1601
  }
@@ -1496,39 +1698,44 @@ function buildProcessForm(state, formInstanceId, options = {}) {
1496
1698
  }
1497
1699
  return result;
1498
1700
  }
1499
- async function validateAsync(pathInput) {
1701
+ async function runImperativeValidation(pathInput, config) {
1500
1702
  const segments = pathInput === void 0 ? void 0 : toSegments(pathInput);
1501
1703
  const dataAtPath = segments === void 0 ? state.form.value : state.getValueAtPath(segments);
1502
1704
  try {
1503
1705
  state.activeValidations.value += 1;
1504
- state.cancelFieldValidation();
1706
+ if (config.cancelInFlight) state.cancelFieldValidation();
1505
1707
  const refinement = await runRefinementValidation(dataAtPath, segments);
1506
- const scopePath = segments ?? [];
1507
- const errors = refinement.success ? [] : refinement.errors;
1508
- const reStamped = segments === void 0 ? errors : errors.map((err) => ({
1509
- ...err,
1510
- path: [...segments, ...err.path]
1511
- }));
1512
- state.applySchemaErrorsForSubtree(scopePath, reStamped);
1513
- return stripData(composeWithDerivedBlank(refinement, segments));
1708
+ if (config.commitToSchemaErrors) {
1709
+ const scopePath = segments ?? [];
1710
+ const errors = refinement.success ? [] : refinement.errors;
1711
+ const reStamped = segments === void 0 ? errors : errors.map((err) => ({
1712
+ ...err,
1713
+ path: [...segments, ...err.path]
1714
+ }));
1715
+ state.applySchemaErrorsForSubtree(scopePath, reStamped);
1716
+ }
1717
+ return { ok: true, refinement, segments };
1514
1718
  } catch (err) {
1515
- return adapterThrowResponse(err);
1719
+ return { ok: false, error: adapterThrowResponse(err) };
1516
1720
  } finally {
1517
1721
  state.activeValidations.value = Math.max(0, state.activeValidations.value - 1);
1518
1722
  }
1519
1723
  }
1724
+ async function validateAsync(pathInput) {
1725
+ const result = await runImperativeValidation(pathInput, {
1726
+ cancelInFlight: true,
1727
+ commitToSchemaErrors: true
1728
+ });
1729
+ if (!result.ok) return result.error;
1730
+ return stripData(composeWithDerivedBlank(result.refinement, result.segments));
1731
+ }
1520
1732
  async function process(pathInput) {
1521
- const segments = pathInput === void 0 ? void 0 : toSegments(pathInput);
1522
- const dataAtPath = segments === void 0 ? state.form.value : state.getValueAtPath(segments);
1523
- try {
1524
- state.activeValidations.value += 1;
1525
- const refinement = await runRefinementValidation(dataAtPath, segments);
1526
- return composeWithDerivedBlank(refinement, segments);
1527
- } catch (err) {
1528
- return adapterThrowResponse(err);
1529
- } finally {
1530
- state.activeValidations.value = Math.max(0, state.activeValidations.value - 1);
1531
- }
1733
+ const result = await runImperativeValidation(pathInput, {
1734
+ cancelInFlight: false,
1735
+ commitToSchemaErrors: false
1736
+ });
1737
+ if (!result.ok) return result.error;
1738
+ return composeWithDerivedBlank(result.refinement, result.segments);
1532
1739
  }
1533
1740
  function adapterThrowResponse(err) {
1534
1741
  return {
@@ -1966,7 +2173,6 @@ function coerceValue(value, accepted, elementAccepted, index) {
1966
2173
  }
1967
2174
 
1968
2175
  const EMPTY_TRANSFORMS = Object.freeze([]);
1969
- const INTERACTIVE_TAG_NAMES = /* @__PURE__ */ new Set(["INPUT", "SELECT", "TEXTAREA"]);
1970
2176
  const attaformListenersSymbol = Symbol.for("attaform:focus-listeners");
1971
2177
  function attachFocusListeners(state, segments, element, instanceMeta) {
1972
2178
  const target = element;
@@ -1994,6 +2200,8 @@ function detachFocusListeners(element) {
1994
2200
  function buildRegister(state, formInstanceId, instanceConfig) {
1995
2201
  const coerceIndex = instanceConfig?.coerce !== void 0 ? resolveCoercionIndex(instanceConfig.coerce) : state.coerceIndex;
1996
2202
  const instanceMeta = instanceConfig?.instanceMeta;
2203
+ const formAutoAria = instanceConfig?.autoAria ?? true;
2204
+ const getDisplayStateAt = instanceConfig?.getDisplayStateAt;
1997
2205
  const withInstanceMeta = (meta) => {
1998
2206
  if (instanceMeta === void 0) return meta;
1999
2207
  return meta === void 0 ? { instance: instanceMeta } : { ...meta, instance: instanceMeta };
@@ -2015,7 +2223,11 @@ function buildRegister(state, formInstanceId, instanceConfig) {
2015
2223
  if (typed !== null && typeof raw === "number" && parseFloat(typed) === raw) {
2016
2224
  return typed;
2017
2225
  }
2018
- return String(raw);
2226
+ try {
2227
+ return String(raw);
2228
+ } catch {
2229
+ return Object.prototype.toString.call(raw);
2230
+ }
2019
2231
  });
2020
2232
  const slimDefault = state.schema.getDefaultAtPath(segments);
2021
2233
  const slimTypes = state.schema.getSlimPrimitiveTypesAtPath(segments);
@@ -2048,6 +2260,10 @@ function buildRegister(state, formInstanceId, instanceConfig) {
2048
2260
  callSite: captureUserCallSite()
2049
2261
  });
2050
2262
  }
2263
+ const { aria } = computeFieldIdentity(formInstanceId, state.formKey, pathKey);
2264
+ const isRequired = state.schema.isRequiredAtPath(segments);
2265
+ const ariaEnabled = options?.autoAria ?? formAutoAria;
2266
+ const ariaDisplayState = getDisplayStateAt !== void 0 ? computed(() => getDisplayStateAt(segments)) : void 0;
2051
2267
  const internalRv = {
2052
2268
  innerRef,
2053
2269
  displayValue,
@@ -2062,6 +2278,9 @@ function buildRegister(state, formInstanceId, instanceConfig) {
2062
2278
  })
2063
2279
  );
2064
2280
  },
2281
+ markInteracted: () => {
2282
+ state.markInteracted(segments);
2283
+ },
2065
2284
  registerElement: (element) => {
2066
2285
  if (!INTERACTIVE_TAG_NAMES.has(element.tagName)) return;
2067
2286
  const added = state.registerElement(segments, element, formInstanceId);
@@ -2106,7 +2325,12 @@ function buildRegister(state, formInstanceId, instanceConfig) {
2106
2325
  coerce,
2107
2326
  ...coerceElement !== void 0 ? { coerceElement } : {},
2108
2327
  acceptsUndefined,
2109
- acceptsString
2328
+ acceptsString,
2329
+ // --- Aria (internal; consumed by the directive) ---
2330
+ aria,
2331
+ isRequired,
2332
+ ariaEnabled,
2333
+ ...ariaDisplayState !== void 0 ? { ariaDisplayState } : {}
2110
2334
  };
2111
2335
  return shallowReadonly(internalRv);
2112
2336
  };
@@ -2157,7 +2381,7 @@ function walk(input, segments, schema, paths) {
2157
2381
  if (slim !== null && slim !== void 0 && typeof slim === "object" && !Array.isArray(slim) && !(slim instanceof Date) && !(slim instanceof RegExp) && !(slim instanceof Map) && !(slim instanceof Set)) {
2158
2382
  for (const k of Object.keys(slim)) allKeys.add(k);
2159
2383
  }
2160
- const out = {};
2384
+ const out = /* @__PURE__ */ Object.create(null);
2161
2385
  let mutated = allKeys.size !== inputKeys.length;
2162
2386
  for (const key of allKeys) {
2163
2387
  const orig = input[key];
@@ -2186,7 +2410,7 @@ function walkUnspecified(slim, segments, paths) {
2186
2410
  }
2187
2411
  if (Array.isArray(slim)) return slim;
2188
2412
  if (slim !== null && typeof slim === "object") {
2189
- const out = {};
2413
+ const out = /* @__PURE__ */ Object.create(null);
2190
2414
  for (const key of Object.keys(slim)) {
2191
2415
  out[key] = walkUnspecified(slim[key], [...segments, key], paths);
2192
2416
  }
@@ -2219,7 +2443,7 @@ function substitute(input, segments, schema, paths) {
2219
2443
  }
2220
2444
  if (typeof input === "object") {
2221
2445
  let mutated = false;
2222
- const out = {};
2446
+ const out = /* @__PURE__ */ Object.create(null);
2223
2447
  for (const key of Object.keys(input)) {
2224
2448
  const orig = input[key];
2225
2449
  const walked = substitute(orig, [...segments, key], schema, paths);
@@ -2271,70 +2495,86 @@ function expandUnsetAt(segments, schema, paths) {
2271
2495
  return result;
2272
2496
  }
2273
2497
 
2274
- function buildValuesProxy(form) {
2275
- const inner = computed(() => readonly(form.value));
2498
+ function buildCallableReadonlySnapshotProxy(opts) {
2276
2499
  const target = (() => {
2277
2500
  });
2278
- const valuesToString = () => JSON.stringify(inner.value);
2279
- const valuesToPrimitive = (hint) => hint === "number" ? NaN : valuesToString();
2501
+ const { toString, valueOf, toJSON, toPrimitive } = makeReadonlyCoercion(opts.snapshot);
2502
+ const callResolve = opts.resolveCall ?? ((arg) => opts.resolveKey(String(arg)));
2280
2503
  return new Proxy(target, {
2281
2504
  apply(_, __, args) {
2282
2505
  const arg = args[0];
2283
- if (arg === void 0) return inner.value;
2284
- const { segments } = canonicalizePath(arg);
2285
- let cursor = inner.value;
2286
- for (const seg of segments) {
2287
- if (cursor === null || cursor === void 0) return void 0;
2288
- cursor = cursor[seg];
2289
- }
2290
- return cursor;
2506
+ if (arg === void 0) return opts.snapshot();
2507
+ return callResolve(arg);
2291
2508
  },
2292
2509
  get(_, key) {
2293
2510
  if (typeof key === "symbol") {
2294
- if (key === Symbol.toPrimitive) return valuesToPrimitive;
2511
+ if (key === Symbol.toPrimitive) return toPrimitive;
2295
2512
  return Reflect.get(target, key);
2296
2513
  }
2297
- if (key === "toJSON") return () => inner.value;
2298
- if (key === "toString") return valuesToString;
2299
- if (key === "valueOf")
2300
- return function() {
2301
- return this;
2302
- };
2303
- return inner.value[key];
2514
+ if (key === "toJSON") return toJSON;
2515
+ if (key === "toString") return toString;
2516
+ if (key === "valueOf") return valueOf;
2517
+ return opts.resolveKey(key);
2304
2518
  },
2305
2519
  has(_, key) {
2306
2520
  if (typeof key === "symbol") return Reflect.has(target, key);
2307
- return Reflect.has(inner.value, key);
2308
- },
2309
- ownKeys() {
2310
- return Reflect.ownKeys(inner.value);
2521
+ return opts.hasKey(key);
2311
2522
  },
2523
+ ownKeys: () => opts.ownKeys(),
2312
2524
  getOwnPropertyDescriptor(_, key) {
2313
- const desc = Reflect.getOwnPropertyDescriptor(inner.value, key);
2314
- if (desc !== void 0) desc.configurable = true;
2315
- return desc;
2525
+ if (typeof key !== "string") return void 0;
2526
+ if (opts.describeKey !== void 0) return opts.describeKey(key);
2527
+ if (!opts.hasKey(key)) return void 0;
2528
+ return {
2529
+ configurable: true,
2530
+ enumerable: true,
2531
+ writable: false,
2532
+ value: opts.resolveKey(key)
2533
+ };
2316
2534
  },
2317
- // Match Vue's `readonly()` semantics: writes warn (in dev) and
2318
- // silently noop (return true). Returning false would throw
2319
- // TypeError in strict-mode consumers, surprising users who
2320
- // assigned through the proxy and expected it to be ignored.
2321
- set(_, key) {
2322
- if (__DEV__) {
2323
- console.warn(
2324
- `[attaform] form.values is read-only \u2014 write to "${String(key)}" was ignored. Use form.setValue / the directive / field-array helpers instead.`
2325
- );
2326
- }
2535
+ set: (_, key) => {
2536
+ warnReadOnly(opts.surface, "write", key);
2327
2537
  return true;
2328
2538
  },
2329
- deleteProperty(_, key) {
2330
- if (__DEV__) {
2331
- console.warn(
2332
- `[attaform] form.values is read-only \u2014 delete of "${String(key)}" was ignored.`
2333
- );
2334
- }
2539
+ deleteProperty: (_, key) => {
2540
+ warnReadOnly(opts.surface, "delete", key);
2335
2541
  return true;
2336
2542
  },
2337
- defineProperty: () => true
2543
+ defineProperty: (_, key) => {
2544
+ warnReadOnly(opts.surface, "define", key);
2545
+ return true;
2546
+ }
2547
+ });
2548
+ }
2549
+
2550
+ function buildValuesProxy(form) {
2551
+ const inner = computed(() => readonly(form.value));
2552
+ return buildCallableReadonlySnapshotProxy({
2553
+ surface: "form.values",
2554
+ snapshot: () => inner.value,
2555
+ // Read through the readonly proxy at access time so Vue's
2556
+ // dependency tracking lands inside the consumer's active effect
2557
+ // — `inner.value[key]` is what triggers per-key tracking.
2558
+ resolveKey: (key) => inner.value[key],
2559
+ // Dynamic path: walk segments through the readonly proxy. Each
2560
+ // step reads through the proxy's own get traps so dependency
2561
+ // tracking propagates at every level.
2562
+ resolveCall: (arg) => {
2563
+ const { segments } = canonicalizePath(arg);
2564
+ let cursor = inner.value;
2565
+ for (const seg of segments) {
2566
+ if (cursor === null || cursor === void 0) return void 0;
2567
+ cursor = cursor[seg];
2568
+ }
2569
+ return cursor;
2570
+ },
2571
+ ownKeys: () => Reflect.ownKeys(inner.value),
2572
+ hasKey: (key) => Reflect.has(inner.value, key),
2573
+ describeKey: (key) => {
2574
+ const desc = Reflect.getOwnPropertyDescriptor(inner.value, key);
2575
+ if (desc !== void 0) desc.configurable = true;
2576
+ return desc;
2577
+ }
2338
2578
  });
2339
2579
  }
2340
2580
 
@@ -2350,15 +2590,34 @@ function buildFormApi(state, formInstanceId, options = {}) {
2350
2590
  if (instanceMeta === void 0) return meta;
2351
2591
  return meta === void 0 ? { instance: instanceMeta } : { ...meta, instance: instanceMeta };
2352
2592
  };
2353
- const registerConfig = {
2354
- ...instanceMeta !== void 0 ? { instanceMeta } : {},
2355
- ...options.coerce !== void 0 ? { coerce: options.coerce } : {}
2593
+ const getFormMetaBase = () => {
2594
+ const rootBase = buildContainerFieldStateBase(state, ROOT_PATH, ROOT_PATH_KEY, formInstanceId);
2595
+ return {
2596
+ ...rootBase,
2597
+ submitting: state.submitting.value,
2598
+ submissionAttempts: state.submissionAttempts.value,
2599
+ departAttempts: state.departAttempts.value,
2600
+ submitError: state.submitError.value,
2601
+ errorCount: rootBase.errors.length,
2602
+ submitted: state.submitted.value,
2603
+ instanceId: formInstanceId
2604
+ };
2356
2605
  };
2357
- const register = buildRegister(
2606
+ const fieldStateAccessorOptions = options.getDisplayState !== void 0 ? { getDisplayState: options.getDisplayState } : void 0;
2607
+ const getRootFieldStateAt = buildFieldStateAccessor(
2358
2608
  state,
2359
2609
  formInstanceId,
2360
- Object.keys(registerConfig).length > 0 ? registerConfig : void 0
2610
+ getFormMetaBase,
2611
+ fieldStateAccessorOptions
2361
2612
  );
2613
+ const getDisplayStateAt = (segments) => getRootFieldStateAt(segments).value.displayState;
2614
+ const registerConfig = {
2615
+ ...instanceMeta !== void 0 ? { instanceMeta } : {},
2616
+ ...options.coerce !== void 0 ? { coerce: options.coerce } : {},
2617
+ ...options.autoAria !== void 0 ? { autoAria: options.autoAria } : {},
2618
+ getDisplayStateAt
2619
+ };
2620
+ const register = buildRegister(state, formInstanceId, registerConfig);
2362
2621
  const processOptions = options.onInvalidSubmit !== void 0 ? { onInvalidSubmit: options.onInvalidSubmit } : {};
2363
2622
  const defaultInvalidSubmitPolicy = options.onInvalidSubmit ?? "focus-first-error";
2364
2623
  const {
@@ -2381,10 +2640,18 @@ function buildFormApi(state, formInstanceId, options = {}) {
2381
2640
  next,
2382
2641
  state.schema
2383
2642
  );
2643
+ const ok2 = state.setValueAtPath([], walked2.cleanedValues, withInstanceMeta());
2644
+ if (!ok2) return false;
2384
2645
  for (const pathKey of walked2.paths) {
2385
- state.blankPaths.add(pathKey);
2646
+ const blankSegments = segmentsForPathKey(pathKey);
2647
+ if (blankSegments === null) continue;
2648
+ state.setValueAtPath(
2649
+ blankSegments,
2650
+ state.getValueAtPath(blankSegments),
2651
+ withInstanceMeta({ blank: true })
2652
+ );
2386
2653
  }
2387
- return state.setValueAtPath([], walked2.cleanedValues, withInstanceMeta());
2654
+ return true;
2388
2655
  }
2389
2656
  const segments = canonicalizePath(pathOrValue).segments;
2390
2657
  const writeUnsetAt = () => {
@@ -2530,43 +2797,48 @@ function buildFormApi(state, formInstanceId, options = {}) {
2530
2797
  const metaErrors = computed(
2531
2798
  () => aggregateErrorsAt(state, [])
2532
2799
  );
2533
- const getFormMetaBase = () => {
2534
- const rootBase = buildContainerFieldStateBase(state, ROOT_PATH);
2535
- return {
2536
- ...rootBase,
2537
- submitting: state.submitting.value,
2538
- submissionAttempts: state.submissionAttempts.value,
2539
- departAttempts: state.departAttempts.value,
2540
- submitError: state.submitError.value,
2541
- errorCount: rootBase.errors.length,
2542
- submitted: state.submitted.value,
2543
- instanceId: formInstanceId
2544
- };
2545
- };
2546
- const fieldStateAccessorOptions = options.shouldShowErrors !== void 0 ? { shouldShowErrors: options.shouldShowErrors } : void 0;
2547
- const getRootFieldStateAt = buildFieldStateAccessor(
2548
- state,
2549
- getFormMetaBase,
2550
- fieldStateAccessorOptions
2551
- );
2552
2800
  const rootFieldState = getRootFieldStateAt([]);
2553
2801
  const formMeta = readonly(
2554
2802
  reactive({
2555
- // FieldState fields — read through one shared root computed. Each
2556
- // property accesses `rootFieldState.value[X]`, so any descendant
2557
- // change re-evaluates the root computed once (Vue's reactive
2558
- // graph dedupes the dependent re-renders).
2559
- value: computed(() => rootFieldState.value.value),
2560
- original: computed(() => rootFieldState.value.original),
2561
- pristine: computed(() => rootFieldState.value.pristine),
2562
- dirty: computed(() => rootFieldState.value.dirty),
2563
- focused: computed(() => rootFieldState.value.focused),
2564
- blurred: computed(() => rootFieldState.value.blurred),
2565
- touched: computed(() => rootFieldState.value.touched),
2566
- connected: computed(() => rootFieldState.value.connected),
2567
- element: computed(() => rootFieldState.value.element),
2568
- elements: computed(() => rootFieldState.value.elements),
2569
- updatedAt: computed(() => rootFieldState.value.updatedAt),
2803
+ get value() {
2804
+ return rootFieldState.value.value;
2805
+ },
2806
+ get original() {
2807
+ return rootFieldState.value.original;
2808
+ },
2809
+ get pristine() {
2810
+ return rootFieldState.value.pristine;
2811
+ },
2812
+ get dirty() {
2813
+ return rootFieldState.value.dirty;
2814
+ },
2815
+ get focused() {
2816
+ return rootFieldState.value.focused;
2817
+ },
2818
+ get blurred() {
2819
+ return rootFieldState.value.blurred;
2820
+ },
2821
+ get touched() {
2822
+ return rootFieldState.value.touched;
2823
+ },
2824
+ get interacted() {
2825
+ return rootFieldState.value.interacted;
2826
+ },
2827
+ get blurredAfterInteraction() {
2828
+ return rootFieldState.value.blurredAfterInteraction;
2829
+ },
2830
+ get connected() {
2831
+ return rootFieldState.value.connected;
2832
+ },
2833
+ get element() {
2834
+ return rootFieldState.value.element;
2835
+ },
2836
+ get elements() {
2837
+ return rootFieldState.value.elements;
2838
+ },
2839
+ get updatedAt() {
2840
+ return rootFieldState.value.updatedAt;
2841
+ },
2570
2842
  // Whole-form validating mirrors the LIFECYCLE counter
2571
2843
  // (`state.activeValidations`) ORed with any per-leaf validation
2572
2844
  // in flight (via `rootFieldState.validating`). A submit-time
@@ -2584,19 +2856,56 @@ function buildFormApi(state, formInstanceId, options = {}) {
2584
2856
  // keep the explicit form-level computation for the gate.
2585
2857
  valid,
2586
2858
  errors: metaErrors,
2587
- // `showErrors` / `firstError` flow through the same root
2588
- // field-state computed as the rest of the FieldState surface,
2589
- // so `form.meta.showErrors` matches `form.fields().showErrors`
2590
- // exactly — the predicate runs once at the root and the result
2591
- // is shared.
2592
- showErrors: computed(() => rootFieldState.value.showErrors),
2593
- firstError: computed(() => rootFieldState.value.firstError),
2594
- path: computed(() => rootFieldState.value.path),
2595
- blank: computed(() => rootFieldState.value.blank),
2596
- label: computed(() => rootFieldState.value.label),
2597
- description: computed(() => rootFieldState.value.description),
2598
- placeholder: computed(() => rootFieldState.value.placeholder),
2599
- meta: computed(() => rootFieldState.value.meta),
2859
+ // `displayState` / the `show*` booleans / `firstError` flow
2860
+ // through the same root field-state computed as the rest of the
2861
+ // FieldState surface, so `form.meta.displayState` matches
2862
+ // `form.fields().displayState` exactly — the predicate runs once
2863
+ // at the root and the result is shared.
2864
+ get displayState() {
2865
+ return rootFieldState.value.displayState;
2866
+ },
2867
+ get showErrors() {
2868
+ return rootFieldState.value.showErrors;
2869
+ },
2870
+ get showPending() {
2871
+ return rootFieldState.value.showPending;
2872
+ },
2873
+ get showSuccess() {
2874
+ return rootFieldState.value.showSuccess;
2875
+ },
2876
+ get showIdle() {
2877
+ return rootFieldState.value.showIdle;
2878
+ },
2879
+ get firstError() {
2880
+ return rootFieldState.value.firstError;
2881
+ },
2882
+ get path() {
2883
+ return rootFieldState.value.path;
2884
+ },
2885
+ get id() {
2886
+ return rootFieldState.value.id;
2887
+ },
2888
+ get aria() {
2889
+ return rootFieldState.value.aria;
2890
+ },
2891
+ get key() {
2892
+ return rootFieldState.value.key;
2893
+ },
2894
+ get blank() {
2895
+ return rootFieldState.value.blank;
2896
+ },
2897
+ get label() {
2898
+ return rootFieldState.value.label;
2899
+ },
2900
+ get description() {
2901
+ return rootFieldState.value.description;
2902
+ },
2903
+ get placeholder() {
2904
+ return rootFieldState.value.placeholder;
2905
+ },
2906
+ get meta() {
2907
+ return rootFieldState.value.meta;
2908
+ },
2600
2909
  // Lifecycle (form-level only — not on FieldState).
2601
2910
  submitting,
2602
2911
  submissionAttempts,
@@ -2605,7 +2914,9 @@ function buildFormApi(state, formInstanceId, options = {}) {
2605
2914
  // Scalar mirror over the array — meta is a single sticky surface
2606
2915
  // for both templates and `useWizard`'s `FormStatus`, so the
2607
2916
  // projection lives here.
2608
- errorCount: computed(() => metaErrors.value.length),
2917
+ get errorCount() {
2918
+ return metaErrors.value.length;
2919
+ },
2609
2920
  submitted,
2610
2921
  // Per-`useForm()`-call identity. Stable for one mount; new on
2611
2922
  // re-mount; orthogonal to `form.key` (which is the user-supplied
@@ -2642,12 +2953,20 @@ function buildFormApi(state, formInstanceId, options = {}) {
2642
2953
  }
2643
2954
  };
2644
2955
  function clear(pathInput) {
2645
- const segments = pathInput === void 0 ? ROOT_PATH : canonicalizePath(pathInput).segments;
2646
- return state.clear(segments);
2956
+ if (pathInput === void 0) {
2957
+ return setValueImpl(unset);
2958
+ }
2959
+ return setValueImpl(pathInput, unset);
2647
2960
  }
2648
2961
  const persist = async (pathInput, options2) => {
2649
2962
  const segments = canonicalizePath(pathInput).segments;
2650
- enforceSensitiveCheck(segments, options2?.acknowledgeSensitive === true, state.isSensitivePath);
2963
+ if (!allowSensitivePersist(
2964
+ segments,
2965
+ options2?.acknowledgeSensitive === true,
2966
+ state.isSensitivePath
2967
+ )) {
2968
+ return;
2969
+ }
2651
2970
  if (persistence === void 0) return;
2652
2971
  await persistence.writePathImmediately(segments);
2653
2972
  };
@@ -2707,13 +3026,43 @@ function buildFormApi(state, formInstanceId, options = {}) {
2707
3026
  return Object.freeze(view);
2708
3027
  });
2709
3028
  const valuesProxy = buildValuesProxy(state.form);
2710
- const fieldStateProxy = buildFieldStateProxy(state, getFormMetaBase, fieldStateAccessorOptions);
3029
+ const fieldStateProxy = buildFieldStateProxy(
3030
+ state,
3031
+ formInstanceId,
3032
+ getFormMetaBase,
3033
+ fieldStateAccessorOptions
3034
+ );
3035
+ const needsLazyGate = state.defaultValuesFactory.value !== void 0 || state.hasSsrPrefetch;
2711
3036
  function gated(fn) {
3037
+ if (!needsLazyGate) return fn;
2712
3038
  return ((...args) => {
2713
3039
  void state.activate();
2714
3040
  return fn(...args);
2715
3041
  });
2716
3042
  }
3043
+ const callTerminal = fieldStateProxy;
3044
+ const EMPTY_FIELD_LIST = Object.freeze([]);
3045
+ function list(path) {
3046
+ const { segments } = canonicalizePath(path);
3047
+ const value = state.getValueAtPath(segments);
3048
+ if (!Array.isArray(value)) return EMPTY_FIELD_LIST;
3049
+ const out = new Array(value.length);
3050
+ for (let i = 0; i < value.length; i += 1) out[i] = callTerminal(`${path}.${i}`);
3051
+ return Object.freeze(out);
3052
+ }
3053
+ const EMPTY_FIELD_RECORD = Object.freeze({});
3054
+ function record(path) {
3055
+ const { segments } = canonicalizePath(path);
3056
+ const value = state.getValueAtPath(segments);
3057
+ if (value === null || typeof value !== "object" || Array.isArray(value)) {
3058
+ return EMPTY_FIELD_RECORD;
3059
+ }
3060
+ const out = /* @__PURE__ */ Object.create(null);
3061
+ for (const key of Object.keys(value)) {
3062
+ out[key] = callTerminal(`${path}.${key}`);
3063
+ }
3064
+ return Object.freeze(out);
3065
+ }
2717
3066
  return {
2718
3067
  handleSubmit: gated(handleSubmit),
2719
3068
  // Callable readonly Proxies (`values`, `fields`, `errors`) and the
@@ -2795,6 +3144,8 @@ function buildFormApi(state, formInstanceId, options = {}) {
2795
3144
  swap: gated(fieldArrays.swap),
2796
3145
  move: gated(fieldArrays.move),
2797
3146
  replace: gated(fieldArrays.replace),
3147
+ list: gated(list),
3148
+ record: gated(record),
2798
3149
  get blankPaths() {
2799
3150
  void state.activate();
2800
3151
  return blankPathsView;
@@ -2802,47 +3153,6 @@ function buildFormApi(state, formInstanceId, options = {}) {
2802
3153
  };
2803
3154
  }
2804
3155
 
2805
- const defaultShouldShowErrors = (field, formMeta) => {
2806
- const hasOwnError = field.errors.some(
2807
- (e) => e.path.length === field.path.length && e.path.every((s, i) => s === field.path[i])
2808
- );
2809
- if (!hasOwnError) return false;
2810
- if (field.validating === true) return false;
2811
- if (formMeta.submissionAttempts > 0) return true;
2812
- return field.touched === true && field.focused !== true;
2813
- };
2814
- const SHOW_ALWAYS = () => true;
2815
- const SHOW_NEVER = () => false;
2816
- function resolveShouldShowErrors(config) {
2817
- if (config === void 0) return defaultShouldShowErrors;
2818
- if (config === true) return SHOW_ALWAYS;
2819
- if (config === false) return SHOW_NEVER;
2820
- return config;
2821
- }
2822
-
2823
- function isHydratedFieldRecord(value) {
2824
- if (typeof value !== "object" || value === null) return false;
2825
- const r = value;
2826
- return Array.isArray(r.path) && (typeof r.updatedAt === "string" || r.updatedAt === null) && typeof r.connected === "boolean" && (typeof r.focused === "boolean" || r.focused === null) && (typeof r.blurred === "boolean" || r.blurred === null) && typeof r.touched === "boolean";
2827
- }
2828
- function isHydratedValidationErrorArray(value) {
2829
- if (!Array.isArray(value)) return false;
2830
- for (const entry of value) {
2831
- if (typeof entry !== "object" || entry === null) return false;
2832
- const e = entry;
2833
- if (typeof e.message !== "string") return false;
2834
- if (!Array.isArray(e.path)) return false;
2835
- if (typeof e.formKey !== "string") return false;
2836
- if (typeof e.code !== "string") return false;
2837
- }
2838
- return true;
2839
- }
2840
- function warnMalformedHydration(formKey, kind, rawKey) {
2841
- if (!__DEV__) return;
2842
- console.warn(
2843
- `[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).`
2844
- );
2845
- }
2846
3156
  function applyDuStubs(schema, data, options = {}) {
2847
3157
  const warned = options.warn === true ? /* @__PURE__ */ new Set() : void 0;
2848
3158
  return walkDuStubs(schema, data, options.basePath ?? [], warned);
@@ -2871,49 +3181,74 @@ function walkDuStubs(schema, value, path, warned) {
2871
3181
  );
2872
3182
  }
2873
3183
  }
2874
- return { [du.discriminatorKey]: discValue };
3184
+ const stub = /* @__PURE__ */ Object.create(null);
3185
+ stub[du.discriminatorKey] = discValue;
3186
+ return stub;
2875
3187
  }
2876
3188
  }
2877
- const out = {};
3189
+ const out = /* @__PURE__ */ Object.create(null);
2878
3190
  for (const k of Object.keys(rec)) {
2879
3191
  out[k] = walkDuStubs(schema, rec[k], [...path, k], warned);
2880
3192
  }
2881
3193
  return out;
2882
3194
  }
2883
- function isPathKeyUnder(existingKey, parentPath) {
2884
- const parsed = segmentsForPathKey(existingKey);
2885
- if (parsed === null) return false;
2886
- if (parsed.length <= parentPath.length) return false;
2887
- for (let i = 0; i < parentPath.length; i++) {
2888
- if (parsed[i] !== parentPath[i]) return false;
2889
- }
2890
- return true;
2891
- }
2892
- function stripSymbolsDeep(value) {
2893
- if (value === null || typeof value !== "object") return value;
2894
- if (Array.isArray(value)) {
2895
- let mutated2 = false;
2896
- const out2 = new Array(value.length);
2897
- for (let i = 0; i < value.length; i++) {
2898
- const cleaned = stripSymbolsDeep(value[i]);
2899
- out2[i] = cleaned;
2900
- if (cleaned !== value[i]) mutated2 = true;
3195
+
3196
+ function createVariantMemory() {
3197
+ const memory = /* @__PURE__ */ new Map();
3198
+ function clearAtArrayIndices(arrayPath, indexFilter) {
3199
+ for (const memKey of [...memory.keys()]) {
3200
+ const segs = segmentsForPathKey(memKey);
3201
+ if (segs === null) continue;
3202
+ if (!isPathPrefix(arrayPath, segs)) continue;
3203
+ if (segs.length <= arrayPath.length) continue;
3204
+ const idxSeg = segs[arrayPath.length];
3205
+ if (typeof idxSeg !== "number") continue;
3206
+ if (indexFilter(idxSeg)) memory.delete(memKey);
2901
3207
  }
2902
- return mutated2 ? out2 : value;
2903
- }
2904
- const proto = Object.getPrototypeOf(value);
2905
- if (proto !== Object.prototype && proto !== null) return value;
2906
- const symKeys = Object.getOwnPropertySymbols(value);
2907
- const stringKeys = Object.keys(value);
2908
- let mutated = symKeys.length > 0;
2909
- const out = {};
2910
- const src = value;
2911
- for (const k of stringKeys) {
2912
- const cleaned = stripSymbolsDeep(src[k]);
2913
- out[k] = cleaned;
2914
- if (cleaned !== src[k]) mutated = true;
2915
3208
  }
2916
- return mutated ? out : value;
3209
+ return {
3210
+ clear() {
3211
+ memory.clear();
3212
+ },
3213
+ clearUnderPath(parentPath) {
3214
+ for (const memKey of [...memory.keys()]) {
3215
+ const segs = segmentsForPathKey(memKey);
3216
+ if (segs === null) continue;
3217
+ if (isPathPrefix(parentPath, segs)) memory.delete(memKey);
3218
+ }
3219
+ },
3220
+ applyArrayOp(arrayPath, op) {
3221
+ switch (op.kind) {
3222
+ case "insert":
3223
+ case "remove":
3224
+ clearAtArrayIndices(arrayPath, (i) => i >= op.index);
3225
+ return;
3226
+ case "move": {
3227
+ const lo = Math.min(op.from, op.to);
3228
+ const hi = Math.max(op.from, op.to);
3229
+ clearAtArrayIndices(arrayPath, (i) => i >= lo && i <= hi);
3230
+ return;
3231
+ }
3232
+ case "swap":
3233
+ clearAtArrayIndices(arrayPath, (i) => i === op.a || i === op.b);
3234
+ return;
3235
+ case "replace-at":
3236
+ clearAtArrayIndices(arrayPath, (i) => i === op.index);
3237
+ return;
3238
+ }
3239
+ },
3240
+ recordOutgoing(unionKey, discValue, snapshot) {
3241
+ let perUnion = memory.get(unionKey);
3242
+ if (perUnion === void 0) {
3243
+ perUnion = /* @__PURE__ */ new Map();
3244
+ memory.set(unionKey, perUnion);
3245
+ }
3246
+ perUnion.set(discValue, snapshot);
3247
+ },
3248
+ lookupIncoming(unionKey, discValue) {
3249
+ return memory.get(unionKey)?.get(discValue);
3250
+ }
3251
+ };
2917
3252
  }
2918
3253
  function cloneVariantSnapshot(value) {
2919
3254
  if (value === null || typeof value !== "object") return value;
@@ -2936,50 +3271,403 @@ function cloneVariantSnapshot(value) {
2936
3271
  return out2;
2937
3272
  }
2938
3273
  const src = raw;
2939
- const out = {};
3274
+ const out = /* @__PURE__ */ Object.create(null);
2940
3275
  for (const k of Object.keys(src)) out[k] = cloneVariantSnapshot(src[k]);
2941
3276
  return out;
2942
3277
  }
2943
- function walkAuthoredFromConstraints(value, prefix, out) {
2944
- if (prefix.length > 0) out.add(canonicalizePath(prefix).key);
2945
- if (isPlainRecord(value)) {
2946
- for (const k of Object.keys(value)) {
2947
- walkAuthoredFromConstraints(value[k], [...prefix, k], out);
3278
+
3279
+ function createArrayIdentity(getArrayLength) {
3280
+ const tokens = /* @__PURE__ */ new Map();
3281
+ const baselines = /* @__PURE__ */ new Map();
3282
+ let counter = 0;
3283
+ const allocate = () => `k${(counter++).toString(36)}`;
3284
+ function ensure(arrayKey, expectedLen) {
3285
+ let ids = tokens.get(arrayKey);
3286
+ const firstTrack = ids === void 0;
3287
+ if (ids === void 0) {
3288
+ ids = [];
3289
+ tokens.set(arrayKey, ids);
3290
+ }
3291
+ while (ids.length < expectedLen) ids.push(allocate());
3292
+ if (ids.length > expectedLen) ids.length = expectedLen;
3293
+ if (firstTrack) baselines.set(arrayKey, [...ids]);
3294
+ return ids;
3295
+ }
3296
+ function orderPristineForKey(arrayKey) {
3297
+ const baseline = baselines.get(arrayKey);
3298
+ const current = tokens.get(arrayKey);
3299
+ if (baseline === void 0 || current === void 0) return true;
3300
+ if (baseline.length !== current.length) return false;
3301
+ for (let i = 0; i < current.length; i++) {
3302
+ if (current[i] !== baseline[i]) return false;
2948
3303
  }
2949
- return;
3304
+ return true;
2950
3305
  }
2951
- if (Array.isArray(value)) {
2952
- for (let i = 0; i < value.length; i++) {
2953
- walkAuthoredFromConstraints(value[i], [...prefix, i], out);
3306
+ return {
3307
+ tokenAt(arraySegs, index) {
3308
+ const len = getArrayLength(arraySegs);
3309
+ if (index < 0 || index >= len) return "";
3310
+ const ids = ensure(canonicalizePath(arraySegs).key, len);
3311
+ return ids[index] ?? "";
3312
+ },
3313
+ applyOp(arraySegs, op) {
3314
+ const arrayKey = canonicalizePath(arraySegs).key;
3315
+ const postLen = getArrayLength(arraySegs);
3316
+ const preLen = op.kind === "insert" ? postLen - 1 : op.kind === "remove" ? postLen + 1 : postLen;
3317
+ const ids = ensure(arrayKey, Math.max(0, preLen));
3318
+ switch (op.kind) {
3319
+ case "insert":
3320
+ ids.splice(op.index, 0, allocate());
3321
+ return;
3322
+ case "remove":
3323
+ ids.splice(op.index, 1);
3324
+ return;
3325
+ case "move": {
3326
+ const [moved] = ids.splice(op.from, 1);
3327
+ ids.splice(op.to, 0, moved ?? allocate());
3328
+ return;
3329
+ }
3330
+ case "swap": {
3331
+ const tmp = ids[op.a] ?? allocate();
3332
+ ids[op.a] = ids[op.b] ?? allocate();
3333
+ ids[op.b] = tmp;
3334
+ return;
3335
+ }
3336
+ case "replace-at":
3337
+ ids[op.index] = allocate();
3338
+ return;
3339
+ }
3340
+ },
3341
+ applyRemap(arrayPath, remap) {
3342
+ if (remap.moved.size === 0 && remap.vacated.size === 0 && remap.fresh.size === 0) return;
3343
+ const relocate = (store) => {
3344
+ const idxPos = arrayPath.length;
3345
+ const snapshots = [];
3346
+ for (const [key, value] of store) {
3347
+ const segments = segmentsForPathKey(key);
3348
+ if (segments === null) continue;
3349
+ if (!isPathPrefix(arrayPath, segments)) continue;
3350
+ if (segments.length <= idxPos) continue;
3351
+ const idxSeg = segments[idxPos];
3352
+ if (typeof idxSeg !== "number") continue;
3353
+ if (!remap.moved.has(idxSeg) && !remap.vacated.has(idxSeg) && !remap.fresh.has(idxSeg)) {
3354
+ continue;
3355
+ }
3356
+ snapshots.push({ segments: [...segments], index: idxSeg, value });
3357
+ }
3358
+ if (snapshots.length === 0) return;
3359
+ for (const snap of snapshots) store.delete(canonicalizePath(snap.segments).key);
3360
+ for (const snap of snapshots) {
3361
+ const target = remap.moved.get(snap.index);
3362
+ if (target === void 0) continue;
3363
+ const relocated = snap.segments.slice();
3364
+ relocated[idxPos] = target;
3365
+ store.set(canonicalizePath(relocated).key, snap.value);
3366
+ }
3367
+ };
3368
+ relocate(tokens);
3369
+ relocate(baselines);
3370
+ },
3371
+ realign(arraySegs) {
3372
+ ensure(canonicalizePath(arraySegs).key, getArrayLength(arraySegs));
3373
+ },
3374
+ hasStructuralChangeUnder(prefix) {
3375
+ for (const arrayKey of tokens.keys()) {
3376
+ if (orderPristineForKey(arrayKey)) continue;
3377
+ const segs = segmentsForPathKey(arrayKey);
3378
+ if (segs === null) continue;
3379
+ if (isPathPrefix(prefix, segs)) return true;
3380
+ }
3381
+ return false;
3382
+ },
3383
+ rebaselineAll() {
3384
+ for (const arrayKey of [...tokens.keys()]) {
3385
+ const segs = segmentsForPathKey(arrayKey);
3386
+ if (segs === null) continue;
3387
+ const ids = ensure(arrayKey, getArrayLength(segs));
3388
+ baselines.set(arrayKey, [...ids]);
3389
+ }
2954
3390
  }
2955
- }
3391
+ };
2956
3392
  }
2957
- function walkAuthoredFromSchemaDiff(withDefaults, withoutDefaults, prefix, out) {
2958
- if (isPlainRecord(withDefaults) && isPlainRecord(withoutDefaults)) {
2959
- const left = withDefaults;
2960
- const right = withoutDefaults;
2961
- const keys = /* @__PURE__ */ new Set([...Object.keys(left), ...Object.keys(right)]);
2962
- for (const k of keys) {
2963
- walkAuthoredFromSchemaDiff(left[k], right[k], [...prefix, k], out);
2964
- }
2965
- return;
2966
- }
2967
- if (Array.isArray(withDefaults) && Array.isArray(withoutDefaults)) {
2968
- const len = Math.max(withDefaults.length, withoutDefaults.length);
2969
- for (let i = 0; i < len; i++) {
2970
- walkAuthoredFromSchemaDiff(withDefaults[i], withoutDefaults[i], [...prefix, i], out);
2971
- }
2972
- return;
2973
- }
2974
- if (!Object.is(withDefaults, withoutDefaults) && prefix.length > 0) {
2975
- out.add(canonicalizePath(prefix).key);
3393
+
3394
+ function remapForOp(op, oldLen) {
3395
+ const moved = /* @__PURE__ */ new Map();
3396
+ const vacated = /* @__PURE__ */ new Set();
3397
+ const fresh = /* @__PURE__ */ new Set();
3398
+ switch (op.kind) {
3399
+ case "insert":
3400
+ for (let i = op.index; i < oldLen; i++) moved.set(i, i + 1);
3401
+ fresh.add(op.index);
3402
+ return { moved, vacated, fresh };
3403
+ case "remove":
3404
+ vacated.add(op.index);
3405
+ for (let i = op.index + 1; i < oldLen; i++) moved.set(i, i - 1);
3406
+ return { moved, vacated, fresh };
3407
+ case "move":
3408
+ if (op.from !== op.to) {
3409
+ moved.set(op.from, op.to);
3410
+ if (op.from < op.to) {
3411
+ for (let i = op.from + 1; i <= op.to; i++) moved.set(i, i - 1);
3412
+ } else {
3413
+ for (let i = op.to; i < op.from; i++) moved.set(i, i + 1);
3414
+ }
3415
+ }
3416
+ return { moved, vacated, fresh };
3417
+ case "swap":
3418
+ if (op.a !== op.b) {
3419
+ moved.set(op.a, op.b);
3420
+ moved.set(op.b, op.a);
3421
+ }
3422
+ return { moved, vacated, fresh };
3423
+ case "replace-at":
3424
+ vacated.add(op.index);
3425
+ fresh.add(op.index);
3426
+ return { moved, vacated, fresh };
3427
+ }
3428
+ }
3429
+ function changedIndices(remap) {
3430
+ const changed = new Set(remap.vacated);
3431
+ for (const [from, to] of remap.moved) {
3432
+ changed.add(from);
3433
+ changed.add(to);
3434
+ }
3435
+ for (const index of remap.fresh) changed.add(index);
3436
+ return changed;
3437
+ }
3438
+ function elementIndexUnder(arrayPath, key, idxPos) {
3439
+ const segments = segmentsForPathKey(key);
3440
+ if (segments === null) return null;
3441
+ if (!isPathPrefix(arrayPath, segments)) return null;
3442
+ if (segments.length <= idxPos) return null;
3443
+ const index = segments[idxPos];
3444
+ return typeof index === "number" ? index : null;
3445
+ }
3446
+ function migrateMapSubtree(map, arrayPath, remap, rewriteValue) {
3447
+ const idxPos = arrayPath.length;
3448
+ const snapshots = [];
3449
+ for (const [key, value] of map) {
3450
+ const index = elementIndexUnder(arrayPath, key, idxPos);
3451
+ if (index === null) continue;
3452
+ if (!remap.moved.has(index) && !remap.vacated.has(index)) continue;
3453
+ snapshots.push({ segments: [...segmentsForPathKey(key)], index, value });
3454
+ }
3455
+ if (snapshots.length === 0) return;
3456
+ for (const snap of snapshots) map.delete(canonicalizePath(snap.segments).key);
3457
+ for (const snap of snapshots) {
3458
+ const target = remap.moved.get(snap.index);
3459
+ if (target === void 0) continue;
3460
+ const relocated = snap.segments.slice();
3461
+ relocated[idxPos] = target;
3462
+ map.set(canonicalizePath(relocated).key, rewriteValue(snap.value, relocated));
3463
+ }
3464
+ }
3465
+ function migrateSetSubtree(set, arrayPath, remap) {
3466
+ const idxPos = arrayPath.length;
3467
+ const snapshots = [];
3468
+ for (const key of set) {
3469
+ const index = elementIndexUnder(arrayPath, key, idxPos);
3470
+ if (index === null) continue;
3471
+ if (!remap.moved.has(index) && !remap.vacated.has(index)) continue;
3472
+ snapshots.push({ segments: [...segmentsForPathKey(key)], index });
3473
+ }
3474
+ if (snapshots.length === 0) return;
3475
+ for (const snap of snapshots) set.delete(canonicalizePath(snap.segments).key);
3476
+ for (const snap of snapshots) {
3477
+ const target = remap.moved.get(snap.index);
3478
+ if (target === void 0) continue;
3479
+ const relocated = snap.segments.slice();
3480
+ relocated[idxPos] = target;
3481
+ set.add(canonicalizePath(relocated).key);
2976
3482
  }
2977
3483
  }
2978
- function createFormStore(options) {
2979
- const { formKey, schema, defaultValues, strict = true, hydration } = options;
2980
- const ssr = options.ssr === true;
2981
- const ssrPrefetch = options.ssrPrefetch;
2982
- const rememberVariants = options.rememberVariants !== false;
3484
+
3485
+ function createArrayBookkeeping(deps) {
3486
+ const {
3487
+ form,
3488
+ fields,
3489
+ userErrors,
3490
+ originals,
3491
+ blankPaths,
3492
+ originalBlankPaths,
3493
+ fieldValidationCounts,
3494
+ fieldValidationState,
3495
+ schemaErrors,
3496
+ activeValidations,
3497
+ arrayIdentity,
3498
+ touchFieldRecord,
3499
+ decFieldValidation
3500
+ } = deps;
3501
+ function migrateElementState(arrayPath, remap) {
3502
+ if (remap.moved.size === 0 && remap.vacated.size === 0) return;
3503
+ migrateMapSubtree(fields, arrayPath, remap, (record, segments) => ({
3504
+ ...record,
3505
+ path: segments
3506
+ }));
3507
+ migrateMapSubtree(
3508
+ userErrors,
3509
+ arrayPath,
3510
+ remap,
3511
+ (errors, segments) => errors.map((error) => ({ ...error, path: [...segments] }))
3512
+ );
3513
+ migrateMapSubtree(originals, arrayPath, remap, (record, segments) => ({
3514
+ segments,
3515
+ value: record.value
3516
+ }));
3517
+ migrateSetSubtree(blankPaths, arrayPath, remap);
3518
+ migrateSetSubtree(originalBlankPaths, arrayPath, remap);
3519
+ migrateMapSubtree(fieldValidationCounts, arrayPath, remap, (count) => count);
3520
+ arrayIdentity.applyRemap(arrayPath, remap);
3521
+ }
3522
+ function seedFreshElement(arrayPath, freshIndex) {
3523
+ const elementPath = [...arrayPath, freshIndex];
3524
+ const now = (/* @__PURE__ */ new Date()).toISOString();
3525
+ diffAndApply(void 0, getAtPath(form.value, elementPath), elementPath, (patch) => {
3526
+ if (patch.kind !== "added") return;
3527
+ const { key } = canonicalizePath(patch.path);
3528
+ if (!originals.has(key)) originals.set(key, { segments: patch.path, value: void 0 });
3529
+ touchFieldRecord(key, patch.path, { updatedAt: now });
3530
+ });
3531
+ }
3532
+ function dropSchemaErrorsAtChangedIndices(arrayPath, remap) {
3533
+ const changed = changedIndices(remap);
3534
+ if (changed.size === 0) return;
3535
+ const idxPos = arrayPath.length;
3536
+ for (const key of [...schemaErrors.keys()]) {
3537
+ const segs = segmentsForPathKey(key);
3538
+ if (segs === null) continue;
3539
+ if (!isPathPrefix(arrayPath, segs)) continue;
3540
+ if (segs.length <= idxPos) continue;
3541
+ const idx = segs[idxPos];
3542
+ if (typeof idx === "number" && changed.has(idx)) schemaErrors.delete(key);
3543
+ }
3544
+ }
3545
+ function abortValidationAtVacatedIndices(arrayPath, remap) {
3546
+ if (remap.vacated.size === 0) return;
3547
+ const idxPos = arrayPath.length;
3548
+ for (const [key, entry] of [...fieldValidationState]) {
3549
+ const segs = segmentsForPathKey(key);
3550
+ if (segs === null) continue;
3551
+ if (!isPathPrefix(arrayPath, segs)) continue;
3552
+ if (segs.length <= idxPos) continue;
3553
+ const idx = segs[idxPos];
3554
+ if (typeof idx !== "number" || !remap.vacated.has(idx)) continue;
3555
+ if (entry.timer !== null) {
3556
+ clearTimeout(entry.timer);
3557
+ } else if (!entry.settled) {
3558
+ activeValidations.value = Math.max(0, activeValidations.value - 1);
3559
+ decFieldValidation(key);
3560
+ }
3561
+ entry.controller.abort();
3562
+ fieldValidationState.delete(key);
3563
+ }
3564
+ }
3565
+ return {
3566
+ migrateElementState,
3567
+ seedFreshElement,
3568
+ dropSchemaErrorsAtChangedIndices,
3569
+ abortValidationAtVacatedIndices
3570
+ };
3571
+ }
3572
+
3573
+ function isHydratedFieldRecord(value) {
3574
+ if (typeof value !== "object" || value === null) return false;
3575
+ const r = value;
3576
+ return Array.isArray(r.path) && (typeof r.updatedAt === "string" || r.updatedAt === null) && typeof r.connected === "boolean" && (typeof r.focused === "boolean" || r.focused === null) && (typeof r.blurred === "boolean" || r.blurred === null) && typeof r.touched === "boolean" && typeof r.interacted === "boolean" && typeof r.blurredAfterInteraction === "boolean";
3577
+ }
3578
+ function isHydratedValidationErrorArray(value) {
3579
+ if (!Array.isArray(value)) return false;
3580
+ for (const entry of value) {
3581
+ if (typeof entry !== "object" || entry === null) return false;
3582
+ const e = entry;
3583
+ if (typeof e.message !== "string") return false;
3584
+ if (!Array.isArray(e.path)) return false;
3585
+ if (typeof e.formKey !== "string") return false;
3586
+ if (typeof e.code !== "string") return false;
3587
+ }
3588
+ return true;
3589
+ }
3590
+ function warnMalformedHydration(formKey, kind, rawKey) {
3591
+ if (!__DEV__) return;
3592
+ console.warn(
3593
+ `[attaform] hydration: skipping malformed ${kind} entry at key '${rawKey}' on form '${formKey}'. This usually means the SSR bundle is on a different version than the client (rolling deploy / stale cache).`
3594
+ );
3595
+ }
3596
+ function isPathKeyUnder(existingKey, parentPath) {
3597
+ const parsed = segmentsForPathKey(existingKey);
3598
+ if (parsed === null) return false;
3599
+ if (parsed.length <= parentPath.length) return false;
3600
+ for (let i = 0; i < parentPath.length; i++) {
3601
+ if (parsed[i] !== parentPath[i]) return false;
3602
+ }
3603
+ return true;
3604
+ }
3605
+ function stripSymbolsDeep(value) {
3606
+ if (value === null || typeof value !== "object") return value;
3607
+ if (Array.isArray(value)) {
3608
+ let mutated2 = false;
3609
+ const out2 = new Array(value.length);
3610
+ for (let i = 0; i < value.length; i++) {
3611
+ const cleaned = stripSymbolsDeep(value[i]);
3612
+ out2[i] = cleaned;
3613
+ if (cleaned !== value[i]) mutated2 = true;
3614
+ }
3615
+ return mutated2 ? out2 : value;
3616
+ }
3617
+ const proto = Object.getPrototypeOf(value);
3618
+ if (proto !== Object.prototype && proto !== null) return value;
3619
+ const symKeys = Object.getOwnPropertySymbols(value);
3620
+ const stringKeys = Object.keys(value);
3621
+ let mutated = symKeys.length > 0;
3622
+ const out = {};
3623
+ const src = value;
3624
+ for (const k of stringKeys) {
3625
+ const cleaned = stripSymbolsDeep(src[k]);
3626
+ out[k] = cleaned;
3627
+ if (cleaned !== src[k]) mutated = true;
3628
+ }
3629
+ return mutated ? out : value;
3630
+ }
3631
+ function walkAuthoredFromConstraints(value, prefix, out) {
3632
+ if (prefix.length > 0) out.add(canonicalizePath(prefix).key);
3633
+ if (isPlainRecord(value)) {
3634
+ for (const k of Object.keys(value)) {
3635
+ walkAuthoredFromConstraints(value[k], [...prefix, k], out);
3636
+ }
3637
+ return;
3638
+ }
3639
+ if (Array.isArray(value)) {
3640
+ for (let i = 0; i < value.length; i++) {
3641
+ walkAuthoredFromConstraints(value[i], [...prefix, i], out);
3642
+ }
3643
+ }
3644
+ }
3645
+ function walkAuthoredFromSchemaDiff(withDefaults, withoutDefaults, prefix, out) {
3646
+ if (isPlainRecord(withDefaults) && isPlainRecord(withoutDefaults)) {
3647
+ const left = withDefaults;
3648
+ const right = withoutDefaults;
3649
+ const keys = /* @__PURE__ */ new Set([...Object.keys(left), ...Object.keys(right)]);
3650
+ for (const k of keys) {
3651
+ walkAuthoredFromSchemaDiff(left[k], right[k], [...prefix, k], out);
3652
+ }
3653
+ return;
3654
+ }
3655
+ if (Array.isArray(withDefaults) && Array.isArray(withoutDefaults)) {
3656
+ const len = Math.max(withDefaults.length, withoutDefaults.length);
3657
+ for (let i = 0; i < len; i++) {
3658
+ walkAuthoredFromSchemaDiff(withDefaults[i], withoutDefaults[i], [...prefix, i], out);
3659
+ }
3660
+ return;
3661
+ }
3662
+ if (!Object.is(withDefaults, withoutDefaults) && prefix.length > 0) {
3663
+ out.add(canonicalizePath(prefix).key);
3664
+ }
3665
+ }
3666
+ function createFormStore(options) {
3667
+ const { formKey, schema, defaultValues, strict = true, hydration } = options;
3668
+ const ssr = options.ssr === true;
3669
+ const ssrPrefetch = options.ssrPrefetch;
3670
+ const rememberVariants = options.rememberVariants !== false;
2983
3671
  const fieldValidationMode = options.validateOn ?? "change";
2984
3672
  const fieldValidationDebounceMs = normalizeNumericOption({
2985
3673
  value: options.debounceMs ?? DEFAULT_FIELD_VALIDATION_DEBOUNCE_MS,
@@ -3010,11 +3698,8 @@ function createFormStore(options) {
3010
3698
  noSyncPathCounts.set(path, current - 1);
3011
3699
  }
3012
3700
  const coerceIndex = resolveCoercionIndex(options.coerce);
3013
- const resolvedShouldShowErrors = resolveShouldShowErrors(
3014
- options.shouldShowErrors
3015
- );
3701
+ const resolvedGetDisplayState = resolveGetDisplayState(options.getDisplayState);
3016
3702
  const resolvedIsSensitivePath = options.isSensitivePath ?? isSensitivePath;
3017
- const resolvedSegmentMatchesSensitive = options.segmentMatchesSensitive ?? segmentMatchesSensitive;
3018
3703
  const cleanupHooks = [];
3019
3704
  const modules = /* @__PURE__ */ new Map();
3020
3705
  const completedConstraints = defaultValues === void 0 ? void 0 : mergeStructural(schema, [], defaultValues);
@@ -3052,6 +3737,16 @@ function createFormStore(options) {
3052
3737
  warn: true
3053
3738
  });
3054
3739
  const form = ref(stubbedInitialData);
3740
+ const arrayIdentity = createArrayIdentity((arraySegs) => {
3741
+ const v = getAtPath(form.value, arraySegs);
3742
+ return Array.isArray(v) ? v.length : 0;
3743
+ });
3744
+ function arrayElementKey(path) {
3745
+ if (path.length === 0) return "";
3746
+ const last = path[path.length - 1];
3747
+ if (typeof last === "number") return arrayIdentity.tokenAt(path.slice(0, -1), last);
3748
+ return "";
3749
+ }
3055
3750
  const fields = reactive(/* @__PURE__ */ new Map());
3056
3751
  const elements = reactive(/* @__PURE__ */ new Map());
3057
3752
  const elementToFormInstance = /* @__PURE__ */ new WeakMap();
@@ -3067,41 +3762,7 @@ function createFormStore(options) {
3067
3762
  blankPaths.add(key);
3068
3763
  originalBlankPaths.add(key);
3069
3764
  }
3070
- const variantMemory = /* @__PURE__ */ new Map();
3071
- function clearVariantMemoryUnderPath(arrayPath) {
3072
- for (const memKey of [...variantMemory.keys()]) {
3073
- const segs = segmentsForPathKey(memKey);
3074
- if (segs === null) continue;
3075
- if (isPathPrefix(arrayPath, segs)) variantMemory.delete(memKey);
3076
- }
3077
- }
3078
- function clearVariantMemoryAtArrayIndices(arrayPath, indexFilter) {
3079
- for (const memKey of [...variantMemory.keys()]) {
3080
- const segs = segmentsForPathKey(memKey);
3081
- if (segs === null) continue;
3082
- if (!isPathPrefix(arrayPath, segs)) continue;
3083
- if (segs.length <= arrayPath.length) continue;
3084
- const idxSeg = segs[arrayPath.length];
3085
- if (typeof idxSeg !== "number") continue;
3086
- if (indexFilter(idxSeg)) variantMemory.delete(memKey);
3087
- }
3088
- }
3089
- function applyArrayOpToMemory(arrayPath, op) {
3090
- switch (op.kind) {
3091
- case "shift-from":
3092
- clearVariantMemoryAtArrayIndices(arrayPath, (i) => i >= op.index);
3093
- return;
3094
- case "shift-range":
3095
- clearVariantMemoryAtArrayIndices(arrayPath, (i) => i >= op.fromIndex && i <= op.toIndex);
3096
- return;
3097
- case "swap":
3098
- clearVariantMemoryAtArrayIndices(arrayPath, (i) => i === op.a || i === op.b);
3099
- return;
3100
- case "replace-at":
3101
- clearVariantMemoryAtArrayIndices(arrayPath, (i) => i === op.index);
3102
- return;
3103
- }
3104
- }
3765
+ const variantMemory = createVariantMemory();
3105
3766
  const pathOrdinals = /* @__PURE__ */ new Map();
3106
3767
  let nextOrdinal = 0;
3107
3768
  function ensurePathOrdinal(key) {
@@ -3139,6 +3800,9 @@ function createFormStore(options) {
3139
3800
  const departAttempts = ref(0);
3140
3801
  const submissionGeneration = ref(0);
3141
3802
  const activeValidations = ref(0);
3803
+ const pathSnapshots = /* @__PURE__ */ new Map();
3804
+ let scheduleEpoch = 0;
3805
+ let lastCommittedEpoch = 0;
3142
3806
  const hydrating = ref(false);
3143
3807
  const hydrateError = ref(null);
3144
3808
  const defaultValuesFactory = ref(void 0);
@@ -3154,9 +3818,12 @@ function createFormStore(options) {
3154
3818
  const pathAsyncCache = /* @__PURE__ */ new Map();
3155
3819
  function pathHasAsyncValidation(path) {
3156
3820
  const { key } = canonicalizePath(path);
3821
+ return pathHasAsyncValidationByKey(key, path);
3822
+ }
3823
+ function pathHasAsyncValidationByKey(key, segments) {
3157
3824
  const cached = pathAsyncCache.get(key);
3158
3825
  if (cached !== void 0) return cached;
3159
- const candidates = schema.getSchemasAtPath(path);
3826
+ const candidates = schema.getSchemasAtPath(segments);
3160
3827
  const hasAsync = candidates.some((sub) => sub.needsAsyncValidation?.() === true);
3161
3828
  pathAsyncCache.set(key, hasAsync);
3162
3829
  return hasAsync;
@@ -3209,7 +3876,9 @@ function createFormStore(options) {
3209
3876
  connected: false,
3210
3877
  focused: null,
3211
3878
  blurred: null,
3212
- touched: false
3879
+ touched: false,
3880
+ interacted: false,
3881
+ blurredAfterInteraction: false
3213
3882
  });
3214
3883
  });
3215
3884
  if (strict && !schemaResponse.success) {
@@ -3238,9 +3907,29 @@ function createFormStore(options) {
3238
3907
  blurred: patch.blurred !== void 0 ? patch.blurred : current?.blurred ?? null,
3239
3908
  // touched is plain `boolean`; `??` is equivalent to the explicit
3240
3909
  // guard here because `false` is not nullish.
3241
- touched: patch.touched ?? current?.touched ?? false
3910
+ touched: patch.touched ?? current?.touched ?? false,
3911
+ // interacted is sticky-true; a merge patch only ever sets it, so
3912
+ // `??` preserves the current bit. It flips back to false solely
3913
+ // through the reset paths, which reconstruct the record outright.
3914
+ interacted: patch.interacted ?? current?.interacted ?? false,
3915
+ blurredAfterInteraction: patch.blurredAfterInteraction ?? current?.blurredAfterInteraction ?? false
3242
3916
  });
3243
3917
  }
3918
+ const arrayBookkeeping = createArrayBookkeeping({
3919
+ form,
3920
+ fields,
3921
+ userErrors,
3922
+ originals,
3923
+ blankPaths,
3924
+ originalBlankPaths,
3925
+ fieldValidationCounts,
3926
+ fieldValidationState,
3927
+ schemaErrors,
3928
+ activeValidations,
3929
+ arrayIdentity,
3930
+ touchFieldRecord,
3931
+ decFieldValidation
3932
+ });
3244
3933
  function applyFormReplacement(next, meta) {
3245
3934
  const prev = form.value;
3246
3935
  if (Object.is(prev, next)) return;
@@ -3364,8 +4053,13 @@ function createFormStore(options) {
3364
4053
  const pathKey = canonicalizePath(path).key;
3365
4054
  if (meta?.blank === true) {
3366
4055
  blankPaths.add(pathKey);
3367
- } else if (blankPaths.has(pathKey)) {
3368
- blankPaths.delete(pathKey);
4056
+ } else {
4057
+ if (blankPaths.has(pathKey)) blankPaths.delete(pathKey);
4058
+ if (meta?.arrayOp === void 0) {
4059
+ for (const existingKey of [...blankPaths]) {
4060
+ if (isPathKeyUnder(existingKey, path)) blankPaths.delete(existingKey);
4061
+ }
4062
+ }
3369
4063
  }
3370
4064
  const wasAuthoredBefore = authoredPaths.has(pathKey);
3371
4065
  walkAuthoredFromConstraints(value, path, authoredPaths);
@@ -3384,12 +4078,20 @@ function createFormStore(options) {
3384
4078
  }
3385
4079
  return true;
3386
4080
  }
4081
+ const oldArrayLength = Array.isArray(currentValue) ? currentValue.length : 0;
3387
4082
  const nextForm = setAtPathWithSchemaFill(form.value, schema, path, completedValue);
3388
4083
  applyFormReplacement(nextForm, meta);
3389
4084
  if (meta?.arrayOp !== void 0) {
3390
- applyArrayOpToMemory(path, meta.arrayOp);
4085
+ const remap = remapForOp(meta.arrayOp, oldArrayLength);
4086
+ arrayBookkeeping.migrateElementState(path, remap);
4087
+ for (const freshIndex of remap.fresh) arrayBookkeeping.seedFreshElement(path, freshIndex);
4088
+ arrayBookkeeping.dropSchemaErrorsAtChangedIndices(path, remap);
4089
+ arrayBookkeeping.abortValidationAtVacatedIndices(path, remap);
4090
+ variantMemory.applyArrayOp(path, meta.arrayOp);
4091
+ arrayIdentity.applyOp(path, meta.arrayOp);
3391
4092
  } else if (Array.isArray(value) && Array.isArray(currentValue)) {
3392
- clearVariantMemoryUnderPath(path);
4093
+ variantMemory.clearUnderPath(path);
4094
+ arrayIdentity.realign(path);
3393
4095
  }
3394
4096
  const effectiveModeAfterWrite = meta?.instance?.validateOn ?? fieldValidationMode;
3395
4097
  if (effectiveModeAfterWrite === "change") {
@@ -3413,18 +4115,12 @@ function createFormStore(options) {
3413
4115
  for (const k of blankPaths) {
3414
4116
  if (isPathKeyUnder(k, parentPath)) outgoingBlanks.push(k);
3415
4117
  }
3416
- let memoryForUnion2 = variantMemory.get(parentKey);
3417
- if (memoryForUnion2 === void 0) {
3418
- memoryForUnion2 = /* @__PURE__ */ new Map();
3419
- variantMemory.set(parentKey, memoryForUnion2);
3420
- }
3421
- memoryForUnion2.set(oldDiscValue, {
4118
+ variantMemory.recordOutgoing(parentKey, oldDiscValue, {
3422
4119
  value: currentValue2,
3423
4120
  blankPaths: outgoingBlanks
3424
4121
  });
3425
4122
  }
3426
- const memoryForUnion = variantMemory.get(parentKey);
3427
- const restored = memoryForUnion?.get(newDiscValue);
4123
+ const restored = variantMemory.lookupIncoming(parentKey, newDiscValue);
3428
4124
  if (restored !== void 0) {
3429
4125
  baseline = restored.value;
3430
4126
  restoredBlanks = [...restored.blankPaths];
@@ -3496,6 +4192,7 @@ function createFormStore(options) {
3496
4192
  const controller = new AbortController();
3497
4193
  const fresh = { controller, timer: null, settled: false };
3498
4194
  fieldValidationState.set(key, fresh);
4195
+ const myEpoch = ++scheduleEpoch;
3499
4196
  const run = () => {
3500
4197
  fresh.timer = null;
3501
4198
  if (controller.signal.aborted) return;
@@ -3510,11 +4207,25 @@ function createFormStore(options) {
3510
4207
  }
3511
4208
  throw err;
3512
4209
  }
3513
- void Promise.resolve().then(() => schema.validateAtPath(form.value, void 0)).then((response) => {
4210
+ const subtreeScope = path.length > 0 && schema.hasContainerOrRootRefine?.() === false;
4211
+ const scopePath = subtreeScope ? path : void 0;
4212
+ const dataAtScope = subtreeScope ? getAtPath(form.value, path) : form.value;
4213
+ const scopeKey = subtreeScope ? canonicalizePath(path).key : ROOT_PATH_KEY;
4214
+ void Promise.resolve().then(() => schema.validateAtPath(dataAtScope, scopePath)).then((response) => {
3514
4215
  if (controller.signal.aborted) return;
4216
+ if (myEpoch <= lastCommittedEpoch) return;
4217
+ lastCommittedEpoch = myEpoch;
4218
+ if (effectiveMode === "blur") {
4219
+ const snapshotSource = scopePath !== void 0 ? getAtPath(form.value, scopePath) : form.value;
4220
+ pathSnapshots.set(scopeKey, structuralSnapshot(snapshotSource));
4221
+ }
3515
4222
  const errors = response.success ? [] : response.errors;
3516
4223
  const filtered = filterAuthoredErrors(errors);
3517
- applySchemaErrorsForSubtree([], filtered);
4224
+ const restamped = subtreeScope ? filtered.map((err) => ({
4225
+ ...err,
4226
+ path: [...path, ...err.path]
4227
+ })) : filtered;
4228
+ applySchemaErrorsForSubtree(scopePath ?? [], restamped);
3518
4229
  }).catch(() => {
3519
4230
  }).finally(() => {
3520
4231
  activeValidations.value = Math.max(0, activeValidations.value - 1);
@@ -3741,24 +4452,55 @@ function createFormStore(options) {
3741
4452
  }
3742
4453
  function markFocused(path, focused, meta) {
3743
4454
  const { key } = canonicalizePath(path);
4455
+ const current = fields.get(key);
3744
4456
  touchFieldRecord(key, path, {
3745
4457
  focused,
3746
4458
  blurred: !focused,
3747
4459
  // `touched` flips to true on blur and stays true thereafter; while
3748
4460
  // a field is currently focused we keep whatever value it held.
3749
- touched: focused ? fields.get(key)?.touched ?? false : true
4461
+ touched: focused ? current?.touched ?? false : true,
4462
+ // `blurredAfterInteraction` flips true on the first blur that lands
4463
+ // after a value edit and stays true. A tab-through blur before any
4464
+ // edit leaves it false (`interacted` is still false at that blur),
4465
+ // which is what keeps a clean tab-through from arming the gate.
4466
+ blurredAfterInteraction: !focused && current?.interacted === true ? true : current?.blurredAfterInteraction ?? false
3750
4467
  });
3751
4468
  const focusMode = meta?.instance?.validateOn ?? fieldValidationMode;
3752
4469
  if (!focused && focusMode === "blur") {
3753
- scheduleFieldValidation(path, true, {
3754
- ...meta?.instance?.validateOn !== void 0 ? { mode: meta.instance.validateOn } : {},
3755
- ...meta?.instance?.debounceMs !== void 0 ? { debounceMs: meta.instance.debounceMs } : {}
3756
- });
4470
+ const firstInteractiveBlur = current?.interacted === true && current.blurredAfterInteraction !== true;
4471
+ let snapshot = void 0;
4472
+ let snapshotScopeLength = 0;
4473
+ for (let i = path.length; i >= 0; i--) {
4474
+ const ancestorKey = canonicalizePath(path.slice(0, i)).key;
4475
+ const entry = pathSnapshots.get(ancestorKey);
4476
+ if (entry !== void 0) {
4477
+ snapshot = entry;
4478
+ snapshotScopeLength = i;
4479
+ break;
4480
+ }
4481
+ }
4482
+ let changed = true;
4483
+ if (!firstInteractiveBlur && snapshot !== void 0) {
4484
+ const relPath = path.slice(snapshotScopeLength);
4485
+ const snapshotSubtree = getAtPath(snapshot, relPath);
4486
+ const liveSubtree = getAtPath(form.value, path);
4487
+ changed = false;
4488
+ diffAndApply(snapshotSubtree, liveSubtree, path, () => {
4489
+ changed = true;
4490
+ });
4491
+ }
4492
+ if (changed) {
4493
+ scheduleFieldValidation(path, true, {
4494
+ ...meta?.instance?.validateOn !== void 0 ? { mode: meta.instance.validateOn } : {},
4495
+ ...meta?.instance?.debounceMs !== void 0 ? { debounceMs: meta.instance.debounceMs } : {}
4496
+ });
4497
+ }
3757
4498
  }
3758
4499
  }
3759
- function markTouched(path) {
4500
+ function markInteracted(path) {
3760
4501
  const { key } = canonicalizePath(path);
3761
- touchFieldRecord(key, path, { touched: true });
4502
+ if (fields.get(key)?.interacted === true) return;
4503
+ touchFieldRecord(key, path, { interacted: true });
3762
4504
  }
3763
4505
  function touchAtPath(segments) {
3764
4506
  const formValue = form.value;
@@ -3778,9 +4520,6 @@ function createFormStore(options) {
3778
4520
  );
3779
4521
  }
3780
4522
  }
3781
- function clear(path) {
3782
- return setValueAtPath(path, schema.getEmptyValueAtPath(path));
3783
- }
3784
4523
  function rehydrate() {
3785
4524
  const factory = defaultValuesFactory.value;
3786
4525
  if (factory === void 0) {
@@ -3870,6 +4609,7 @@ function createFormStore(options) {
3870
4609
  const next = resetResponse.data;
3871
4610
  rebuildAuthoredPaths(resetSource, next);
3872
4611
  applyFormReplacement(next);
4612
+ arrayIdentity.rebaselineAll();
3873
4613
  originals.clear();
3874
4614
  diffAndApply({}, next, [], (patch) => {
3875
4615
  if (patch.kind !== "added") return;
@@ -3913,7 +4653,9 @@ function createFormStore(options) {
3913
4653
  connected: record.connected,
3914
4654
  focused: record.focused,
3915
4655
  blurred: record.blurred,
3916
- touched: false
4656
+ touched: false,
4657
+ interacted: false,
4658
+ blurredAfterInteraction: false
3917
4659
  });
3918
4660
  }
3919
4661
  submissionGeneration.value += 1;
@@ -3924,6 +4666,9 @@ function createFormStore(options) {
3924
4666
  submitError.value = null;
3925
4667
  departAttempts.value = 0;
3926
4668
  cancelFieldValidation();
4669
+ pathSnapshots.clear();
4670
+ scheduleEpoch = 0;
4671
+ lastCommittedEpoch = 0;
3927
4672
  variantMemory.clear();
3928
4673
  for (const listener of resetListeners) {
3929
4674
  try {
@@ -3935,13 +4680,7 @@ function createFormStore(options) {
3935
4680
  }
3936
4681
  function resetField(path) {
3937
4682
  const { key: targetKey, segments: targetSegments } = canonicalizePath(path);
3938
- for (const memKey of [...variantMemory.keys()]) {
3939
- const memSegments = segmentsForPathKey(memKey);
3940
- if (memSegments === null) continue;
3941
- if (isPathPrefix(targetSegments, memSegments)) {
3942
- variantMemory.delete(memKey);
3943
- }
3944
- }
4683
+ variantMemory.clearUnderPath(targetSegments);
3945
4684
  const leafEntry = originals.get(targetKey);
3946
4685
  if (leafEntry !== void 0) {
3947
4686
  const wrote = setValueAtPath(targetSegments, leafEntry.value);
@@ -3997,23 +4736,24 @@ function createFormStore(options) {
3997
4736
  connected: record.connected,
3998
4737
  focused: record.focused,
3999
4738
  blurred: record.blurred,
4000
- touched: false
4739
+ touched: false,
4740
+ interacted: false,
4741
+ blurredAfterInteraction: false
4001
4742
  });
4002
4743
  }
4003
- function isPathPrefix(prefix, candidate) {
4004
- if (prefix.length > candidate.length) return false;
4005
- for (let i = 0; i < prefix.length; i++) {
4006
- if (prefix[i] !== candidate[i]) return false;
4007
- }
4008
- return true;
4009
- }
4010
4744
  function isPristineAtPath(path) {
4011
4745
  const { key, segments } = canonicalizePath(path);
4746
+ return isPristineAtPathByKey(key, segments);
4747
+ }
4748
+ function isPristineAtPathByKey(key, segments) {
4012
4749
  if (blankPaths.has(key) !== originalBlankPaths.has(key)) return false;
4013
4750
  const entry = originals.get(key);
4014
4751
  if (entry === void 0) return true;
4015
4752
  return Object.is(getAtPath(form.value, segments), entry.value);
4016
4753
  }
4754
+ function hasStructuralChangeUnder(path) {
4755
+ return arrayIdentity.hasStructuralChangeUnder(path);
4756
+ }
4017
4757
  function getFieldRecord(path) {
4018
4758
  const { key } = canonicalizePath(path);
4019
4759
  return fields.get(key);
@@ -4057,7 +4797,7 @@ function createFormStore(options) {
4057
4797
  originals,
4058
4798
  schema,
4059
4799
  ssr,
4060
- shouldShowErrors: resolvedShouldShowErrors,
4800
+ getDisplayState: resolvedGetDisplayState,
4061
4801
  submitting,
4062
4802
  activeSubmissions,
4063
4803
  submissionAttempts,
@@ -4067,6 +4807,7 @@ function createFormStore(options) {
4067
4807
  hydrating,
4068
4808
  hydrateError,
4069
4809
  defaultValuesFactory,
4810
+ hasSsrPrefetch: ssrPrefetch !== void 0,
4070
4811
  defaultsResolved,
4071
4812
  activated,
4072
4813
  activationPromise,
@@ -4076,13 +4817,14 @@ function createFormStore(options) {
4076
4817
  activeValidations,
4077
4818
  firstValidationDone,
4078
4819
  pathHasAsyncValidation,
4820
+ pathHasAsyncValidationByKey,
4079
4821
  fieldValidationCounts,
4080
4822
  applyFormReplacement,
4081
4823
  setValueAtPath,
4082
4824
  getValueAtPath,
4825
+ arrayElementKey,
4083
4826
  reset,
4084
4827
  resetField,
4085
- clear,
4086
4828
  setSchemaErrorsForPath,
4087
4829
  setAllSchemaErrors,
4088
4830
  clearSchemaErrors,
@@ -4095,10 +4837,12 @@ function createFormStore(options) {
4095
4837
  registerElement,
4096
4838
  deregisterElement,
4097
4839
  markFocused,
4098
- markTouched,
4840
+ markInteracted,
4099
4841
  touchAtPath,
4100
4842
  markConnectedOptimistically,
4101
4843
  isPristineAtPath,
4844
+ isPristineAtPathByKey,
4845
+ hasStructuralChangeUnder,
4102
4846
  getFieldRecord,
4103
4847
  getOriginalAtPath,
4104
4848
  getFirstErrorElement,
@@ -4114,7 +4858,6 @@ function createFormStore(options) {
4114
4858
  modules,
4115
4859
  persistOptIns,
4116
4860
  isSensitivePath: resolvedIsSensitivePath,
4117
- segmentMatchesSensitive: resolvedSegmentMatchesSensitive,
4118
4861
  noSyncPaths,
4119
4862
  incrementNoSyncOptOut,
4120
4863
  decrementNoSyncOptOut,
@@ -4315,939 +5058,988 @@ function createHistoryModule(state, config) {
4315
5058
  };
4316
5059
  }
4317
5060
 
4318
- function hashStableString(input, seed = 0) {
4319
- let h1 = 3735928559 ^ seed;
4320
- let h2 = 1103547991 ^ seed;
4321
- for (let i = 0; i < input.length; i++) {
4322
- const ch = input.charCodeAt(i);
4323
- h1 = Math.imul(h1 ^ ch, 2654435761);
4324
- h2 = Math.imul(h2 ^ ch, 1597334677);
4325
- }
4326
- h1 = Math.imul(h1 ^ h1 >>> 16, 2246822507) ^ Math.imul(h2 ^ h2 >>> 13, 3266489909);
4327
- h2 = Math.imul(h2 ^ h2 >>> 16, 2246822507) ^ Math.imul(h1 ^ h1 >>> 13, 3266489909);
4328
- return (4294967296 * (2097151 & h2) + (h1 >>> 0)).toString(36).padStart(11, "0");
4329
- }
4330
-
4331
- const PROTOCOL_VERSION = 1;
4332
- const JOIN_COLLECTION_WINDOW_MS = 50;
4333
- const SNAPSHOT_TIMEOUT_MS = 200;
4334
- const MAX_LEADER_ATTEMPTS = 3;
4335
- function isFileLikeValue(value) {
4336
- if (typeof File !== "undefined" && value instanceof File) return true;
4337
- if (typeof Blob !== "undefined" && value instanceof Blob) return true;
4338
- return false;
4339
- }
4340
- function isDangerousSegment(s) {
4341
- return s === "__proto__" || s === "constructor" || s === "prototype";
4342
- }
4343
- function pathContainsDangerousSegment(path) {
4344
- for (let i = 0; i < path.length; i++) {
4345
- if (isDangerousSegment(path[i])) return true;
4346
- }
4347
- return false;
4348
- }
4349
- function isInboundShapeAcceptable(schema, path, value) {
4350
- if (isFileLikeValue(value)) return false;
4351
- let kind;
4352
- if (Array.isArray(value)) {
4353
- kind = "array";
4354
- } else if (value !== null && typeof value === "object" && isPlainRecord(value)) {
4355
- kind = "object";
4356
- } else {
4357
- kind = slimKindOf(value);
4358
- }
4359
- const accepted = schema.getSlimPrimitiveTypesAtPath(path);
4360
- if (!accepted.has(kind)) return false;
4361
- if (Array.isArray(value)) {
4362
- for (let i = 0; i < value.length; i++) {
4363
- if (!isInboundShapeAcceptable(schema, [...path, i], value[i])) return false;
4364
- }
4365
- return true;
4366
- }
4367
- if (isPlainRecord(value)) {
4368
- for (const key of Object.keys(value)) {
4369
- if (!isInboundShapeAcceptable(schema, [...path, key], value[key])) return false;
5061
+ function wirePersistence(state, config) {
5062
+ let fingerprint;
5063
+ try {
5064
+ fingerprint = hashStableString(state.schema.fingerprint());
5065
+ } catch (err) {
5066
+ if (__DEV__) {
5067
+ console.warn(
5068
+ `[attaform] Could not fingerprint the schema for form '${state.formKey}': ${err instanceof Error ? err.message : String(err)}. Persistence falls back to a fingerprint-free key, so a schema change won't auto-invalidate a saved draft.`
5069
+ );
4370
5070
  }
4371
- return true;
5071
+ fingerprint = "unfingerprinted";
4372
5072
  }
4373
- return true;
4374
- }
4375
- function diffBlankPaths(prev, curr) {
4376
- const added = [];
4377
- const removed = [];
4378
- const prevSet = new Set(prev);
4379
- for (const k of curr) if (!prevSet.has(k)) added.push(k);
4380
- for (const k of prev) if (!curr.has(k)) removed.push(k);
4381
- return { added, removed };
4382
- }
4383
- function snapshotForm(form) {
4384
- return structuralSnapshot(form);
4385
- }
4386
- function stripSensitivePathsDeep(value, pathSoFar, isSensitivePath) {
4387
- if (isFileLikeValue(value)) return void 0;
4388
- if (value === null || typeof value !== "object") return value;
4389
- if (Array.isArray(value)) {
4390
- return value.map((item, i) => stripSensitivePathsDeep(item, [...pathSoFar, i], isSensitivePath));
4391
- }
4392
- const proto = Object.getPrototypeOf(value);
4393
- if (proto !== Object.prototype && proto !== null) return value;
4394
- const out = {};
4395
- const src = value;
4396
- for (const key of Object.keys(src)) {
4397
- const childPath = [...pathSoFar, key];
4398
- if (isSensitivePath(childPath)) {
4399
- out[key] = void 0;
4400
- continue;
4401
- }
4402
- out[key] = stripSensitivePathsDeep(src[key], childPath, isSensitivePath);
4403
- }
4404
- return out;
4405
- }
4406
- function isValidSyncMessage(data) {
4407
- if (data === null || typeof data !== "object") return false;
4408
- const m = data;
4409
- if (m["v"] !== PROTOCOL_VERSION) return false;
4410
- if (typeof m["senderId"] !== "string") return false;
4411
- if (typeof m["kind"] !== "string") return false;
4412
- switch (m["kind"]) {
4413
- case "hello":
4414
- case "announce":
4415
- return true;
4416
- case "requestSnapshot":
4417
- return typeof m["targetId"] === "string";
4418
- case "snapshot":
4419
- return Array.isArray(m["blankPaths"]) && "form" in m;
4420
- case "patches":
4421
- return Array.isArray(m["formPatches"]) && Array.isArray(m["blankPathsAdded"]) && Array.isArray(m["blankPathsRemoved"]);
4422
- default:
4423
- return false;
4424
- }
4425
- }
4426
- function generateSenderId() {
4427
- try {
4428
- return globalThis.crypto.randomUUID();
4429
- } catch {
4430
- return `atta-${Math.random().toString(36).slice(2)}-${Date.now().toString(36)}`;
4431
- }
4432
- }
4433
- function createMultiTabSyncModule(state, channelName, options) {
4434
- if (typeof BroadcastChannel === "undefined") {
4435
- return {
4436
- dispose: () => void 0,
4437
- lifecycle: () => "established",
4438
- senderId: "",
4439
- channelName
4440
- };
4441
- }
4442
- let channel;
4443
- try {
4444
- channel = new BroadcastChannel(channelName);
4445
- } catch {
4446
- return {
4447
- dispose: () => void 0,
4448
- lifecycle: () => "established",
4449
- senderId: "",
4450
- channelName
4451
- };
4452
- }
4453
- const senderId = generateSenderId();
4454
- let lifecycle = "joining";
5073
+ const base = resolveStorageKeyBase(config, state.formKey);
5074
+ const key = `${base}:${fingerprint}`;
5075
+ const debounceMs = normalizeNumericOption({
5076
+ value: config.debounceMs ?? DEFAULT_PERSISTENCE_DEBOUNCE_MS,
5077
+ source: "useForm.persist.debounceMs",
5078
+ allowInfinity: false,
5079
+ min: 0,
5080
+ defaultValue: DEFAULT_PERSISTENCE_DEBOUNCE_MS
5081
+ });
5082
+ const include = config.include ?? "form";
5083
+ const clearOnSubmitSuccess = config.clearOnSubmitSuccess ?? true;
5084
+ const adapterPromise = getStorageAdapter(config.storage);
4455
5085
  let disposed = false;
4456
- const peerIds = /* @__PURE__ */ new Set();
4457
- let joinCollectionTimer = null;
4458
- let snapshotTimeoutTimer = null;
4459
- let leaderAttempts = 0;
4460
- let prior = {
4461
- form: snapshotForm(state.form.value),
4462
- blankPathsSnapshot: [...state.blankPaths]
4463
- };
4464
- function safePost(msg) {
4465
- if (disposed) return;
4466
- try {
4467
- channel.postMessage(msg);
4468
- } catch {
4469
- }
4470
- }
4471
- function refreshPrior() {
4472
- prior = {
4473
- form: snapshotForm(state.form.value),
4474
- blankPathsSnapshot: [...state.blankPaths]
4475
- };
4476
- }
4477
- function isPathLocallySuppressed(path) {
4478
- if (pathContainsDangerousSegment(path)) return true;
4479
- if (options.isSensitivePath(path)) return true;
4480
- const { key } = canonicalizePath([...path]);
4481
- if (options.noSyncPaths.has(key)) return true;
4482
- return false;
4483
- }
4484
- function postPatches() {
4485
- if (lifecycle !== "established") return;
4486
- const next = snapshotForm(state.form.value);
4487
- const rawPatches = [];
4488
- diffAndApply(prior.form, next, [], (p) => rawPatches.push(p));
4489
- const safePatches = [];
4490
- for (const p of rawPatches) {
4491
- if (isPathLocallySuppressed(p.path)) continue;
4492
- if ("value" in p && isFileLikeValue(p.value)) continue;
4493
- safePatches.push(p);
4494
- }
4495
- const { added, removed } = diffBlankPaths(prior.blankPathsSnapshot, state.blankPaths);
4496
- if (safePatches.length === 0 && added.length === 0 && removed.length === 0) {
4497
- prior = { form: next, blankPathsSnapshot: [...state.blankPaths] };
5086
+ const isDisposed = () => disposed;
5087
+ let inFlightFinalFlush = null;
5088
+ let pendingOptedInPaths = null;
5089
+ const writer = createDebouncedWriter(async () => {
5090
+ const optedInPaths = pendingOptedInPaths ?? new Set(state.persistOptIns.optedInPaths());
5091
+ pendingOptedInPaths = null;
5092
+ const adapter = await adapterPromise;
5093
+ if (isDisposed()) return;
5094
+ if (optedInPaths.size === 0) {
5095
+ await adapter.removeItem(key);
4498
5096
  return;
4499
5097
  }
4500
- safePost({
4501
- v: PROTOCOL_VERSION,
4502
- kind: "patches",
4503
- senderId,
4504
- formPatches: safePatches,
4505
- blankPathsAdded: added,
4506
- blankPathsRemoved: removed
4507
- });
4508
- prior = { form: next, blankPathsSnapshot: [...state.blankPaths] };
4509
- }
5098
+ const rawForm = toRaw(state.form.value);
5099
+ const filteredForm = stripUnacknowledgedSensitiveLeaves(
5100
+ pluckPaths(rawForm, optedInPaths),
5101
+ optedInPaths,
5102
+ state.isSensitivePath
5103
+ );
5104
+ const filteredSchemaErrors = filterErrorsByPaths(state.schemaErrors, optedInPaths);
5105
+ const filteredUserErrors = filterErrorsByPaths(state.userErrors, optedInPaths);
5106
+ const filteredTransientEmpty = /* @__PURE__ */ new Set();
5107
+ for (const tk of state.blankPaths) {
5108
+ if (optedInPaths.has(tk)) filteredTransientEmpty.add(tk);
5109
+ }
5110
+ const payload = buildPersistedPayload(
5111
+ filteredForm,
5112
+ include,
5113
+ filteredSchemaErrors,
5114
+ filteredUserErrors,
5115
+ filteredTransientEmpty
5116
+ );
5117
+ await adapter.setItem(key, payload);
5118
+ }, debounceMs);
4510
5119
  const unsubscribeChange = state.onFormChange((_next, meta) => {
4511
- if (disposed) return;
4512
- if (lifecycle !== "established") return;
5120
+ if (isDisposed() || inFlightFinalFlush !== null) return;
4513
5121
  if (meta?.crossTab === true) return;
4514
- if (meta?.hydration === true) {
4515
- refreshPrior();
4516
- return;
4517
- }
4518
- postPatches();
5122
+ if (meta?.persist !== true) return;
5123
+ pendingOptedInPaths = new Set(state.persistOptIns.optedInPaths());
5124
+ writer.schedule();
4519
5125
  });
4520
- function applyIncomingForm(form, blankPaths) {
4521
- state.blankPaths.clear();
4522
- for (const k of blankPaths) state.blankPaths.add(k);
4523
- state.applyFormReplacement(form, { crossTab: true, persist: false });
4524
- refreshPrior();
5126
+ const unsubscribeSuccess = clearOnSubmitSuccess ? state.onSubmitSuccess(() => {
5127
+ if (isDisposed()) return;
5128
+ void (async () => {
5129
+ await writer.flush();
5130
+ if (isDisposed()) return;
5131
+ const adapter = await adapterPromise;
5132
+ if (isDisposed()) return;
5133
+ await adapter.removeItem(key);
5134
+ })();
5135
+ }) : () => void 0;
5136
+ const handlePageHide = () => {
5137
+ if (isDisposed()) return;
5138
+ void writer.flush();
5139
+ };
5140
+ if (typeof window !== "undefined") {
5141
+ window.addEventListener("pagehide", handlePageHide);
4525
5142
  }
4526
- function handlePatches(msg) {
4527
- if (lifecycle !== "established") return;
4528
- const safePatches = [];
4529
- for (const p of msg.formPatches) {
4530
- if (!Array.isArray(p.path)) continue;
4531
- if (isPathLocallySuppressed(p.path)) continue;
4532
- if ("value" in p && !isInboundShapeAcceptable(state.schema, p.path, p.value)) {
4533
- continue;
4534
- }
4535
- safePatches.push(p);
4536
- }
4537
- const safeBlankAdded = [];
4538
- for (const k of msg.blankPathsAdded) {
4539
- const segs = canonicalizePath(k).segments;
4540
- if (isPathLocallySuppressed(segs)) continue;
4541
- safeBlankAdded.push(k);
4542
- }
4543
- const safeBlankRemoved = [];
4544
- for (const k of msg.blankPathsRemoved) {
4545
- const segs = canonicalizePath(k).segments;
4546
- if (isPathLocallySuppressed(segs)) continue;
4547
- safeBlankRemoved.push(k);
4548
- }
4549
- if (safePatches.length === 0 && safeBlankAdded.length === 0 && safeBlankRemoved.length === 0) {
4550
- return;
4551
- }
4552
- const candidate = applyPatchesForward(state.form.value, safePatches);
5143
+ void (async () => {
5144
+ const adapter = await adapterPromise;
5145
+ if (isDisposed()) return;
5146
+ void cleanupOrphanKeys(adapter, base, key);
4553
5147
  try {
4554
- options.validateForm(state.form.value);
4555
- try {
4556
- options.validateForm(candidate);
4557
- } catch {
5148
+ const raw = await adapter.getItem(key);
5149
+ const payload = readPersistedPayload(raw);
5150
+ if (payload === null) {
5151
+ if (raw !== null && raw !== void 0) {
5152
+ await adapter.removeItem(key);
5153
+ }
4558
5154
  return;
4559
5155
  }
5156
+ if (isDisposed()) return;
5157
+ const merged = mergeSparseHydration(
5158
+ toRaw(state.form.value),
5159
+ payload.data.form,
5160
+ state.schema
5161
+ );
5162
+ state.applyFormReplacement(merged, { hydration: true });
5163
+ const persistedLeafPaths = collectPersistedLeafPaths(payload.data.form);
5164
+ for (const k of persistedLeafPaths) {
5165
+ state.blankPaths.delete(k);
5166
+ state.originalBlankPaths.delete(k);
5167
+ }
5168
+ for (const k of payload.data.blankPaths ?? []) {
5169
+ state.blankPaths.add(k);
5170
+ state.originalBlankPaths.add(k);
5171
+ }
5172
+ if (include === "form+errors") {
5173
+ if (payload.data.schemaErrors !== void 0) {
5174
+ const flat = payload.data.schemaErrors.flatMap(([, errs]) => errs);
5175
+ state.setAllSchemaErrors(flat);
5176
+ }
5177
+ if (payload.data.userErrors !== void 0) {
5178
+ const flat = payload.data.userErrors.flatMap(([, errs]) => errs);
5179
+ state.setAllUserErrors(flat);
5180
+ }
5181
+ }
5182
+ state.scheduleFieldValidation(
5183
+ [],
5184
+ true
5185
+ /* immediate */
5186
+ );
4560
5187
  } catch {
4561
5188
  }
4562
- const nextBlankPaths = new Set(state.blankPaths);
4563
- for (const k of safeBlankRemoved) nextBlankPaths.delete(k);
4564
- for (const k of safeBlankAdded) nextBlankPaths.add(k);
4565
- applyIncomingForm(candidate, [...nextBlankPaths]);
4566
- }
4567
- function handleSnapshot(msg) {
4568
- if (lifecycle !== "joining") return;
4569
- if (!isInboundShapeAcceptable(state.schema, [], msg.form)) return;
4570
- if (snapshotTimeoutTimer !== null) {
4571
- clearTimeout(snapshotTimeoutTimer);
4572
- snapshotTimeoutTimer = null;
5189
+ })();
5190
+ async function writePathImmediately(path) {
5191
+ if (isDisposed()) return;
5192
+ await writer.flush();
5193
+ if (isDisposed()) return;
5194
+ const adapter = await adapterPromise;
5195
+ if (isDisposed()) return;
5196
+ const raw = await adapter.getItem(key);
5197
+ const existing = readPersistedPayload(raw);
5198
+ const baseForm = existing?.data.form ?? /* @__PURE__ */ Object.create(null);
5199
+ const value = getAtPath(toRaw(state.form.value), path);
5200
+ const nextForm = setAtPath(baseForm, path, value);
5201
+ const { key: pathKey } = canonicalizePath(path);
5202
+ const transientSet = new Set(
5203
+ (existing?.data.blankPaths ?? []).filter(
5204
+ (k) => k !== pathKey && !isDescendantPathKey(k, pathKey)
5205
+ )
5206
+ );
5207
+ for (const liveKey of state.blankPaths) {
5208
+ if (liveKey === pathKey || isDescendantPathKey(liveKey, pathKey)) {
5209
+ transientSet.add(liveKey);
5210
+ }
4573
5211
  }
4574
- if (joinCollectionTimer !== null) {
4575
- clearTimeout(joinCollectionTimer);
4576
- joinCollectionTimer = null;
5212
+ if (include === "form") {
5213
+ await adapter.setItem(
5214
+ key,
5215
+ buildPersistedPayload(nextForm, "form", /* @__PURE__ */ new Map(), /* @__PURE__ */ new Map(), transientSet)
5216
+ );
5217
+ return;
4577
5218
  }
4578
- applyIncomingForm(msg.form, msg.blankPaths);
4579
- lifecycle = "established";
4580
- peerIds.clear();
4581
- }
4582
- function respondToHello() {
4583
- safePost({ v: PROTOCOL_VERSION, kind: "announce", senderId });
4584
- }
4585
- function respondToSnapshotRequest() {
4586
- const scrubbedForm = stripSensitivePathsDeep(state.form.value, [], options.isSensitivePath);
4587
- safePost({
4588
- v: PROTOCOL_VERSION,
4589
- kind: "snapshot",
4590
- senderId,
4591
- form: scrubbedForm,
4592
- blankPaths: [...state.blankPaths]
4593
- });
5219
+ const schemaMap = new Map(existing?.data.schemaErrors ?? []);
5220
+ const userMap = new Map(existing?.data.userErrors ?? []);
5221
+ const currentSchema = state.schemaErrors.get(pathKey);
5222
+ const currentUser = state.userErrors.get(pathKey);
5223
+ if (currentSchema !== void 0 && currentSchema.length > 0) {
5224
+ schemaMap.set(pathKey, [...currentSchema]);
5225
+ } else {
5226
+ schemaMap.delete(pathKey);
5227
+ }
5228
+ if (currentUser !== void 0 && currentUser.length > 0) {
5229
+ userMap.set(pathKey, [...currentUser]);
5230
+ } else {
5231
+ userMap.delete(pathKey);
5232
+ }
5233
+ await adapter.setItem(
5234
+ key,
5235
+ buildPersistedPayload(nextForm, "form+errors", schemaMap, userMap, transientSet)
5236
+ );
4594
5237
  }
4595
- channel.onmessage = (event) => {
4596
- if (disposed) return;
4597
- const data = event.data;
4598
- if (!isValidSyncMessage(data)) return;
4599
- const msg = data;
4600
- if (msg.senderId === senderId) return;
4601
- switch (msg.kind) {
4602
- case "hello":
4603
- if (lifecycle !== "established") return;
4604
- respondToHello();
4605
- break;
4606
- case "announce":
4607
- if (lifecycle === "joining") peerIds.add(msg.senderId);
4608
- break;
4609
- case "requestSnapshot":
4610
- if (lifecycle !== "established") return;
4611
- if (msg.targetId !== senderId) return;
4612
- respondToSnapshotRequest();
4613
- break;
4614
- case "snapshot":
4615
- handleSnapshot(msg);
4616
- break;
4617
- case "patches":
4618
- handlePatches(msg);
4619
- break;
5238
+ async function clearPersistedDraft(path) {
5239
+ if (isDisposed()) return;
5240
+ await writer.flush();
5241
+ if (isDisposed()) return;
5242
+ const adapter = await adapterPromise;
5243
+ if (isDisposed()) return;
5244
+ if (path === void 0) {
5245
+ await adapter.removeItem(key);
5246
+ return;
4620
5247
  }
4621
- };
4622
- function electLeaderAndRequest() {
4623
- if (disposed) return;
4624
- if (peerIds.size === 0) {
4625
- lifecycle = "established";
4626
- refreshPrior();
5248
+ const raw = await adapter.getItem(key);
5249
+ const existing = readPersistedPayload(raw);
5250
+ if (existing === null) return;
5251
+ const nextForm = deleteAtPath(existing.data.form, path);
5252
+ if (isEmptyContainer(nextForm)) {
5253
+ await adapter.removeItem(key);
4627
5254
  return;
4628
5255
  }
4629
- const sorted = [...peerIds].sort();
4630
- const leaderId = sorted[0];
4631
- peerIds.delete(leaderId);
4632
- leaderAttempts++;
4633
- safePost({
4634
- v: PROTOCOL_VERSION,
4635
- kind: "requestSnapshot",
4636
- senderId,
4637
- targetId: leaderId
4638
- });
4639
- snapshotTimeoutTimer = setTimeout(() => {
4640
- snapshotTimeoutTimer = null;
4641
- if (disposed) return;
4642
- if (lifecycle === "established") return;
4643
- if (leaderAttempts >= MAX_LEADER_ATTEMPTS || peerIds.size === 0) {
4644
- lifecycle = "established";
4645
- refreshPrior();
4646
- return;
4647
- }
4648
- electLeaderAndRequest();
4649
- }, SNAPSHOT_TIMEOUT_MS);
5256
+ const { key: pathKey } = canonicalizePath(path);
5257
+ const transientSet = new Set(
5258
+ (existing.data.blankPaths ?? []).filter(
5259
+ (k) => k !== pathKey && !isDescendantPathKey(k, pathKey)
5260
+ )
5261
+ );
5262
+ if (include === "form") {
5263
+ await adapter.setItem(
5264
+ key,
5265
+ buildPersistedPayload(nextForm, "form", /* @__PURE__ */ new Map(), /* @__PURE__ */ new Map(), transientSet)
5266
+ );
5267
+ return;
5268
+ }
5269
+ const schemaErrors = (existing.data.schemaErrors ?? []).filter(([k]) => k !== pathKey);
5270
+ const userErrors = (existing.data.userErrors ?? []).filter(([k]) => k !== pathKey);
5271
+ const schemaMap = new Map(schemaErrors.map(([k, v]) => [k, [...v]]));
5272
+ const userMap = new Map(userErrors.map(([k, v]) => [k, [...v]]));
5273
+ await adapter.setItem(
5274
+ key,
5275
+ buildPersistedPayload(nextForm, "form+errors", schemaMap, userMap, transientSet)
5276
+ );
4650
5277
  }
4651
- function joinFlow() {
4652
- safePost({ v: PROTOCOL_VERSION, kind: "hello", senderId });
4653
- joinCollectionTimer = setTimeout(() => {
4654
- joinCollectionTimer = null;
4655
- if (disposed) return;
4656
- if (lifecycle === "established") return;
4657
- electLeaderAndRequest();
4658
- }, JOIN_COLLECTION_WINDOW_MS);
5278
+ function awaitPendingWrites() {
5279
+ if (inFlightFinalFlush !== null) return inFlightFinalFlush;
5280
+ if (isDisposed()) return Promise.resolve();
5281
+ return writer.flush().catch(() => void 0);
4659
5282
  }
4660
- joinFlow();
4661
- return {
4662
- dispose: () => {
4663
- if (disposed) return;
5283
+ function dispose() {
5284
+ if (isDisposed() || inFlightFinalFlush !== null) return;
5285
+ unsubscribeChange();
5286
+ unsubscribeSuccess();
5287
+ if (typeof window !== "undefined") {
5288
+ window.removeEventListener("pagehide", handlePageHide);
5289
+ }
5290
+ inFlightFinalFlush = writer.flush().catch(() => void 0).finally(() => {
4664
5291
  disposed = true;
4665
- if (joinCollectionTimer !== null) {
4666
- clearTimeout(joinCollectionTimer);
4667
- joinCollectionTimer = null;
4668
- }
4669
- if (snapshotTimeoutTimer !== null) {
4670
- clearTimeout(snapshotTimeoutTimer);
4671
- snapshotTimeoutTimer = null;
4672
- }
4673
- unsubscribeChange();
4674
- try {
4675
- channel.close();
4676
- } catch {
4677
- }
4678
- },
4679
- lifecycle: () => lifecycle,
4680
- senderId,
4681
- channelName
5292
+ inFlightFinalFlush = null;
5293
+ });
5294
+ }
5295
+ return {
5296
+ wiredConfig: config,
5297
+ writePathImmediately,
5298
+ clearPersistedDraft,
5299
+ awaitPendingWrites,
5300
+ dispose
4682
5301
  };
4683
5302
  }
4684
- const MULTI_TAB_SYNC_MODULE_KEY = "multiTabSync";
4685
-
4686
- const warned = /* @__PURE__ */ new Set();
4687
- function warnOnceInsecureContext(feature) {
4688
- if (!__DEV__) return;
4689
- if (warned.has(feature)) return;
4690
- warned.add(feature);
4691
- const message = featureMessage(feature);
4692
- console.warn(`[attaform] ${message}`);
5303
+ function isEmptyContainer(value) {
5304
+ if (value === void 0 || value === null) return true;
5305
+ if (Array.isArray(value)) return value.length === 0;
5306
+ if (isPlainRecord(value)) return Object.keys(value).length === 0;
5307
+ return false;
4693
5308
  }
4694
- function featureMessage(feature) {
4695
- switch (feature) {
4696
- case "multiTab":
4697
- 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.";
4698
- case "persist:local":
4699
- 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.";
4700
- case "persist:session":
4701
- return "Built-in `persist: 'session'` storage requires a secure context (HTTPS or localhost). Plain HTTP on a real hostname is MITM-interceptable, so the persistence layer is disabled. Serve over HTTPS to enable sessionStorage persistence, or pass a custom storage adapter to opt out of the secure-context gate.";
5309
+ function collectPersistedLeafPaths(form) {
5310
+ const out = [];
5311
+ walk(form, []);
5312
+ return out;
5313
+ function walk(node, prefix) {
5314
+ if (Array.isArray(node)) {
5315
+ for (let i = 0; i < node.length; i++) {
5316
+ walk(node[i], [...prefix, i]);
5317
+ }
5318
+ return;
5319
+ }
5320
+ if (isPlainRecord(node)) {
5321
+ for (const key of Object.keys(node)) {
5322
+ walk(node[key], [...prefix, key]);
5323
+ }
5324
+ return;
5325
+ }
5326
+ if (prefix.length === 0) return;
5327
+ out.push(canonicalizePath(prefix).key);
4702
5328
  }
4703
5329
  }
4704
- function isSecureContext() {
4705
- return typeof window !== "undefined" && window.isSecureContext === true;
5330
+ function isDescendantPathKey(candidate, ancestor) {
5331
+ if (candidate.length <= ancestor.length) return false;
5332
+ if (!ancestor.endsWith("]")) return false;
5333
+ const childPrefix = `${ancestor.slice(0, -1)},`;
5334
+ return candidate.startsWith(childPrefix);
4706
5335
  }
4707
5336
 
4708
- function resolveTrichotomy(input) {
4709
- if (typeof input === "function") {
4710
- return { kind: "async", factory: input };
4711
- }
4712
- return { kind: "sync", value: input };
5337
+ const PROTOCOL_VERSION = 1;
5338
+ const JOIN_COLLECTION_WINDOW_MS = 50;
5339
+ const SNAPSHOT_TIMEOUT_MS = 200;
5340
+ const MAX_LEADER_ATTEMPTS = 3;
5341
+ const SNAPSHOT_RESPONSE_MIN_INTERVAL_MS = 500;
5342
+ function isFileLikeValue(value) {
5343
+ if (typeof File !== "undefined" && value instanceof File) return true;
5344
+ if (typeof Blob !== "undefined" && value instanceof Blob) return true;
5345
+ return false;
4713
5346
  }
4714
-
4715
- function useAbstractForm(configuration, options) {
4716
- if (configuration === void 0 || configuration === null || configuration.schema === void 0) {
4717
- throw new InvalidUseFormConfigError();
5347
+ function isInboundShapeAcceptable(schema, path, value) {
5348
+ if (isFileLikeValue(value)) return false;
5349
+ let kind;
5350
+ if (Array.isArray(value)) {
5351
+ kind = "array";
5352
+ } else if (value !== null && typeof value === "object" && isPlainRecord(value)) {
5353
+ kind = "object";
5354
+ } else {
5355
+ kind = slimKindOf(value);
4718
5356
  }
4719
- const key = resolveFormKey(configuration.key);
4720
- const instance = getCurrentInstance();
4721
- if (instance !== null) ensureAttaformInstalled(instance.appContext.app);
4722
- const registry = options?.registry ?? useRegistry();
4723
- const resolvedDefaults = resolveTrichotomy(configuration.defaultValues);
4724
- const materialisedDefaults = resolvedDefaults.kind === "sync" ? resolvedDefaults.value : void 0;
4725
- const { defaultValues: _droppedDefaults, ...configWithoutDefaults } = configuration;
4726
- const trichotomyOverride = materialisedDefaults === void 0 ? configWithoutDefaults : { ...configWithoutDefaults, defaultValues: materialisedDefaults };
4727
- const merged = mergeWithDefaults(registry.defaults, trichotomyOverride);
4728
- const maxRecursionDepth = normalizeNumericOption({
4729
- value: merged.maxRecursionDepth ?? DEFAULT_MAX_RECURSION_DEPTH,
4730
- source: "useForm.maxRecursionDepth",
4731
- allowInfinity: true,
4732
- min: 0,
4733
- defaultValue: DEFAULT_MAX_RECURSION_DEPTH
4734
- });
4735
- const resolvedSchema = getComputedSchema(key, configuration.schema, { maxRecursionDepth });
4736
- if (configuration.persist !== void 0 && configuration.key === void 0) {
4737
- throw new AnonPersistError({
4738
- cause: "no-key",
4739
- schemaFields: extractSchemaFields(resolvedSchema),
4740
- callSite: captureUserCallSite()
4741
- });
4742
- }
4743
- const existing = registry.forms.get(key);
4744
- if (__DEV__ && existing !== void 0) {
4745
- warnOnSchemaFingerprintMismatch(key, existing.schema, resolvedSchema);
4746
- warnOnPersistDivergence(key, existing, configuration.persist);
4747
- }
4748
- const hadPendingHydration = registry.pendingHydration.has(key);
4749
- const state = existing ?? buildFreshState(key, resolvedSchema, merged, registry);
4750
- if (existing !== void 0) ; else if (resolvedDefaults.kind === "sync") {
4751
- state.defaultsResolved.value = true;
4752
- }
4753
- if (existing === void 0 && resolvedDefaults.kind === "async") {
4754
- const factory = resolvedDefaults.factory;
4755
- state.defaultValuesFactory.value = factory;
4756
- if (hadPendingHydration) {
4757
- state.hydrating.value = false;
4758
- state.defaultsResolved.value = true;
4759
- } else if (registry.ssr) {
4760
- if (configuration.__ssrAccessed === true) {
4761
- registry.enqueuePrefetch(key);
4762
- }
4763
- onServerPrefetch(() => {
4764
- if (!registry.shouldPrefetch(key)) return;
4765
- return state.activate();
4766
- });
4767
- }
4768
- }
4769
- if (getCurrentScope() !== void 0) {
4770
- const releaseConsumer = registry.trackConsumer(key);
4771
- onScopeDispose(releaseConsumer);
4772
- }
4773
- const persistDisabledByAnonRule = merged.persist !== void 0 && enforceAnonPersistRule(state.formKey, registry.ssr);
4774
- if (existing === void 0 && !registry.ssr) {
4775
- if (merged.persist !== void 0 && !persistDisabledByAnonRule) {
4776
- const resolvedPersist = normalizePersistConfig(merged.persist);
4777
- const storageKind = resolvedPersist.storage;
4778
- const isBuiltinStorage = typeof storageKind === "string";
4779
- const secureContextOk = !isBuiltinStorage || isSecureContext();
4780
- if (!secureContextOk) {
4781
- const feature = storageKind === "session" ? "persist:session" : "persist:local";
4782
- warnOnceInsecureContext(feature);
4783
- void sweepAllOrphansAcrossStandardStores(`${PERSISTENCE_KEY_PREFIX}${state.formKey}`);
4784
- } else {
4785
- const persistenceBase = resolveStorageKeyBase(resolvedPersist, state.formKey);
4786
- void sweepNonConfiguredStandardStoresForOrphans(resolvedPersist.storage, persistenceBase);
4787
- const persistenceModule = wirePersistence(state, resolvedPersist);
4788
- state.modules.set(PERSISTENCE_MODULE_KEY, persistenceModule);
4789
- state.registerDrain(() => persistenceModule.awaitPendingWrites());
4790
- state.registerCleanup(() => persistenceModule.dispose());
4791
- }
4792
- } else {
4793
- void sweepAllOrphansAcrossStandardStores(`${PERSISTENCE_KEY_PREFIX}${state.formKey}`);
5357
+ const accepted = schema.getSlimPrimitiveTypesAtPath(path);
5358
+ if (!accepted.has(kind)) return false;
5359
+ if (Array.isArray(value)) {
5360
+ for (let i = 0; i < value.length; i++) {
5361
+ if (!isInboundShapeAcceptable(schema, [...path, i], value[i])) return false;
4794
5362
  }
5363
+ return true;
4795
5364
  }
4796
- if (existing === void 0 && merged.multiTab === true && configuration.key !== void 0 && !registry.ssr) {
4797
- const hasBroadcastChannel = typeof BroadcastChannel !== "undefined";
4798
- const secureContext = isSecureContext();
4799
- if (hasBroadcastChannel && secureContext) {
4800
- let channelName;
4801
- try {
4802
- channelName = `attaform:sync:${state.formKey}:${hashStableString(state.schema.fingerprint())}`;
4803
- } catch {
4804
- channelName = null;
4805
- }
4806
- if (channelName !== null) {
4807
- const syncModule = createMultiTabSyncModule(state, channelName, {
4808
- isSensitivePath: state.isSensitivePath,
4809
- noSyncPaths: state.noSyncPaths,
4810
- validateForm: (form) => {
4811
- const result = state.schema.validateAtPath(form, void 0, { sync: true });
4812
- if (result instanceof Promise) return;
4813
- if (!result.success) {
4814
- throw new Error("attaform multi-tab sync: post-apply schema validation failed");
4815
- }
4816
- }
4817
- });
4818
- state.modules.set(MULTI_TAB_SYNC_MODULE_KEY, syncModule);
4819
- state.registerCleanup(() => syncModule.dispose());
4820
- }
4821
- } else if (hasBroadcastChannel && !secureContext) {
4822
- warnOnceInsecureContext("multiTab");
5365
+ if (isPlainRecord(value)) {
5366
+ for (const key of Object.keys(value)) {
5367
+ if (!isInboundShapeAcceptable(schema, [...path, key], value[key])) return false;
4823
5368
  }
5369
+ return true;
4824
5370
  }
4825
- if (existing === void 0 && merged.history !== void 0) {
4826
- const historyModule = createHistoryModule(state, merged.history);
4827
- state.modules.set(HISTORY_MODULE_KEY, historyModule);
4828
- state.registerCleanup(() => historyModule.dispose());
5371
+ return true;
5372
+ }
5373
+ function diffBlankPaths(prev, curr) {
5374
+ const added = [];
5375
+ const removed = [];
5376
+ const prevSet = new Set(prev);
5377
+ for (const k of curr) if (!prevSet.has(k)) added.push(k);
5378
+ for (const k of prev) if (!curr.has(k)) removed.push(k);
5379
+ return { added, removed };
5380
+ }
5381
+ function snapshotForm(form) {
5382
+ return structuralSnapshot(form);
5383
+ }
5384
+ function stripSensitivePathsDeep(value, pathSoFar, isSensitivePath) {
5385
+ if (isFileLikeValue(value)) return void 0;
5386
+ if (value === null || typeof value !== "object") return value;
5387
+ if (Array.isArray(value)) {
5388
+ return value.map((item, i) => stripSensitivePathsDeep(item, [...pathSoFar, i], isSensitivePath));
4829
5389
  }
4830
- if (configuration.key === void 0) {
4831
- recordAmbientProvide(registry.ssr);
4832
- provide(kFormContext, state);
5390
+ const proto = Object.getPrototypeOf(value);
5391
+ if (proto !== Object.prototype && proto !== null) return value;
5392
+ const out = /* @__PURE__ */ Object.create(null);
5393
+ const src = value;
5394
+ for (const key of Object.keys(src)) {
5395
+ const childPath = [...pathSoFar, key];
5396
+ if (isSensitivePath(childPath)) {
5397
+ out[key] = void 0;
5398
+ continue;
5399
+ }
5400
+ out[key] = stripSensitivePathsDeep(src[key], childPath, isSensitivePath);
4833
5401
  }
4834
- const formInstanceId = getCurrentInstance() !== null ? useId() : `atta:form-instance:${formInstanceCounter++}`;
4835
- if (getCurrentInstance() !== null) {
4836
- provide(kFormInstanceId, formInstanceId);
5402
+ return out;
5403
+ }
5404
+ function isStringArray(value) {
5405
+ return Array.isArray(value) && value.every((item) => typeof item === "string");
5406
+ }
5407
+ function isPatchArray(value) {
5408
+ return Array.isArray(value) && value.every(
5409
+ (p) => p !== null && typeof p === "object" && Array.isArray(p.path)
5410
+ );
5411
+ }
5412
+ function isValidSyncMessage(data) {
5413
+ if (data === null || typeof data !== "object") return false;
5414
+ const m = data;
5415
+ if (m["v"] !== PROTOCOL_VERSION) return false;
5416
+ if (typeof m["senderId"] !== "string") return false;
5417
+ if (typeof m["kind"] !== "string") return false;
5418
+ switch (m["kind"]) {
5419
+ case "hello":
5420
+ case "announce":
5421
+ return true;
5422
+ case "requestSnapshot":
5423
+ return typeof m["targetId"] === "string";
5424
+ case "snapshot":
5425
+ return isStringArray(m["blankPaths"]) && "form" in m;
5426
+ case "patches":
5427
+ return isPatchArray(m["formPatches"]) && isStringArray(m["blankPathsAdded"]) && isStringArray(m["blankPathsRemoved"]);
5428
+ default:
5429
+ return false;
4837
5430
  }
4838
- const apiOptions = {};
4839
- if (merged.onInvalidSubmit !== void 0) {
4840
- apiOptions.onInvalidSubmit = merged.onInvalidSubmit;
5431
+ }
5432
+ function generateSenderId() {
5433
+ try {
5434
+ return globalThis.crypto.randomUUID();
5435
+ } catch {
5436
+ return `atta-${Math.random().toString(36).slice(2)}-${Date.now().toString(36)}`;
4841
5437
  }
4842
- const history = state.modules.get(HISTORY_MODULE_KEY);
4843
- if (history !== void 0) {
4844
- apiOptions.history = history;
5438
+ }
5439
+ function createMultiTabSyncModule(state, channelName, options) {
5440
+ if (typeof BroadcastChannel === "undefined") {
5441
+ return {
5442
+ dispose: () => void 0,
5443
+ lifecycle: () => "established",
5444
+ senderId: "",
5445
+ channelName
5446
+ };
4845
5447
  }
4846
- if (merged.validateOn !== void 0) {
4847
- apiOptions.validateOn = merged.validateOn;
5448
+ let channel;
5449
+ try {
5450
+ channel = new BroadcastChannel(channelName);
5451
+ } catch {
5452
+ return {
5453
+ dispose: () => void 0,
5454
+ lifecycle: () => "established",
5455
+ senderId: "",
5456
+ channelName
5457
+ };
4848
5458
  }
4849
- const mergedDebounceMs = merged.debounceMs;
4850
- if (mergedDebounceMs !== void 0) {
4851
- apiOptions.debounceMs = mergedDebounceMs;
5459
+ const senderId = generateSenderId();
5460
+ let lifecycle = "joining";
5461
+ let disposed = false;
5462
+ const peerIds = /* @__PURE__ */ new Set();
5463
+ const lastSnapshotResponseAt = /* @__PURE__ */ new Map();
5464
+ let joinCollectionTimer = null;
5465
+ let snapshotTimeoutTimer = null;
5466
+ let leaderAttempts = 0;
5467
+ let prior = {
5468
+ form: snapshotForm(state.form.value),
5469
+ blankPathsSnapshot: [...state.blankPaths]
5470
+ };
5471
+ function safePost(msg) {
5472
+ if (disposed) return;
5473
+ try {
5474
+ channel.postMessage(msg);
5475
+ } catch {
5476
+ }
4852
5477
  }
4853
- if (merged.shouldShowErrors !== void 0) {
4854
- apiOptions.shouldShowErrors = resolveShouldShowErrors(merged.shouldShowErrors);
5478
+ function refreshPrior() {
5479
+ prior = {
5480
+ form: snapshotForm(state.form.value),
5481
+ blankPathsSnapshot: [...state.blankPaths]
5482
+ };
4855
5483
  }
4856
- if (merged.coerce !== void 0) {
4857
- apiOptions.coerce = merged.coerce;
5484
+ function isPathLocallySuppressed(path) {
5485
+ if (options.isSensitivePath(path)) return true;
5486
+ const { key } = canonicalizePath([...path]);
5487
+ if (options.noSyncPaths.has(key)) return true;
5488
+ return false;
4858
5489
  }
4859
- if (merged.rememberVariants !== void 0) {
4860
- apiOptions.rememberVariants = merged.rememberVariants;
5490
+ function postPatches() {
5491
+ if (lifecycle !== "established") return;
5492
+ const next = snapshotForm(state.form.value);
5493
+ const rawPatches = [];
5494
+ diffAndApply(prior.form, next, [], (p) => rawPatches.push(p));
5495
+ const safePatches = [];
5496
+ for (const p of rawPatches) {
5497
+ if (isPathLocallySuppressed(p.path)) continue;
5498
+ if ("value" in p && isFileLikeValue(p.value)) continue;
5499
+ safePatches.push(p);
5500
+ }
5501
+ const { added, removed } = diffBlankPaths(prior.blankPathsSnapshot, state.blankPaths);
5502
+ if (safePatches.length === 0 && added.length === 0 && removed.length === 0) {
5503
+ prior = { form: next, blankPathsSnapshot: [...state.blankPaths] };
5504
+ return;
5505
+ }
5506
+ safePost({
5507
+ v: PROTOCOL_VERSION,
5508
+ kind: "patches",
5509
+ senderId,
5510
+ formPatches: safePatches,
5511
+ blankPathsAdded: added,
5512
+ blankPathsRemoved: removed
5513
+ });
5514
+ prior = { form: next, blankPathsSnapshot: [...state.blankPaths] };
4861
5515
  }
4862
- return buildFormApi(
4863
- state,
4864
- formInstanceId,
4865
- apiOptions
4866
- );
4867
- }
4868
- function mergeWithDefaults(defaults, configuration) {
4869
- const strict = configuration.strict ?? defaults.strict;
4870
- const onInvalidSubmit = configuration.onInvalidSubmit ?? defaults.onInvalidSubmit;
4871
- const history = configuration.history ?? defaults.history;
4872
- const rememberVariants = configuration.rememberVariants ?? defaults.rememberVariants;
4873
- const coerce = configuration.coerce ?? defaults.coerce;
4874
- const validateOn = configuration.validateOn ?? defaults.validateOn;
4875
- const debounceMs = configuration.debounceMs ?? defaults.debounceMs;
4876
- const shouldShowErrors = configuration.shouldShowErrors ?? defaults.shouldShowErrors;
4877
- const maxRecursionDepth = configuration.maxRecursionDepth ?? defaults.maxRecursionDepth;
4878
- const sensitiveNames = configuration.sensitiveNames ?? defaults.sensitiveNames;
4879
- const multiTab = configuration.multiTab ?? defaults.multiTab;
4880
- return {
4881
- ...configuration,
4882
- ...strict === void 0 ? {} : { strict },
4883
- ...onInvalidSubmit === void 0 ? {} : { onInvalidSubmit },
4884
- ...history === void 0 ? {} : { history },
4885
- ...rememberVariants === void 0 ? {} : { rememberVariants },
4886
- ...coerce === void 0 ? {} : { coerce },
4887
- ...validateOn === void 0 ? {} : { validateOn },
4888
- ...debounceMs === void 0 ? {} : { debounceMs },
4889
- ...shouldShowErrors === void 0 ? {} : { shouldShowErrors },
4890
- ...maxRecursionDepth === void 0 ? {} : { maxRecursionDepth },
4891
- ...sensitiveNames === void 0 ? {} : { sensitiveNames },
4892
- ...multiTab === void 0 ? {} : { multiTab }
4893
- };
4894
- }
4895
- const HISTORY_MODULE_KEY = "history";
4896
- function buildFreshState(key, schema, configuration, registry) {
4897
- const pending = registry.pendingHydration.get(key);
4898
- if (pending !== void 0) registry.pendingHydration.delete(key);
4899
- const walked = walkUnsetSentinels(
4900
- configuration.defaultValues,
4901
- schema
4902
- );
4903
- let initialBlankPaths;
4904
- if (pending === void 0) {
4905
- initialBlankPaths = walked.paths;
5516
+ const unsubscribeChange = state.onFormChange((_next, meta) => {
5517
+ if (disposed) return;
5518
+ if (lifecycle !== "established") return;
5519
+ if (meta?.crossTab === true) return;
5520
+ if (meta?.hydration === true) {
5521
+ refreshPrior();
5522
+ return;
5523
+ }
5524
+ postPatches();
5525
+ });
5526
+ function applyIncomingForm(form, blankPaths) {
5527
+ state.blankPaths.clear();
5528
+ for (const k of blankPaths) state.blankPaths.add(k);
5529
+ state.applyFormReplacement(form, { crossTab: true, persist: false });
5530
+ refreshPrior();
4906
5531
  }
4907
- const resolvedSensitiveNames = configuration.sensitiveNames;
4908
- const resolvedIsSensitivePath = resolvedSensitiveNames === void 0 ? void 0 : createIsSensitivePath(resolvedSensitiveNames);
4909
- const resolvedSegmentMatchesSensitive = resolvedSensitiveNames === void 0 ? void 0 : createSegmentMatchesSensitive(resolvedSensitiveNames);
4910
- const createOptions = {
4911
- formKey: key,
4912
- schema,
4913
- defaultValues: walked.cleanedValues,
4914
- ...configuration.strict !== void 0 ? { strict: configuration.strict } : {},
4915
- hydration: pending,
4916
- ...configuration.validateOn !== void 0 ? { validateOn: configuration.validateOn } : {},
4917
- ...configuration.debounceMs !== void 0 ? { debounceMs: configuration.debounceMs } : {},
4918
- ssr: registry.ssr,
4919
- // Server-only: bind the SSR prefetch coordination handles. `enqueue`
4920
- // records intent on every `state.activate()` so a wizard skip-list
4921
- // override or a future transform mark has a consistent set to diff
4922
- // against; `shouldFire` lets the activate path bail when the
4923
- // wizard explicitly skipped this key — even an explicit
4924
- // `form.activate()` defers to the wizard's render-efficiency
4925
- // skip-list on the server.
4926
- ...registry.ssr ? {
4927
- ssrPrefetch: {
4928
- enqueue: () => {
4929
- registry.enqueuePrefetch(key);
4930
- },
4931
- shouldFire: () => registry.shouldPrefetch(key)
5532
+ function handlePatches(msg) {
5533
+ if (lifecycle !== "established") return;
5534
+ const safePatches = [];
5535
+ for (const p of msg.formPatches) {
5536
+ if (!Array.isArray(p.path)) continue;
5537
+ if (isPathLocallySuppressed(p.path)) continue;
5538
+ if ("value" in p && !isInboundShapeAcceptable(state.schema, p.path, p.value)) {
5539
+ continue;
4932
5540
  }
4933
- } : {},
4934
- ...configuration.rememberVariants !== void 0 ? { rememberVariants: configuration.rememberVariants } : {},
4935
- ...configuration.coerce !== void 0 ? { coerce: configuration.coerce } : {},
4936
- ...configuration.shouldShowErrors !== void 0 ? { shouldShowErrors: configuration.shouldShowErrors } : {},
4937
- ...initialBlankPaths !== void 0 ? { initialBlankPaths } : {},
4938
- ...resolvedIsSensitivePath !== void 0 ? { isSensitivePath: resolvedIsSensitivePath } : {},
4939
- ...resolvedSegmentMatchesSensitive !== void 0 ? { segmentMatchesSensitive: resolvedSegmentMatchesSensitive } : {}
4940
- };
4941
- const state = createFormStore(createOptions);
4942
- registry.forms.set(
4943
- key,
4944
- state
4945
- );
4946
- return state;
4947
- }
4948
- let anonCounter = 0;
4949
- let formInstanceCounter = 0;
4950
- const ambientProvideHistory = __DEV__ ? /* @__PURE__ */ new WeakMap() : null;
4951
- function recordAmbientProvide(ssr) {
4952
- if (!__DEV__ || ssr || ambientProvideHistory === null) return;
4953
- const instance = getCurrentInstance();
4954
- if (instance === null) return;
4955
- const instanceKey = instance;
4956
- const entry = {
4957
- source: captureUserCallSite()
4958
- };
4959
- const existing = ambientProvideHistory.get(instanceKey);
4960
- if (existing === void 0) {
4961
- ambientProvideHistory.set(instanceKey, [entry]);
4962
- return;
4963
- }
4964
- existing.push(entry);
4965
- }
4966
- function resolveFormKey(key) {
4967
- if (key !== void 0 && key !== null && key !== "") {
4968
- if (key.startsWith(RESERVED_KEY_PREFIX)) {
4969
- throw new ReservedFormKeyError(key);
5541
+ safePatches.push(p);
4970
5542
  }
4971
- return key;
5543
+ const safeBlankAdded = [];
5544
+ for (const k of msg.blankPathsAdded) {
5545
+ const segs = canonicalizePath(k).segments;
5546
+ if (isPathLocallySuppressed(segs)) continue;
5547
+ safeBlankAdded.push(k);
5548
+ }
5549
+ const safeBlankRemoved = [];
5550
+ for (const k of msg.blankPathsRemoved) {
5551
+ const segs = canonicalizePath(k).segments;
5552
+ if (isPathLocallySuppressed(segs)) continue;
5553
+ safeBlankRemoved.push(k);
5554
+ }
5555
+ if (safePatches.length === 0 && safeBlankAdded.length === 0 && safeBlankRemoved.length === 0) {
5556
+ return;
5557
+ }
5558
+ const candidate = applyPatchesForward(state.form.value, safePatches);
5559
+ try {
5560
+ options.validateForm(state.form.value);
5561
+ try {
5562
+ options.validateForm(candidate);
5563
+ } catch {
5564
+ return;
5565
+ }
5566
+ } catch {
5567
+ }
5568
+ const nextBlankPaths = new Set(state.blankPaths);
5569
+ for (const k of safeBlankRemoved) nextBlankPaths.delete(k);
5570
+ for (const k of safeBlankAdded) nextBlankPaths.add(k);
5571
+ applyIncomingForm(candidate, [...nextBlankPaths]);
4972
5572
  }
4973
- if (getCurrentInstance() !== null) {
4974
- return `${ANONYMOUS_FORM_KEY_PREFIX}${useId()}`;
5573
+ function handleSnapshot(msg) {
5574
+ if (lifecycle !== "joining") return;
5575
+ if (!isInboundShapeAcceptable(state.schema, [], msg.form)) return;
5576
+ if (snapshotTimeoutTimer !== null) {
5577
+ clearTimeout(snapshotTimeoutTimer);
5578
+ snapshotTimeoutTimer = null;
5579
+ }
5580
+ if (joinCollectionTimer !== null) {
5581
+ clearTimeout(joinCollectionTimer);
5582
+ joinCollectionTimer = null;
5583
+ }
5584
+ applyIncomingForm(msg.form, msg.blankPaths);
5585
+ lifecycle = "established";
5586
+ peerIds.clear();
4975
5587
  }
4976
- return `${ANONYMOUS_FORM_KEY_PREFIX}${anonCounter++}`;
4977
- }
4978
- function warnOnSchemaFingerprintMismatch(key, existing, incoming) {
4979
- let existingFp;
4980
- let incomingFp;
4981
- try {
4982
- existingFp = existing.fingerprint();
4983
- incomingFp = incoming.fingerprint();
4984
- } catch (error) {
4985
- console.error(
4986
- `[attaform] fingerprint() threw for key "${key}"; skipping mismatch check.`,
4987
- error
4988
- );
4989
- return;
5588
+ function respondToHello() {
5589
+ safePost({ v: PROTOCOL_VERSION, kind: "announce", senderId });
4990
5590
  }
4991
- if (existingFp === incomingFp) return;
4992
- console.warn(
4993
- `[attaform] useForm() calls with key "${key}" use different schemas; first wins, second is ignored. Use identical schemas or unique keys.
4994
- existing: ${existingFp}
4995
- incoming: ${incomingFp}`
4996
- );
4997
- }
4998
- function warnOnPersistDivergence(key, existing, incomingPersist) {
4999
- if (incomingPersist === void 0) return;
5000
- const wired = existing.modules.get(PERSISTENCE_MODULE_KEY);
5001
- const incomingNormalized = normalizePersistConfig(incomingPersist);
5002
- if (wired === void 0) {
5003
- console.warn(
5004
- `[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.`
5005
- );
5006
- return;
5591
+ function respondToSnapshotRequest(requesterId) {
5592
+ const now = Date.now();
5593
+ const last = lastSnapshotResponseAt.get(requesterId);
5594
+ if (last !== void 0 && now - last < SNAPSHOT_RESPONSE_MIN_INTERVAL_MS) return;
5595
+ lastSnapshotResponseAt.set(requesterId, now);
5596
+ const scrubbedForm = stripSensitivePathsDeep(state.form.value, [], options.isSensitivePath);
5597
+ safePost({
5598
+ v: PROTOCOL_VERSION,
5599
+ kind: "snapshot",
5600
+ senderId,
5601
+ form: scrubbedForm,
5602
+ blankPaths: [...state.blankPaths]
5603
+ });
5007
5604
  }
5008
- if (persistConfigsEquivalent(wired.wiredConfig, incomingNormalized)) return;
5009
- console.warn(
5010
- `[attaform] useForm({ key: "${key}" }) passed a persist config that differs from the first useForm({ key }) call's; first wins, this one is ignored.
5011
- wired: ${describePersist(wired.wiredConfig)}
5012
- incoming: ${describePersist(incomingNormalized)}`
5013
- );
5014
- }
5015
- function persistConfigsEquivalent(a, b) {
5016
- if (a.storage !== b.storage) return false;
5017
- if ((a.key ?? void 0) !== (b.key ?? void 0)) return false;
5018
- if ((a.debounceMs ?? void 0) !== (b.debounceMs ?? void 0)) return false;
5019
- return true;
5020
- }
5021
- function describePersist(config) {
5022
- const storage = typeof config.storage === "string" ? config.storage : "custom-adapter";
5023
- const parts = [`storage=${storage}`];
5024
- if (config.key !== void 0) parts.push(`key=${config.key}`);
5025
- if (config.debounceMs !== void 0) parts.push(`debounceMs=${config.debounceMs}`);
5026
- return `{ ${parts.join(", ")} }`;
5027
- }
5028
- function wirePersistence(state, config) {
5029
- const fingerprint = hashStableString(state.schema.fingerprint());
5030
- const base = resolveStorageKeyBase(config, state.formKey);
5031
- const key = `${base}:${fingerprint}`;
5032
- const debounceMs = normalizeNumericOption({
5033
- value: config.debounceMs ?? DEFAULT_PERSISTENCE_DEBOUNCE_MS,
5034
- source: "useForm.persist.debounceMs",
5035
- allowInfinity: false,
5036
- min: 0,
5037
- defaultValue: DEFAULT_PERSISTENCE_DEBOUNCE_MS
5038
- });
5039
- const include = config.include ?? "form";
5040
- const clearOnSubmitSuccess = config.clearOnSubmitSuccess ?? true;
5041
- const adapterPromise = getStorageAdapter(config.storage);
5042
- let disposed = false;
5043
- let inFlightFinalFlush = null;
5044
- let pendingOptedInPaths = null;
5045
- const writer = createDebouncedWriter(async () => {
5046
- const optedInPaths = pendingOptedInPaths ?? new Set(state.persistOptIns.optedInPaths());
5047
- pendingOptedInPaths = null;
5048
- const adapter = await adapterPromise;
5605
+ channel.onmessage = (event) => {
5049
5606
  if (disposed) return;
5050
- if (optedInPaths.size === 0) {
5051
- await adapter.removeItem(key);
5052
- return;
5607
+ try {
5608
+ const data = event.data;
5609
+ if (!isValidSyncMessage(data)) return;
5610
+ const msg = data;
5611
+ if (msg.senderId === senderId) return;
5612
+ switch (msg.kind) {
5613
+ case "hello":
5614
+ if (lifecycle !== "established") return;
5615
+ respondToHello();
5616
+ break;
5617
+ case "announce":
5618
+ if (lifecycle === "joining") peerIds.add(msg.senderId);
5619
+ break;
5620
+ case "requestSnapshot":
5621
+ if (lifecycle !== "established") return;
5622
+ if (msg.targetId !== senderId) return;
5623
+ respondToSnapshotRequest(msg.senderId);
5624
+ break;
5625
+ case "snapshot":
5626
+ handleSnapshot(msg);
5627
+ break;
5628
+ case "patches":
5629
+ handlePatches(msg);
5630
+ break;
5631
+ }
5632
+ } catch {
5053
5633
  }
5054
- const rawForm = toRaw(state.form.value);
5055
- const filteredForm = pluckPaths(rawForm, optedInPaths);
5056
- const filteredSchemaErrors = filterErrorsByPaths(state.schemaErrors, optedInPaths);
5057
- const filteredUserErrors = filterErrorsByPaths(state.userErrors, optedInPaths);
5058
- const filteredTransientEmpty = /* @__PURE__ */ new Set();
5059
- for (const tk of state.blankPaths) {
5060
- if (optedInPaths.has(tk)) filteredTransientEmpty.add(tk);
5061
- }
5062
- const payload = buildPersistedPayload(
5063
- filteredForm,
5064
- include,
5065
- filteredSchemaErrors,
5066
- filteredUserErrors,
5067
- filteredTransientEmpty
5068
- );
5069
- await adapter.setItem(key, payload);
5070
- }, debounceMs);
5071
- const unsubscribeChange = state.onFormChange((_next, meta) => {
5072
- if (disposed || inFlightFinalFlush !== null) return;
5073
- if (meta?.crossTab === true) return;
5074
- if (meta?.persist !== true) return;
5075
- pendingOptedInPaths = new Set(state.persistOptIns.optedInPaths());
5076
- writer.schedule();
5077
- });
5078
- const unsubscribeSuccess = clearOnSubmitSuccess ? state.onSubmitSuccess(() => {
5634
+ };
5635
+ function electLeaderAndRequest() {
5079
5636
  if (disposed) return;
5080
- void (async () => {
5081
- await writer.flush();
5082
- if (disposed) return;
5083
- const adapter = await adapterPromise;
5637
+ if (peerIds.size === 0) {
5638
+ lifecycle = "established";
5639
+ refreshPrior();
5640
+ return;
5641
+ }
5642
+ const sorted = [...peerIds].sort();
5643
+ const leaderId = sorted[0];
5644
+ peerIds.delete(leaderId);
5645
+ leaderAttempts++;
5646
+ safePost({
5647
+ v: PROTOCOL_VERSION,
5648
+ kind: "requestSnapshot",
5649
+ senderId,
5650
+ targetId: leaderId
5651
+ });
5652
+ snapshotTimeoutTimer = setTimeout(() => {
5653
+ snapshotTimeoutTimer = null;
5084
5654
  if (disposed) return;
5085
- await adapter.removeItem(key);
5086
- })();
5087
- }) : () => void 0;
5088
- void (async () => {
5089
- const adapter = await adapterPromise;
5090
- if (disposed) return;
5091
- void cleanupOrphanKeys(adapter, base, key);
5092
- try {
5093
- const raw = await adapter.getItem(key);
5094
- const payload = readPersistedPayload(raw);
5095
- if (payload === null) {
5096
- if (raw !== null && raw !== void 0) {
5097
- await adapter.removeItem(key);
5098
- }
5655
+ if (lifecycle === "established") return;
5656
+ if (leaderAttempts >= MAX_LEADER_ATTEMPTS || peerIds.size === 0) {
5657
+ lifecycle = "established";
5658
+ refreshPrior();
5099
5659
  return;
5100
5660
  }
5661
+ electLeaderAndRequest();
5662
+ }, SNAPSHOT_TIMEOUT_MS);
5663
+ }
5664
+ function joinFlow() {
5665
+ safePost({ v: PROTOCOL_VERSION, kind: "hello", senderId });
5666
+ joinCollectionTimer = setTimeout(() => {
5667
+ joinCollectionTimer = null;
5101
5668
  if (disposed) return;
5102
- const merged = mergeSparseHydration(
5103
- toRaw(state.form.value),
5104
- payload.data.form,
5105
- state.schema
5106
- );
5107
- state.applyFormReplacement(merged, { hydration: true });
5108
- const persistedLeafPaths = collectPersistedLeafPaths(payload.data.form);
5109
- for (const k of persistedLeafPaths) {
5110
- state.blankPaths.delete(k);
5111
- state.originalBlankPaths.delete(k);
5669
+ if (lifecycle === "established") return;
5670
+ electLeaderAndRequest();
5671
+ }, JOIN_COLLECTION_WINDOW_MS);
5672
+ }
5673
+ joinFlow();
5674
+ return {
5675
+ dispose: () => {
5676
+ if (disposed) return;
5677
+ disposed = true;
5678
+ if (joinCollectionTimer !== null) {
5679
+ clearTimeout(joinCollectionTimer);
5680
+ joinCollectionTimer = null;
5112
5681
  }
5113
- for (const k of payload.data.blankPaths ?? []) {
5114
- const key2 = coerceToPathKey(k);
5115
- state.blankPaths.add(key2);
5116
- state.originalBlankPaths.add(key2);
5682
+ if (snapshotTimeoutTimer !== null) {
5683
+ clearTimeout(snapshotTimeoutTimer);
5684
+ snapshotTimeoutTimer = null;
5117
5685
  }
5118
- if (include === "form+errors") {
5119
- if (payload.data.schemaErrors !== void 0) {
5120
- const flat = payload.data.schemaErrors.flatMap(([, errs]) => errs);
5121
- state.setAllSchemaErrors(flat);
5122
- }
5123
- if (payload.data.userErrors !== void 0) {
5124
- const flat = payload.data.userErrors.flatMap(([, errs]) => errs);
5125
- state.setAllUserErrors(flat);
5126
- }
5686
+ unsubscribeChange();
5687
+ try {
5688
+ channel.close();
5689
+ } catch {
5127
5690
  }
5128
- state.scheduleFieldValidation(
5129
- [],
5130
- true
5131
- /* immediate */
5132
- );
5133
- } catch {
5134
- }
5135
- })();
5136
- async function writePathImmediately(path) {
5137
- if (disposed) return;
5138
- await writer.flush();
5139
- if (disposed) return;
5140
- const adapter = await adapterPromise;
5141
- if (disposed) return;
5142
- const raw = await adapter.getItem(key);
5143
- const existing = readPersistedPayload(raw);
5144
- const baseForm = existing?.data.form ?? {};
5145
- const value = getAtPath(toRaw(state.form.value), path);
5146
- const nextForm = setAtPath(baseForm, path, value);
5147
- const { key: pathKey } = canonicalizePath(path);
5148
- const transientSet = new Set(
5149
- (existing?.data.blankPaths ?? []).filter(
5150
- (k) => k !== pathKey && !isDescendantPathKey(k, pathKey)
5151
- )
5152
- );
5153
- for (const liveKey of state.blankPaths) {
5154
- if (liveKey === pathKey || isDescendantPathKey(liveKey, pathKey)) {
5155
- transientSet.add(liveKey);
5691
+ },
5692
+ lifecycle: () => lifecycle,
5693
+ senderId,
5694
+ channelName
5695
+ };
5696
+ }
5697
+ const MULTI_TAB_SYNC_MODULE_KEY = "multiTabSync";
5698
+
5699
+ const warned = /* @__PURE__ */ new Set();
5700
+ function warnOnceInsecureContext(feature) {
5701
+ if (!__DEV__) return;
5702
+ if (warned.has(feature)) return;
5703
+ warned.add(feature);
5704
+ const message = featureMessage(feature);
5705
+ console.warn(`[attaform] ${message}`);
5706
+ }
5707
+ function featureMessage(feature) {
5708
+ switch (feature) {
5709
+ case "multiTab":
5710
+ return "Multi-tab sync requires a secure context (HTTPS or localhost). Plain HTTP on a real hostname is interceptable by network observers, so the sync module is disabled. Serve over HTTPS in production (or develop on `localhost`) to enable cross-tab synchronisation. Use `multiTab: false` on `useForm` to silence this warning.";
5711
+ case "persist:local":
5712
+ return "Built-in `persist: 'local'` storage requires a secure context (HTTPS or localhost). Plain HTTP on a real hostname is MITM-interceptable, so the persistence layer is disabled. Serve over HTTPS to enable localStorage persistence, or pass a custom storage adapter to opt out of the secure-context gate.";
5713
+ case "persist:session":
5714
+ return "Built-in `persist: 'session'` storage requires a secure context (HTTPS or localhost). Plain HTTP on a real hostname is MITM-interceptable, so the persistence layer is disabled. Serve over HTTPS to enable sessionStorage persistence, or pass a custom storage adapter to opt out of the secure-context gate.";
5715
+ }
5716
+ }
5717
+ function isSecureContext() {
5718
+ return typeof window !== "undefined" && window.isSecureContext === true;
5719
+ }
5720
+
5721
+ function resolveTrichotomy(input) {
5722
+ if (typeof input === "function") {
5723
+ return { kind: "async", factory: input };
5724
+ }
5725
+ return { kind: "sync", value: input };
5726
+ }
5727
+
5728
+ function useAbstractForm(configuration, options) {
5729
+ if (configuration === void 0 || configuration === null || configuration.schema === void 0) {
5730
+ throw new InvalidUseFormConfigError();
5731
+ }
5732
+ const key = resolveFormKey(configuration.key);
5733
+ const instance = getCurrentInstance();
5734
+ if (instance !== null) ensureAttaformInstalled(instance.appContext.app);
5735
+ const registry = options?.registry ?? useRegistry();
5736
+ const resolvedDefaults = resolveTrichotomy(configuration.defaultValues);
5737
+ const materialisedDefaults = resolvedDefaults.kind === "sync" ? resolvedDefaults.value : void 0;
5738
+ const { defaultValues: _droppedDefaults, ...configWithoutDefaults } = configuration;
5739
+ const trichotomyOverride = materialisedDefaults === void 0 ? configWithoutDefaults : { ...configWithoutDefaults, defaultValues: materialisedDefaults };
5740
+ const merged = mergeWithDefaults(registry.defaults, trichotomyOverride);
5741
+ const maxRecursionDepth = normalizeNumericOption({
5742
+ value: merged.maxRecursionDepth ?? DEFAULT_MAX_RECURSION_DEPTH,
5743
+ source: "useForm.maxRecursionDepth",
5744
+ allowInfinity: true,
5745
+ min: 0,
5746
+ defaultValue: DEFAULT_MAX_RECURSION_DEPTH
5747
+ });
5748
+ const resolvedSchema = getComputedSchema(key, configuration.schema, { maxRecursionDepth });
5749
+ if (configuration.persist !== void 0 && configuration.key === void 0) {
5750
+ throw new AnonPersistError({
5751
+ cause: "no-key",
5752
+ schemaFields: extractSchemaFields(resolvedSchema),
5753
+ callSite: captureUserCallSite()
5754
+ });
5755
+ }
5756
+ const existing = registry.forms.get(key);
5757
+ if (__DEV__ && existing !== void 0) {
5758
+ warnOnSchemaFingerprintMismatch(key, existing.schema, resolvedSchema);
5759
+ warnOnPersistDivergence(key, existing, configuration.persist);
5760
+ }
5761
+ const hadPendingHydration = registry.pendingHydration.has(key);
5762
+ const state = existing ?? buildFreshState(key, resolvedSchema, merged, registry);
5763
+ if (existing !== void 0) ; else if (resolvedDefaults.kind === "sync") {
5764
+ state.defaultsResolved.value = true;
5765
+ }
5766
+ if (existing === void 0 && resolvedDefaults.kind === "async") {
5767
+ const factory = resolvedDefaults.factory;
5768
+ state.defaultValuesFactory.value = factory;
5769
+ if (hadPendingHydration) {
5770
+ state.hydrating.value = false;
5771
+ state.defaultsResolved.value = true;
5772
+ } else if (registry.ssr) {
5773
+ if (configuration.__ssrAccessed === true) {
5774
+ registry.enqueuePrefetch(key);
5156
5775
  }
5776
+ onServerPrefetch(() => {
5777
+ if (!registry.shouldPrefetch(key)) return;
5778
+ return state.activate();
5779
+ });
5157
5780
  }
5158
- if (include === "form") {
5159
- await adapter.setItem(
5160
- key,
5161
- buildPersistedPayload(nextForm, "form", /* @__PURE__ */ new Map(), /* @__PURE__ */ new Map(), transientSet)
5162
- );
5163
- return;
5164
- }
5165
- const schemaMap = new Map(existing?.data.schemaErrors ?? []);
5166
- const userMap = new Map(existing?.data.userErrors ?? []);
5167
- const currentSchema = state.schemaErrors.get(pathKey);
5168
- const currentUser = state.userErrors.get(pathKey);
5169
- if (currentSchema !== void 0 && currentSchema.length > 0) {
5170
- schemaMap.set(pathKey, [...currentSchema]);
5171
- } else {
5172
- schemaMap.delete(pathKey);
5173
- }
5174
- if (currentUser !== void 0 && currentUser.length > 0) {
5175
- userMap.set(pathKey, [...currentUser]);
5781
+ }
5782
+ if (getCurrentScope() !== void 0) {
5783
+ const releaseConsumer = registry.trackConsumer(key);
5784
+ onScopeDispose(releaseConsumer);
5785
+ }
5786
+ const persistDisabledByAnonRule = merged.persist !== void 0 && enforceAnonPersistRule(state.formKey, registry.ssr);
5787
+ if (existing === void 0 && !registry.ssr) {
5788
+ if (merged.persist !== void 0 && !persistDisabledByAnonRule) {
5789
+ const resolvedPersist = normalizePersistConfig(merged.persist);
5790
+ const storageKind = resolvedPersist.storage;
5791
+ const isBuiltinStorage = typeof storageKind === "string";
5792
+ const secureContextOk = !isBuiltinStorage || isSecureContext();
5793
+ if (!secureContextOk) {
5794
+ const feature = storageKind === "session" ? "persist:session" : "persist:local";
5795
+ warnOnceInsecureContext(feature);
5796
+ void sweepAllOrphansAcrossStandardStores(`${PERSISTENCE_KEY_PREFIX}${state.formKey}`);
5797
+ } else {
5798
+ const persistenceBase = resolveStorageKeyBase(resolvedPersist, state.formKey);
5799
+ void sweepNonConfiguredStandardStoresForOrphans(resolvedPersist.storage, persistenceBase);
5800
+ const persistenceModule = wirePersistence(state, resolvedPersist);
5801
+ state.modules.set(PERSISTENCE_MODULE_KEY, persistenceModule);
5802
+ state.registerDrain(() => persistenceModule.awaitPendingWrites());
5803
+ state.registerCleanup(() => persistenceModule.dispose());
5804
+ }
5176
5805
  } else {
5177
- userMap.delete(pathKey);
5806
+ void sweepAllOrphansAcrossStandardStores(`${PERSISTENCE_KEY_PREFIX}${state.formKey}`);
5178
5807
  }
5179
- await adapter.setItem(
5180
- key,
5181
- buildPersistedPayload(nextForm, "form+errors", schemaMap, userMap, transientSet)
5182
- );
5183
5808
  }
5184
- async function clearPersistedDraft(path) {
5185
- if (disposed) return;
5186
- await writer.flush();
5187
- if (disposed) return;
5188
- const adapter = await adapterPromise;
5189
- if (disposed) return;
5190
- if (path === void 0) {
5191
- await adapter.removeItem(key);
5192
- return;
5193
- }
5194
- const raw = await adapter.getItem(key);
5195
- const existing = readPersistedPayload(raw);
5196
- if (existing === null) return;
5197
- const nextForm = deleteAtPath(existing.data.form, path);
5198
- if (isEmptyContainer(nextForm)) {
5199
- await adapter.removeItem(key);
5200
- return;
5809
+ if (existing === void 0 && merged.multiTab === true && configuration.key !== void 0 && !registry.ssr) {
5810
+ const hasBroadcastChannel = typeof BroadcastChannel !== "undefined";
5811
+ const secureContext = isSecureContext();
5812
+ if (hasBroadcastChannel && secureContext) {
5813
+ let channelName;
5814
+ try {
5815
+ channelName = `attaform:sync:${state.formKey}:${hashStableString(state.schema.fingerprint())}`;
5816
+ } catch {
5817
+ channelName = null;
5818
+ }
5819
+ if (channelName !== null) {
5820
+ const syncModule = createMultiTabSyncModule(state, channelName, {
5821
+ isSensitivePath: state.isSensitivePath,
5822
+ noSyncPaths: state.noSyncPaths,
5823
+ validateForm: (form) => {
5824
+ const result = state.schema.validateAtPath(form, void 0, { sync: true });
5825
+ if (result instanceof Promise) return;
5826
+ if (!result.success) {
5827
+ throw new Error("attaform multi-tab sync: post-apply schema validation failed");
5828
+ }
5829
+ }
5830
+ });
5831
+ state.modules.set(MULTI_TAB_SYNC_MODULE_KEY, syncModule);
5832
+ state.registerCleanup(() => syncModule.dispose());
5833
+ }
5834
+ } else if (hasBroadcastChannel && !secureContext) {
5835
+ warnOnceInsecureContext("multiTab");
5201
5836
  }
5202
- const { key: pathKey } = canonicalizePath(path);
5203
- const transientSet = new Set(
5204
- (existing.data.blankPaths ?? []).filter(
5205
- (k) => k !== pathKey && !isDescendantPathKey(k, pathKey)
5206
- )
5207
- );
5208
- if (include === "form") {
5209
- await adapter.setItem(
5210
- key,
5211
- buildPersistedPayload(nextForm, "form", /* @__PURE__ */ new Map(), /* @__PURE__ */ new Map(), transientSet)
5212
- );
5213
- return;
5837
+ }
5838
+ if (existing === void 0 && merged.history !== void 0) {
5839
+ const historyModule = createHistoryModule(state, merged.history);
5840
+ state.modules.set(HISTORY_MODULE_KEY, historyModule);
5841
+ state.registerCleanup(() => historyModule.dispose());
5842
+ }
5843
+ if (configuration.key === void 0) {
5844
+ recordAmbientProvide(registry.ssr);
5845
+ provide(kFormContext, state);
5846
+ }
5847
+ const formInstanceId = getCurrentInstance() !== null ? useId() : `atta:form-instance:${formInstanceCounter++}`;
5848
+ if (getCurrentInstance() !== null) {
5849
+ provide(kFormInstanceId, formInstanceId);
5850
+ }
5851
+ const apiOptions = {};
5852
+ if (merged.onInvalidSubmit !== void 0) {
5853
+ apiOptions.onInvalidSubmit = merged.onInvalidSubmit;
5854
+ }
5855
+ const history = state.modules.get(HISTORY_MODULE_KEY);
5856
+ if (history !== void 0) {
5857
+ apiOptions.history = history;
5858
+ }
5859
+ if (merged.validateOn !== void 0) {
5860
+ apiOptions.validateOn = merged.validateOn;
5861
+ }
5862
+ const mergedDebounceMs = merged.debounceMs;
5863
+ if (mergedDebounceMs !== void 0) {
5864
+ apiOptions.debounceMs = mergedDebounceMs;
5865
+ }
5866
+ if (merged.getDisplayState !== void 0) {
5867
+ apiOptions.getDisplayState = merged.getDisplayState;
5868
+ }
5869
+ if (merged.coerce !== void 0) {
5870
+ apiOptions.coerce = merged.coerce;
5871
+ }
5872
+ if (merged.rememberVariants !== void 0) {
5873
+ apiOptions.rememberVariants = merged.rememberVariants;
5874
+ }
5875
+ if (merged.autoAria !== void 0) {
5876
+ apiOptions.autoAria = merged.autoAria;
5877
+ }
5878
+ return buildFormApi(
5879
+ state,
5880
+ formInstanceId,
5881
+ apiOptions
5882
+ );
5883
+ }
5884
+ function mergeWithDefaults(defaults, configuration) {
5885
+ const strict = configuration.strict ?? defaults.strict;
5886
+ const onInvalidSubmit = configuration.onInvalidSubmit ?? defaults.onInvalidSubmit;
5887
+ const history = configuration.history ?? defaults.history;
5888
+ const rememberVariants = configuration.rememberVariants ?? defaults.rememberVariants;
5889
+ const coerce = configuration.coerce ?? defaults.coerce;
5890
+ const validateOn = configuration.validateOn ?? defaults.validateOn;
5891
+ const debounceMs = configuration.debounceMs ?? defaults.debounceMs;
5892
+ const getDisplayState = configuration.getDisplayState ?? defaults.getDisplayState;
5893
+ const maxRecursionDepth = configuration.maxRecursionDepth ?? defaults.maxRecursionDepth;
5894
+ const sensitiveNames = configuration.sensitiveNames ?? defaults.sensitiveNames;
5895
+ const multiTab = configuration.multiTab ?? defaults.multiTab;
5896
+ const autoAria = configuration.autoAria ?? defaults.autoAria;
5897
+ return {
5898
+ ...configuration,
5899
+ ...strict === void 0 ? {} : { strict },
5900
+ ...onInvalidSubmit === void 0 ? {} : { onInvalidSubmit },
5901
+ ...history === void 0 ? {} : { history },
5902
+ ...rememberVariants === void 0 ? {} : { rememberVariants },
5903
+ ...coerce === void 0 ? {} : { coerce },
5904
+ ...validateOn === void 0 ? {} : { validateOn },
5905
+ ...debounceMs === void 0 ? {} : { debounceMs },
5906
+ ...getDisplayState === void 0 ? {} : { getDisplayState },
5907
+ ...maxRecursionDepth === void 0 ? {} : { maxRecursionDepth },
5908
+ ...sensitiveNames === void 0 ? {} : { sensitiveNames },
5909
+ ...multiTab === void 0 ? {} : { multiTab },
5910
+ ...autoAria === void 0 ? {} : { autoAria }
5911
+ };
5912
+ }
5913
+ const HISTORY_MODULE_KEY = "history";
5914
+ function buildFreshState(key, schema, configuration, registry) {
5915
+ const pending = registry.pendingHydration.get(key);
5916
+ if (pending !== void 0) registry.pendingHydration.delete(key);
5917
+ const walked = walkUnsetSentinels(
5918
+ configuration.defaultValues,
5919
+ schema
5920
+ );
5921
+ let initialBlankPaths;
5922
+ if (pending === void 0) {
5923
+ initialBlankPaths = walked.paths;
5924
+ }
5925
+ const resolvedSensitiveNames = configuration.sensitiveNames;
5926
+ const resolvedIsSensitivePath = resolvedSensitiveNames === void 0 ? void 0 : createIsSensitivePath(resolvedSensitiveNames);
5927
+ const createOptions = {
5928
+ formKey: key,
5929
+ schema,
5930
+ defaultValues: walked.cleanedValues,
5931
+ ...configuration.strict !== void 0 ? { strict: configuration.strict } : {},
5932
+ hydration: pending,
5933
+ ...configuration.validateOn !== void 0 ? { validateOn: configuration.validateOn } : {},
5934
+ ...configuration.debounceMs !== void 0 ? { debounceMs: configuration.debounceMs } : {},
5935
+ ssr: registry.ssr,
5936
+ // Server-only: bind the SSR prefetch coordination handles. `enqueue`
5937
+ // records intent on every `state.activate()` so a wizard skip-list
5938
+ // override or a future transform mark has a consistent set to diff
5939
+ // against; `shouldFire` lets the activate path bail when the
5940
+ // wizard explicitly skipped this key — even an explicit
5941
+ // `form.activate()` defers to the wizard's render-efficiency
5942
+ // skip-list on the server.
5943
+ ...registry.ssr ? {
5944
+ ssrPrefetch: {
5945
+ enqueue: () => {
5946
+ registry.enqueuePrefetch(key);
5947
+ },
5948
+ shouldFire: () => registry.shouldPrefetch(key)
5949
+ }
5950
+ } : {},
5951
+ ...configuration.rememberVariants !== void 0 ? { rememberVariants: configuration.rememberVariants } : {},
5952
+ ...configuration.coerce !== void 0 ? { coerce: configuration.coerce } : {},
5953
+ ...configuration.getDisplayState !== void 0 ? { getDisplayState: configuration.getDisplayState } : {},
5954
+ ...initialBlankPaths !== void 0 ? { initialBlankPaths } : {},
5955
+ ...resolvedIsSensitivePath !== void 0 ? { isSensitivePath: resolvedIsSensitivePath } : {}
5956
+ };
5957
+ const state = createFormStore(createOptions);
5958
+ registry.forms.set(
5959
+ key,
5960
+ state
5961
+ );
5962
+ return state;
5963
+ }
5964
+ let anonCounter = 0;
5965
+ let formInstanceCounter = 0;
5966
+ const ambientProvideHistory = __DEV__ ? /* @__PURE__ */ new WeakMap() : null;
5967
+ function recordAmbientProvide(ssr) {
5968
+ if (!__DEV__ || ssr || ambientProvideHistory === null) return;
5969
+ const instance = getCurrentInstance();
5970
+ if (instance === null) return;
5971
+ const instanceKey = instance;
5972
+ const entry = {
5973
+ source: captureUserCallSite()
5974
+ };
5975
+ const existing = ambientProvideHistory.get(instanceKey);
5976
+ if (existing === void 0) {
5977
+ ambientProvideHistory.set(instanceKey, [entry]);
5978
+ return;
5979
+ }
5980
+ existing.push(entry);
5981
+ }
5982
+ function resolveFormKey(key) {
5983
+ if (key !== void 0 && key !== null && key !== "") {
5984
+ if (key.startsWith(RESERVED_KEY_PREFIX)) {
5985
+ throw new ReservedFormKeyError(key);
5214
5986
  }
5215
- const schemaErrors = (existing.data.schemaErrors ?? []).filter(([k]) => k !== pathKey);
5216
- const userErrors = (existing.data.userErrors ?? []).filter(([k]) => k !== pathKey);
5217
- const schemaMap = new Map(schemaErrors.map(([k, v]) => [k, [...v]]));
5218
- const userMap = new Map(userErrors.map(([k, v]) => [k, [...v]]));
5219
- await adapter.setItem(
5220
- key,
5221
- buildPersistedPayload(nextForm, "form+errors", schemaMap, userMap, transientSet)
5222
- );
5987
+ return key;
5223
5988
  }
5224
- function awaitPendingWrites() {
5225
- if (inFlightFinalFlush !== null) return inFlightFinalFlush;
5226
- if (disposed) return Promise.resolve();
5227
- return writer.flush().catch(() => void 0);
5989
+ if (getCurrentInstance() !== null) {
5990
+ return `${ANONYMOUS_FORM_KEY_PREFIX}${useId()}`;
5228
5991
  }
5229
- function dispose() {
5230
- if (disposed || inFlightFinalFlush !== null) return;
5231
- unsubscribeChange();
5232
- unsubscribeSuccess();
5233
- inFlightFinalFlush = writer.flush().catch(() => void 0).finally(() => {
5234
- disposed = true;
5235
- inFlightFinalFlush = null;
5236
- });
5992
+ return `${ANONYMOUS_FORM_KEY_PREFIX}${anonCounter++}`;
5993
+ }
5994
+ function warnOnSchemaFingerprintMismatch(key, existing, incoming) {
5995
+ let existingFp;
5996
+ let incomingFp;
5997
+ try {
5998
+ existingFp = existing.fingerprint();
5999
+ incomingFp = incoming.fingerprint();
6000
+ } catch (error) {
6001
+ console.error(
6002
+ `[attaform] fingerprint() threw for key "${key}"; skipping mismatch check.`,
6003
+ error
6004
+ );
6005
+ return;
5237
6006
  }
5238
- return {
5239
- wiredConfig: config,
5240
- writePathImmediately,
5241
- clearPersistedDraft,
5242
- awaitPendingWrites,
5243
- dispose
5244
- };
6007
+ if (existingFp === incomingFp) return;
6008
+ console.warn(
6009
+ `[attaform] useForm() calls with key "${key}" use different schemas; first wins, second is ignored. Use identical schemas or unique keys.
6010
+ existing: ${existingFp}
6011
+ incoming: ${incomingFp}`
6012
+ );
5245
6013
  }
5246
- function isEmptyContainer(value) {
5247
- if (value === void 0 || value === null) return true;
5248
- if (Array.isArray(value)) return value.length === 0;
5249
- if (isPlainRecord(value)) return Object.keys(value).length === 0;
5250
- return false;
6014
+ function warnOnPersistDivergence(key, existing, incomingPersist) {
6015
+ if (incomingPersist === void 0) return;
6016
+ const wired = existing.modules.get(PERSISTENCE_MODULE_KEY);
6017
+ const incomingNormalized = normalizePersistConfig(incomingPersist);
6018
+ if (wired === void 0) {
6019
+ console.warn(
6020
+ `[attaform] useForm({ key: "${key}" }) passed a persist config but the first useForm({ key }) call didn't wire persistence; the new config is silently dropped. Pass persist on the first call, or remove persist here to make the inheritance explicit.`
6021
+ );
6022
+ return;
6023
+ }
6024
+ if (persistConfigsEquivalent(wired.wiredConfig, incomingNormalized)) return;
6025
+ console.warn(
6026
+ `[attaform] useForm({ key: "${key}" }) passed a persist config that differs from the first useForm({ key }) call's; first wins, this one is ignored.
6027
+ wired: ${describePersist(wired.wiredConfig)}
6028
+ incoming: ${describePersist(incomingNormalized)}`
6029
+ );
6030
+ }
6031
+ function persistConfigsEquivalent(a, b) {
6032
+ if (a.storage !== b.storage) return false;
6033
+ if ((a.key ?? void 0) !== (b.key ?? void 0)) return false;
6034
+ if ((a.debounceMs ?? void 0) !== (b.debounceMs ?? void 0)) return false;
6035
+ return true;
6036
+ }
6037
+ function describePersist(config) {
6038
+ const storage = typeof config.storage === "string" ? config.storage : "custom-adapter";
6039
+ const parts = [`storage=${storage}`];
6040
+ if (config.key !== void 0) parts.push(`key=${config.key}`);
6041
+ if (config.debounceMs !== void 0) parts.push(`debounceMs=${config.debounceMs}`);
6042
+ return `{ ${parts.join(", ")} }`;
5251
6043
  }
5252
6044
  const warnedAnonPersistKeys = /* @__PURE__ */ new Set();
5253
6045
  function enforceAnonPersistRule(formKey, ssr) {
@@ -5265,33 +6057,6 @@ function enforceAnonPersistRule(formKey, ssr) {
5265
6057
  }
5266
6058
  return true;
5267
6059
  }
5268
- function collectPersistedLeafPaths(form) {
5269
- const out = [];
5270
- walk(form, []);
5271
- return out;
5272
- function walk(node, prefix) {
5273
- if (Array.isArray(node)) {
5274
- for (let i = 0; i < node.length; i++) {
5275
- walk(node[i], [...prefix, i]);
5276
- }
5277
- return;
5278
- }
5279
- if (isPlainRecord(node)) {
5280
- for (const key of Object.keys(node)) {
5281
- walk(node[key], [...prefix, key]);
5282
- }
5283
- return;
5284
- }
5285
- if (prefix.length === 0) return;
5286
- out.push(canonicalizePath(prefix).key);
5287
- }
5288
- }
5289
- function isDescendantPathKey(candidate, ancestor) {
5290
- if (candidate.length <= ancestor.length) return false;
5291
- if (!ancestor.endsWith("]")) return false;
5292
- const childPrefix = `${ancestor.slice(0, -1)},`;
5293
- return candidate.startsWith(childPrefix);
5294
- }
5295
6060
 
5296
6061
  let injectedInstanceCounter = 0;
5297
6062
  function injectForm(input) {
@@ -5371,8 +6136,6 @@ function isLazyMarker(value) {
5371
6136
  }
5372
6137
 
5373
6138
  const NOOP_WIZARD_HISTORY = {
5374
- push() {
5375
- },
5376
6139
  replace() {
5377
6140
  },
5378
6141
  read() {
@@ -5399,21 +6162,16 @@ function createWizardHistory(param) {
5399
6162
  for (const subscriber of subscribers) subscriber(value);
5400
6163
  }
5401
6164
  window.addEventListener("popstate", handlePopstate);
5402
- function safeWriteState(key, op) {
6165
+ function safeReplaceState(key) {
5403
6166
  try {
5404
- const fn = op === "push" ? window.history.pushState : window.history.replaceState;
5405
- fn.call(window.history, {}, "", buildUrl(key));
6167
+ window.history.replaceState({}, "", buildUrl(key));
5406
6168
  } catch {
5407
6169
  }
5408
6170
  }
5409
6171
  return {
5410
- push(key) {
5411
- if (disposed) return;
5412
- safeWriteState(key, "push");
5413
- },
5414
6172
  replace(key) {
5415
6173
  if (disposed) return;
5416
- safeWriteState(key, "replace");
6174
+ safeReplaceState(key);
5417
6175
  },
5418
6176
  read() {
5419
6177
  const url = new URL(window.location.href);
@@ -5472,68 +6230,16 @@ function buildWizardStatusesProxy(statuses) {
5472
6230
  }
5473
6231
  return result;
5474
6232
  });
5475
- const target = (() => {
5476
- });
5477
- const proxyToString = () => JSON.stringify(snapshot.value);
5478
- const proxyToPrimitive = (hint) => hint === "number" ? NaN : proxyToString();
5479
- return new Proxy(target, {
5480
- apply(_, __, args) {
5481
- const key = args[0];
5482
- if (key === void 0) return snapshot.value;
5483
- const computedEntry = statuses[key];
5484
- if (computedEntry === void 0) return void 0;
5485
- return computedEntry.value;
5486
- },
5487
- get(_, key) {
5488
- if (typeof key === "symbol") {
5489
- if (key === Symbol.toPrimitive) return proxyToPrimitive;
5490
- return Reflect.get(target, key);
5491
- }
5492
- if (key === "toJSON") return () => snapshot.value;
5493
- if (key === "toString") return proxyToString;
5494
- if (key === "valueOf")
5495
- return function() {
5496
- return this;
5497
- };
5498
- const computedEntry = statuses[key];
5499
- if (computedEntry === void 0) return void 0;
5500
- return computedEntry.value;
5501
- },
5502
- has(_, key) {
5503
- if (typeof key === "symbol") return Reflect.has(target, key);
5504
- return Object.hasOwn(statuses, key);
5505
- },
5506
- ownKeys() {
5507
- return Object.keys(statuses);
5508
- },
5509
- getOwnPropertyDescriptor(_, key) {
5510
- if (typeof key === "symbol") return void 0;
5511
- const computedEntry = statuses[key];
5512
- if (computedEntry === void 0) return void 0;
5513
- return {
5514
- configurable: true,
5515
- enumerable: true,
5516
- writable: false,
5517
- value: computedEntry.value
5518
- };
5519
- },
5520
- set(_, key) {
5521
- if (__DEV__) {
5522
- console.warn(
5523
- `[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.`
5524
- );
5525
- }
5526
- return true;
5527
- },
5528
- deleteProperty(_, key) {
5529
- if (__DEV__) {
5530
- console.warn(
5531
- `[attaform] wizard.statuses is read-only \u2014 delete of "${String(key)}" was ignored.`
5532
- );
5533
- }
5534
- return true;
5535
- },
5536
- defineProperty: () => true
6233
+ return buildCallableReadonlySnapshotProxy({
6234
+ surface: "wizard.statuses",
6235
+ snapshot: () => snapshot.value,
6236
+ resolveKey: (key) => statuses[key]?.value,
6237
+ // Single-key callable form. Strings stringify naturally; non-
6238
+ // string args coerce via `String(arg)` and miss the lookup, which
6239
+ // resolves to `undefined` (consistent with property-access).
6240
+ resolveCall: (arg) => statuses[String(arg)]?.value,
6241
+ ownKeys: () => Object.keys(statuses),
6242
+ hasKey: (key) => Object.hasOwn(statuses, key)
5537
6243
  });
5538
6244
  }
5539
6245
 
@@ -5550,6 +6256,12 @@ const NOOP_VALID_STATUS = {
5550
6256
  submitted: false,
5551
6257
  errorCount: 0
5552
6258
  };
6259
+ function asStatusSource(form) {
6260
+ return form;
6261
+ }
6262
+ function asSubmissionSource(form) {
6263
+ return form;
6264
+ }
5553
6265
  function useWizard(options) {
5554
6266
  const rawSteps = Array.isArray(options.steps) ? options.steps : [];
5555
6267
  if (rawSteps.length === 0 && __DEV__) {
@@ -5637,10 +6349,12 @@ function useWizard(options) {
5637
6349
  return { configurable: true, enumerable: true, writable: false, value: form };
5638
6350
  }
5639
6351
  });
5640
- const slotCtx = computed(() => ({
6352
+ const slotCtx = {
5641
6353
  forms: slotForms,
5642
- currentKey: activeKey.value === "" ? void 0 : activeKey.value
5643
- }));
6354
+ get currentKey() {
6355
+ return activeKey.value === "" ? void 0 : activeKey.value;
6356
+ }
6357
+ };
5644
6358
  function resolveSlot(slot, index, ctx) {
5645
6359
  if (typeof slot === "string") {
5646
6360
  return getOrBuildNoop(slot);
@@ -5663,12 +6377,6 @@ function useWizard(options) {
5663
6377
  }
5664
6378
  return result;
5665
6379
  }
5666
- const lazyCtx = {
5667
- forms: slotForms,
5668
- get currentKey() {
5669
- return activeKey.value === "" ? void 0 : activeKey.value;
5670
- }
5671
- };
5672
6380
  for (let i = 0; i < rawSteps.length; i++) {
5673
6381
  const slot = rawSteps[i];
5674
6382
  if (isLazyMarker(slot)) {
@@ -5678,18 +6386,17 @@ function useWizard(options) {
5678
6386
  idx,
5679
6387
  computed(() => {
5680
6388
  void lazyEpoch.value;
5681
- return marker.resolve(lazyCtx);
6389
+ return marker.resolve(slotCtx);
5682
6390
  })
5683
6391
  );
5684
6392
  }
5685
6393
  }
5686
6394
  const compiledSteps = computed(() => {
5687
- const ctx = slotCtx.value;
5688
6395
  const out = [];
5689
6396
  const seen = /* @__PURE__ */ new Set();
5690
6397
  for (let i = 0; i < rawSteps.length; i++) {
5691
6398
  const slot = rawSteps[i];
5692
- const form = resolveSlot(slot, i, ctx);
6399
+ const form = resolveSlot(slot, i, slotCtx);
5693
6400
  if (form === void 0) continue;
5694
6401
  if (seen.has(form.key)) {
5695
6402
  if (__DEV__) {
@@ -5715,9 +6422,12 @@ function useWizard(options) {
5715
6422
  return -1;
5716
6423
  });
5717
6424
  const currentStep = computed(() => {
5718
- const key = activeKey.value;
5719
- if (key !== "") return key;
5720
- const first = compiledSteps.value[0];
6425
+ const list = compiledSteps.value;
6426
+ const idx = activeIndex.value;
6427
+ if (idx >= 0 && idx < list.length) {
6428
+ return list[idx].key;
6429
+ }
6430
+ const first = list[0];
5721
6431
  return first === void 0 ? void 0 : first.key;
5722
6432
  });
5723
6433
  const activeForm = computed(() => {
@@ -5740,36 +6450,94 @@ function useWizard(options) {
5740
6450
  for (const step of compiledSteps.value) out[step.key] = step.form;
5741
6451
  return out;
5742
6452
  });
5743
- const allValues = computed(() => {
5744
- const out = {};
5745
- for (const step of compiledSteps.value) {
5746
- const source = step.form;
5747
- out[step.key] = source.values;
6453
+ function isFormReady(key) {
6454
+ const store = registry.forms.get(key);
6455
+ return store?.defaultsResolved.value === true;
6456
+ }
6457
+ function toWizardAggregateError(err, fallbackKey) {
6458
+ const entry = {
6459
+ formKey: err.formKey ?? fallbackKey,
6460
+ path: err.path,
6461
+ message: err.message
6462
+ };
6463
+ if (err.code !== void 0) entry.code = err.code;
6464
+ return entry;
6465
+ }
6466
+ const valuesCache = /* @__PURE__ */ new Map();
6467
+ function valuesFor(form) {
6468
+ const cached = valuesCache.get(form.key);
6469
+ if (cached !== void 0) return cached;
6470
+ const source = asStatusSource(form);
6471
+ const computedValues = computed(() => source.values);
6472
+ valuesCache.set(form.key, computedValues);
6473
+ return computedValues;
6474
+ }
6475
+ const errorsCache = /* @__PURE__ */ new Map();
6476
+ function errorsFor(form) {
6477
+ const cached = errorsCache.get(form.key);
6478
+ if (cached !== void 0) return cached;
6479
+ const source = asStatusSource(form);
6480
+ const computedErrors = computed(() => {
6481
+ if (!isFormReady(form.key)) return [];
6482
+ const errors = source.meta?.errors ?? [];
6483
+ const list = [];
6484
+ for (const err of errors) list.push(toWizardAggregateError(err, form.key));
6485
+ return list;
6486
+ });
6487
+ errorsCache.set(form.key, computedErrors);
6488
+ return computedErrors;
6489
+ }
6490
+ const allValues = new Proxy({}, {
6491
+ get(_, key) {
6492
+ if (typeof key !== "string") return void 0;
6493
+ const form = formsRecord.value[key];
6494
+ if (form === void 0) return void 0;
6495
+ return valuesFor(form).value;
6496
+ },
6497
+ has(_, key) {
6498
+ if (typeof key !== "string") return false;
6499
+ return formsRecord.value[key] !== void 0;
6500
+ },
6501
+ ownKeys() {
6502
+ return Object.keys(formsRecord.value);
6503
+ },
6504
+ getOwnPropertyDescriptor(_, key) {
6505
+ if (typeof key !== "string") return void 0;
6506
+ const form = formsRecord.value[key];
6507
+ if (form === void 0) return void 0;
6508
+ return {
6509
+ configurable: true,
6510
+ enumerable: true,
6511
+ writable: false,
6512
+ value: valuesFor(form).value
6513
+ };
5748
6514
  }
5749
- return out;
5750
6515
  });
5751
- const allErrors = computed(() => {
5752
- const out = {};
5753
- for (const step of compiledSteps.value) {
5754
- const source = step.form;
5755
- const list = [];
5756
- const store = registry.forms.get(step.key);
5757
- const resolved = store?.defaultsResolved.value === true;
5758
- if (resolved) {
5759
- const errors = source.meta?.errors ?? [];
5760
- for (const err of errors) {
5761
- const entry = {
5762
- formKey: step.key,
5763
- path: err.path,
5764
- message: err.message
5765
- };
5766
- if (err.code !== void 0) entry.code = err.code;
5767
- list.push(entry);
5768
- }
5769
- }
5770
- out[step.key] = list;
6516
+ const allErrors = new Proxy({}, {
6517
+ get(_, key) {
6518
+ if (typeof key !== "string") return void 0;
6519
+ const form = formsRecord.value[key];
6520
+ if (form === void 0) return void 0;
6521
+ return errorsFor(form).value;
6522
+ },
6523
+ has(_, key) {
6524
+ if (typeof key !== "string") return false;
6525
+ return formsRecord.value[key] !== void 0;
6526
+ },
6527
+ ownKeys() {
6528
+ return Object.keys(formsRecord.value);
6529
+ },
6530
+ getOwnPropertyDescriptor(_, key) {
6531
+ if (typeof key !== "string") return void 0;
6532
+ const form = formsRecord.value[key];
6533
+ if (form === void 0) return void 0;
6534
+ return {
6535
+ configurable: true,
6536
+ enumerable: true,
6537
+ writable: false,
6538
+ value: errorsFor(form).value
6539
+ };
5771
6540
  }
5772
- return out;
5773
6541
  });
5774
6542
  const seedRef = ref(void 0);
5775
6543
  const seedInput = options.defaultStatuses;
@@ -5792,11 +6560,9 @@ function useWizard(options) {
5792
6560
  function statusFor(form) {
5793
6561
  const cached = statusCache.get(form.key);
5794
6562
  if (cached !== void 0) return cached;
5795
- const source = form;
6563
+ const source = asStatusSource(form);
5796
6564
  const computedStatus = computed(() => {
5797
- const store = registry.forms.get(form.key);
5798
- const resolved = store?.defaultsResolved.value === true;
5799
- if (resolved) {
6565
+ if (isFormReady(form.key)) {
5800
6566
  const meta = source.meta;
5801
6567
  if (meta !== void 0 && meta !== null) {
5802
6568
  return {
@@ -5939,7 +6705,7 @@ function useWizard(options) {
5939
6705
  }
5940
6706
  if (!registry.ssr) {
5941
6707
  for (const step of compiledSteps.value) {
5942
- const source = step.form;
6708
+ const source = asSubmissionSource(step.form);
5943
6709
  if (typeof source.activate === "function") void source.activate();
5944
6710
  }
5945
6711
  }
@@ -5983,7 +6749,7 @@ function useWizard(options) {
5983
6749
  const submissionAttempts = ref(0);
5984
6750
  const done = ref(false);
5985
6751
  function activateForm(form) {
5986
- const source = form;
6752
+ const source = asSubmissionSource(form);
5987
6753
  if (typeof source.activate === "function") {
5988
6754
  void source.activate();
5989
6755
  }
@@ -6083,7 +6849,7 @@ function useWizard(options) {
6083
6849
  };
6084
6850
  }
6085
6851
  async function processOne(form) {
6086
- const full = form;
6852
+ const full = asSubmissionSource(form);
6087
6853
  let activationFailure;
6088
6854
  try {
6089
6855
  if (typeof full.activate === "function") await full.activate();
@@ -6115,15 +6881,7 @@ function useWizard(options) {
6115
6881
  for (const step of compiledSteps.value) {
6116
6882
  const processed = results.get(step.key);
6117
6883
  if (processed === void 0 || processed.success === true) continue;
6118
- for (const err of processed.errors) {
6119
- const entry = {
6120
- formKey: err.formKey,
6121
- path: err.path,
6122
- message: err.message
6123
- };
6124
- if (err.code !== void 0) entry.code = err.code;
6125
- out.push(entry);
6126
- }
6884
+ for (const err of processed.errors) out.push(toWizardAggregateError(err, step.key));
6127
6885
  }
6128
6886
  return out;
6129
6887
  }
@@ -6177,8 +6935,7 @@ function useWizard(options) {
6177
6935
  if (processed !== void 0 && processed.success === true) {
6178
6936
  valuesMap[step.key] = processed.data;
6179
6937
  } else {
6180
- const source = step.form;
6181
- valuesMap[step.key] = source.values;
6938
+ valuesMap[step.key] = asStatusSource(step.form).values;
6182
6939
  }
6183
6940
  }
6184
6941
  const ctx = buildSubmitContext(valuesMap, currentKey, final);
@@ -6199,8 +6956,11 @@ function useWizard(options) {
6199
6956
  moveTo(firstFailedKey);
6200
6957
  await nextTick();
6201
6958
  const failedForm = formsRecord.value[firstFailedKey];
6202
- if (failedForm !== void 0 && typeof failedForm.applyInvalidSubmitPolicy === "function") {
6203
- failedForm.applyInvalidSubmitPolicy();
6959
+ if (failedForm !== void 0) {
6960
+ const failedSource = asSubmissionSource(failedForm);
6961
+ if (typeof failedSource.applyInvalidSubmitPolicy === "function") {
6962
+ failedSource.applyInvalidSubmitPolicy();
6963
+ }
6204
6964
  }
6205
6965
  }
6206
6966
  }
@@ -6215,7 +6975,7 @@ function useWizard(options) {
6215
6975
  done.value = false;
6216
6976
  lazyEpoch.value += 1;
6217
6977
  for (const step of compiledSteps.value) {
6218
- const full = step.form;
6978
+ const full = asSubmissionSource(step.form);
6219
6979
  if (typeof full.reset === "function") full.reset();
6220
6980
  }
6221
6981
  const firstStep = compiledSteps.value[0];
@@ -6265,12 +7025,8 @@ function useWizard(options) {
6265
7025
  return count.value;
6266
7026
  },
6267
7027
  statuses,
6268
- get allValues() {
6269
- return allValues.value;
6270
- },
6271
- get allErrors() {
6272
- return allErrors.value;
6273
- },
7028
+ allValues,
7029
+ allErrors,
6274
7030
  get progress() {
6275
7031
  return progress.value;
6276
7032
  },
@@ -6401,5 +7157,5 @@ function warnIfAmbientWizardProviderHadDuplicates() {
6401
7157
  }
6402
7158
  }
6403
7159
 
6404
- export { AttaformErrorCode as A, defaultShouldShowErrors 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 };
6405
- //# sourceMappingURL=attaform.CZ-XtZt_.mjs.map
7160
+ export { AttaformErrorCode as A, defaultDisplayState as a, defineCoercion as b, injectWizard as c, defaultCoercionRules as d, isPlainRecord as e, isUnset as f, getAtPath as g, humanize as h, injectForm as i, slimKindOf as j, useAbstractForm as k, lazy as l, useWizard as m, normalizeNumericOption as n, setAtPath as s, unset as u };
7161
+ //# sourceMappingURL=attaform.CKFbKFb6.mjs.map