jsonisch 0.1.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.
@@ -0,0 +1,1123 @@
1
+ //#region src/core/field/walk-field-store.ts
2
+ /**
3
+ * Walks through the field store and all nested children, calling the
4
+ * callback for each field store in depth-first order. The callback may
5
+ * return `true` to stop the walk early, in which case `walkFieldStore`
6
+ * returns `true` as well.
7
+ *
8
+ * The walk reads array `items` reactively, so a reactive caller subscribes
9
+ * to structural changes naturally. Imperative callers that must not
10
+ * subscribe should wrap the call in `untrack`.
11
+ *
12
+ * @param internalFieldStore The field store to walk.
13
+ * @param callback The callback to invoke for each field store. Return `true` to stop the walk early.
14
+ *
15
+ * @returns Whether the walk was stopped early by the callback.
16
+ */
17
+ function walkFieldStore(internalFieldStore, callback) {
18
+ if (callback(internalFieldStore)) return true;
19
+ if (internalFieldStore.kind === "array") {
20
+ for (let index = 0; index < internalFieldStore.items.value.length; index++) if (walkFieldStore(internalFieldStore.children[index], callback)) return true;
21
+ } else if (internalFieldStore.kind === "object") {
22
+ for (const key in internalFieldStore.children) if (walkFieldStore(internalFieldStore.children[key], callback)) return true;
23
+ }
24
+ return false;
25
+ }
26
+
27
+ //#endregion
28
+ //#region src/core/field/get-field-bool.ts
29
+ /**
30
+ * Returns whether the specified boolean property is true for the field
31
+ * store or any of its nested children.
32
+ *
33
+ * @param internalFieldStore The field store to check.
34
+ * @param type The boolean property type to check.
35
+ *
36
+ * @returns Whether the property is true.
37
+ */
38
+ function getFieldBool(internalFieldStore, type) {
39
+ return walkFieldStore(internalFieldStore, (fieldStore) => Boolean(fieldStore[type].value));
40
+ }
41
+
42
+ //#endregion
43
+ //#region src/core/control.ts
44
+ /**
45
+ * Derived from the closed set so the union and the lookup can never drift
46
+ * apart. An unknown name resolves to `undefined` and falls through to the
47
+ * format/type inference — never to a different kind.
48
+ */
49
+ const controlKinds = new Set([
50
+ "text",
51
+ "textarea",
52
+ "number",
53
+ "boolean",
54
+ "date",
55
+ "address",
56
+ "email",
57
+ "email-array",
58
+ "phone",
59
+ "phone-array",
60
+ "link-array",
61
+ "currency",
62
+ "percent",
63
+ "interest-rate",
64
+ "ein",
65
+ "ssn",
66
+ "us-state",
67
+ "legal-id",
68
+ "select",
69
+ "multiselect",
70
+ "formula",
71
+ "estimate",
72
+ "amount-or-percent",
73
+ "line-item",
74
+ "object-array",
75
+ "hidden"
76
+ ]);
77
+ /**
78
+ * Reads the explicit control of a node from the `x-ui.control` namespace —
79
+ * the only explicit channel. Hosts with schemas in another dialect (e.g.
80
+ * the origin app's `x-field-type`) translate before handing schemas over.
81
+ */
82
+ function readExplicitControl(schema) {
83
+ const ui = schema["x-ui"];
84
+ if (!ui || typeof ui !== "object") return void 0;
85
+ const control = ui.control;
86
+ return typeof control === "string" && controlKinds.has(control) ? control : void 0;
87
+ }
88
+ /**
89
+ * Returns whether the node's `type` includes the given JSON-Schema type.
90
+ */
91
+ function hasType(schema, type) {
92
+ return Array.isArray(schema.type) ? schema.type.includes(type) : schema.type === type;
93
+ }
94
+ /**
95
+ * Reads the relation config of a node from the `x-relation` namespace or the
96
+ * flat vendor `x-relation-target`/`x-relation-multiple` keys (the sibling-key
97
+ * form the canonical `$ref: schema://…` shape also uses for its extras).
98
+ */
99
+ function readRelation(schema) {
100
+ const ns = schema["x-relation"];
101
+ if (ns && typeof ns === "object") {
102
+ const obj = ns;
103
+ if (typeof obj.target !== "string") return void 0;
104
+ return { multiple: typeof obj.multiple === "boolean" ? obj.multiple : void 0 };
105
+ }
106
+ if (typeof schema["x-relation-target"] !== "string") return void 0;
107
+ const multiple = schema["x-relation-multiple"];
108
+ return { multiple: typeof multiple === "boolean" ? multiple : void 0 };
109
+ }
110
+ /**
111
+ * Decides which UI widget kind a JSON-Schema node renders as.
112
+ *
113
+ * Precedence:
114
+ * 1. Relations — `$ref`, array-of-`$ref`, or `x-relation`/flat
115
+ * `x-relation-target`. Single → select, many → multiselect.
116
+ * 2. Explicit `x-ui.control`, plus the `estimate: true` attribute
117
+ * promoting a formula to an estimate.
118
+ * 3. `format` — JSON-Schema standard widget hints on strings.
119
+ * 4. `type` — JSON-Schema primitive fallback.
120
+ *
121
+ * @param schema The JSON-Schema node.
122
+ *
123
+ * @returns The control kind.
124
+ */
125
+ function inferControl(schema) {
126
+ if (typeof schema.$ref === "string") return "select";
127
+ const items = Array.isArray(schema.items) ? void 0 : schema.items;
128
+ if (hasType(schema, "array") && typeof items?.$ref === "string") return "multiselect";
129
+ const relation = readRelation(schema);
130
+ if (relation) return relation.multiple ?? hasType(schema, "array") ? "multiselect" : "select";
131
+ const explicit = readExplicitControl(schema);
132
+ if (explicit) {
133
+ if (explicit === "formula" && schema.estimate === true) return "estimate";
134
+ return explicit;
135
+ }
136
+ if (hasType(schema, "string")) {
137
+ switch (schema.format) {
138
+ case "date": return "date";
139
+ case "email": return "email";
140
+ case "phone": return "phone";
141
+ case "ein": return "ein";
142
+ case "us-state": return "us-state";
143
+ case "currency": return "currency";
144
+ case "percent": return "percent";
145
+ }
146
+ return "text";
147
+ }
148
+ if (hasType(schema, "number") || hasType(schema, "integer")) return "number";
149
+ if (hasType(schema, "boolean")) return "boolean";
150
+ if (hasType(schema, "array")) {
151
+ if (schema.uniqueItems === true && Array.isArray(items?.oneOf)) return "multiselect";
152
+ if (items && hasType(items, "object") && typeof items.properties === "object") return "object-array";
153
+ return "text";
154
+ }
155
+ return "text";
156
+ }
157
+
158
+ //#endregion
159
+ //#region src/core/signal.ts
160
+ /**
161
+ * The currently active listener, if any. Reads while it is set subscribe it.
162
+ */
163
+ let currentListener;
164
+ /**
165
+ * Runs a function with the given listener active, restoring the previous
166
+ * listener afterwards (throw included). This is the ONLY way to activate a
167
+ * listener — a set-and-forget global (the v1 `setListener`) let one
168
+ * component's tracking window leak into whatever rendered next; the scoped
169
+ * form makes that structurally impossible.
170
+ *
171
+ * @param listener The listener to activate (or `undefined` to suspend
172
+ * tracking, the `untrack` case).
173
+ * @param fn The function whose signal reads subscribe the listener.
174
+ *
175
+ * @returns The return value of the function.
176
+ */
177
+ function withListener(listener, fn) {
178
+ const previousListener = currentListener;
179
+ currentListener = listener;
180
+ try {
181
+ return fn();
182
+ } finally {
183
+ currentListener = previousListener;
184
+ }
185
+ }
186
+ /**
187
+ * Returns the currently active listener, if any.
188
+ *
189
+ * Advanced/internal: exposed for tests that verify subscription bookkeeping.
190
+ */
191
+ function getListener() {
192
+ return currentListener;
193
+ }
194
+ /**
195
+ * Creates a tracker: the non-React primitive the react adapter's snapshot
196
+ * store is built on. `onInvalidate` fires when any signal read during the
197
+ * last `read` changes (deferred and de-duplicated by an active `batch`).
198
+ *
199
+ * @param onInvalidate Called when a tracked signal changes.
200
+ *
201
+ * @returns The created tracker.
202
+ */
203
+ function createTracker(onInvalidate) {
204
+ const listener = {
205
+ notify: onInvalidate,
206
+ subscriptions: /* @__PURE__ */ new Set()
207
+ };
208
+ return {
209
+ read: (fn) => withListener(listener, fn),
210
+ dispose: () => {
211
+ for (const subscribers of listener.subscriptions) subscribers.delete(listener);
212
+ listener.subscriptions.clear();
213
+ }
214
+ };
215
+ }
216
+ /**
217
+ * Plain-listener notifications collected while a batch is active.
218
+ */
219
+ let batchQueue;
220
+ /**
221
+ * Current batch nesting depth.
222
+ */
223
+ let batchDepth = 0;
224
+ /**
225
+ * Subscribes the active listener (if any) to the given subscriber set.
226
+ */
227
+ function track(subscribers) {
228
+ if (currentListener) {
229
+ subscribers.add(currentListener);
230
+ currentListener.subscriptions.add(subscribers);
231
+ }
232
+ }
233
+ /**
234
+ * Consumes and notifies a subscriber set (one-shot semantics).
235
+ *
236
+ * Invalidation hooks run first and always synchronously, so computed
237
+ * staleness propagates through the whole graph before any plain listener is
238
+ * notified. Plain notifications are deferred into the batch queue when a
239
+ * batch is active.
240
+ */
241
+ function notifySubscribers(subscribers) {
242
+ const snapshot = [...subscribers];
243
+ for (const subscriber of snapshot) subscriber.subscriptions.delete(subscribers);
244
+ subscribers.clear();
245
+ for (const subscriber of snapshot) subscriber.invalidate?.();
246
+ for (const subscriber of snapshot) if (batchQueue) batchQueue.add(subscriber);
247
+ else subscriber.notify();
248
+ }
249
+ function createSignal(initialValue) {
250
+ const subscribers = /* @__PURE__ */ new Set();
251
+ let value = initialValue;
252
+ return {
253
+ get value() {
254
+ track(subscribers);
255
+ return value;
256
+ },
257
+ set value(nextValue) {
258
+ if (!Object.is(nextValue, value)) {
259
+ value = nextValue;
260
+ notifySubscribers(subscribers);
261
+ }
262
+ }
263
+ };
264
+ }
265
+ /**
266
+ * Creates a lazy, cached, read-only signal derived from other signals.
267
+ *
268
+ * The compute function runs on first read and again after any signal it read
269
+ * during its last run changes (dependencies are re-tracked on every run, so
270
+ * conditional reads narrow or widen the dependency set). Subscribers of the
271
+ * computed are notified when it is invalidated; the fresh value is produced
272
+ * on the next read.
273
+ *
274
+ * @param compute The function deriving the value.
275
+ *
276
+ * @returns The created read-only signal.
277
+ */
278
+ function computed(compute) {
279
+ const subscribers = /* @__PURE__ */ new Set();
280
+ let cache;
281
+ let stale = true;
282
+ let computing = false;
283
+ const self = {
284
+ notify: () => {},
285
+ subscriptions: /* @__PURE__ */ new Set(),
286
+ invalidate() {
287
+ if (!stale) {
288
+ stale = true;
289
+ notifySubscribers(subscribers);
290
+ }
291
+ }
292
+ };
293
+ return { get value() {
294
+ if (computing) throw new Error("Cycle detected: a computed signal's compute function reads its own value");
295
+ track(subscribers);
296
+ if (stale) {
297
+ for (const sourceSubscribers of self.subscriptions) sourceSubscribers.delete(self);
298
+ self.subscriptions.clear();
299
+ computing = true;
300
+ try {
301
+ cache = withListener(self, compute);
302
+ } finally {
303
+ computing = false;
304
+ }
305
+ stale = false;
306
+ }
307
+ return cache;
308
+ } };
309
+ }
310
+ /**
311
+ * Batches signal writes: plain listener notifications (e.g. component
312
+ * re-renders) are collected, de-duplicated, and delivered once when the
313
+ * outermost batch ends. Computed invalidation is NOT deferred, so reads
314
+ * inside the batch observe the written values.
315
+ *
316
+ * @param fn The function to execute in the batch.
317
+ *
318
+ * @returns The return value of the function.
319
+ */
320
+ function batch(fn) {
321
+ batchDepth++;
322
+ batchQueue ??= /* @__PURE__ */ new Set();
323
+ try {
324
+ return fn();
325
+ } finally {
326
+ batchDepth--;
327
+ if (batchDepth === 0) {
328
+ const queue = batchQueue;
329
+ batchQueue = void 0;
330
+ for (const listener of queue) listener.notify();
331
+ }
332
+ }
333
+ }
334
+ /**
335
+ * Executes a function without tracking signal reads as subscriptions.
336
+ *
337
+ * @param fn The function to execute untracked.
338
+ *
339
+ * @returns The return value of the function.
340
+ */
341
+ function untrack(fn) {
342
+ return withListener(void 0, fn);
343
+ }
344
+
345
+ //#endregion
346
+ //#region src/core/framework.ts
347
+ /**
348
+ * Counter backing `createId`.
349
+ */
350
+ let idCounter = 0;
351
+ /**
352
+ * Creates a unique ID (stable array item identity for keying and
353
+ * reorder-aware state transfer).
354
+ *
355
+ * @returns The unique ID.
356
+ */
357
+ function createId() {
358
+ return `${idCounter++}`;
359
+ }
360
+
361
+ //#endregion
362
+ //#region src/core/plugin/driver.ts
363
+ /**
364
+ * The members a plugin object may carry. Anything else is a typo'd hook
365
+ * name and throws at `createFormStore` instead of silently never running.
366
+ */
367
+ const KNOWN_MEMBERS = new Set([
368
+ "name",
369
+ "key",
370
+ "dependsOn",
371
+ "wire",
372
+ "build",
373
+ "buildScope",
374
+ "reseedScope",
375
+ "resetField",
376
+ "syncInput",
377
+ "syncInitial",
378
+ "rebase",
379
+ "transferField",
380
+ "swapField",
381
+ "fieldIsDirty",
382
+ "encodeValue",
383
+ "isDirty",
384
+ "fieldSnapshot"
385
+ ]);
386
+ /**
387
+ * The hooks the driver precomputes implementer lists for (the per-field
388
+ * ones run in loops; scanning all plugins per field would be quadratic).
389
+ */
390
+ const HOOK_NAMES = [
391
+ "buildScope",
392
+ "reseedScope",
393
+ "resetField",
394
+ "syncInput",
395
+ "syncInitial",
396
+ "rebase",
397
+ "transferField",
398
+ "swapField",
399
+ "fieldIsDirty",
400
+ "encodeValue",
401
+ "isDirty",
402
+ "fieldSnapshot"
403
+ ];
404
+ /**
405
+ * Wraps a hook dispatch for error attribution: a throwing plugin surfaces
406
+ * as `Error running "build" for jsonisch plugin "derivation": …` instead of
407
+ * an anonymous stack out of `createFormStore`.
408
+ */
409
+ function attributed(plugin, hook, run) {
410
+ try {
411
+ return run();
412
+ } catch (error) {
413
+ throw new Error(`Error running "${hook}" for jsonisch plugin "${plugin.name}": ${error instanceof Error ? error.message : String(error)}`, { cause: error });
414
+ }
415
+ }
416
+ /**
417
+ * Flattens and validates the `plugins` config into the driver: falsy
418
+ * entries and one level of nesting are accepted; names and keys must be
419
+ * unique; `dependsOn` keys must belong to EARLIER plugins (a missing or
420
+ * later dependency is a startup error naming both plugins); every member
421
+ * must be a known hook with the right shape.
422
+ */
423
+ function resolvePlugins(input) {
424
+ const plugins = [];
425
+ for (const entry of (input ?? []).flat()) {
426
+ if (!entry) continue;
427
+ plugins.push(entry);
428
+ }
429
+ const names = /* @__PURE__ */ new Set();
430
+ const keys = /* @__PURE__ */ new Map();
431
+ for (const plugin of plugins) {
432
+ if (typeof plugin !== "object" || typeof plugin.name !== "string" || !plugin.name) throw new Error("A jsonisch plugin must be an object with a name");
433
+ if (names.has(plugin.name)) throw new Error(`Duplicate jsonisch plugin name "${plugin.name}"`);
434
+ names.add(plugin.name);
435
+ if (!plugin.key || typeof plugin.key !== "object") throw new Error(`Jsonisch plugin "${plugin.name}" has no key`);
436
+ if (keys.has(plugin.key)) throw new Error(`Jsonisch plugins "${keys.get(plugin.key)}" and "${plugin.name}" share the same key`);
437
+ if (typeof plugin.build !== "function") throw new Error(`Jsonisch plugin "${plugin.name}" has no build hook`);
438
+ for (const member of Object.keys(plugin)) if (!KNOWN_MEMBERS.has(member)) throw new Error(`Unknown member "${member}" on jsonisch plugin "${plugin.name}" — a typo'd hook never runs, so it throws instead`);
439
+ for (const dependency of plugin.dependsOn ?? []) if (keys.get(dependency) === void 0) throw new Error(`Jsonisch plugin "${plugin.name}" requires plugin "${dependency.name}" earlier in the plugins array`);
440
+ keys.set(plugin.key, plugin.name);
441
+ }
442
+ const hooks = {};
443
+ for (const hook of HOOK_NAMES) hooks[hook] = plugins.filter((plugin) => typeof plugin[hook] === "function");
444
+ const envelopes = /* @__PURE__ */ new Map();
445
+ for (const plugin of plugins) for (const control of plugin.wire?.envelopeControls ?? []) {
446
+ if (envelopes.has(control)) throw new Error(`Two jsonisch plugins declare an envelope for the "${control}" control`);
447
+ envelopes.set(control, plugin.wire);
448
+ }
449
+ return {
450
+ plugins,
451
+ hooks,
452
+ envelopes
453
+ };
454
+ }
455
+ /**
456
+ * The `PluginCtx` for one plugin on one form (state read fresh off the
457
+ * store, so hooks always see the current container).
458
+ */
459
+ function ctxOf(form, plugin) {
460
+ return {
461
+ form,
462
+ state: form.pluginState.get(plugin.key)
463
+ };
464
+ }
465
+ /**
466
+ * Runs every plugin's `build` in array order, storing each state container
467
+ * under its key. Runs BEFORE the walk (see `JsonischPlugin.build`).
468
+ */
469
+ function dispatchBuild(form, config) {
470
+ for (const plugin of form.pluginDriver.plugins) form.pluginState.set(plugin.key, attributed(plugin, "build", () => plugin.build(form, config)));
471
+ }
472
+ /**
473
+ * Wires one object scope (the root after the walk, or an array-item object
474
+ * as the walk creates it), in plugin array order.
475
+ */
476
+ function dispatchBuildScope(form, scope, raw) {
477
+ for (const plugin of form.pluginDriver?.hooks.buildScope ?? []) attributed(plugin, "buildScope", () => plugin.buildScope(ctxOf(form, plugin), scope, raw));
478
+ }
479
+ /**
480
+ * Re-seeds an existing row scope that adopts a different row.
481
+ */
482
+ function dispatchReseedScope(form, scope, raw) {
483
+ for (const plugin of form.pluginDriver?.hooks.reseedScope ?? []) attributed(plugin, "reseedScope", () => plugin.reseedScope(ctxOf(form, plugin), scope, raw));
484
+ }
485
+ /**
486
+ * Rebases one object scope's plugin state on fresh raw data — the root
487
+ * from `applyBaseline`, rows from the baseline-rebase walk. Runs AFTER the
488
+ * scope's value rebase.
489
+ */
490
+ function dispatchRebase(form, scope, raw) {
491
+ for (const plugin of form.pluginDriver?.hooks.rebase ?? []) attributed(plugin, "rebase", () => plugin.rebase(ctxOf(form, plugin), scope, raw));
492
+ }
493
+ /**
494
+ * Restores one value leaf's plugin state to its decode-time baseline, from
495
+ * inside reset's walk.
496
+ */
497
+ function dispatchResetField(form, store) {
498
+ for (const plugin of form.pluginDriver?.hooks.resetField ?? []) attributed(plugin, "resetField", () => plugin.resetField(ctxOf(form, plugin), store));
499
+ }
500
+ /**
501
+ * Asks plugins to take a live leaf write (`setFieldInput`). Returns
502
+ * `true` when a plugin wrote `store.input` so core must not write it
503
+ * again — otherwise the envelope.value and the number would fork.
504
+ */
505
+ function dispatchSyncInput(form, store, input) {
506
+ let handled = false;
507
+ for (const plugin of form.pluginDriver?.hooks.syncInput ?? []) if (attributed(plugin, "syncInput", () => plugin.syncInput(ctxOf(form, plugin), store, input))) handled = true;
508
+ return handled;
509
+ }
510
+ /**
511
+ * Re-decodes each plugin's start baseline from the same raw
512
+ * `reset({ initialInput })` just wrote into `initialInput`.
513
+ */
514
+ function dispatchSyncInitial(form, store, raw) {
515
+ for (const plugin of form.pluginDriver?.hooks.syncInitial ?? []) attributed(plugin, "syncInitial", () => plugin.syncInitial(ctxOf(form, plugin), store, raw));
516
+ }
517
+ /**
518
+ * Transfers per-field plugin state between two value stores (item-state
519
+ * copy during array inserts/removes/moves).
520
+ */
521
+ function dispatchTransferField(form, from, to) {
522
+ for (const plugin of form.pluginDriver?.hooks.transferField ?? []) attributed(plugin, "transferField", () => plugin.transferField(ctxOf(form, plugin), from, to));
523
+ }
524
+ /**
525
+ * Swaps per-field plugin state between two value stores.
526
+ */
527
+ function dispatchSwapField(form, first, second) {
528
+ for (const plugin of form.pluginDriver?.hooks.swapField ?? []) attributed(plugin, "swapField", () => plugin.swapField(ctxOf(form, plugin), first, second));
529
+ }
530
+ /**
531
+ * Whether any plugin's form-level state is dirty. Runs inside the
532
+ * `isDirty` aggregate computed, so it reads EVERY implementer — never
533
+ * short-circuits: an unran handler contributes no signal reads and would
534
+ * deafen the projection.
535
+ */
536
+ function pluginsDirty(form) {
537
+ let dirty = false;
538
+ for (const plugin of form.pluginDriver?.hooks.isDirty ?? []) if (attributed(plugin, "isDirty", () => plugin.isDirty(ctxOf(form, plugin)))) dirty = true;
539
+ return dirty;
540
+ }
541
+ /**
542
+ * Whether THIS value leaf's plugin state is dirty (must serialize). Reads
543
+ * every implementer for the same no-short-circuit reason as `pluginsDirty`
544
+ * — callers may sit inside computeds.
545
+ */
546
+ function fieldPluginDirty(form, store) {
547
+ let dirty = false;
548
+ for (const plugin of form.pluginDriver?.hooks.fieldIsDirty ?? []) if (attributed(plugin, "fieldIsDirty", () => plugin.fieldIsDirty(ctxOf(form, plugin), store))) dirty = true;
549
+ return dirty;
550
+ }
551
+ /**
552
+ * Whether any value leaf in the subtree has dirty plugin state — the
553
+ * plugin half of the dirty walks that decide payload emission (a mode flip
554
+ * with an unchanged value must still produce a payload). Reads array
555
+ * `items` (not raw `children`, which may hold stale stores past the end
556
+ * after a shrink), so a reactive caller subscribes to structural changes.
557
+ */
558
+ function hasPluginDirtyField(form, store) {
559
+ if (store.kind === "value") return fieldPluginDirty(form, store);
560
+ if (store.kind === "array") {
561
+ const length = store.items.value.length;
562
+ let dirty$1 = false;
563
+ for (let index = 0; index < length; index++) {
564
+ const child = store.children[index];
565
+ if (child && hasPluginDirtyField(form, child)) dirty$1 = true;
566
+ }
567
+ return dirty$1;
568
+ }
569
+ let dirty = false;
570
+ for (const key in store.children) if (hasPluginDirtyField(form, store.children[key])) dirty = true;
571
+ return dirty;
572
+ }
573
+ /**
574
+ * Encodes one dirty value leaf's payload entry: asks the `encodeValue`
575
+ * implementers to wrap it (the LOS-573 envelope). Exactly one plugin may
576
+ * claim a field; with no claim the outgoing value is emitted as-is.
577
+ *
578
+ * @param form The form store.
579
+ * @param store The value leaf being encoded.
580
+ * @param valueOut The outgoing value (`undefined` when only plugin state
581
+ * is dirty).
582
+ *
583
+ * @returns The wire entry to emit.
584
+ */
585
+ function encodeFieldValue(form, store, valueOut) {
586
+ let claimed;
587
+ let result = valueOut;
588
+ for (const plugin of form.pluginDriver?.hooks.encodeValue ?? []) {
589
+ const wrapped = attributed(plugin, "encodeValue", () => plugin.encodeValue(ctxOf(form, plugin), store, valueOut));
590
+ if (wrapped === void 0) continue;
591
+ if (claimed !== void 0) throw new Error(`Jsonisch plugins "${claimed}" and "${plugin.name}" both encode the "${store.name}" field`);
592
+ claimed = plugin.name;
593
+ result = wrapped;
594
+ }
595
+ return result;
596
+ }
597
+ /**
598
+ * Merges every plugin's `fieldSnapshot` contribution for one field into a
599
+ * single flat object — the fastify-decorate collision rule: a key already
600
+ * claimed (by another plugin, or by a core field member via `reserved`)
601
+ * throws instead of silently shadowing. Runs inside the react adapter's
602
+ * tracked read, so contributions' signal reads subscribe the component.
603
+ *
604
+ * @param form The form store.
605
+ * @param store The field store being snapshotted.
606
+ * @param path The field's path (for callback contributions).
607
+ * @param reserved The core snapshot member names no plugin may claim.
608
+ *
609
+ * @returns The merged plugin members, flat.
610
+ */
611
+ function dispatchFieldSnapshot(form, store, path, reserved) {
612
+ const slots = {};
613
+ const claimedBy = /* @__PURE__ */ new Map();
614
+ for (const plugin of form.pluginDriver?.hooks.fieldSnapshot ?? []) {
615
+ const contribution = attributed(plugin, "fieldSnapshot", () => plugin.fieldSnapshot(ctxOf(form, plugin), store, path));
616
+ for (const key of Object.keys(contribution)) {
617
+ if (reserved.has(key)) throw new Error(`Jsonisch plugin "${plugin.name}" contributes the fieldSnapshot key "${key}", which is a core field member`);
618
+ const owner = claimedBy.get(key);
619
+ if (owner !== void 0) throw new Error(`Jsonisch plugins "${owner}" and "${plugin.name}" both contribute the fieldSnapshot key "${key}"`);
620
+ claimedBy.set(key, plugin.name);
621
+ slots[key] = contribution[key];
622
+ }
623
+ }
624
+ return slots;
625
+ }
626
+ /**
627
+ * Unwraps a raw persisted leaf entry through the form's envelope wire
628
+ * contracts: an envelope-control leaf resolves its value half, everything
629
+ * else passes through. The single decode seam shared by the walk, reset,
630
+ * and the baseline rebase — a leaf can never round-trip to two different
631
+ * values.
632
+ */
633
+ function unwrapLeafInput(form, control, raw) {
634
+ const wire = form.pluginDriver?.envelopes.get(control);
635
+ if (!wire?.unwrap || raw === void 0) return raw;
636
+ return wire.unwrap(raw).value;
637
+ }
638
+
639
+ //#endregion
640
+ //#region src/core/schema-utils.ts
641
+ /**
642
+ * Keys that must never become field names or object keys in any store or
643
+ * output — assigning them on a plain object mutates its prototype chain
644
+ * instead of creating an own property (OWASP CWE-915). Applies to schema
645
+ * property names AND record keys, so neither a malicious schema nor a
646
+ * malicious record can pollute.
647
+ */
648
+ const UNSAFE_KEYS = new Set([
649
+ "__proto__",
650
+ "constructor",
651
+ "prototype"
652
+ ]);
653
+ /**
654
+ * Returns whether a key is safe to use as an object property name.
655
+ */
656
+ function isSafeKey(key) {
657
+ return !UNSAFE_KEYS.has(key);
658
+ }
659
+ /**
660
+ * Reads an own property of a record, or `undefined`. A declared key like
661
+ * `"toString"` must resolve to a record value or nothing, never to an
662
+ * inherited prototype member.
663
+ */
664
+ function readOwn(source, key) {
665
+ return source != null && typeof source === "object" && Object.prototype.hasOwnProperty.call(source, key) ? source[key] : void 0;
666
+ }
667
+ /**
668
+ * Returns the node's JSON-Schema types as a list (`type` may be a union
669
+ * like `["string", "null"]`).
670
+ */
671
+ function typeList(schema) {
672
+ if (schema.type === void 0) return [];
673
+ return Array.isArray(schema.type) ? schema.type : [schema.type];
674
+ }
675
+ /**
676
+ * Returns the node's primary (non-`"null"`) JSON-Schema type, if any.
677
+ */
678
+ function primaryType(schema) {
679
+ return typeList(schema).find((type) => type !== "null");
680
+ }
681
+ /**
682
+ * Resolves the presence sentinel a container (array/object) input maps to:
683
+ * the nullish value itself when the field accepts it, `true` otherwise.
684
+ * The single rule shared by the walk, reset, and setInput so a container
685
+ * value can never round-trip to two different states.
686
+ */
687
+ function containerPresence(nullish, input) {
688
+ return nullish && input == null ? input : true;
689
+ }
690
+ /**
691
+ * Resolves a value-leaf input, falling back to the configured empty input
692
+ * for the field's type when no input is provided (e.g. `""` for a required
693
+ * string, so an untouched empty field matches the DOM). Nullish fields keep
694
+ * `undefined`/`null` as they accept it. The single rule shared by the walk
695
+ * and reset so a reset field can never disagree with a freshly mounted one.
696
+ */
697
+ function resolveValueInput(emptyInput, schema, nullish, input) {
698
+ const primary = primaryType(schema);
699
+ return input === void 0 && !nullish && primary !== void 0 ? emptyInput[primary] : input;
700
+ }
701
+
702
+ //#endregion
703
+ //#region src/core/field/initialize-field-store.ts
704
+ /**
705
+ * Initializes a field store recursively based on the JSON-Schema structure:
706
+ * `properties` → object store, single-schema `items` → array store,
707
+ * everything else → value store. The schema is the allow-list — only
708
+ * declared property keys create nodes, so undeclared `initialInput` keys
709
+ * never enter form state.
710
+ *
711
+ * @param internalFormStore The form store providing the empty input config.
712
+ * @param internalFieldStore The partial field store to initialize.
713
+ * @param schema The JSON-Schema node defining the field structure.
714
+ * @param initialInput The initial input value.
715
+ * @param path The path to the field in the form.
716
+ * @param optional Whether the parent declares this field optional (not in
717
+ * its `required` list).
718
+ */
719
+ function initializeFieldStore(internalFormStore, internalFieldStore, schema, initialInput, path, optional = false) {
720
+ const types = typeList(schema);
721
+ const nullish = optional || types.includes("null");
722
+ const primary = primaryType(schema);
723
+ internalFieldStore.schema = schema;
724
+ internalFieldStore.name = String(path[path.length - 1] ?? "");
725
+ internalFieldStore.path = path;
726
+ internalFieldStore.control = inferControl(schema);
727
+ internalFieldStore.isNullish = nullish;
728
+ const initialElements = [];
729
+ internalFieldStore.initialElements = initialElements;
730
+ internalFieldStore.elements = initialElements;
731
+ const validationErrors = createSignal(null);
732
+ internalFieldStore.validationErrors = validationErrors;
733
+ internalFieldStore.errors = validationErrors;
734
+ internalFieldStore.isTouched = createSignal(false);
735
+ internalFieldStore.isEdited = createSignal(false);
736
+ internalFieldStore.isDirty = createSignal(false);
737
+ if (schema.properties && typeof schema.properties === "object") {
738
+ const objectStore = internalFieldStore;
739
+ objectStore.kind = "object";
740
+ objectStore.children ??= Object.create(null);
741
+ const required = new Set(Array.isArray(schema.required) ? schema.required : []);
742
+ for (const key of Object.keys(schema.properties)) {
743
+ if (!isSafeKey(key)) continue;
744
+ objectStore.children[key] ??= {};
745
+ initializeFieldStore(internalFormStore, objectStore.children[key], schema.properties[key], readOwn(initialInput, key), [...path, key], !required.has(key));
746
+ }
747
+ const objectInput = containerPresence(nullish, initialInput);
748
+ objectStore.initialInput = createSignal(objectInput);
749
+ objectStore.startInput = createSignal(objectInput);
750
+ objectStore.input = createSignal(objectInput);
751
+ if (typeof path[path.length - 1] === "number") dispatchBuildScope(internalFormStore, objectStore, initialInput);
752
+ return;
753
+ }
754
+ if ((primary === "array" || primary === void 0) && schema.items !== void 0 && !isOpaqueObject(schema.items)) {
755
+ if (Array.isArray(schema.items)) throw new Error(`Tuple "items" schemas are not supported (at ${JSON.stringify(path)})`);
756
+ const arrayStore = internalFieldStore;
757
+ arrayStore.kind = "array";
758
+ arrayStore.itemSchema = schema.items;
759
+ arrayStore.children ??= [];
760
+ if (Array.isArray(initialInput)) for (let index = 0; index < initialInput.length; index++) {
761
+ arrayStore.children[index] = {};
762
+ initializeFieldStore(internalFormStore, arrayStore.children[index], schema.items, initialInput[index], [...path, index]);
763
+ }
764
+ const arrayInput = containerPresence(nullish, initialInput);
765
+ arrayStore.initialInput = createSignal(arrayInput);
766
+ arrayStore.startInput = createSignal(arrayInput);
767
+ arrayStore.input = createSignal(arrayInput);
768
+ const initialItems = Array.isArray(initialInput) ? initialInput.map(() => createId()) : [];
769
+ arrayStore.initialItems = createSignal(initialItems);
770
+ arrayStore.startItems = createSignal(initialItems);
771
+ arrayStore.items = createSignal(initialItems);
772
+ return;
773
+ }
774
+ if (primary !== void 0 || types.includes("null") || typeof schema.$ref === "string" || schema.enum !== void 0 || schema.oneOf !== void 0 || schema.const !== void 0) {
775
+ const valueStore = internalFieldStore;
776
+ valueStore.kind = "value";
777
+ const valueInput = resolveValueInput(internalFormStore.emptyInput, schema, nullish, unwrapLeafInput(internalFormStore, internalFieldStore.control, initialInput));
778
+ valueStore.initialInput = createSignal(valueInput);
779
+ valueStore.startInput = createSignal(valueInput);
780
+ valueStore.input = createSignal(valueInput);
781
+ return;
782
+ }
783
+ throw new Error(`Unsupported schema without "type", "properties", "items", "$ref", "enum", "oneOf" or "const" (at ${JSON.stringify(path)})`);
784
+ }
785
+ /**
786
+ * Returns whether a single item schema is an opaque object shape: object-
787
+ * typed with no declared `properties` — no allow-list to walk into, so the
788
+ * parent array stays a value leaf holding the whole array.
789
+ */
790
+ function isOpaqueObject(items) {
791
+ if (Array.isArray(items)) return false;
792
+ if (!typeList(items).includes("object")) return false;
793
+ if (items.properties === void 0 || typeof items.properties !== "object") return true;
794
+ return Object.keys(items.properties).length === 0;
795
+ }
796
+
797
+ //#endregion
798
+ //#region src/core/form/create-form-store.ts
799
+ /**
800
+ * The default empty input of a form. Required string fields start as an
801
+ * empty string, while every other type starts as `undefined`.
802
+ */
803
+ const DEFAULT_EMPTY_INPUT = { string: "" };
804
+ /**
805
+ * The default root-record alias (`FormConfig.rootRecordAlias`). The
806
+ * store's resolved value is the one home at runtime; this default
807
+ * exists so store-less readers (`computeBag`) resolve identically.
808
+ */
809
+ const DEFAULT_ROOT_RECORD_ALIAS = "record";
810
+ /**
811
+ * Creates a new internal form store from the provided configuration: walks
812
+ * the JSON-Schema once and builds the field-store tree (`kind:
813
+ * array|object|value`), with the schema as the allow-list — `initialInput`
814
+ * keys not declared in the schema never enter form state.
815
+ *
816
+ * Plugins are resolved and their state containers created BEFORE the walk,
817
+ * so the walk can dispatch `buildScope` for every array-item object it
818
+ * creates (a row wired by the walk behaves exactly like one built later by
819
+ * an insert). The ROOT scope is dispatched after the walk, in plugin array
820
+ * order — for the standard trio that means envelopes (the estimate pin's
821
+ * mode signal) before derivation before visibility.
822
+ *
823
+ * @param config The form configuration.
824
+ *
825
+ * @returns The internal form store.
826
+ */
827
+ function createFormStore(config) {
828
+ if (!config.schema.properties || typeof config.schema.properties !== "object") throw new Error("The form schema must be an \"object\" schema with \"properties\"");
829
+ const store = {};
830
+ store.emptyInput = {
831
+ ...DEFAULT_EMPTY_INPUT,
832
+ ...config.emptyInput
833
+ };
834
+ store.rootRecordAlias = config.rootRecordAlias ?? DEFAULT_ROOT_RECORD_ALIAS;
835
+ store.validator = config.validator;
836
+ store.validate = config.validate ?? "submit";
837
+ store.revalidate = config.revalidate ?? "input";
838
+ store.validators = 0;
839
+ store.pluginDriver = resolvePlugins(config.plugins);
840
+ store.pluginState = /* @__PURE__ */ new Map();
841
+ store.isSubmitting = createSignal(false);
842
+ store.isSubmitted = createSignal(false);
843
+ store.isValidating = createSignal(false);
844
+ store.offFormValues = createSignal(config.offFormValues ?? {});
845
+ const form = store;
846
+ dispatchBuild(form, config);
847
+ initializeFieldStore(form, store, config.schema, config.initialInput, []);
848
+ dispatchBuildScope(form, form, config.initialInput);
849
+ store.aggregates = {
850
+ isTouched: computed(() => getFieldBool(form, "isTouched")),
851
+ isEdited: computed(() => getFieldBool(form, "isEdited")),
852
+ isDirty: computed(() => {
853
+ const fieldsDirty = getFieldBool(form, "isDirty");
854
+ const pluginDirty = pluginsDirty(form);
855
+ return fieldsDirty || pluginDirty;
856
+ }),
857
+ isValid: computed(() => !getFieldBool(form, "validationErrors"))
858
+ };
859
+ return form;
860
+ }
861
+
862
+ //#endregion
863
+ //#region src/core/dirty.ts
864
+ /**
865
+ * Returns whether a value is semantically empty: `undefined`, `null`, the
866
+ * empty string, or `NaN` (what a cleared number input parses to). These are
867
+ * all "no value entered" and must never make a field dirty against each
868
+ * other — the core semantic-dirty promise.
869
+ */
870
+ function isEmptyish(value) {
871
+ return value === void 0 || value === null || value === "" || typeof value === "number" && Number.isNaN(value);
872
+ }
873
+ /**
874
+ * Returns whether a value is a plain object (prototype `Object.prototype`
875
+ * or `null`, e.g. from `JSON.parse`). Class instances, Dates, Maps etc. are
876
+ * NOT plain — comparing them by enumerable keys would call any two Dates
877
+ * equal.
878
+ */
879
+ function isPlainObject(value) {
880
+ if (value === null || typeof value !== "object" || Array.isArray(value)) return false;
881
+ const proto = Object.getPrototypeOf(value);
882
+ return proto === null || proto === Object.prototype;
883
+ }
884
+ /**
885
+ * Deep structural equality for leaf values. Value leaves can hold arrays and
886
+ * plain objects (e.g. multiselect relation arrays, `items`-less array
887
+ * fields), which are recreated on every edit, so identity comparison is not
888
+ * enough. Dates compare by time; other non-plain objects only by identity.
889
+ */
890
+ function isDeepEqual(a, b) {
891
+ if (Object.is(a, b)) return true;
892
+ if (a instanceof Date && b instanceof Date) return a.getTime() === b.getTime();
893
+ if (Array.isArray(a) && Array.isArray(b)) return a.length === b.length && a.every((item, index) => isDeepEqual(item, b[index]));
894
+ if (isPlainObject(a) && isPlainObject(b)) {
895
+ const aKeys = Object.keys(a);
896
+ const bKeys = Object.keys(b);
897
+ return aKeys.length === bKeys.length && aKeys.every((key) => Object.prototype.hasOwnProperty.call(b, key) && isDeepEqual(a[key], b[key]));
898
+ }
899
+ return false;
900
+ }
901
+ /**
902
+ * Semantic, empty-aware equality between two leaf inputs: equal when both
903
+ * are semantically empty (`null` ≡ `undefined` ≡ `""` ≡ `NaN`) or deeply
904
+ * structurally equal.
905
+ */
906
+ function isSemanticEqual(a, b) {
907
+ if (isEmptyish(a) && isEmptyish(b)) return true;
908
+ return isDeepEqual(a, b);
909
+ }
910
+ /**
911
+ * Semantic equality between two container presence sentinels: `true` only
912
+ * equals `true`; `null` and `undefined` (absent container) are equivalent.
913
+ */
914
+ function isPresenceEqual(a, b) {
915
+ return Object.is(a, b) || a == null && b == null;
916
+ }
917
+
918
+ //#endregion
919
+ //#region src/core/field/get-field-store.ts
920
+ /**
921
+ * Returns the chain of field stores along the specified path, from the form
922
+ * root (inclusive) to the target field (inclusive). Throws on a path
923
+ * segment that does not resolve to a declared field — the schema is the
924
+ * allow-list, so navigating to an undeclared key is a caller bug, never a
925
+ * silent no-op. Array segments are bounded by the CURRENT item count:
926
+ * stale child stores kept past the end of a shrunk array (for baseline
927
+ * reuse on regrow) are not addressable.
928
+ *
929
+ * @param internalFormStore The form store to traverse.
930
+ * @param path The path to the field store.
931
+ *
932
+ * @returns The store chain, `[root, …, target]`.
933
+ */
934
+ function getFieldStoreChain(internalFormStore, path) {
935
+ let internalFieldStore = internalFormStore;
936
+ const chain = [internalFieldStore];
937
+ for (const key of path) {
938
+ let child;
939
+ if (internalFieldStore.kind === "object" && typeof key === "string") child = Object.prototype.hasOwnProperty.call(internalFieldStore.children, key) ? internalFieldStore.children[key] : void 0;
940
+ else if (internalFieldStore.kind === "array" && typeof key === "number" && Number.isInteger(key) && key >= 0 && key < internalFieldStore.items.value.length) child = internalFieldStore.children[key];
941
+ if (!child) throw new Error(`No field at path ${JSON.stringify(path)} — segment ${JSON.stringify(key)} is not declared in the schema`);
942
+ internalFieldStore = child;
943
+ chain.push(child);
944
+ }
945
+ return chain;
946
+ }
947
+ /**
948
+ * Returns the field store at the specified path. See `getFieldStoreChain`
949
+ * for the resolution and throw semantics.
950
+ *
951
+ * @param internalFormStore The form store to traverse.
952
+ * @param path The path to the field store.
953
+ *
954
+ * @returns The field store.
955
+ */
956
+ function getFieldStore(internalFormStore, path) {
957
+ const chain = getFieldStoreChain(internalFormStore, path);
958
+ return chain[chain.length - 1];
959
+ }
960
+
961
+ //#endregion
962
+ //#region src/core/field/focus-field-element.ts
963
+ /**
964
+ * Focuses the first focusable element of a field store. The elements are
965
+ * tried in order and the first one that actually receives focus wins, so
966
+ * detached, disabled or hidden elements are skipped. The browser decides
967
+ * focusability, which is read back via the element's root `activeElement`
968
+ * so elements in a shadow root or another document are handled correctly.
969
+ *
970
+ * Hint: a `display: none` or `hidden` element is correctly skipped in real
971
+ * browsers, but jsdom has no layout and focuses it anyway, so that case
972
+ * cannot be covered by unit tests.
973
+ *
974
+ * @param internalFieldStore The field store to focus.
975
+ *
976
+ * @returns Whether an element was focused.
977
+ */
978
+ function focusFieldElement(internalFieldStore) {
979
+ for (const element of internalFieldStore.elements) {
980
+ element.focus();
981
+ if (element.getRootNode().activeElement === element) return true;
982
+ }
983
+ return false;
984
+ }
985
+
986
+ //#endregion
987
+ //#region src/core/field/get-field-input.ts
988
+ /**
989
+ * Returns the current input of the field store. For arrays and objects,
990
+ * recursively collects input from all children (the tree is the allow-list:
991
+ * only declared fields can ever appear in the result). Returns `null` or
992
+ * `undefined` for nullish container inputs, or the leaf value for value
993
+ * fields.
994
+ *
995
+ * @param internalFieldStore The field store to get input from.
996
+ *
997
+ * @returns The field input.
998
+ */
999
+ function getFieldInput(internalFieldStore) {
1000
+ if (internalFieldStore.kind === "array") {
1001
+ if (internalFieldStore.input.value) {
1002
+ const value = [];
1003
+ for (let index = 0; index < internalFieldStore.items.value.length; index++) value[index] = getFieldInput(internalFieldStore.children[index]);
1004
+ return value;
1005
+ }
1006
+ return internalFieldStore.input.value;
1007
+ }
1008
+ if (internalFieldStore.kind === "object") {
1009
+ if (internalFieldStore.input.value) {
1010
+ const value = {};
1011
+ for (const key in internalFieldStore.children) value[key] = getFieldInput(internalFieldStore.children[key]);
1012
+ return value;
1013
+ }
1014
+ return internalFieldStore.input.value;
1015
+ }
1016
+ return internalFieldStore.input.value;
1017
+ }
1018
+
1019
+ //#endregion
1020
+ //#region src/core/form/validate-form-input.ts
1021
+ /**
1022
+ * The message used for an issue the validator produced without one.
1023
+ */
1024
+ const FALLBACK_MESSAGE = "Invalid value";
1025
+ /**
1026
+ * Decodes one JSON-Pointer segment (`~1` → `/`, `~0` → `~`).
1027
+ */
1028
+ function decodePointerSegment(segment) {
1029
+ return segment.replace(/~1/g, "/").replace(/~0/g, "~");
1030
+ }
1031
+ /**
1032
+ * Resolves the field store an issue routes to: walk the pointer segments
1033
+ * from the root as far as the tree can follow them and return the deepest
1034
+ * reached store. An unroutable remainder (undeclared key, index past the
1035
+ * current item count, a pointer into a value leaf's interior) lands the
1036
+ * issue on the nearest addressable ancestor — errors must surface even for
1037
+ * fields that are not currently rendered or declared exactly as pointed.
1038
+ */
1039
+ function resolveIssueTarget(internalFormStore, issue) {
1040
+ const pointer = issue.instancePath ?? "";
1041
+ const segments = pointer === "" ? [] : pointer.slice(1).split("/");
1042
+ if (issue.keyword === "required" && typeof issue.params?.missingProperty === "string") segments.push(issue.params.missingProperty);
1043
+ let store = internalFormStore;
1044
+ for (const rawSegment of segments) {
1045
+ const segment = decodePointerSegment(rawSegment);
1046
+ let child;
1047
+ if (store.kind === "object") child = Object.prototype.hasOwnProperty.call(store.children, segment) ? store.children[segment] : void 0;
1048
+ else if (store.kind === "array" && /^\d+$/.test(segment)) {
1049
+ const index = Number(segment);
1050
+ if (index < store.items.value.length) child = store.children[index];
1051
+ }
1052
+ if (!child) break;
1053
+ store = child;
1054
+ }
1055
+ return store;
1056
+ }
1057
+ /**
1058
+ * Validates the form input using the injected validator. Runs the validator
1059
+ * against the current tree input, routes each issue's `instancePath` to its
1060
+ * field store's `errors` signal (accumulating multiple issues per field,
1061
+ * clearing every other field), and optionally focuses the first field with
1062
+ * an error. A form without a validator always validates successfully.
1063
+ *
1064
+ * @param internalFormStore The form store to validate.
1065
+ * @param config The validation configuration.
1066
+ *
1067
+ * @returns The validation result.
1068
+ */
1069
+ function validateFormInput(internalFormStore, config) {
1070
+ internalFormStore.validators++;
1071
+ internalFormStore.isValidating.value = true;
1072
+ try {
1073
+ const output = untrack(() => getFieldInput(internalFormStore));
1074
+ const issues = internalFormStore.validator?.(output);
1075
+ let fieldErrors;
1076
+ if (issues && issues.length > 0) {
1077
+ fieldErrors = /* @__PURE__ */ new Map();
1078
+ untrack(() => {
1079
+ for (const issue of issues) {
1080
+ const target = resolveIssueTarget(internalFormStore, issue);
1081
+ const message = issue.message || FALLBACK_MESSAGE;
1082
+ const existing = fieldErrors.get(target);
1083
+ if (existing) existing.push(message);
1084
+ else fieldErrors.set(target, [message]);
1085
+ }
1086
+ });
1087
+ }
1088
+ let shouldFocus = config?.shouldFocus ?? false;
1089
+ batch(() => {
1090
+ untrack(() => {
1091
+ walkFieldStore(internalFormStore, (internalFieldStore) => {
1092
+ const errors = fieldErrors?.get(internalFieldStore) ?? null;
1093
+ internalFieldStore.validationErrors.value = errors;
1094
+ if (shouldFocus && errors && internalFieldStore.path.length > 0 && focusFieldElement(internalFieldStore)) shouldFocus = false;
1095
+ });
1096
+ });
1097
+ internalFormStore.validators--;
1098
+ internalFormStore.isValidating.value = internalFormStore.validators > 0;
1099
+ });
1100
+ return {
1101
+ success: !fieldErrors,
1102
+ output
1103
+ };
1104
+ } catch (error) {
1105
+ batch(() => {
1106
+ internalFormStore.validators--;
1107
+ internalFormStore.isValidating.value = internalFormStore.validators > 0;
1108
+ });
1109
+ throw error;
1110
+ }
1111
+ }
1112
+
1113
+ //#endregion
1114
+ //#region src/methods/form-ref.ts
1115
+ /**
1116
+ * Unwraps a `FormRef` to the internal form store.
1117
+ */
1118
+ function internalOf(form) {
1119
+ return "internal" in form ? form.internal : form;
1120
+ }
1121
+
1122
+ //#endregion
1123
+ export { unwrapLeafInput as A, getFieldBool as B, dispatchSyncInitial as C, fieldPluginDirty as D, encodeFieldValue as E, createTracker as F, getListener as I, untrack as L, batch as M, computed as N, hasPluginDirtyField as O, createSignal as P, withListener as R, dispatchSwapField as S, dispatchTransferField as T, walkFieldStore as V, resolveValueInput as _, getFieldStore as a, dispatchReseedScope as b, isPresenceEqual as c, DEFAULT_ROOT_RECORD_ALIAS as d, createFormStore as f, readOwn as g, isSafeKey as h, focusFieldElement as i, createId as j, pluginsDirty as k, isSemanticEqual as l, containerPresence as m, validateFormInput as n, getFieldStoreChain as o, initializeFieldStore as p, getFieldInput as r, isEmptyish as s, internalOf as t, DEFAULT_EMPTY_INPUT as u, dispatchFieldSnapshot as v, dispatchSyncInput as w, dispatchResetField as x, dispatchRebase as y, inferControl as z };