formstand 0.20.0 → 0.22.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.
package/dist/index.cjs CHANGED
@@ -112,8 +112,10 @@ var isPlainObject = (v) => {
112
112
  };
113
113
  var valuesEqual = (a, b) => {
114
114
  if (Object.is(a, b)) return true;
115
+ if (typeof a === "number" && typeof b === "number") return a === b;
115
116
  if (a instanceof Date && b instanceof Date) {
116
- return a.getTime() === b.getTime();
117
+ const [ta, tb] = [a.getTime(), b.getTime()];
118
+ return ta === tb || Number.isNaN(ta) && Number.isNaN(tb);
117
119
  }
118
120
  if (Array.isArray(a) && Array.isArray(b)) {
119
121
  return a.length === b.length && a.every((v, i) => valuesEqual(v, b[i]));
@@ -143,6 +145,18 @@ var isFieldDirty = (value, initialValue) => {
143
145
  return dirty;
144
146
  };
145
147
 
148
+ // src/core/env.ts
149
+ var isDevBuild = () => typeof process === "undefined" || process.env?.["NODE_ENV"] !== "production";
150
+ var isDevToolsBuild = () => {
151
+ if (typeof process === "undefined") return false;
152
+ const env = process.env?.["NODE_ENV"];
153
+ return typeof env === "string" && env !== "production";
154
+ };
155
+
156
+ // src/core/maps.ts
157
+ var entryAt = (map, key) => Object.hasOwn(map, key) ? map[key] : void 0;
158
+ var flagAt = (map, key) => Object.hasOwn(map, key) && map[key] === true;
159
+
146
160
  // src/core/path.ts
147
161
  var MAX_ARRAY_INDEX = 1e5;
148
162
  var isIndexSegment = (s) => {
@@ -153,11 +167,18 @@ var PARSE_CACHE_MAX = 4096;
153
167
  var parseCache = /* @__PURE__ */ new Map();
154
168
  var parsePath = (path) => {
155
169
  const cached = parseCache.get(path);
156
- if (cached !== void 0) return cached;
170
+ if (cached !== void 0) {
171
+ parseCache.delete(path);
172
+ parseCache.set(path, cached);
173
+ return cached;
174
+ }
157
175
  const segments = Object.freeze(
158
176
  path === "" ? [] : path.split(".").map((s) => isIndexSegment(s) ? Number(s) : s)
159
177
  );
160
- if (parseCache.size >= PARSE_CACHE_MAX) parseCache.clear();
178
+ if (parseCache.size >= PARSE_CACHE_MAX) {
179
+ const oldest = parseCache.keys().next().value;
180
+ if (oldest !== void 0) parseCache.delete(oldest);
181
+ }
161
182
  parseCache.set(path, segments);
162
183
  return segments;
163
184
  };
@@ -246,10 +267,17 @@ var expandIssue = (issue, prefix) => {
246
267
  }
247
268
  return [{ path: issuePathToFormPath(at), message: issue.message }];
248
269
  };
249
- var flattenIssues = (issues) => issues.flatMap((issue) => expandIssue(issue, [])).reduce((acc, { path, message }) => {
250
- const existing = (Object.hasOwn(acc, path) ? acc[path] : void 0) ?? [];
251
- return existing.includes(message) ? acc : { ...acc, [path]: [...existing, message] };
252
- }, {});
270
+ var flattenIssues = (issues) => Object.fromEntries(
271
+ issues.flatMap((issue) => expandIssue(issue, [])).reduce((acc, { path, message }) => {
272
+ const existing = acc.get(path);
273
+ if (existing === void 0) {
274
+ acc.set(path, [message]);
275
+ } else if (!existing.includes(message)) {
276
+ existing.push(message);
277
+ }
278
+ return acc;
279
+ }, /* @__PURE__ */ new Map())
280
+ );
253
281
  var validateSync = (schema, values) => {
254
282
  const result = schema.safeParse(values);
255
283
  return result.success ? { kind: "valid", data: result.data } : { kind: "invalid", errors: flattenIssues(result.error.issues) };
@@ -314,6 +342,34 @@ var hasChecks = (s) => {
314
342
  const checks = s.def.checks;
315
343
  return checks !== void 0 && checks.length > 0;
316
344
  };
345
+ var fieldSchemaForIntrospection = (schema, path) => {
346
+ const childOf = (s, segment) => {
347
+ const inner = unwrapForWalk(s);
348
+ if (inner !== null) return childOf(inner, segment);
349
+ if (s instanceof zod.z.ZodObject && typeof segment === "string") {
350
+ const shape = s.shape;
351
+ return Object.hasOwn(shape, segment) ? shape[segment] ?? null : null;
352
+ }
353
+ if (s instanceof zod.z.ZodArray && typeof segment === "number") {
354
+ return s.element;
355
+ }
356
+ if (s instanceof zod.z.ZodTuple && typeof segment === "number") {
357
+ return s.def.items[segment] ?? null;
358
+ }
359
+ if (s instanceof zod.z.ZodDiscriminatedUnion || s instanceof zod.z.ZodUnion) {
360
+ const options = s.def.options;
361
+ return options.reduce(
362
+ (found, option) => found ?? childOf(option, segment),
363
+ null
364
+ );
365
+ }
366
+ return null;
367
+ };
368
+ return parsePath(path).reduce(
369
+ (current, segment) => current === null ? null : childOf(current, segment),
370
+ schema
371
+ );
372
+ };
317
373
  var fieldSchemaAtPath = (schema, path) => parsePath(path).reduce((current, segment) => {
318
374
  if (current === null || hasChecks(current)) return null;
319
375
  if (current instanceof zod.z.ZodObject && typeof segment === "string") {
@@ -332,6 +388,12 @@ var emptyErrors = {};
332
388
  var emptyBools = {};
333
389
  var identityMapper = (n) => n;
334
390
  var omitKey = (map, key) => Object.fromEntries(Object.entries(map).filter(([k]) => k !== key));
391
+ var omitKeys = (map, keys) => {
392
+ if (keys.size === 0) return map;
393
+ const entries = Object.entries(map);
394
+ const kept = entries.filter(([k]) => !keys.has(k));
395
+ return kept.length === entries.length ? map : Object.fromEntries(kept);
396
+ };
335
397
  var inScopeOfAny = (key, paths) => paths.some((p) => isPathOrChild(key, p));
336
398
  var omitScope = (map, paths) => {
337
399
  const entries = Object.entries(map);
@@ -370,13 +432,13 @@ var stringArraysEqual = (a, b) => a === b || a.length === b.length && a.every((v
370
432
  var reuseErrorRefs = (next, prev) => {
371
433
  if (next === prev) return prev;
372
434
  const entries = Object.entries(next).map(([k, v]) => {
373
- const old = prev[k];
435
+ const old = entryAt(prev, k);
374
436
  return [
375
437
  k,
376
438
  old !== void 0 && stringArraysEqual(old, v) ? old : v
377
439
  ];
378
440
  });
379
- const sameShape = entries.length === Object.keys(prev).length && entries.every(([k, v]) => prev[k] === v);
441
+ const sameShape = entries.length === Object.keys(prev).length && entries.every(([k, v]) => entryAt(prev, k) === v);
380
442
  return sameShape ? prev : Object.fromEntries(entries);
381
443
  };
382
444
  var scopeSettledResult = (result, path, viaSubschema) => result.kind !== "invalid" ? emptyErrors : viaSubschema ? prefixErrorKeys(result.errors, path) : path === "" ? result.errors : pickScope(result.errors, [path]);
@@ -433,9 +495,10 @@ var createForm = (schema, options) => {
433
495
  // opting in during development must NOT stream every keystroke to
434
496
  // an end user who happens to have the extension installed, where
435
497
  // a module-singleton form's unbounded time-travel history is a
436
- // real memory + serialization cost. Guarded for environments
437
- // without process (a bundler defines NODE_ENV in real dev).
438
- enabled: typeof process !== "undefined" && process.env?.["NODE_ENV"] !== "production"
498
+ // real memory + serialization cost. Fail-closed gate: a runtime
499
+ // without a readable NODE_ENV (CDN/ESM, Deno, a worker) reads as
500
+ // production here, unlike the console-warning gate.
501
+ enabled: isDevToolsBuild()
439
502
  })
440
503
  );
441
504
  const commitFullPass = (state, next) => errorChannels(state, {
@@ -551,13 +614,19 @@ var createForm = (schema, options) => {
551
614
  const slot = slotAtPath(values, path);
552
615
  return slot.exists ? { kind: "parse", schema: sub, value: slot.value, viaSubschema: true } : { kind: "skip" };
553
616
  };
554
- const scopedFieldErrorsAsync = async (path, values) => {
555
- const scope = fieldScopeFor(path, values);
556
- return scope.kind === "skip" ? emptyErrors : scopeSettledResult(
557
- await validateAsync(scope.schema, scope.value),
558
- path,
559
- scope.viaSubschema
560
- );
617
+ const parseScopeAsync = async (path, scope) => scope.kind === "skip" ? emptyErrors : scopeSettledResult(
618
+ await validateAsync(scope.schema, scope.value),
619
+ path,
620
+ scope.viaSubschema
621
+ );
622
+ const scopeStale = (scope, path, valuesAtStart, live) => {
623
+ if (live === valuesAtStart) return false;
624
+ switch (scope.kind) {
625
+ case "skip":
626
+ return slotAtPath(live, path).exists;
627
+ case "parse":
628
+ return scope.viaSubschema ? !Object.is(getAtPath(live, path), scope.value) : true;
629
+ }
561
630
  };
562
631
  const fieldResult = (scoped) => {
563
632
  const messages = Object.values(scoped).flat();
@@ -605,10 +674,10 @@ var createForm = (schema, options) => {
605
674
  const value = getAtPath(state.values, path);
606
675
  return {
607
676
  value,
608
- error: state.errors[path],
609
- touched: state.touched[path] ?? false,
677
+ error: entryAt(state.errors, path),
678
+ touched: flagAt(state.touched, path),
610
679
  dirty: isFieldDirty(value, getAtPath(state.initialValues, path)),
611
- isValidating: state.isValidating[path] ?? false
680
+ isValidating: flagAt(state.isValidating, path)
612
681
  };
613
682
  };
614
683
  const sequences = /* @__PURE__ */ new Map();
@@ -647,44 +716,66 @@ var createForm = (schema, options) => {
647
716
  }
648
717
  const passes = new Map(paths.map((p) => [p, claimPass(p)]));
649
718
  const valuesAtStart = store.getState().values;
719
+ const scopes = paths.map((path) => ({
720
+ path,
721
+ scope: fieldScopeFor(path, valuesAtStart)
722
+ }));
723
+ const allViaSubschema = scopes.every(
724
+ ({ scope }) => scope.kind === "skip" || scope.kind === "parse" && scope.viaSubschema
725
+ );
650
726
  store.setState((state) => ({
651
727
  ...state,
652
- isValidating: paths.reduce(
653
- (acc, p) => ({ ...acc, [p]: true }),
654
- { ...state.isValidating }
655
- )
728
+ isValidating: {
729
+ ...state.isValidating,
730
+ ...Object.fromEntries(paths.map((p) => [p, true]))
731
+ }
656
732
  }));
657
- const result = await validateAsync(schema, valuesAtStart);
658
- const fullErrors = result.kind === "invalid" ? result.errors : emptyErrors;
659
- const requestedErrors = pickScope(fullErrors, paths);
733
+ const parsed = allViaSubschema ? {
734
+ kind: "slots",
735
+ byPath: new Map(
736
+ await Promise.all(
737
+ scopes.map(
738
+ async ({ path, scope }) => [path, await parseScopeAsync(path, scope)]
739
+ )
740
+ )
741
+ )
742
+ } : {
743
+ kind: "full",
744
+ errors: await validateAsync(schema, valuesAtStart).then(
745
+ (result) => result.kind === "invalid" ? result.errors : emptyErrors
746
+ )
747
+ };
748
+ const errorsFor = (path) => {
749
+ switch (parsed.kind) {
750
+ case "slots":
751
+ return parsed.byPath.get(path) ?? emptyErrors;
752
+ case "full":
753
+ return pickScope(parsed.errors, [path]);
754
+ }
755
+ };
756
+ const requestedErrors = Object.fromEntries(
757
+ paths.flatMap((path) => Object.entries(errorsFor(path)))
758
+ );
660
759
  const requested = settledFields(requestedErrors);
661
760
  const live = store.getState();
662
- const stillCurrent = paths.filter(
663
- (p) => sequences.get(p) === passes.get(p)
761
+ const owned = scopes.filter(
762
+ ({ path }) => sequences.get(path) === passes.get(path)
664
763
  );
665
- if (live.values !== valuesAtStart) {
666
- if (stillCurrent.length > 0) {
667
- store.setState((state) => ({
668
- ...state,
669
- isValidating: stillCurrent.reduce(
670
- (acc, p) => omitKey(acc, p),
671
- state.isValidating
672
- )
673
- }));
674
- }
675
- return requested;
676
- }
764
+ const fresh = owned.filter(
765
+ ({ path, scope }) => !scopeStale(scope, path, valuesAtStart, live.values)
766
+ ).map(({ path }) => path);
767
+ const ownedKeys = new Set(owned.map(({ path }) => path));
768
+ if (owned.length === 0) return requested;
677
769
  store.setState((state) => ({
678
770
  ...state,
679
- ...commitScopedErrors(
771
+ ...fresh.length === 0 ? {} : commitScopedErrors(
680
772
  state,
681
- pickScope(fullErrors, stillCurrent),
682
- stillCurrent
773
+ Object.fromEntries(
774
+ fresh.flatMap((path) => Object.entries(errorsFor(path)))
775
+ ),
776
+ fresh
683
777
  ),
684
- isValidating: stillCurrent.reduce(
685
- (acc, p) => omitKey(acc, p),
686
- state.isValidating
687
- )
778
+ isValidating: omitKeys(state.isValidating, ownedKeys)
688
779
  }));
689
780
  return requested;
690
781
  };
@@ -698,14 +789,15 @@ var createForm = (schema, options) => {
698
789
  }
699
790
  const pass = claimPass(path);
700
791
  const valuesAtStart = store.getState().values;
792
+ const scope = fieldScopeFor(path, valuesAtStart);
701
793
  store.setState((state) => ({
702
794
  ...state,
703
795
  isValidating: { ...state.isValidating, [path]: true }
704
796
  }));
705
- const scoped = await scopedFieldErrorsAsync(path, valuesAtStart);
797
+ const scoped = await parseScopeAsync(path, scope);
706
798
  const live = store.getState();
707
799
  const stillOwns = sequences.get(path) === pass;
708
- if (!stillOwns || live.values !== valuesAtStart) {
800
+ if (!stillOwns || scopeStale(scope, path, valuesAtStart, live.values)) {
709
801
  if (stillOwns) {
710
802
  store.setState((state) => ({
711
803
  ...state,
@@ -940,17 +1032,24 @@ var createForm = (schema, options) => {
940
1032
  ...state,
941
1033
  ...errorChannels(state, { serverErrors: errors })
942
1034
  })),
943
- addErrors: (errors) => store.setState((state) => ({
944
- ...state,
945
- ...errorChannels(state, {
946
- serverErrors: Object.entries(errors).reduce(
947
- // Computed keys throughout, so a server field named
948
- // "__proto__" lands as an own property like everywhere else.
949
- (acc, [key, list]) => list === void 0 || list.length === 0 ? omitKey(acc, key) : { ...acc, [key]: list },
950
- state.serverErrors
951
- )
952
- })
953
- })),
1035
+ addErrors: (errors) => store.setState((state) => {
1036
+ const entries = Object.entries(errors);
1037
+ const removed = new Set(
1038
+ entries.filter(([, list]) => list === void 0 || list.length === 0).map(([key]) => key)
1039
+ );
1040
+ const added = Object.fromEntries(
1041
+ entries.filter(([, list]) => list !== void 0 && list.length > 0)
1042
+ );
1043
+ return {
1044
+ ...state,
1045
+ ...errorChannels(state, {
1046
+ serverErrors: {
1047
+ ...omitKeys(state.serverErrors, removed),
1048
+ ...added
1049
+ }
1050
+ })
1051
+ };
1052
+ }),
954
1053
  clearErrors: (path) => store.setState((state) => {
955
1054
  const scrub = (map) => path === void 0 ? emptyErrors : omitScope(map, [path]);
956
1055
  return {
@@ -962,27 +1061,26 @@ var createForm = (schema, options) => {
962
1061
  };
963
1062
  }),
964
1063
  updateState: (updater) => {
965
- const valuesBefore = store.getState().values;
966
- store.setState((state) => {
967
- const patch = updater(state);
968
- if (Object.keys(patch).length === 0) return state;
969
- if (patch.errors !== void 0 && patch.errors !== state.errors) {
970
- console.warn(
971
- "[formstand] `errors` is derived from schemaErrors/serverErrors \u2014 patch those channels instead; the direct `errors` patch is ignored."
972
- );
973
- }
974
- const next = { ...state, ...patch };
975
- return {
976
- ...next,
977
- ...errorChannels(state, {
978
- schemaErrors: next.schemaErrors,
979
- serverErrors: next.serverErrors
980
- })
981
- };
982
- });
983
- if (store.getState().values !== valuesBefore) {
1064
+ const state = store.getState();
1065
+ const patch = updater(state);
1066
+ if (Object.keys(patch).length === 0) return;
1067
+ if (patch.errors !== void 0 && patch.errors !== state.errors) {
1068
+ console.warn(
1069
+ "[formstand] `errors` is derived from schemaErrors/serverErrors \u2014 patch those channels instead; the direct `errors` patch is ignored."
1070
+ );
1071
+ }
1072
+ const next = { ...state, ...patch };
1073
+ if (next.values !== state.values) {
984
1074
  clearArrayOpsUnder(store, "");
985
1075
  }
1076
+ const serverErrors = patch.serverErrors !== void 0 ? patch.serverErrors : next.values !== state.values ? releaseChangedSlices(state.serverErrors, state.values, next.values) : state.serverErrors;
1077
+ store.setState(() => ({
1078
+ ...next,
1079
+ ...errorChannels(state, {
1080
+ schemaErrors: next.schemaErrors,
1081
+ serverErrors
1082
+ })
1083
+ }));
986
1084
  },
987
1085
  adoptValues: (values) => {
988
1086
  sequences.clear();
@@ -1091,7 +1189,7 @@ var createForm = (schema, options) => {
1091
1189
  handleSubmit: (onValid, onInvalid, opts) => async (event) => {
1092
1190
  event?.preventDefault();
1093
1191
  const result = await submit(onValid, onInvalid, opts);
1094
- if (result.kind === "error" && opts?.onError === void 0 && process.env.NODE_ENV !== "production") {
1192
+ if (result.kind === "error" && opts?.onError === void 0 && isDevBuild()) {
1095
1193
  console.warn(
1096
1194
  "[formstand] submit handler threw and nothing observed it \u2014 the form shows no feedback. Pass { onError } to handleSubmit (or setError inside your handler):",
1097
1195
  result.error
@@ -1159,6 +1257,35 @@ var shouldValidateOn = (trigger, mode, reValidateMode, submitAttempted, touched
1159
1257
  }
1160
1258
  };
1161
1259
 
1260
+ // src/core/debounce.ts
1261
+ var createDebouncer = (ms) => {
1262
+ const timer = {
1263
+ current: null
1264
+ };
1265
+ const cancel = () => {
1266
+ if (timer.current !== null) {
1267
+ clearTimeout(timer.current);
1268
+ timer.current = null;
1269
+ }
1270
+ };
1271
+ const schedule = (run) => {
1272
+ cancel();
1273
+ if (ms === 0) {
1274
+ run();
1275
+ return;
1276
+ }
1277
+ timer.current = setTimeout(() => {
1278
+ timer.current = null;
1279
+ run();
1280
+ }, ms);
1281
+ };
1282
+ return Object.freeze({
1283
+ schedule,
1284
+ cancel,
1285
+ pending: () => timer.current !== null
1286
+ });
1287
+ };
1288
+
1162
1289
  // src/core/persist.ts
1163
1290
  var kindOf = (value) => value === null ? "null" : Array.isArray(value) ? "array" : typeof value;
1164
1291
  var conflictsWith = (draft, reference) => {
@@ -1190,7 +1317,7 @@ var persistForm = (form, options) => {
1190
1317
  const storage = options.storage ?? defaultStorage();
1191
1318
  const debounceMs = options.debounceMs ?? 300;
1192
1319
  const apply = options.apply ?? "adopt";
1193
- const applyDraft = (values) => apply === "restore" ? form.setValues(values) : form.adoptValues(values);
1320
+ const applyDraft = (values) => apply === "edit" || apply === "restore" ? form.setValues(values) : form.adoptValues(values);
1194
1321
  const referenceShape = () => {
1195
1322
  try {
1196
1323
  return JSON.parse(JSON.stringify(form.getState().initialValues));
@@ -1212,6 +1339,7 @@ var persistForm = (form, options) => {
1212
1339
  if (versioned && wrapper.__v !== options.version) return false;
1213
1340
  if (versioned && wrapper.values === void 0) return false;
1214
1341
  const values = versioned ? wrapper.values : parsed;
1342
+ if (values === null || values === void 0) return false;
1215
1343
  const reference = referenceShape();
1216
1344
  if (reference !== void 0 && conflictsWith(values, reference)) {
1217
1345
  return false;
@@ -1233,28 +1361,13 @@ var persistForm = (form, options) => {
1233
1361
  } catch {
1234
1362
  }
1235
1363
  };
1236
- const timerRef = {
1237
- current: null
1238
- };
1239
- const cancelPending = () => {
1240
- if (timerRef.current !== null) {
1241
- clearTimeout(timerRef.current);
1242
- timerRef.current = null;
1243
- }
1244
- };
1364
+ const writes = createDebouncer(debounceMs);
1365
+ const cancelPending = () => writes.cancel();
1245
1366
  if (apply !== "manual") {
1246
1367
  load();
1247
1368
  }
1248
1369
  const unsubscribe = form.watchValues((values) => {
1249
- cancelPending();
1250
- if (debounceMs === 0) {
1251
- writeDraft(values);
1252
- } else {
1253
- timerRef.current = setTimeout(() => {
1254
- timerRef.current = null;
1255
- writeDraft(values);
1256
- }, debounceMs);
1257
- }
1370
+ writes.schedule(() => writeDraft(values));
1258
1371
  });
1259
1372
  return Object.freeze({
1260
1373
  load,
@@ -1436,7 +1549,7 @@ var useMaskedInput = (field, options) => {
1436
1549
  field.setValue(parsed.value);
1437
1550
  return;
1438
1551
  case "empty": {
1439
- const empty = field.emptyValue;
1552
+ const empty = field.clearable || typeof field.initialValue !== "string" ? field.emptyValue : "";
1440
1553
  setEdit({ raw: text, pushed: empty });
1441
1554
  field.setValue(empty);
1442
1555
  return;
@@ -1484,9 +1597,14 @@ var useFormSteps = (form, steps) => {
1484
1597
  },
1485
1598
  [form]
1486
1599
  );
1600
+ const currentStep = react.useRef(step);
1601
+ react.useEffect(() => {
1602
+ currentStep.current = step;
1603
+ });
1487
1604
  const next = react.useCallback(async () => {
1488
1605
  const ok = await validateStep(step);
1489
1606
  if (!ok) return false;
1607
+ if (currentStep.current !== step) return false;
1490
1608
  const target = Math.min(step + 1, latest.current.length - 1);
1491
1609
  setStep(target);
1492
1610
  setVisited((prev) => /* @__PURE__ */ new Set([...prev, target]));
@@ -1508,6 +1626,7 @@ var useFormSteps = (form, steps) => {
1508
1626
  return await validateStep(from) ? walk(from + 1) : from;
1509
1627
  };
1510
1628
  const landed = await walk(step);
1629
+ if (currentStep.current !== step) return currentStep.current;
1511
1630
  setStep(landed);
1512
1631
  setVisited(
1513
1632
  (prev) => /* @__PURE__ */ new Set([
@@ -1553,7 +1672,7 @@ var useFormSteps = (form, steps) => {
1553
1672
  );
1554
1673
  };
1555
1674
  var warnUnobserved = (result, options, site) => {
1556
- if (result.kind === "error" && options?.onError === void 0 && process.env.NODE_ENV !== "production") {
1675
+ if (result.kind === "error" && options?.onError === void 0 && isDevBuild()) {
1557
1676
  console.warn(
1558
1677
  `[formstand] action handler threw and nothing observed it \u2014 the form shows no feedback. Pass { onError } to ${site} (or setError inside your handler):`,
1559
1678
  result.error
@@ -1622,10 +1741,12 @@ function useField(form, pathArg, options) {
1622
1741
  path: p,
1623
1742
  value,
1624
1743
  initialValue: initialValue2,
1625
- error: state.errors[p],
1626
- touched: state.touched[p] ?? false,
1744
+ // Own-key reads: a field named "constructor" or "toString" would
1745
+ // otherwise read Object.prototype's function as its error/flag.
1746
+ error: entryAt(state.errors, p),
1747
+ touched: flagAt(state.touched, p),
1627
1748
  dirty: isFieldDirty(value, initialValue2),
1628
- isValidating: state.isValidating[p] ?? false
1749
+ isValidating: flagAt(state.isValidating, p)
1629
1750
  };
1630
1751
  })
1631
1752
  );
@@ -1634,7 +1755,7 @@ function useField(form, pathArg, options) {
1634
1755
  const initialValue = slice.initialValue;
1635
1756
  const { emptyValue, clearable } = react.useMemo(() => {
1636
1757
  if (formSchema !== void 0) {
1637
- const sub = fieldSchemaAtPath(formSchema, path);
1758
+ const sub = fieldSchemaForIntrospection(formSchema, path);
1638
1759
  if (sub !== null) {
1639
1760
  return {
1640
1761
  emptyValue: emptyValueForSchema(sub),
@@ -1645,28 +1766,26 @@ function useField(form, pathArg, options) {
1645
1766
  return initialValue === null ? { emptyValue: null, clearable: true } : { emptyValue: void 0, clearable: false };
1646
1767
  }, [formSchema, path, initialValue]);
1647
1768
  const debounceMs = options?.debounceMs;
1648
- const debounceTimerRef = react.useRef(null);
1769
+ const debouncerRef = react.useRef(null);
1649
1770
  react.useEffect(() => {
1650
1771
  return () => {
1651
- if (debounceTimerRef.current !== null) {
1652
- clearTimeout(debounceTimerRef.current);
1653
- debounceTimerRef.current = null;
1654
- }
1772
+ debouncerRef.current?.cancel();
1773
+ debouncerRef.current = null;
1655
1774
  };
1656
1775
  }, [form, path, debounceMs]);
1657
1776
  const triggerValidate = react.useCallback(() => {
1658
1777
  if (debounceMs !== void 0 && debounceMs > 0) {
1659
- if (debounceTimerRef.current !== null) {
1660
- clearTimeout(debounceTimerRef.current);
1661
- }
1662
- const scheduled = getAtPath(form.store.getState().values, path);
1663
- debounceTimerRef.current = setTimeout(() => {
1664
- debounceTimerRef.current = null;
1778
+ const debouncer = debouncerRef.current ?? createDebouncer(debounceMs);
1779
+ debouncerRef.current = debouncer;
1780
+ const scheduledValues = form.store.getState().values;
1781
+ const scheduled = getAtPath(scheduledValues, path);
1782
+ debouncer.schedule(() => {
1665
1783
  const state = form.store.getState();
1666
- if (state.values === state.initialValues) return;
1784
+ const rebased = state.values !== scheduledValues && state.values === state.initialValues;
1785
+ if (rebased) return;
1667
1786
  if (!Object.is(getAtPath(state.values, path), scheduled)) return;
1668
1787
  void form.validateFieldAsync(path);
1669
- }, debounceMs);
1788
+ });
1670
1789
  return;
1671
1790
  }
1672
1791
  form.validateField(path);
@@ -1682,7 +1801,7 @@ function useField(form, pathArg, options) {
1682
1801
  state.mode,
1683
1802
  state.reValidateMode,
1684
1803
  state.submitCount > 0,
1685
- state.touched[path] ?? false
1804
+ flagAt(state.touched, path)
1686
1805
  )) {
1687
1806
  triggerValidate();
1688
1807
  }
@@ -1763,7 +1882,11 @@ var reconcileIds = (prev, nextItems) => {
1763
1882
  if (prev.items.length === nextItems.length && prev.items.every((item, i) => Object.is(item, nextItems[i]))) {
1764
1883
  return { ...prev, items: nextItems };
1765
1884
  }
1885
+ const keptAtIndex = nextItems.map(
1886
+ (item, i) => i < prev.items.length && Object.is(item, prev.items[i])
1887
+ );
1766
1888
  const buckets = prev.items.reduce((acc, item, i) => {
1889
+ if (keptAtIndex[i] === true) return acc;
1767
1890
  const bucket = acc.get(item);
1768
1891
  if (bucket === void 0) {
1769
1892
  acc.set(item, [i]);
@@ -1772,7 +1895,8 @@ var reconcileIds = (prev, nextItems) => {
1772
1895
  }
1773
1896
  return acc;
1774
1897
  }, /* @__PURE__ */ new Map());
1775
- const matchedIds = nextItems.map((item) => {
1898
+ const matchedIds = nextItems.map((item, i) => {
1899
+ if (keptAtIndex[i] === true) return prev.ids[i] ?? null;
1776
1900
  const prevIndex = buckets.get(item)?.shift();
1777
1901
  return prevIndex === void 0 ? null : prev.ids[prevIndex] ?? null;
1778
1902
  });
@@ -1866,7 +1990,9 @@ function useFieldArray(form, pathArg) {
1866
1990
  const value = getAtPath(state.values, path);
1867
1991
  return {
1868
1992
  items: Array.isArray(value) ? value : EMPTY_ITEMS,
1869
- error: state.errors[path]
1993
+ // Own-key read, like useField: an array named "constructor" must
1994
+ // not read Object as its error list.
1995
+ error: entryAt(state.errors, path)
1870
1996
  };
1871
1997
  })
1872
1998
  );
@@ -1880,7 +2006,7 @@ function useFieldArray(form, pathArg) {
1880
2006
  state.mode,
1881
2007
  state.reValidateMode,
1882
2008
  state.submitCount > 0,
1883
- state.touched[path] ?? false
2009
+ flagAt(state.touched, path)
1884
2010
  )) {
1885
2011
  form.validateField?.(path);
1886
2012
  }