@granularjs/core 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (70) hide show
  1. package/README.md +576 -0
  2. package/dist/granular.min.js +2 -0
  3. package/dist/granular.min.js.map +7 -0
  4. package/package.json +54 -0
  5. package/src/core/bootstrap.js +63 -0
  6. package/src/core/collections/observable-array.js +204 -0
  7. package/src/core/component/function-component.js +82 -0
  8. package/src/core/context.js +172 -0
  9. package/src/core/dom/dom.js +25 -0
  10. package/src/core/dom/element.js +725 -0
  11. package/src/core/dom/error-boundary.js +111 -0
  12. package/src/core/dom/input-format.js +82 -0
  13. package/src/core/dom/list.js +185 -0
  14. package/src/core/dom/portal.js +57 -0
  15. package/src/core/dom/tags.js +182 -0
  16. package/src/core/dom/virtual-list.js +242 -0
  17. package/src/core/dom/when.js +138 -0
  18. package/src/core/events/event-hub.js +97 -0
  19. package/src/core/forms/form.js +127 -0
  20. package/src/core/internal/symbols.js +5 -0
  21. package/src/core/network/websocket.js +165 -0
  22. package/src/core/query/query-client.js +529 -0
  23. package/src/core/reactivity/after-flush.js +20 -0
  24. package/src/core/reactivity/computed.js +51 -0
  25. package/src/core/reactivity/concat.js +89 -0
  26. package/src/core/reactivity/dirty-host.js +162 -0
  27. package/src/core/reactivity/observe.js +421 -0
  28. package/src/core/reactivity/persist.js +180 -0
  29. package/src/core/reactivity/resolve.js +8 -0
  30. package/src/core/reactivity/signal.js +97 -0
  31. package/src/core/reactivity/state.js +294 -0
  32. package/src/core/renderable/render-string.js +51 -0
  33. package/src/core/renderable/renderable.js +21 -0
  34. package/src/core/renderable/renderer.js +66 -0
  35. package/src/core/router/router.js +865 -0
  36. package/src/core/runtime.js +28 -0
  37. package/src/index.js +42 -0
  38. package/types/core/bootstrap.d.ts +11 -0
  39. package/types/core/collections/observable-array.d.ts +25 -0
  40. package/types/core/component/function-component.d.ts +14 -0
  41. package/types/core/context.d.ts +29 -0
  42. package/types/core/dom/dom.d.ts +13 -0
  43. package/types/core/dom/element.d.ts +10 -0
  44. package/types/core/dom/error-boundary.d.ts +8 -0
  45. package/types/core/dom/input-format.d.ts +6 -0
  46. package/types/core/dom/list.d.ts +8 -0
  47. package/types/core/dom/portal.d.ts +8 -0
  48. package/types/core/dom/tags.d.ts +114 -0
  49. package/types/core/dom/virtual-list.d.ts +8 -0
  50. package/types/core/dom/when.d.ts +13 -0
  51. package/types/core/events/event-hub.d.ts +48 -0
  52. package/types/core/forms/form.d.ts +9 -0
  53. package/types/core/internal/symbols.d.ts +4 -0
  54. package/types/core/network/websocket.d.ts +18 -0
  55. package/types/core/query/query-client.d.ts +73 -0
  56. package/types/core/reactivity/after-flush.d.ts +4 -0
  57. package/types/core/reactivity/computed.d.ts +1 -0
  58. package/types/core/reactivity/concat.d.ts +1 -0
  59. package/types/core/reactivity/dirty-host.d.ts +42 -0
  60. package/types/core/reactivity/observe.d.ts +10 -0
  61. package/types/core/reactivity/persist.d.ts +1 -0
  62. package/types/core/reactivity/resolve.d.ts +1 -0
  63. package/types/core/reactivity/signal.d.ts +11 -0
  64. package/types/core/reactivity/state.d.ts +14 -0
  65. package/types/core/renderable/render-string.d.ts +2 -0
  66. package/types/core/renderable/renderable.d.ts +15 -0
  67. package/types/core/renderable/renderer.d.ts +38 -0
  68. package/types/core/router/router.d.ts +57 -0
  69. package/types/core/runtime.d.ts +26 -0
  70. package/types/index.d.ts +2 -0
@@ -0,0 +1,242 @@
1
+ import { Renderable } from '../renderable/renderable.js';
2
+ import { Renderer } from '../renderable/renderer.js';
3
+ import { isObservableArray } from '../collections/observable-array.js';
4
+ import { isSignal, readSignal, subscribeSignal } from '../reactivity/signal.js';
5
+ import { isState, isStatePath, readState, subscribeState } from '../reactivity/state.js';
6
+
7
+ function clamp(value, min, max) {
8
+ return Math.max(min, Math.min(max, value));
9
+ }
10
+
11
+ function isNumber(value) {
12
+ return typeof value === 'number' && !Number.isNaN(value);
13
+ }
14
+
15
+ export class VirtualListNode extends Renderable {
16
+ #items;
17
+ #renderItem;
18
+ #direction;
19
+ #overscan;
20
+ #itemSize;
21
+ #container = null;
22
+ #spacer = null;
23
+ #itemsEl = null;
24
+ #mounted = false;
25
+ #unsub = null;
26
+ #resizeObserver = null;
27
+ #viewportSize = 0;
28
+ #startIndex = 0;
29
+ #endIndex = -1;
30
+ #mountedValues = [];
31
+ #measuring = false;
32
+
33
+ constructor(items, options = {}) {
34
+ super();
35
+ this.#items = items;
36
+ this.#renderItem = options.render;
37
+ this.#direction = options.direction === 'horizontal' ? 'horizontal' : 'vertical';
38
+ this.#overscan = isNumber(options.overscan) ? Math.max(0, options.overscan) : 2;
39
+ this.#itemSize = isNumber(options.itemSize) ? options.itemSize : null;
40
+ }
41
+
42
+ mountInto(parent, beforeNode) {
43
+ if (this.#mounted) return;
44
+ if (typeof this.#renderItem !== 'function') {
45
+ throw new Error('virtualList(items, options): options.render is required');
46
+ }
47
+ this.#mounted = true;
48
+
49
+ const container = document.createElement('div');
50
+ container.style.position = 'relative';
51
+ container.style.overflow = 'auto';
52
+ container.style.width = '100%';
53
+ container.style.height = '100%';
54
+ container.style.contain = 'layout paint';
55
+
56
+ const spacer = document.createElement('div');
57
+ spacer.style.position = 'relative';
58
+ spacer.style.width = this.#direction === 'horizontal' ? '0px' : '100%';
59
+ spacer.style.height = this.#direction === 'horizontal' ? '100%' : '0px';
60
+
61
+ const itemsEl = document.createElement('div');
62
+ itemsEl.style.position = 'absolute';
63
+ itemsEl.style.top = '0';
64
+ itemsEl.style.left = '0';
65
+ itemsEl.style.willChange = 'transform';
66
+ if (this.#direction === 'horizontal') {
67
+ itemsEl.style.display = 'flex';
68
+ itemsEl.style.flexDirection = 'row';
69
+ }
70
+
71
+ container.appendChild(spacer);
72
+ container.appendChild(itemsEl);
73
+ parent.insertBefore(container, beforeNode);
74
+
75
+ this.#container = container;
76
+ this.#spacer = spacer;
77
+ this.#itemsEl = itemsEl;
78
+
79
+ container.addEventListener('scroll', this.#onScroll);
80
+ this.#observeResize(parent);
81
+ this.#updateViewport(parent);
82
+ this.#render();
83
+ this.#wire();
84
+ }
85
+
86
+ unmount() {
87
+ if (!this.#mounted) return;
88
+ this.#mounted = false;
89
+ if (this.#unsub) this.#unsub();
90
+ this.#unsub = null;
91
+ if (this.#container) {
92
+ this.#container.removeEventListener('scroll', this.#onScroll);
93
+ }
94
+ if (this.#resizeObserver) {
95
+ this.#resizeObserver.disconnect();
96
+ this.#resizeObserver = null;
97
+ }
98
+ this.#cleanup();
99
+ this.#container?.remove();
100
+ this.#container = null;
101
+ this.#spacer = null;
102
+ this.#itemsEl = null;
103
+ }
104
+
105
+ #readItems() {
106
+ if (isObservableArray(this.#items)) return this.#items;
107
+ if (isSignal(this.#items)) return readSignal(this.#items) || [];
108
+ if (isState(this.#items) || isStatePath(this.#items)) return readState(this.#items) || [];
109
+ return Array.isArray(this.#items) ? this.#items : [];
110
+ }
111
+
112
+ #wire() {
113
+ if (isObservableArray(this.#items)) {
114
+ this.#unsub = this.#items.subscribe(() => this.#render());
115
+ return;
116
+ }
117
+ if (isSignal(this.#items)) {
118
+ this.#unsub = subscribeSignal(this.#items, () => this.#render());
119
+ return;
120
+ }
121
+ if (isState(this.#items) || isStatePath(this.#items)) {
122
+ this.#unsub = subscribeState(this.#items, () => this.#render());
123
+ }
124
+ }
125
+
126
+ #observeResize(parent) {
127
+ if (typeof ResizeObserver === 'undefined') return;
128
+ this.#resizeObserver = new ResizeObserver(() => {
129
+ this.#updateViewport(parent);
130
+ this.#render();
131
+ });
132
+ this.#resizeObserver.observe(parent);
133
+ }
134
+
135
+ #updateViewport(parent) {
136
+ const rect = parent?.getBoundingClientRect?.();
137
+ if (!rect) return;
138
+ this.#viewportSize = this.#direction === 'horizontal' ? rect.width : rect.height;
139
+ }
140
+
141
+ #measureItemSize() {
142
+ if (this.#itemSize != null) return;
143
+ if (!this.#itemsEl) return;
144
+ const first = this.#itemsEl.firstElementChild;
145
+ if (!first) return;
146
+ const rect = first.getBoundingClientRect();
147
+ const size = this.#direction === 'horizontal' ? rect.width : rect.height;
148
+ if (isNumber(size) && size > 0) this.#itemSize = size;
149
+ }
150
+
151
+ #cleanup() {
152
+ for (const r of this.#mountedValues) Renderer.unmount(r);
153
+ this.#mountedValues = [];
154
+ if (this.#itemsEl) this.#itemsEl.replaceChildren();
155
+ }
156
+
157
+ #renderRange(items, start, end, offset) {
158
+ if (!this.#itemsEl) return;
159
+ this.#cleanup();
160
+ const slice = items.slice(start, end + 1);
161
+ const values = [];
162
+ for (let i = 0; i < slice.length; i++) {
163
+ const index = start + i;
164
+ const value = this.#renderItem(slice[i], index);
165
+ const normalized = Renderer.normalize(value);
166
+ for (const r of normalized) values.push(r);
167
+ }
168
+ this.#mountedValues = values;
169
+ for (const r of values) {
170
+ if (Renderer.isRenderable(r)) {
171
+ r.mountInto(this.#itemsEl, null);
172
+ } else if (Renderer.isDomNode(r)) {
173
+ this.#itemsEl.appendChild(r);
174
+ }
175
+ }
176
+ if (this.#direction === 'horizontal') {
177
+ this.#itemsEl.style.transform = `translateX(${offset}px)`;
178
+ } else {
179
+ this.#itemsEl.style.transform = `translateY(${offset}px)`;
180
+ }
181
+ }
182
+
183
+ #render() {
184
+ if (!this.#mounted || !this.#container) return;
185
+ const items = this.#readItems();
186
+ const count = items.length;
187
+ if (!this.#spacer) return;
188
+
189
+ if (count === 0) {
190
+ this.#spacer.style.width = this.#direction === 'horizontal' ? '0px' : '100%';
191
+ this.#spacer.style.height = this.#direction === 'horizontal' ? '100%' : '0px';
192
+ this.#cleanup();
193
+ return;
194
+ }
195
+
196
+ if (this.#itemSize == null && !this.#measuring) {
197
+ this.#measuring = true;
198
+ this.#renderRange(items, 0, 0, 0);
199
+ requestAnimationFrame(() => {
200
+ this.#measureItemSize();
201
+ this.#measuring = false;
202
+ this.#render();
203
+ });
204
+ return;
205
+ }
206
+
207
+ const size = this.#itemSize || 1;
208
+ const viewport = this.#viewportSize || (this.#direction === 'horizontal' ? this.#container.clientWidth : this.#container.clientHeight);
209
+ const scrollPos = this.#direction === 'horizontal' ? this.#container.scrollLeft : this.#container.scrollTop;
210
+ const visibleCount = Math.ceil(viewport / size);
211
+ const start = clamp(Math.floor(scrollPos / size) - this.#overscan, 0, Math.max(0, count - 1));
212
+ const end = clamp(start + visibleCount + this.#overscan * 2 - 1, 0, count - 1);
213
+ const offset = start * size;
214
+
215
+ const total = count * size;
216
+ if (this.#direction === 'horizontal') {
217
+ this.#spacer.style.width = `${total}px`;
218
+ this.#spacer.style.height = '100%';
219
+ } else {
220
+ this.#spacer.style.height = `${total}px`;
221
+ this.#spacer.style.width = '100%';
222
+ }
223
+
224
+ if (start === this.#startIndex && end === this.#endIndex) return;
225
+ this.#startIndex = start;
226
+ this.#endIndex = end;
227
+ this.#renderRange(items, start, end, offset);
228
+ }
229
+
230
+ #onScroll = () => {
231
+ this.#render();
232
+ };
233
+
234
+ renderToString(render) {
235
+ const items = this.#readItems();
236
+ return items.map((item, index) => render(this.#renderItem(item, index))).join('');
237
+ }
238
+ }
239
+
240
+ export function virtualList(items, options) {
241
+ return new VirtualListNode(items, options);
242
+ }
@@ -0,0 +1,138 @@
1
+ import { Renderable } from '../renderable/renderable.js';
2
+ import { Renderer } from '../renderable/renderer.js';
3
+ import { createComment, clearBetween } from './dom.js';
4
+ import { isState, isStatePath, readState, subscribeState } from '../reactivity/state.js';
5
+ import { isSignal, readSignal, subscribeSignal } from '../reactivity/signal.js';
6
+
7
+ const WHEN = Symbol('zb.when');
8
+
9
+ function isValidAttributeValue(value) {
10
+ if (value == null) return true;
11
+ const type = typeof value;
12
+ if (type === 'string' || type === 'number' || type === 'boolean') return true;
13
+ if (type === 'object' && !Array.isArray(value)) return true;
14
+ return false;
15
+ }
16
+
17
+ export class WhenNode extends Renderable {
18
+ #source;
19
+ #renderTrue;
20
+ #renderFalse;
21
+ #start = null;
22
+ #end = null;
23
+ #mounted = false;
24
+ #unsub = null;
25
+ #mountedValues = [];
26
+
27
+ constructor(source, renderTrue, renderFalse) {
28
+ super();
29
+ this.#source = source;
30
+ this.#renderTrue = renderTrue;
31
+ this.#renderFalse = renderFalse;
32
+ Object.defineProperty(this, WHEN, { value: true });
33
+ }
34
+
35
+ mountInto(parent, beforeNode) {
36
+ if (this.#mounted) return;
37
+ this.#mounted = true;
38
+ this.#start = createComment('zb:when:start', 'when');
39
+ this.#end = createComment('zb:when:end', 'when');
40
+ parent.insertBefore(this.#start, beforeNode);
41
+ parent.insertBefore(this.#end, beforeNode);
42
+
43
+ this.#update();
44
+ this.#wire();
45
+ }
46
+
47
+ unmount() {
48
+ if (!this.#mounted) return;
49
+ this.#mounted = false;
50
+ if (this.#unsub) this.#unsub();
51
+ this.#unsub = null;
52
+ this.#cleanup();
53
+ if (this.#start && this.#end) {
54
+ clearBetween(this.#start, this.#end);
55
+ this.#start.remove();
56
+ this.#end.remove();
57
+ }
58
+ this.#start = null;
59
+ this.#end = null;
60
+ }
61
+
62
+ #wire() {
63
+ if (isState(this.#source) || isStatePath(this.#source)) {
64
+ this.#unsub = subscribeState(this.#source, () => this.#update());
65
+ return;
66
+ }
67
+ if (isSignal(this.#source)) {
68
+ this.#unsub = subscribeSignal(this.#source, () => this.#update());
69
+ }
70
+ }
71
+
72
+ #read() {
73
+ if (isState(this.#source) || isStatePath(this.#source)) return !!readState(this.#source);
74
+ if (isSignal(this.#source)) return !!readSignal(this.#source);
75
+ return !!this.#source;
76
+ }
77
+
78
+ readValue() {
79
+ const predicate = this.#read();
80
+ const value = predicate ? this.#renderTrue() : this.#renderFalse?.();
81
+ if (Renderer.isRenderable(value) || Renderer.isDomNode(value)) return undefined;
82
+ if (!isValidAttributeValue(value)) return undefined;
83
+ return value;
84
+ }
85
+
86
+ subscribeValue(fn) {
87
+ if (isState(this.#source) || isStatePath(this.#source)) {
88
+ return subscribeState(this.#source, () => fn(this.readValue()));
89
+ }
90
+ if (isSignal(this.#source)) {
91
+ return subscribeSignal(this.#source, () => fn(this.readValue()));
92
+ }
93
+ return null;
94
+ }
95
+
96
+ #cleanup() {
97
+ for (const r of this.#mountedValues) Renderer.unmount(r);
98
+ this.#mountedValues = [];
99
+ if (this.#start && this.#end) clearBetween(this.#start, this.#end);
100
+ }
101
+
102
+ #update() {
103
+ this.#cleanup();
104
+ const predicate = this.#read();
105
+ const value = predicate ? this.#renderTrue() : this.#renderFalse?.();
106
+ const values = Renderer.normalize(value);
107
+ this.#mountedValues = values;
108
+ for (const r of values) {
109
+ if (Renderer.isRenderable(r)) {
110
+ r.mountInto(this.#end.parentNode, this.#end);
111
+ } else if (Renderer.isDomNode(r)) {
112
+ this.#end.parentNode.insertBefore(r, this.#end);
113
+ }
114
+ }
115
+ }
116
+
117
+ renderToString(render) {
118
+ const predicate = this.#read();
119
+ const value = predicate ? this.#renderTrue() : this.#renderFalse?.();
120
+ return render(value);
121
+ }
122
+ }
123
+
124
+ export function when(source, renderTrue, renderFalse) {
125
+ return new WhenNode(source, renderTrue, renderFalse);
126
+ }
127
+
128
+ export function isWhen(value) {
129
+ return !!value && value[WHEN] === true;
130
+ }
131
+
132
+ export function readWhenValue(value) {
133
+ return value?.readValue?.();
134
+ }
135
+
136
+ export function subscribeWhenValue(value, fn) {
137
+ return value?.subscribeValue?.(fn);
138
+ }
@@ -0,0 +1,97 @@
1
+ /**
2
+ * Minimal before/after event hub.
3
+ *
4
+ * - `before` handlers may return `false` to cancel the operation.
5
+ * - `after` handlers are fire-and-forget.
6
+ */
7
+ export class EventHub {
8
+ #before = new Map(); // type -> Set<fn>
9
+ #after = new Map(); // type -> Set<fn>
10
+ #afterAny = new Set();
11
+
12
+ /**
13
+ * @param {'before'|'after'} phase
14
+ * @param {string} type
15
+ * @param {(payload: any, ctx: any) => (void|boolean)} fn
16
+ * @returns {() => void}
17
+ */
18
+ on(phase, type, fn) {
19
+ const map = phase === 'before' ? this.#before : this.#after;
20
+ if (phase === 'after' && type === '*') {
21
+ this.#afterAny.add(fn);
22
+ return () => this.#afterAny.delete(fn);
23
+ }
24
+ let set = map.get(type);
25
+ if (!set) {
26
+ set = new Set();
27
+ map.set(type, set);
28
+ }
29
+ set.add(fn);
30
+ return () => set.delete(fn);
31
+ }
32
+
33
+ /**
34
+ * Emits a before event. Returns false when cancelled.
35
+ * @param {string} type
36
+ * @param {any} payload
37
+ * @param {any} ctx
38
+ * @returns {boolean}
39
+ */
40
+ emitBefore(type, payload, ctx) {
41
+ const set = this.#before.get(type);
42
+ if (!set) return true;
43
+ for (const fn of set) {
44
+ const r = fn(payload, ctx);
45
+ if (r === false) return false;
46
+ }
47
+ return true;
48
+ }
49
+
50
+ /**
51
+ * Emits an after event.
52
+ * @param {string} type
53
+ * @param {any} payload
54
+ * @param {any} ctx
55
+ */
56
+ emitAfter(type, payload, ctx) {
57
+ const set = this.#after.get(type);
58
+ if (set) {
59
+ for (const fn of set) fn(payload, ctx);
60
+ }
61
+ for (const fn of this.#afterAny) fn(payload, ctx);
62
+ }
63
+
64
+ /**
65
+ * Returns a fluent API for registering hooks.
66
+ * @param {'before'|'after'} phase
67
+ */
68
+ phase(phase) {
69
+ const hub = this;
70
+ const api = {
71
+ /**
72
+ * Registers a handler for a given type.
73
+ * @param {string} type
74
+ * @param {(payload: any, ctx: any) => (void|boolean)} fn
75
+ */
76
+ on(type, fn) {
77
+ return hub.on(phase, type, fn);
78
+ },
79
+ /**
80
+ * Registers a handler for any type.
81
+ * @param {(payload: any, ctx: any) => (void|boolean)} fn
82
+ */
83
+ any(fn) {
84
+ return hub.on(phase, '*', fn);
85
+ },
86
+ };
87
+
88
+ return new Proxy(api, {
89
+ get(target, prop) {
90
+ if (typeof prop !== 'string') return target[prop];
91
+ if (prop in target) return target[prop];
92
+ return (fn) => hub.on(phase, prop, fn);
93
+ },
94
+ });
95
+ }
96
+ }
97
+
@@ -0,0 +1,127 @@
1
+ import { state } from '../reactivity/state.js';
2
+ import { after } from '../reactivity/observe.js';
3
+
4
+ function isObject(value) {
5
+ return value !== null && typeof value === 'object';
6
+ }
7
+
8
+ function cloneValue(value) {
9
+ if (!isObject(value)) return value;
10
+ if (Array.isArray(value)) return value.map(cloneValue);
11
+ const out = {};
12
+ for (const [k, v] of Object.entries(value)) {
13
+ out[k] = cloneValue(v);
14
+ }
15
+ return out;
16
+ }
17
+
18
+ function deepEqual(a, b) {
19
+ if (a === b) return true;
20
+ if (typeof a !== typeof b) return false;
21
+ if (!isObject(a) || !isObject(b)) return false;
22
+ if (Array.isArray(a) !== Array.isArray(b)) return false;
23
+ if (Array.isArray(a)) {
24
+ if (a.length !== b.length) return false;
25
+ for (let i = 0; i < a.length; i++) {
26
+ if (!deepEqual(a[i], b[i])) return false;
27
+ }
28
+ return true;
29
+ }
30
+ const aKeys = Object.keys(a);
31
+ const bKeys = Object.keys(b);
32
+ if (aKeys.length !== bKeys.length) return false;
33
+ for (const k of aKeys) {
34
+ if (!Object.prototype.hasOwnProperty.call(b, k)) return false;
35
+ if (!deepEqual(a[k], b[k])) return false;
36
+ }
37
+ return true;
38
+ }
39
+
40
+ function mergeErrors(target, value) {
41
+ if (value == null || value === true) return target;
42
+ if (value === false) {
43
+ target._form = true;
44
+ return target;
45
+ }
46
+ if (typeof value === 'string') {
47
+ target._form = value;
48
+ return target;
49
+ }
50
+ if (isObject(value)) {
51
+ for (const [k, v] of Object.entries(value)) {
52
+ target[k] = v;
53
+ }
54
+ return target;
55
+ }
56
+ return target;
57
+ }
58
+
59
+ export function form(initial) {
60
+ const initialSnapshot = cloneValue(initial);
61
+ const values = state(cloneValue(initial));
62
+ const meta = state({});
63
+ const errors = state({});
64
+ const touched = state({});
65
+ const dirty = state(false);
66
+ const validators = new Set();
67
+
68
+ let runId = 0;
69
+
70
+ const runValidators = () => {
71
+ const current = ++runId;
72
+ const nextErrors = {};
73
+ const snapshot = values.get();
74
+ const tasks = [];
75
+
76
+ for (const validator of validators) {
77
+ try {
78
+ const result = validator(snapshot);
79
+ if (result && typeof result.then === 'function') {
80
+ tasks.push(
81
+ result.then((value) => {
82
+ mergeErrors(nextErrors, value);
83
+ })
84
+ );
85
+ } else {
86
+ mergeErrors(nextErrors, result);
87
+ }
88
+ } catch (err) {
89
+ mergeErrors(nextErrors, err?.message || true);
90
+ }
91
+ }
92
+
93
+ if (tasks.length) {
94
+ Promise.all(tasks).then(() => {
95
+ if (current !== runId) return;
96
+ errors.set(nextErrors);
97
+ });
98
+ return;
99
+ }
100
+
101
+ errors.set(nextErrors);
102
+ };
103
+
104
+ after(values).change(() => {
105
+ const isDirty = !deepEqual(values.get(), initialSnapshot);
106
+ if (dirty.get() !== isDirty) dirty.set(isDirty);
107
+ if (validators.size) runValidators();
108
+ });
109
+
110
+ const reset = () => {
111
+ values.set(cloneValue(initialSnapshot));
112
+ touched.set({});
113
+ errors.set({});
114
+ dirty.set(false);
115
+ meta.set({});
116
+ };
117
+
118
+ return {
119
+ values,
120
+ meta,
121
+ errors,
122
+ touched,
123
+ dirty,
124
+ validators,
125
+ reset,
126
+ };
127
+ }
@@ -0,0 +1,5 @@
1
+ export const INTERNAL = Object.freeze({
2
+ instrumentBoundProp: Symbol('zb.instrumentBoundProp'),
3
+ subscribeProp: Symbol('zb.subscribeProp'),
4
+ });
5
+