@thalesfp/snapstate 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,1177 @@
1
+ "use strict";
2
+ var __defProp = Object.defineProperty;
3
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
4
+ var __getOwnPropNames = Object.getOwnPropertyNames;
5
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
6
+ var __export = (target, all) => {
7
+ for (var name in all)
8
+ __defProp(target, name, { get: all[name], enumerable: true });
9
+ };
10
+ var __copyProps = (to, from, except, desc) => {
11
+ if (from && typeof from === "object" || typeof from === "function") {
12
+ for (let key of __getOwnPropNames(from))
13
+ if (!__hasOwnProp.call(to, key) && key !== except)
14
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
15
+ }
16
+ return to;
17
+ };
18
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
19
+
20
+ // src/form/index.ts
21
+ var form_exports = {};
22
+ __export(form_exports, {
23
+ SnapFormStore: () => SnapFormStore,
24
+ asyncStatus: () => asyncStatus,
25
+ getBaseSchemaType: () => getBaseSchemaType,
26
+ getObjectSchema: () => getObjectSchema,
27
+ setDefaultHeaders: () => setDefaultHeaders,
28
+ setHttpClient: () => setHttpClient
29
+ });
30
+ module.exports = __toCommonJS(form_exports);
31
+
32
+ // src/form/form.ts
33
+ var import_zod = require("zod");
34
+
35
+ // src/react/store.ts
36
+ var import_react = require("react");
37
+
38
+ // src/core/types.ts
39
+ var _statuses = {
40
+ idle: Object.freeze({ value: "idle", isIdle: true, isLoading: false, isReady: false, isError: false }),
41
+ loading: Object.freeze({ value: "loading", isIdle: false, isLoading: true, isReady: false, isError: false }),
42
+ ready: Object.freeze({ value: "ready", isIdle: false, isLoading: false, isReady: true, isError: false }),
43
+ error: Object.freeze({ value: "error", isIdle: false, isLoading: false, isReady: false, isError: true })
44
+ };
45
+ function asyncStatus(value) {
46
+ return _statuses[value];
47
+ }
48
+
49
+ // src/core/trie.ts
50
+ function invokeAll(listeners) {
51
+ let firstError;
52
+ for (const l of listeners) {
53
+ try {
54
+ l();
55
+ } catch (e) {
56
+ firstError ??= e;
57
+ }
58
+ }
59
+ if (firstError !== void 0) {
60
+ throw firstError;
61
+ }
62
+ }
63
+ function createNode() {
64
+ return { listeners: /* @__PURE__ */ new Set(), children: /* @__PURE__ */ new Map() };
65
+ }
66
+ function parsePath(path) {
67
+ if (path === "") {
68
+ return [];
69
+ }
70
+ return path.split(".");
71
+ }
72
+ var SubscriptionTrie = class {
73
+ root = createNode();
74
+ globalListeners = /* @__PURE__ */ new Set();
75
+ /** Subscribe to a specific path. Returns unsubscribe function. */
76
+ add(path, listener) {
77
+ const segments = parsePath(path);
78
+ const parents = [];
79
+ let node = this.root;
80
+ for (const seg of segments) {
81
+ if (!node.children.has(seg)) {
82
+ node.children.set(seg, createNode());
83
+ }
84
+ parents.push({ parent: node, segment: seg });
85
+ node = node.children.get(seg);
86
+ }
87
+ node.listeners.add(listener);
88
+ return () => {
89
+ node.listeners.delete(listener);
90
+ for (let i = parents.length - 1; i >= 0; i--) {
91
+ const { parent, segment } = parents[i];
92
+ const child = parent.children.get(segment);
93
+ if (child.listeners.size === 0 && child.children.size === 0) {
94
+ parent.children.delete(segment);
95
+ } else {
96
+ break;
97
+ }
98
+ }
99
+ };
100
+ }
101
+ /** Subscribe to all changes (no path filter). */
102
+ addGlobal(listener) {
103
+ this.globalListeners.add(listener);
104
+ return () => {
105
+ this.globalListeners.delete(listener);
106
+ };
107
+ }
108
+ /** Notify listeners for exact path, all ancestors, and all descendants. */
109
+ notify(path) {
110
+ const segments = parsePath(path);
111
+ const collected = /* @__PURE__ */ new Set();
112
+ for (const l of this.globalListeners) collected.add(l);
113
+ let node = this.root;
114
+ for (const l of node.listeners) collected.add(l);
115
+ let matched = true;
116
+ for (const seg of segments) {
117
+ const wildcard = node.children.get("*");
118
+ if (wildcard) {
119
+ for (const l of wildcard.listeners) collected.add(l);
120
+ this.collectDescendants(wildcard, collected);
121
+ }
122
+ const child = node.children.get(seg);
123
+ if (!child) {
124
+ matched = false;
125
+ break;
126
+ }
127
+ node = child;
128
+ for (const l of node.listeners) collected.add(l);
129
+ }
130
+ if (matched) {
131
+ this.collectDescendants(node, collected);
132
+ }
133
+ invokeAll(collected);
134
+ }
135
+ /** Notify all listeners in the trie. */
136
+ notifyAll() {
137
+ const collected = /* @__PURE__ */ new Set();
138
+ for (const l of this.globalListeners) collected.add(l);
139
+ this.collectDescendants(this.root, collected);
140
+ for (const l of this.root.listeners) collected.add(l);
141
+ invokeAll(collected);
142
+ }
143
+ collectDescendants(node, out) {
144
+ for (const child of node.children.values()) {
145
+ for (const l of child.listeners) out.add(l);
146
+ this.collectDescendants(child, out);
147
+ }
148
+ }
149
+ clear() {
150
+ this.root = createNode();
151
+ this.globalListeners.clear();
152
+ }
153
+ };
154
+
155
+ // src/core/structural.ts
156
+ function applyUpdate(state, path, value) {
157
+ const segments = path.split(".");
158
+ return updateAtPath(state, segments, 0, value);
159
+ }
160
+ function updateAtPath(current, segments, index, value) {
161
+ if (index === segments.length) {
162
+ if (typeof value === "function") {
163
+ return value(current);
164
+ }
165
+ return value;
166
+ }
167
+ const key = segments[index];
168
+ if (Array.isArray(current)) {
169
+ const i = Number(key);
170
+ const next2 = updateAtPath(current[i], segments, index + 1, value);
171
+ if (Object.is(next2, current[i])) {
172
+ return current;
173
+ }
174
+ const copy = current.slice();
175
+ copy[i] = next2;
176
+ return copy;
177
+ }
178
+ if (current !== null && typeof current === "object") {
179
+ const obj = current;
180
+ const next2 = updateAtPath(obj[key], segments, index + 1, value);
181
+ if (Object.is(next2, obj[key])) {
182
+ return current;
183
+ }
184
+ return { ...obj, [key]: next2 };
185
+ }
186
+ const next = updateAtPath(void 0, segments, index + 1, value);
187
+ return { [key]: next };
188
+ }
189
+ function getAtPath(state, path) {
190
+ if (path === "") {
191
+ return state;
192
+ }
193
+ const segments = path.split(".");
194
+ let current = state;
195
+ for (const seg of segments) {
196
+ if (current === null || current === void 0) {
197
+ return void 0;
198
+ }
199
+ current = current[seg];
200
+ }
201
+ return current;
202
+ }
203
+
204
+ // src/core/computed.ts
205
+ function createComputed(host, deps, fn) {
206
+ let cachedValue;
207
+ let dirty = true;
208
+ const unsubs = [];
209
+ const markDirty = () => {
210
+ dirty = true;
211
+ };
212
+ for (const dep of deps) {
213
+ unsubs.push(host.subscribe(dep, markDirty));
214
+ }
215
+ try {
216
+ cachedValue = fn(host.getSnapshot());
217
+ } catch (e) {
218
+ for (const unsub of unsubs) unsub();
219
+ unsubs.length = 0;
220
+ throw e;
221
+ }
222
+ dirty = false;
223
+ return {
224
+ get() {
225
+ if (dirty) {
226
+ cachedValue = fn(host.getSnapshot());
227
+ dirty = false;
228
+ }
229
+ return cachedValue;
230
+ },
231
+ destroy() {
232
+ for (const unsub of unsubs) unsub();
233
+ unsubs.length = 0;
234
+ }
235
+ };
236
+ }
237
+
238
+ // src/core/store.ts
239
+ function createStore(initialState, options = {}) {
240
+ const { autoBatch = true } = options;
241
+ let state = initialState;
242
+ const trie = new SubscriptionTrie();
243
+ let batchDepth = 0;
244
+ let pendingPaths = /* @__PURE__ */ new Set();
245
+ let microtaskScheduled = false;
246
+ function flushNotifications() {
247
+ const paths = pendingPaths;
248
+ pendingPaths = /* @__PURE__ */ new Set();
249
+ microtaskScheduled = false;
250
+ if (paths.size === 0) {
251
+ return;
252
+ }
253
+ const sorted = [...paths].sort();
254
+ const deduped = [];
255
+ for (const p of sorted) {
256
+ const last = deduped[deduped.length - 1];
257
+ if (last !== void 0 && p.startsWith(last + ".")) {
258
+ continue;
259
+ }
260
+ deduped.push(p);
261
+ }
262
+ for (const path of deduped) {
263
+ trie.notify(path);
264
+ }
265
+ }
266
+ function scheduleFlush() {
267
+ if (batchDepth > 0) {
268
+ return;
269
+ }
270
+ if (autoBatch && !microtaskScheduled) {
271
+ microtaskScheduled = true;
272
+ queueMicrotask(flushNotifications);
273
+ } else if (!autoBatch) {
274
+ flushNotifications();
275
+ }
276
+ }
277
+ function get(path) {
278
+ if (path === void 0 || path === "") {
279
+ return state;
280
+ }
281
+ return getAtPath(state, path);
282
+ }
283
+ function set(path, value) {
284
+ if (path === "") {
285
+ throw new Error("Cannot set with an empty path. Use a specific path to update state.");
286
+ }
287
+ const prev = state;
288
+ state = applyUpdate(state, path, value);
289
+ if (state !== prev) {
290
+ pendingPaths.add(path);
291
+ scheduleFlush();
292
+ }
293
+ }
294
+ function batch(fn) {
295
+ batchDepth++;
296
+ try {
297
+ fn();
298
+ } finally {
299
+ batchDepth--;
300
+ if (batchDepth === 0) {
301
+ flushNotifications();
302
+ }
303
+ }
304
+ }
305
+ function subscribe(pathOrCallback, callback) {
306
+ if (typeof pathOrCallback === "function") {
307
+ return trie.addGlobal(pathOrCallback);
308
+ }
309
+ return trie.add(pathOrCallback, callback);
310
+ }
311
+ function getSnapshot() {
312
+ return state;
313
+ }
314
+ function computed(deps, fn) {
315
+ return createComputed({ getSnapshot, subscribe }, deps, fn);
316
+ }
317
+ function notify() {
318
+ trie.notifyAll();
319
+ }
320
+ function destroy() {
321
+ trie.clear();
322
+ pendingPaths.clear();
323
+ }
324
+ return {
325
+ get,
326
+ set,
327
+ batch,
328
+ subscribe,
329
+ getSnapshot,
330
+ computed,
331
+ notify,
332
+ destroy
333
+ };
334
+ }
335
+
336
+ // src/core/base.ts
337
+ var IDLE_STATE = { status: asyncStatus("idle"), error: null };
338
+ var defaultHttpClient = {
339
+ async request(url, init) {
340
+ const fetchInit = { method: init?.method ?? "GET" };
341
+ const merged = { ...defaultHeaders, ...init?.headers };
342
+ if (Object.keys(merged).length) {
343
+ fetchInit.headers = merged;
344
+ }
345
+ if (init?.body !== void 0) {
346
+ fetchInit.body = JSON.stringify(init.body);
347
+ fetchInit.headers = { "Content-Type": "application/json", ...merged };
348
+ }
349
+ const res = await fetch(url, fetchInit);
350
+ if (!res.ok) {
351
+ let message = `HTTP ${res.status}`;
352
+ try {
353
+ const text2 = await res.text();
354
+ if (text2) {
355
+ const json = JSON.parse(text2);
356
+ message = json.error ?? json.message ?? message;
357
+ }
358
+ } catch {
359
+ }
360
+ throw new Error(message);
361
+ }
362
+ const text = await res.text();
363
+ return text ? JSON.parse(text) : void 0;
364
+ }
365
+ };
366
+ var httpClient = defaultHttpClient;
367
+ var defaultHeaders = {};
368
+ function setHttpClient(client) {
369
+ httpClient = client;
370
+ }
371
+ function setDefaultHeaders(headers) {
372
+ defaultHeaders = headers;
373
+ }
374
+ var SnapStore = class {
375
+ _store;
376
+ _operations = /* @__PURE__ */ new Map();
377
+ _generations = /* @__PURE__ */ new Map();
378
+ state;
379
+ api;
380
+ constructor(initialState, options) {
381
+ this._store = createStore(initialState, options);
382
+ const store = this._store;
383
+ const operations = this._operations;
384
+ const generations = this._generations;
385
+ const doFetch = async (key, fn) => {
386
+ const gen = (generations.get(key) ?? 0) + 1;
387
+ generations.set(key, gen);
388
+ operations.set(key, { status: asyncStatus("loading"), error: null });
389
+ store.notify();
390
+ try {
391
+ await fn();
392
+ if (generations.get(key) !== gen) {
393
+ return;
394
+ }
395
+ operations.set(key, { status: asyncStatus("ready"), error: null });
396
+ } catch (e) {
397
+ if (generations.get(key) !== gen) {
398
+ return;
399
+ }
400
+ operations.set(key, {
401
+ status: asyncStatus("error"),
402
+ error: e instanceof Error ? e.message : "Unknown error"
403
+ });
404
+ store.notify();
405
+ throw e;
406
+ }
407
+ store.notify();
408
+ };
409
+ const doSend = async (key, method, url, options2) => {
410
+ await doFetch(key, async () => {
411
+ try {
412
+ const data = await httpClient.request(url, {
413
+ method,
414
+ body: options2?.body,
415
+ headers: options2?.headers
416
+ });
417
+ options2?.onSuccess?.(data);
418
+ } catch (e) {
419
+ options2?.onError?.(e instanceof Error ? e : new Error("Unknown error"));
420
+ throw e;
421
+ }
422
+ });
423
+ };
424
+ this.state = {
425
+ get: ((path) => {
426
+ if (path === void 0) {
427
+ return store.get();
428
+ }
429
+ return store.get(path);
430
+ }),
431
+ set: (path, value) => store.set(path, value),
432
+ batch: (fn) => store.batch(fn),
433
+ computed: (deps, fn) => store.computed(deps, fn),
434
+ append: (path, ...items) => {
435
+ store.set(path, ((prev) => [...prev, ...items]));
436
+ },
437
+ prepend: (path, ...items) => {
438
+ store.set(path, ((prev) => [...items, ...prev]));
439
+ },
440
+ insertAt: (path, index, ...items) => {
441
+ store.set(path, ((prev) => {
442
+ const arr = prev;
443
+ return [...arr.slice(0, index), ...items, ...arr.slice(index)];
444
+ }));
445
+ },
446
+ patch: (path, predicate, updates) => {
447
+ store.set(path, ((prev) => {
448
+ const arr = prev;
449
+ let changed = false;
450
+ const result = arr.map((item) => {
451
+ if (item == null) {
452
+ return item;
453
+ }
454
+ if (predicate(item)) {
455
+ changed = true;
456
+ return Object.assign(Object.create(Object.getPrototypeOf(item)), item, updates);
457
+ }
458
+ return item;
459
+ });
460
+ return changed ? result : arr;
461
+ }));
462
+ },
463
+ remove: (path, predicate) => {
464
+ store.set(path, ((prev) => {
465
+ const arr = prev;
466
+ const result = arr.filter((item) => !predicate(item));
467
+ return result.length === arr.length ? arr : result;
468
+ }));
469
+ },
470
+ removeAt: (path, index) => {
471
+ store.set(path, ((prev) => {
472
+ const arr = prev;
473
+ const i = index < 0 ? arr.length + index : index;
474
+ if (i < 0 || i >= arr.length) {
475
+ throw new RangeError(`Index ${index} out of bounds for array of length ${arr.length}`);
476
+ }
477
+ return [...arr.slice(0, i), ...arr.slice(i + 1)];
478
+ }));
479
+ },
480
+ at: (path, index) => {
481
+ return store.get(path).at(index);
482
+ },
483
+ filter: (path, predicate) => {
484
+ return store.get(path).filter(predicate);
485
+ },
486
+ find: (path, predicate) => {
487
+ return store.get(path).find(predicate);
488
+ },
489
+ findIndexOf: (path, predicate) => {
490
+ return store.get(path).findIndex(predicate);
491
+ },
492
+ count: (path, predicate) => {
493
+ return store.get(path).filter(predicate).length;
494
+ }
495
+ };
496
+ this.api = {
497
+ fetch: doFetch,
498
+ get: async (key, url, onSuccess) => {
499
+ await doFetch(key, async () => {
500
+ const data = await httpClient.request(url);
501
+ onSuccess?.(data);
502
+ });
503
+ },
504
+ post: (key, url, options2) => doSend(key, "POST", url, options2),
505
+ put: (key, url, options2) => doSend(key, "PUT", url, options2),
506
+ patch: (key, url, options2) => doSend(key, "PATCH", url, options2),
507
+ delete: (key, url, options2) => doSend(key, "DELETE", url, options2)
508
+ };
509
+ }
510
+ subscribe(pathOrCallback, callback) {
511
+ if (typeof pathOrCallback === "function") {
512
+ return this._store.subscribe(pathOrCallback);
513
+ }
514
+ return this._store.subscribe(pathOrCallback, callback);
515
+ }
516
+ /** Return a snapshot of the current state. Compatible with React's `useSyncExternalStore`. */
517
+ getSnapshot = () => {
518
+ return this._store.getSnapshot();
519
+ };
520
+ /** Get the async status of an operation by key. Returns `idle` if never started. */
521
+ getStatus(key) {
522
+ return { ...this._operations.get(key) ?? IDLE_STATE };
523
+ }
524
+ /** Tear down subscriptions and cleanup. */
525
+ destroy() {
526
+ this._store.destroy();
527
+ }
528
+ };
529
+
530
+ // src/react/store.ts
531
+ function shallowEqual(a, b) {
532
+ const keysA = Object.keys(a);
533
+ const keysB = Object.keys(b);
534
+ if (keysA.length !== keysB.length) {
535
+ return false;
536
+ }
537
+ for (const key of keysA) {
538
+ if (a[key] !== b[key]) {
539
+ return false;
540
+ }
541
+ }
542
+ return true;
543
+ }
544
+ var ReactSnapStore = class extends SnapStore {
545
+ constructor(initialState, options) {
546
+ super(initialState, options);
547
+ }
548
+ connect(Component, configOrMapper) {
549
+ const store = this;
550
+ if (typeof configOrMapper === "object" && "select" in configOrMapper) {
551
+ return this._connectWithSelect(Component, configOrMapper.select);
552
+ }
553
+ const mapToProps = typeof configOrMapper === "function" ? configOrMapper : configOrMapper.props;
554
+ const fetchFn = typeof configOrMapper === "function" ? void 0 : configOrMapper.fetch;
555
+ const loadingComponent = typeof configOrMapper === "function" ? void 0 : configOrMapper.loading;
556
+ const errorComponent = typeof configOrMapper === "function" ? void 0 : configOrMapper.error;
557
+ const Connected = (0, import_react.forwardRef)(function Connected2(ownProps, ref) {
558
+ const cachedRef = (0, import_react.useRef)(null);
559
+ const revisionRef = (0, import_react.useRef)(0);
560
+ const subscribe = (0, import_react.useCallback)(
561
+ (cb) => store.subscribe(() => {
562
+ revisionRef.current++;
563
+ cb();
564
+ }),
565
+ [store]
566
+ );
567
+ const getSnapshot = (0, import_react.useCallback)(() => {
568
+ const currentRevision = revisionRef.current;
569
+ if (cachedRef.current && cachedRef.current.revision === currentRevision) {
570
+ return cachedRef.current.props;
571
+ }
572
+ const next = mapToProps(store);
573
+ if (cachedRef.current && shallowEqual(cachedRef.current.props, next)) {
574
+ cachedRef.current = { revision: currentRevision, props: cachedRef.current.props };
575
+ return cachedRef.current.props;
576
+ }
577
+ cachedRef.current = { revision: currentRevision, props: next };
578
+ return next;
579
+ }, [store]);
580
+ const mappedProps = (0, import_react.useSyncExternalStore)(subscribe, getSnapshot, getSnapshot);
581
+ const [asyncState, setAsyncState] = (0, import_react.useState)({ status: asyncStatus("idle"), error: null });
582
+ const fetchGenRef = (0, import_react.useRef)(0);
583
+ (0, import_react.useEffect)(() => {
584
+ if (!fetchFn) {
585
+ return;
586
+ }
587
+ let cancelled = false;
588
+ const gen = ++fetchGenRef.current;
589
+ setAsyncState({ status: asyncStatus("loading"), error: null });
590
+ Promise.resolve().then(() => {
591
+ if (cancelled) {
592
+ return;
593
+ }
594
+ return fetchFn(store);
595
+ }).then(() => {
596
+ if (gen === fetchGenRef.current) {
597
+ setAsyncState({ status: asyncStatus("ready"), error: null });
598
+ }
599
+ }).catch((e) => {
600
+ if (gen === fetchGenRef.current) {
601
+ setAsyncState({
602
+ status: asyncStatus("error"),
603
+ error: e instanceof Error ? e.message : "Unknown error"
604
+ });
605
+ }
606
+ });
607
+ return () => {
608
+ cancelled = true;
609
+ };
610
+ }, []);
611
+ if (fetchFn) {
612
+ if (loadingComponent && (asyncState.status.isIdle || asyncState.status.isLoading)) {
613
+ return (0, import_react.createElement)(loadingComponent);
614
+ }
615
+ if (errorComponent && asyncState.status.isError) {
616
+ return (0, import_react.createElement)(errorComponent, { error: asyncState.error });
617
+ }
618
+ }
619
+ return (0, import_react.createElement)(Component, {
620
+ ...ownProps,
621
+ ...mappedProps,
622
+ ...fetchFn ? asyncState : {},
623
+ ref
624
+ });
625
+ });
626
+ Connected.displayName = `Connect(${Component.displayName || Component.name || "Component"})`;
627
+ return Connected;
628
+ }
629
+ _connectWithSelect(Component, selectFn) {
630
+ const store = this;
631
+ const resolvePathValue = (path) => {
632
+ const segments = path.split(".");
633
+ let val = store.getSnapshot();
634
+ for (const seg of segments) {
635
+ if (val == null) {
636
+ return void 0;
637
+ }
638
+ val = val[seg];
639
+ }
640
+ return val;
641
+ };
642
+ const trackedPaths = [];
643
+ const trackingPick = ((path) => {
644
+ trackedPaths.push(path);
645
+ return resolvePathValue(path);
646
+ });
647
+ selectFn(trackingPick);
648
+ const paths = [...trackedPaths];
649
+ const readPick = ((path) => {
650
+ return resolvePathValue(path);
651
+ });
652
+ const Connected = (0, import_react.forwardRef)(function Connected2(ownProps, ref) {
653
+ const cachedRef = (0, import_react.useRef)(null);
654
+ const revisionRef = (0, import_react.useRef)(0);
655
+ const subscribe = (0, import_react.useCallback)(
656
+ (cb) => {
657
+ const unsubs = paths.map(
658
+ (p) => store.subscribe(p, () => {
659
+ revisionRef.current++;
660
+ cb();
661
+ })
662
+ );
663
+ return () => unsubs.forEach((u) => u());
664
+ },
665
+ [store]
666
+ );
667
+ const getSnapshot = (0, import_react.useCallback)(() => {
668
+ const currentRevision = revisionRef.current;
669
+ if (cachedRef.current && cachedRef.current.revision === currentRevision) {
670
+ return cachedRef.current.props;
671
+ }
672
+ const next = selectFn(readPick);
673
+ if (cachedRef.current && shallowEqual(cachedRef.current.props, next)) {
674
+ cachedRef.current = { revision: currentRevision, props: cachedRef.current.props };
675
+ return cachedRef.current.props;
676
+ }
677
+ cachedRef.current = { revision: currentRevision, props: next };
678
+ return next;
679
+ }, [store]);
680
+ const mappedProps = (0, import_react.useSyncExternalStore)(subscribe, getSnapshot, getSnapshot);
681
+ return (0, import_react.createElement)(Component, {
682
+ ...ownProps,
683
+ ...mappedProps,
684
+ ref
685
+ });
686
+ });
687
+ Connected.displayName = `Connect(${Component.displayName || Component.name || "Component"})`;
688
+ return Connected;
689
+ }
690
+ };
691
+
692
+ // src/form/form.ts
693
+ function getObjectSchema(schema) {
694
+ if (schema instanceof import_zod.z.ZodObject) return schema;
695
+ if (schema instanceof import_zod.z.ZodPipe)
696
+ return getObjectSchema(schema._zod.def.in);
697
+ return null;
698
+ }
699
+ var INNER_TYPE_WRAPPERS = /* @__PURE__ */ new Set([
700
+ "optional",
701
+ "nullable",
702
+ "default",
703
+ "prefault",
704
+ "catch",
705
+ "nonoptional",
706
+ "success",
707
+ "readonly"
708
+ ]);
709
+ function getBaseSchemaType(schema) {
710
+ if (!schema || typeof schema !== "object" || !("_zod" in schema) || !schema._zod?.def?.type) {
711
+ return null;
712
+ }
713
+ const type = schema._zod.def.type;
714
+ if (INNER_TYPE_WRAPPERS.has(type)) {
715
+ return getBaseSchemaType(schema._zod.def.innerType);
716
+ }
717
+ if (type === "pipe") {
718
+ return getBaseSchemaType(schema._zod.def.in);
719
+ }
720
+ if (type === "literal") {
721
+ const vals = schema._zod.def.values;
722
+ if (Array.isArray(vals) && vals.length > 0) {
723
+ if (vals[0] === null) {
724
+ return "null";
725
+ }
726
+ return typeof vals[0];
727
+ }
728
+ return "string";
729
+ }
730
+ return type;
731
+ }
732
+ var SnapFormStore = class extends ReactSnapStore {
733
+ schema;
734
+ objectSchema;
735
+ formConfig;
736
+ _refs = /* @__PURE__ */ new Map();
737
+ _radioRefs = /* @__PURE__ */ new Map();
738
+ constructor(schema, initialValues, config) {
739
+ super({
740
+ values: { ...initialValues },
741
+ initial: { ...initialValues },
742
+ errors: {},
743
+ submitStatus: { status: asyncStatus("idle"), error: null }
744
+ });
745
+ this.schema = schema;
746
+ this.objectSchema = getObjectSchema(schema);
747
+ this.formConfig = {
748
+ validationMode: config?.validationMode ?? "onSubmit"
749
+ };
750
+ }
751
+ get values() {
752
+ return this.state.get("values");
753
+ }
754
+ get errors() {
755
+ return this.state.get("errors");
756
+ }
757
+ get isDirty() {
758
+ const values = this.state.get("values");
759
+ const initial = this.state.get("initial");
760
+ const shapeKeys = this.objectSchema ? Object.keys(this.objectSchema.shape) : [];
761
+ const allKeys = /* @__PURE__ */ new Set([...Object.keys(initial), ...shapeKeys]);
762
+ for (const key of allKeys) {
763
+ if (!this.valuesEqual(values[key], initial[key])) {
764
+ return true;
765
+ }
766
+ }
767
+ return false;
768
+ }
769
+ get isValid() {
770
+ const errors = this.state.get("errors");
771
+ return Object.keys(errors).length === 0;
772
+ }
773
+ register(field) {
774
+ const value = this.state.get(`values.${field}`);
775
+ const isBool = this.getFieldType(field) === "boolean";
776
+ const trackedEls = /* @__PURE__ */ new Set();
777
+ return {
778
+ ref: (el) => {
779
+ if (el) {
780
+ if (el.type === "radio") {
781
+ if (!this._radioRefs.has(field)) this._refs.delete(field);
782
+ let set = this._radioRefs.get(field);
783
+ if (!set) {
784
+ set = /* @__PURE__ */ new Set();
785
+ this._radioRefs.set(field, set);
786
+ }
787
+ set.add(el);
788
+ } else {
789
+ this._refs.set(field, el);
790
+ if (value instanceof Date || Array.isArray(value)) {
791
+ this.syncValueToDom(field, value);
792
+ }
793
+ }
794
+ trackedEls.add(el);
795
+ } else {
796
+ for (const tracked of trackedEls) {
797
+ const set = this._radioRefs.get(field);
798
+ if (set) {
799
+ set.delete(tracked);
800
+ if (set.size === 0) this._radioRefs.delete(field);
801
+ } else {
802
+ this._refs.delete(field);
803
+ }
804
+ }
805
+ trackedEls.clear();
806
+ }
807
+ },
808
+ name: field,
809
+ ...isBool ? { defaultChecked: Boolean(value) } : Array.isArray(value) ? {} : { defaultValue: value instanceof Date ? this.formatLocalDateTime(value) : String(value ?? "") },
810
+ onBlur: () => {
811
+ this.syncRefToState(field);
812
+ this.handleBlur(field);
813
+ },
814
+ onChange: () => {
815
+ this.syncRefToState(field);
816
+ this.handleChange(field);
817
+ }
818
+ };
819
+ }
820
+ getValues() {
821
+ const stateValues = this.state.get("values");
822
+ const merged = { ...stateValues };
823
+ for (const [field, el] of this._refs) {
824
+ merged[field] = this.coerceRefValue(field, el);
825
+ }
826
+ for (const [field] of this._radioRefs) {
827
+ merged[field] = this.coerceRadioValue(field);
828
+ }
829
+ return merged;
830
+ }
831
+ getValue(field) {
832
+ if (this._radioRefs.has(field)) {
833
+ return this.coerceRadioValue(field);
834
+ }
835
+ const el = this._refs.get(field);
836
+ if (el) return this.coerceRefValue(field, el);
837
+ return this.state.get(`values.${field}`);
838
+ }
839
+ setValue(field, value) {
840
+ this.state.set(`values.${field}`, value);
841
+ this.syncValueToDom(field, value);
842
+ this.handleChange(field);
843
+ }
844
+ handleBlur(field) {
845
+ if (this.formConfig.validationMode === "onBlur") {
846
+ this.validateField(field);
847
+ }
848
+ }
849
+ handleChange(field) {
850
+ if (this.formConfig.validationMode === "onChange") {
851
+ this.validateField(field);
852
+ }
853
+ }
854
+ isFieldDirty(field) {
855
+ return !this.valuesEqual(
856
+ this.state.get(`values.${field}`),
857
+ this.state.get(`initial.${field}`)
858
+ );
859
+ }
860
+ setError(field, message) {
861
+ const errors = this.state.get("errors");
862
+ const existing = errors[field] ?? [];
863
+ this.state.set(`errors.${field}`, [...existing, message]);
864
+ }
865
+ clearErrors() {
866
+ this.state.set("errors", {});
867
+ }
868
+ validate() {
869
+ this.syncFromRefs();
870
+ const result = this.schema.safeParse(this.state.get("values"));
871
+ if (result.success) {
872
+ this.clearErrors();
873
+ return result.data;
874
+ }
875
+ const errors = {};
876
+ for (const issue of result.error.issues) {
877
+ const field = issue.path[0];
878
+ if (!errors[field]) errors[field] = [];
879
+ errors[field].push(issue.message);
880
+ }
881
+ this.state.set("errors", errors);
882
+ return null;
883
+ }
884
+ validateField(field) {
885
+ if (!this.objectSchema) return;
886
+ const fieldSchema = this.objectSchema.shape[field];
887
+ if (!fieldSchema) return;
888
+ const value = this.state.get(`values.${field}`);
889
+ const result = fieldSchema.safeParse(value);
890
+ if (result.success) {
891
+ const errors = { ...this.state.get("errors") };
892
+ delete errors[field];
893
+ this.state.set("errors", errors);
894
+ } else {
895
+ const messages = result.error.issues.map(
896
+ (i) => i.message
897
+ );
898
+ this.state.set(`errors.${field}`, messages);
899
+ }
900
+ }
901
+ reset() {
902
+ const initial = this.state.get("initial");
903
+ this.state.batch(() => {
904
+ this.state.set("values", { ...initial });
905
+ this.state.set("errors", {});
906
+ this.state.set("submitStatus", { status: asyncStatus("idle"), error: null });
907
+ });
908
+ this.syncToDom();
909
+ }
910
+ clear() {
911
+ const initial = this.state.get("initial");
912
+ const shapeKeys = this.objectSchema ? Object.keys(this.objectSchema.shape) : [];
913
+ const allKeys = /* @__PURE__ */ new Set([...Object.keys(initial), ...shapeKeys]);
914
+ const empty = [...allKeys].reduce(
915
+ (acc, key) => {
916
+ const val = initial[key];
917
+ if (val === void 0 || val === null) {
918
+ const ft = this.getFieldType(key);
919
+ if (ft === "number") {
920
+ acc[key] = 0;
921
+ } else if (ft === "boolean") {
922
+ acc[key] = false;
923
+ } else if (ft === "null") {
924
+ acc[key] = null;
925
+ } else if (val === null && ft === "string") {
926
+ acc[key] = "";
927
+ } else if (val === null && ft === "date") {
928
+ acc[key] = null;
929
+ } else if (val === null && ft === "array") {
930
+ acc[key] = [];
931
+ } else if (val === null && (ft === "object" || ft === "record")) {
932
+ acc[key] = {};
933
+ } else {
934
+ acc[key] = val;
935
+ }
936
+ } else if (val instanceof Date) {
937
+ acc[key] = null;
938
+ } else if (typeof val === "number") {
939
+ acc[key] = 0;
940
+ } else if (typeof val === "boolean") {
941
+ acc[key] = false;
942
+ } else if (Array.isArray(val)) {
943
+ acc[key] = [];
944
+ } else if (typeof val === "object") {
945
+ acc[key] = {};
946
+ } else {
947
+ acc[key] = "";
948
+ }
949
+ return acc;
950
+ },
951
+ {}
952
+ );
953
+ this.state.batch(() => {
954
+ this.state.set("values", empty);
955
+ this.state.set("errors", {});
956
+ });
957
+ this.syncToDom();
958
+ }
959
+ setInitialValues(values) {
960
+ const current = this.state.get("initial");
961
+ const merged = { ...current, ...values };
962
+ this.state.batch(() => {
963
+ this.state.set("values", { ...merged });
964
+ this.state.set("initial", merged);
965
+ });
966
+ this.syncToDom();
967
+ }
968
+ valuesEqual(a, b) {
969
+ if (a === b) return true;
970
+ if (a instanceof Date && b instanceof Date) return a.getTime() === b.getTime();
971
+ if (Array.isArray(a) && Array.isArray(b)) {
972
+ return a.length === b.length && a.every((v, i) => v === b[i]);
973
+ }
974
+ return false;
975
+ }
976
+ getFieldType(field) {
977
+ const initial = this.state.get("initial");
978
+ const val = initial[field];
979
+ if (val instanceof Date) {
980
+ return "date";
981
+ }
982
+ if (Array.isArray(val)) {
983
+ return "array";
984
+ }
985
+ if (val !== void 0 && val !== null) {
986
+ return typeof val;
987
+ }
988
+ if (this.objectSchema) {
989
+ const base = getBaseSchemaType(this.objectSchema.shape[field]);
990
+ if (base) {
991
+ return base;
992
+ }
993
+ }
994
+ return "string";
995
+ }
996
+ getArrayItemType(field) {
997
+ const initial = this.state.get("initial");
998
+ const val = initial[field];
999
+ if (Array.isArray(val) && val.length > 0) return typeof val[0];
1000
+ if (this.objectSchema) {
1001
+ let schema = this.objectSchema.shape[field];
1002
+ while (schema?._zod?.def) {
1003
+ if (schema._zod.def.type === "array") {
1004
+ const itemType = getBaseSchemaType(schema._zod.def.element);
1005
+ if (itemType) return itemType;
1006
+ break;
1007
+ }
1008
+ schema = schema._zod.def.innerType ?? schema._zod.def.in;
1009
+ if (!schema) break;
1010
+ }
1011
+ }
1012
+ return "string";
1013
+ }
1014
+ getRadioValue(field) {
1015
+ const set = this._radioRefs.get(field);
1016
+ if (!set) return void 0;
1017
+ for (const el of set) {
1018
+ if (el.checked) return el.value;
1019
+ }
1020
+ return void 0;
1021
+ }
1022
+ formatLocalDateTime(date) {
1023
+ const p = (n) => String(n).padStart(2, "0");
1024
+ return `${date.getFullYear()}-${p(date.getMonth() + 1)}-${p(date.getDate())}T${p(date.getHours())}:${p(date.getMinutes())}`;
1025
+ }
1026
+ formatDateForInput(el, date) {
1027
+ const full = this.formatLocalDateTime(date);
1028
+ if (el.type === "datetime-local") return full;
1029
+ if (el.type === "time") return full.slice(11);
1030
+ return full.slice(0, 10);
1031
+ }
1032
+ coerceStringValue(field, raw) {
1033
+ const typ = this.getFieldType(field);
1034
+ if (typ === "number") return raw === "" ? NaN : Number(raw);
1035
+ if (typ === "date") {
1036
+ if (raw === "") return null;
1037
+ if (/^\d{2}:\d{2}$/.test(raw)) {
1038
+ const [h, m] = raw.split(":").map(Number);
1039
+ const d2 = /* @__PURE__ */ new Date();
1040
+ d2.setHours(h, m, 0, 0);
1041
+ return d2;
1042
+ }
1043
+ if (/^\d{4}-\d{2}-\d{2}$/.test(raw)) {
1044
+ raw = raw + "T00:00:00";
1045
+ }
1046
+ const d = new Date(raw);
1047
+ return isNaN(d.getTime()) ? null : d;
1048
+ }
1049
+ return raw;
1050
+ }
1051
+ coerceRadioValue(field) {
1052
+ const raw = this.getRadioValue(field);
1053
+ if (raw === void 0) return void 0;
1054
+ return this.coerceStringValue(field, raw);
1055
+ }
1056
+ coerceRefValue(field, el) {
1057
+ if (el instanceof HTMLInputElement && el.type === "file") {
1058
+ if (el.files && el.files.length > 0) {
1059
+ return el.multiple ? Array.from(el.files) : el.files[0];
1060
+ }
1061
+ return this.state.get(`values.${field}`);
1062
+ }
1063
+ const typ = this.getFieldType(field);
1064
+ if (typ === "boolean") {
1065
+ return el.checked;
1066
+ }
1067
+ if (typ === "array" && el instanceof HTMLSelectElement && el.multiple) {
1068
+ const itemType = this.getArrayItemType(field);
1069
+ return Array.from(
1070
+ el.selectedOptions,
1071
+ (o) => itemType === "number" ? o.value === "" ? NaN : Number(o.value) : o.value
1072
+ );
1073
+ }
1074
+ return this.coerceStringValue(field, el.value);
1075
+ }
1076
+ syncRefToState(field) {
1077
+ if (this._radioRefs.has(field)) {
1078
+ const val = this.coerceRadioValue(field);
1079
+ if (val !== void 0) {
1080
+ this.state.set(`values.${field}`, val);
1081
+ }
1082
+ return;
1083
+ }
1084
+ const el = this._refs.get(field);
1085
+ if (!el) {
1086
+ return;
1087
+ }
1088
+ this.state.set(`values.${field}`, this.coerceRefValue(field, el));
1089
+ }
1090
+ syncFromRefs() {
1091
+ if (this._refs.size === 0 && this._radioRefs.size === 0) return;
1092
+ this.state.batch(() => {
1093
+ for (const [field, el] of this._refs) {
1094
+ this.state.set(`values.${field}`, this.coerceRefValue(field, el));
1095
+ }
1096
+ for (const [field] of this._radioRefs) {
1097
+ const val = this.coerceRadioValue(field);
1098
+ if (val !== void 0) {
1099
+ this.state.set(`values.${field}`, val);
1100
+ }
1101
+ }
1102
+ });
1103
+ }
1104
+ syncValueToDom(field, value) {
1105
+ const radioSet = this._radioRefs.get(field);
1106
+ if (radioSet) {
1107
+ const strVal = String(value ?? "");
1108
+ for (const el2 of radioSet) {
1109
+ el2.checked = el2.value === strVal;
1110
+ }
1111
+ return;
1112
+ }
1113
+ const el = this._refs.get(field);
1114
+ if (!el) {
1115
+ return;
1116
+ }
1117
+ if (el instanceof HTMLInputElement && el.type === "file") {
1118
+ if (value == null || Array.isArray(value) && value.length === 0) el.value = "";
1119
+ return;
1120
+ }
1121
+ const ft = this.getFieldType(field);
1122
+ if (ft === "boolean") {
1123
+ el.checked = Boolean(value);
1124
+ } else if (ft === "date" && value instanceof Date) {
1125
+ el.value = this.formatDateForInput(el, value);
1126
+ } else if (ft === "array" && el instanceof HTMLSelectElement && el.multiple && Array.isArray(value)) {
1127
+ const vals = new Set(value.map(String));
1128
+ for (let i = 0; i < el.options.length; i++) {
1129
+ el.options[i].selected = vals.has(el.options[i].value);
1130
+ }
1131
+ } else {
1132
+ el.value = String(value ?? "");
1133
+ }
1134
+ }
1135
+ syncToDom() {
1136
+ if (this._refs.size === 0 && this._radioRefs.size === 0) return;
1137
+ const values = this.state.get("values");
1138
+ for (const [field] of this._refs) {
1139
+ this.syncValueToDom(field, values[field]);
1140
+ }
1141
+ for (const [field] of this._radioRefs) {
1142
+ this.syncValueToDom(field, values[field]);
1143
+ }
1144
+ }
1145
+ submit(key, handler) {
1146
+ const data = this.validate();
1147
+ if (!data) return void 0;
1148
+ const promise = this.api.fetch(key, () => handler(data));
1149
+ this.syncSubmitStatus(key);
1150
+ return promise;
1151
+ }
1152
+ syncSubmitStatus(key) {
1153
+ const update = () => {
1154
+ const status = this.getStatus(key);
1155
+ const current = this.state.get("submitStatus");
1156
+ if (current.status === status.status && current.error === status.error)
1157
+ return;
1158
+ this.state.set("submitStatus", status);
1159
+ };
1160
+ update();
1161
+ const unsub = this.subscribe(() => {
1162
+ update();
1163
+ const s = this.getStatus(key).status;
1164
+ if (s.isReady || s.isError) unsub();
1165
+ });
1166
+ }
1167
+ };
1168
+ // Annotate the CommonJS export names for ESM import in node:
1169
+ 0 && (module.exports = {
1170
+ SnapFormStore,
1171
+ asyncStatus,
1172
+ getBaseSchemaType,
1173
+ getObjectSchema,
1174
+ setDefaultHeaders,
1175
+ setHttpClient
1176
+ });
1177
+ //# sourceMappingURL=index.cjs.map