@neovici/cosmoz-form 2.1.0 → 2.2.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 (40) hide show
  1. package/dist/async-rule.d.ts +83 -0
  2. package/dist/async-rule.d.ts.map +1 -0
  3. package/dist/async-rule.js +20 -0
  4. package/dist/index.d.ts +13 -5
  5. package/dist/index.d.ts.map +1 -1
  6. package/dist/index.js +18 -5
  7. package/dist/make-debounce-runner.d.ts +4 -0
  8. package/dist/make-debounce-runner.d.ts.map +1 -0
  9. package/dist/make-debounce-runner.js +50 -0
  10. package/dist/make-take-latest-runner.d.ts +4 -0
  11. package/dist/make-take-latest-runner.d.ts.map +1 -0
  12. package/dist/make-take-latest-runner.js +27 -0
  13. package/dist/test/make-debounce-runner.test.d.ts +2 -0
  14. package/dist/test/make-debounce-runner.test.d.ts.map +1 -0
  15. package/dist/test/make-debounce-runner.test.js +123 -0
  16. package/dist/test/make-take-latest-runner.test.d.ts +2 -0
  17. package/dist/test/make-take-latest-runner.test.d.ts.map +1 -0
  18. package/dist/test/make-take-latest-runner.test.js +110 -0
  19. package/dist/test/use-async-form-core.test.d.ts +2 -0
  20. package/dist/test/use-async-form-core.test.d.ts.map +1 -0
  21. package/dist/test/use-async-form-core.test.js +238 -0
  22. package/dist/test/use-async-rules.test.d.ts +2 -0
  23. package/dist/test/use-async-rules.test.d.ts.map +1 -0
  24. package/dist/test/use-async-rules.test.js +180 -0
  25. package/dist/types/index.d.ts +1 -1
  26. package/dist/types/index.d.ts.map +1 -1
  27. package/dist/use-async-form-core.d.ts +21 -0
  28. package/dist/use-async-form-core.d.ts.map +1 -0
  29. package/dist/use-async-form-core.js +71 -0
  30. package/dist/use-items/use-async-rules.d.ts +17 -0
  31. package/dist/use-items/use-async-rules.d.ts.map +1 -0
  32. package/dist/use-items/use-async-rules.js +78 -0
  33. package/dist/use-items/use-items.d.ts.map +1 -1
  34. package/dist/use-items/use-items.js +2 -2
  35. package/dist/use-validated-form$.d.ts +4 -1
  36. package/dist/use-validated-form$.d.ts.map +1 -1
  37. package/dist/use-validated-form$.js +4 -1
  38. package/dist/validation/rules.d.ts.map +1 -1
  39. package/dist/validation/rules.js +7 -4
  40. package/package.json +6 -5
@@ -0,0 +1,238 @@
1
+ import { renderHook } from '@neovici/testing';
2
+ import { assert, waitUntil } from '@open-wc/testing';
3
+ import { spy } from 'sinon';
4
+ import { delay } from '../async-rule';
5
+ import { makeDebounceRunner } from '../make-debounce-runner';
6
+ import { touched } from '../touch';
7
+ import { useAsyncFormCore } from '../use-async-form-core';
8
+ import { useForm } from '../use-form';
9
+ // ── Helpers ───────────────────────────────────────────────────────────────────
10
+ const tick = (ms = 0) => new Promise((r) => setTimeout(r, ms));
11
+ // ── Suite ─────────────────────────────────────────────────────────────────────
12
+ suite('useAsyncFormCore', () => {
13
+ // Basic rule: when name changes, set city = name + '-city'
14
+ const cityFromName = [
15
+ async (current) => ({ city: current.name + '-city' }),
16
+ ({ name }) => [name],
17
+ ];
18
+ test('rule runs on initialization (prev deps are null → runs)', async () => {
19
+ const { result } = await renderHook(() => {
20
+ const form = useForm({ name: 'Alice', city: '' });
21
+ useAsyncFormCore(form, [cityFromName]);
22
+ return form;
23
+ });
24
+ await waitUntil(() => result.current.values.city === 'Alice-city');
25
+ });
26
+ test('rule does NOT run again if deps unchanged (onChange with unrelated field)', async () => {
27
+ const { result } = await renderHook(() => {
28
+ const form = useForm({ name: 'Alice', city: '' });
29
+ useAsyncFormCore(form, [cityFromName]);
30
+ return form;
31
+ });
32
+ await waitUntil(() => result.current.values.city === 'Alice-city');
33
+ // change city manually — name (dep) didn't change so rule should NOT re-run
34
+ result.current.onChange({ city: 'manual' });
35
+ await tick(50); // give rule time to NOT re-run
36
+ assert.equal(result.current.values.city, 'manual');
37
+ });
38
+ test('rule runs again when deps change (name changes → city updated)', async () => {
39
+ const { result } = await renderHook(() => {
40
+ const form = useForm({ name: 'Alice', city: '' });
41
+ useAsyncFormCore(form, [cityFromName]);
42
+ return form;
43
+ });
44
+ await waitUntil(() => result.current.values.city === 'Alice-city');
45
+ result.current.onChange({ name: 'Bob' }); // dep changed
46
+ await waitUntil(() => result.current.values.city === 'Bob-city');
47
+ });
48
+ test('takeLatest — slow rule is cancelled when deps change again', async () => {
49
+ const slowRule = [
50
+ async (current, { signal }) => {
51
+ await delay(signal, 200);
52
+ return { city: current.name + '-slow' };
53
+ },
54
+ ({ name }) => [name],
55
+ ];
56
+ const { result } = await renderHook(() => {
57
+ const form = useForm({ name: 'Alice', city: '' });
58
+ useAsyncFormCore(form, [slowRule]);
59
+ return form;
60
+ });
61
+ // initial rule for Alice kicked off (slow, 200ms)
62
+ await tick(10); // let Alice rule start
63
+ result.current.onChange({ name: 'Bob' }); // triggers second rule, cancels first
64
+ await waitUntil(() => result.current.values.city === 'Bob-slow', 'Bob-slow should appear', { timeout: 2000 });
65
+ // Alice-slow must never appear
66
+ assert.notEqual(result.current.values.city, 'Alice-slow');
67
+ });
68
+ test('opts.update() fires intermediate patch before rule completes', async () => {
69
+ const loadingRule = [
70
+ async (current, { update, signal }) => {
71
+ update({ city: 'loading...' });
72
+ await delay(signal, 50);
73
+ return { city: current.name + '-done' };
74
+ },
75
+ ({ name }) => [name],
76
+ ];
77
+ const { result } = await renderHook(() => {
78
+ const form = useForm({ name: 'Alice', city: '' });
79
+ useAsyncFormCore(form, [loadingRule]);
80
+ return form;
81
+ });
82
+ // loading patch fires before the delay
83
+ await waitUntil(() => result.current.values.city === 'loading...');
84
+ // then the final patch arrives after the delay
85
+ await waitUntil(() => result.current.values.city === 'Alice-done', 'Alice-done should appear', { timeout: 2000 });
86
+ });
87
+ test('async patches do not set form.touched', async () => {
88
+ const { result } = await renderHook(() => {
89
+ const form = useForm({ name: 'Alice', city: '' });
90
+ useAsyncFormCore(form, [cityFromName]);
91
+ return form;
92
+ });
93
+ await waitUntil(() => result.current.values.city === 'Alice-city');
94
+ assert.isFalse(touched(result.current.values));
95
+ });
96
+ test('onError called with (err, rule) when rule throws', async () => {
97
+ const boom = [
98
+ async () => {
99
+ throw new Error('rule boom');
100
+ },
101
+ ({ name }) => [name],
102
+ ];
103
+ const onError = spy();
104
+ await renderHook(() => {
105
+ const form = useForm({ name: 'Alice', city: '' });
106
+ useAsyncFormCore(form, [boom], { onError });
107
+ return form;
108
+ });
109
+ await waitUntil(() => onError.called, 'onError should be called');
110
+ assert.instanceOf(onError.args[0][0], Error);
111
+ assert.equal(onError.args[0][0].message, 'rule boom');
112
+ assert.equal(onError.args[0][1], boom);
113
+ });
114
+ test('uses runner factory from the rule — debounce semantics observed', async () => {
115
+ const debounceRule = [
116
+ async (current) => ({ city: current.name + '-debounced' }),
117
+ ({ name }) => [name],
118
+ () => makeDebounceRunner(100),
119
+ ];
120
+ const { result } = await renderHook(() => {
121
+ const form = useForm({ name: 'Alice', city: '' });
122
+ useAsyncFormCore(form, [debounceRule]);
123
+ return form;
124
+ });
125
+ // Two rapid dep changes within the 100ms debounce window
126
+ result.current.onChange({ name: 'Bob' });
127
+ await tick(30); // within debounce window
128
+ result.current.onChange({ name: 'Carol' });
129
+ // Wait long enough for the debounce to fire and rule to complete
130
+ await waitUntil(() => result.current.values.city !== '', 'city should be set', { timeout: 2000 });
131
+ // Only Carol's rule should have applied — Bob's was debounced away
132
+ assert.equal(result.current.values.city, 'Carol-debounced');
133
+ assert.notEqual(result.current.values.city, 'Bob-debounced');
134
+ });
135
+ test('processing is false initially', async () => {
136
+ const { result } = await renderHook(() => {
137
+ const form = useForm({ name: 'Alice', city: '' });
138
+ return useAsyncFormCore(form, [cityFromName]);
139
+ });
140
+ await waitUntil(() => result.current.processing === false);
141
+ assert.isFalse(result.current.processing);
142
+ });
143
+ test('processing is true while a rule is in flight', async () => {
144
+ const slowRule = [
145
+ async (current, { signal }) => {
146
+ await delay(signal, 100);
147
+ return { city: current.name + '-slow' };
148
+ },
149
+ ({ name }) => [name],
150
+ ];
151
+ const { result } = await renderHook(() => {
152
+ const form = useForm({ name: 'Alice', city: '' });
153
+ return useAsyncFormCore(form, [slowRule]);
154
+ });
155
+ await waitUntil(() => result.current.processing === true);
156
+ assert.isTrue(result.current.processing);
157
+ await waitUntil(() => result.current.processing === false, undefined, {
158
+ timeout: 2000,
159
+ });
160
+ assert.isFalse(result.current.processing);
161
+ });
162
+ test('processing stays true while multiple rules are in flight', async () => {
163
+ const slowRule1 = [
164
+ async (current, { signal }) => {
165
+ await delay(signal, 80);
166
+ return { city: current.name + '-1' };
167
+ },
168
+ ({ name }) => [name],
169
+ ];
170
+ const slowRule2 = [
171
+ async (current, { signal }) => {
172
+ await delay(signal, 160);
173
+ return { city: current.name + '-2' };
174
+ },
175
+ ({ name }) => [name],
176
+ ];
177
+ const { result } = await renderHook(() => {
178
+ const form = useForm({ name: 'Alice', city: '' });
179
+ return useAsyncFormCore(form, [slowRule1, slowRule2]);
180
+ });
181
+ await waitUntil(() => result.current.processing === true);
182
+ // After first settles (~80ms) processing must still be true (second still running)
183
+ await tick(100);
184
+ assert.isTrue(result.current.processing);
185
+ // After both settle — processing false
186
+ await waitUntil(() => result.current.processing === false, undefined, {
187
+ timeout: 2000,
188
+ });
189
+ assert.isFalse(result.current.processing);
190
+ });
191
+ test('processing returns to false when a rule is cancelled (runner returns null)', async () => {
192
+ const slowRule = [
193
+ async (current, { signal }) => {
194
+ await delay(signal, 200);
195
+ return { city: current.name + '-slow' };
196
+ },
197
+ ({ name }) => [name],
198
+ ];
199
+ const { result } = await renderHook(() => {
200
+ const form = useForm({ name: 'Alice', city: '' });
201
+ return useAsyncFormCore(form, [slowRule]);
202
+ });
203
+ await waitUntil(() => result.current.processing === true);
204
+ // Trigger a new dep change — cancels previous rule, starts a new one
205
+ result.current.onChange({ name: 'Bob' });
206
+ await waitUntil(() => result.current.processing === false, undefined, {
207
+ timeout: 2000,
208
+ });
209
+ assert.isFalse(result.current.processing);
210
+ });
211
+ test('cleanup — no state update after unmount', async () => {
212
+ const slowRule = [
213
+ async (current, { signal }) => {
214
+ await delay(signal, 100);
215
+ return { city: current.name + '-after-unmount' };
216
+ },
217
+ ({ name }) => [name],
218
+ ];
219
+ const onChangeSpy = spy();
220
+ const { unmount } = await renderHook(() => {
221
+ const form = useForm({ name: 'Alice', city: '' });
222
+ const wrappedForm = {
223
+ ...form,
224
+ onChange: (...args) => {
225
+ onChangeSpy(...args);
226
+ return form.onChange(...args);
227
+ },
228
+ };
229
+ useAsyncFormCore(wrappedForm, [slowRule]);
230
+ return wrappedForm;
231
+ });
232
+ await tick(10); // mid-delay
233
+ const callsBefore = onChangeSpy.callCount;
234
+ unmount(); // cancel in-flight rule
235
+ await tick(200); // wait for what would have been the delay
236
+ assert.equal(onChangeSpy.callCount, callsBefore, 'no onChange after unmount');
237
+ });
238
+ });
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=use-async-rules.test.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"use-async-rules.test.d.ts","sourceRoot":"","sources":["../../src/test/use-async-rules.test.ts"],"names":[],"mappings":""}
@@ -0,0 +1,180 @@
1
+ import { renderHook } from '@neovici/testing';
2
+ import { assert, waitUntil } from '@open-wc/testing';
3
+ import { spy } from 'sinon';
4
+ import { delay } from '../async-rule';
5
+ import { makeDebounceRunner } from '../make-debounce-runner';
6
+ import { useItems } from '../use-items';
7
+ import { useAsyncRules } from '../use-items/use-async-rules';
8
+ // ── Helpers ───────────────────────────────────────────────────────────────────
9
+ const tick = (ms = 0) => new Promise((r) => setTimeout(r, ms));
10
+ // ── Suite ─────────────────────────────────────────────────────────────────────
11
+ suite('useAsyncRules (items)', () => {
12
+ // Basic rule: derived = name + '-' + idx
13
+ const derivedFromName = [
14
+ async (current, { index }) => ({ derived: current.name + '-' + index }),
15
+ ({ name }, idx) => [name, idx],
16
+ ];
17
+ test('rule runs per-item on initialization', async () => {
18
+ const { result } = await renderHook(() => {
19
+ const core = useItems({
20
+ initial: [{ name: 'A' }, { name: 'B' }],
21
+ });
22
+ useAsyncRules(core.items, [derivedFromName], core.update);
23
+ return core;
24
+ });
25
+ await waitUntil(() => result.current.items[0]?.derived !== undefined &&
26
+ result.current.items[1]?.derived !== undefined);
27
+ assert.equal(result.current.items[0].derived, 'A-0');
28
+ assert.equal(result.current.items[1].derived, 'B-1');
29
+ });
30
+ test('rule result applied to correct item index', async () => {
31
+ const { result } = await renderHook(() => {
32
+ const core = useItems({
33
+ initial: [{ name: 'X' }, { name: 'Y' }],
34
+ });
35
+ useAsyncRules(core.items, [derivedFromName], core.update);
36
+ return core;
37
+ });
38
+ await waitUntil(() => result.current.items[0]?.derived !== undefined);
39
+ assert.equal(result.current.items[0].derived, 'X-0');
40
+ assert.equal(result.current.items[1].derived, 'Y-1');
41
+ });
42
+ test('rule does not run for items whose deps are unchanged', async () => {
43
+ const { result } = await renderHook(() => {
44
+ const core = useItems({ initial: [{ name: 'A' }] });
45
+ useAsyncRules(core.items, [derivedFromName], core.update);
46
+ return core;
47
+ });
48
+ await waitUntil(() => result.current.items[0]?.derived === 'A-0');
49
+ // Manually set derived to 'manual', then trigger a re-render without changing name/idx
50
+ result.current.update(0, { derived: 'manual' });
51
+ await tick(50); // give rule time to NOT re-run
52
+ assert.equal(result.current.items[0].derived, 'manual');
53
+ });
54
+ test('changing item[0] triggers rule for item[0] only, not item[1]', async () => {
55
+ const runCounts = { 0: 0, 1: 0 };
56
+ const countingRule = [
57
+ async (current, { index }) => {
58
+ runCounts[index]++;
59
+ return { derived: current.name + '-' + index };
60
+ },
61
+ ({ name }, idx) => [name, idx],
62
+ ];
63
+ const { result } = await renderHook(() => {
64
+ const core = useItems({
65
+ initial: [{ name: 'A' }, { name: 'B' }],
66
+ });
67
+ useAsyncRules(core.items, [countingRule], core.update);
68
+ return core;
69
+ });
70
+ await waitUntil(() => result.current.items[0]?.derived !== undefined);
71
+ const runBefore1 = runCounts[1];
72
+ result.current.update(0, { name: 'A2' }); // only item[0] changes
73
+ await waitUntil(() => result.current.items[0].derived === 'A2-0');
74
+ assert.equal(runCounts[1], runBefore1);
75
+ });
76
+ test('per-item takeLatest — item[0] rule cancelled independently of item[1]', async () => {
77
+ const slowRule = [
78
+ async (current, { signal, index }) => {
79
+ await delay(signal, 150);
80
+ return { derived: current.name + '-slow-' + index };
81
+ },
82
+ ({ name }, idx) => [name, idx],
83
+ ];
84
+ const { result } = await renderHook(() => {
85
+ const core = useItems({
86
+ initial: [{ name: 'A' }, { name: 'B' }],
87
+ });
88
+ useAsyncRules(core.items, [slowRule], core.update);
89
+ return core;
90
+ });
91
+ await tick(10);
92
+ // Change item[0] name mid-flight (cancels item[0] first rule)
93
+ result.current.update(0, { name: 'A2' });
94
+ await waitUntil(() => result.current.items[0]?.derived === 'A2-slow-0', 'A2-slow-0 should appear', { timeout: 2000 });
95
+ assert.equal(result.current.items[1].derived, 'B-slow-1');
96
+ assert.notEqual(result.current.items[0].derived, 'A-slow-0');
97
+ });
98
+ test('intermediate opts.update patch applied to correct item only', async () => {
99
+ const loadingRule = [
100
+ async (current, { update, signal, index }) => {
101
+ update({ derived: 'loading-' + index });
102
+ await delay(signal, 50);
103
+ return { derived: current.name + '-done-' + index };
104
+ },
105
+ ({ name }, idx) => [name, idx],
106
+ ];
107
+ const { result } = await renderHook(() => {
108
+ const core = useItems({
109
+ initial: [{ name: 'A' }, { name: 'B' }],
110
+ });
111
+ useAsyncRules(core.items, [loadingRule], core.update);
112
+ return core;
113
+ });
114
+ await waitUntil(() => result.current.items[0]?.derived === 'loading-0');
115
+ assert.equal(result.current.items[1].derived, 'loading-1');
116
+ await waitUntil(() => result.current.items[0]?.derived === 'A-done-0', undefined, { timeout: 2000 });
117
+ assert.equal(result.current.items[1].derived, 'B-done-1');
118
+ });
119
+ test('onError called with (err, rule, index)', async () => {
120
+ const boom = [
121
+ async () => {
122
+ throw new Error('item boom');
123
+ },
124
+ ({ name }, idx) => [name, idx],
125
+ ];
126
+ const onError = spy();
127
+ await renderHook(() => {
128
+ const core = useItems({ initial: [{ name: 'A' }] });
129
+ useAsyncRules(core.items, [boom], core.update, { onError });
130
+ return core;
131
+ });
132
+ await waitUntil(() => onError.called, 'onError should be called');
133
+ assert.instanceOf(onError.args[0][0], Error);
134
+ assert.equal(onError.args[0][1], boom);
135
+ assert.equal(onError.args[0][2], 0); // index
136
+ });
137
+ test('runner map grows when items are appended — new item rule runs', async () => {
138
+ const { result } = await renderHook(() => {
139
+ const core = useItems({ initial: [{ name: 'A' }] });
140
+ useAsyncRules(core.items, [derivedFromName], core.update);
141
+ return core;
142
+ });
143
+ await waitUntil(() => result.current.items[0]?.derived === 'A-0');
144
+ result.current.update(1, { name: 'B' }); // append
145
+ await waitUntil(() => result.current.items[1]?.derived === 'B-1');
146
+ });
147
+ test('uses runner factory from the rule — debounce semantics observed', async () => {
148
+ const debounceRule = [
149
+ async (current, { index }) => ({
150
+ derived: current.name + '-debounced-' + index,
151
+ }),
152
+ ({ name }, idx) => [name, idx],
153
+ () => makeDebounceRunner(100),
154
+ ];
155
+ const { result } = await renderHook(() => {
156
+ const core = useItems({ initial: [{ name: 'A' }] });
157
+ useAsyncRules(core.items, [debounceRule], core.update);
158
+ return core;
159
+ });
160
+ // Two rapid dep changes within the 100ms debounce window
161
+ result.current.update(0, { name: 'B' });
162
+ await tick(30);
163
+ result.current.update(0, { name: 'C' });
164
+ await waitUntil(() => result.current.items[0]?.derived !== undefined, 'derived should be set', { timeout: 2000 });
165
+ assert.equal(result.current.items[0].derived, 'C-debounced-0');
166
+ assert.notEqual(result.current.items[0].derived, 'B-debounced-0');
167
+ });
168
+ test('no stale runner for removed item — new item at index 0 gets a fresh rule', async () => {
169
+ const { result } = await renderHook(() => {
170
+ const core = useItems({
171
+ initial: [{ name: 'A' }, { name: 'B' }],
172
+ });
173
+ useAsyncRules(core.items, [derivedFromName], core.update);
174
+ return core;
175
+ });
176
+ await waitUntil(() => result.current.items[0]?.derived === 'A-0');
177
+ result.current.remove(0); // remove item[0], 'B' shifts to index 0
178
+ await waitUntil(() => result.current.items[0]?.derived === 'B-0', 'B-0 should appear after removal', { timeout: 2000 });
179
+ });
180
+ });
@@ -7,7 +7,7 @@ export type Invokable<T, F, V, R> = R | Invoked<T, F, V, R>;
7
7
  export type Resolvable<T, A extends unknown[] = []> = T | PromiseLike<T> | ((...args: A) => T | PromiseLike<T>);
8
8
  export type OnFocusFn<T, F, V> = (onChange: (value: V, touched?: boolean) => void, value: V, values: T, field: F) => (event: FocusEvent) => void;
9
9
  export type OnChange<T, K, V> = (update: (changes: Partial<T>, touched?: boolean) => void, id: K, value: V, values: T) => void;
10
- export type Rule<T extends object, K extends keyof T, V extends T[K]> = (value: V, values: T, field: Field<T, K, V>) => false | string;
10
+ export type Rule<T extends object, K extends keyof T, V extends T[K]> = (value: V, values?: T, field?: Field<T, K, V>) => false | string;
11
11
  export type Errors = {
12
12
  [key: string]: string | true | undefined;
13
13
  };
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/types/index.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/types/index.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,21 @@
1
+ import type { AsyncItemRule } from './async-rule';
2
+ import type { UseForm } from './use-form-core';
3
+ /**
4
+ * Composes with UseForm<T> to add async rules.
5
+ * Returns UseForm<T> & { processing } where processing is true while any
6
+ * async rule is in flight.
7
+ *
8
+ * Async patches call onChange(patch, false) — they do not mark the form touched.
9
+ * Intermediate patches (from opts.update(...)) go through onChange like any other
10
+ * patch — sync rules cascade on top of them, which is expected.
11
+ *
12
+ * Usage:
13
+ * const form = useValidatedForm({ fields, initial, rules });
14
+ * const { processing } = useAsyncFormCore(form, asyncRules);
15
+ */
16
+ export declare const useAsyncFormCore: <T extends object>(form: UseForm<T>, asyncRules: readonly AsyncItemRule<T>[] | undefined, opts?: {
17
+ onError?: (err: unknown, rule: AsyncItemRule<T>) => void;
18
+ }) => UseForm<T> & {
19
+ processing: boolean;
20
+ };
21
+ //# sourceMappingURL=use-async-form-core.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"use-async-form-core.d.ts","sourceRoot":"","sources":["../src/use-async-form-core.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,aAAa,EAAe,MAAM,cAAc,CAAC;AAE/D,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,iBAAiB,CAAC;AAU/C;;;;;;;;;;;;GAYG;AACH,eAAO,MAAM,gBAAgB,GAAI,CAAC,SAAS,MAAM,EAChD,MAAM,OAAO,CAAC,CAAC,CAAC,EAChB,YAAY,SAAS,aAAa,CAAC,CAAC,CAAC,EAAE,GAAG,SAAS,EACnD,OAAO;IAAE,OAAO,CAAC,EAAE,CAAC,GAAG,EAAE,OAAO,EAAE,IAAI,EAAE,aAAa,CAAC,CAAC,CAAC,KAAK,IAAI,CAAA;CAAE,KACjE,OAAO,CAAC,CAAC,CAAC,GAAG;IAAE,UAAU,EAAE,OAAO,CAAA;CAkEpC,CAAC"}
@@ -0,0 +1,71 @@
1
+ import { useEffect, useRef, useState } from '@pionjs/pion';
2
+ import { makeTakeLatestRunner } from './make-take-latest-runner';
3
+ const changed = (a, b) => a.length !== b.length || a.some((v, i) => !Object.is(v, b[i]));
4
+ const DEFAULT_ON_ERROR = (err) => {
5
+ // eslint-disable-next-line no-console
6
+ console.error('[cosmoz-form] async rule error:', err);
7
+ };
8
+ /**
9
+ * Composes with UseForm<T> to add async rules.
10
+ * Returns UseForm<T> & { processing } where processing is true while any
11
+ * async rule is in flight.
12
+ *
13
+ * Async patches call onChange(patch, false) — they do not mark the form touched.
14
+ * Intermediate patches (from opts.update(...)) go through onChange like any other
15
+ * patch — sync rules cascade on top of them, which is expected.
16
+ *
17
+ * Usage:
18
+ * const form = useValidatedForm({ fields, initial, rules });
19
+ * const { processing } = useAsyncFormCore(form, asyncRules);
20
+ */
21
+ export const useAsyncFormCore = (form, asyncRules, opts) => {
22
+ const onError = opts?.onError ?? DEFAULT_ON_ERROR;
23
+ // Refs persist across renders without triggering re-renders
24
+ const runnersRef = useRef(new Map());
25
+ const prevDepsRef = useRef(new Map());
26
+ // pendingCount tracks in-flight rules without causing re-renders itself.
27
+ // processing state is updated only on 0→1 and 1→0 transitions.
28
+ const pendingCount = useRef(0);
29
+ const [processing, setProcessing] = useState(false);
30
+ // Cleanup: cancel all in-flight rules on unmount
31
+ useEffect(() => () => {
32
+ for (const runner of runnersRef.current.values())
33
+ runner.cancel();
34
+ }, []);
35
+ // Dep-check + rule dispatch: runs after every values change
36
+ useEffect(() => {
37
+ if (!asyncRules?.length)
38
+ return;
39
+ for (const rule of asyncRules) {
40
+ const [ruleFn, depsFn, runnerFactory = makeTakeLatestRunner] = rule;
41
+ if (!runnersRef.current.has(rule)) {
42
+ runnersRef.current.set(rule, runnerFactory());
43
+ }
44
+ const deps = depsFn(form.values);
45
+ const prev = prevDepsRef.current.get(rule);
46
+ // Skip if deps unchanged (Object.is per element, same as applyRules)
47
+ if (prev != null && !changed(deps, prev)) {
48
+ continue;
49
+ }
50
+ prevDepsRef.current.set(rule, deps);
51
+ const runner = runnersRef.current.get(rule);
52
+ pendingCount.current++;
53
+ if (pendingCount.current === 1)
54
+ setProcessing(true);
55
+ runner
56
+ .run(ruleFn, form.values, (patch) => form.onChange(patch, false))
57
+ .then((result) => {
58
+ if (result !== null) {
59
+ form.onChange(result, false); // final: no touch
60
+ }
61
+ })
62
+ .catch((err) => onError(err, rule))
63
+ .finally(() => {
64
+ pendingCount.current--;
65
+ if (pendingCount.current === 0)
66
+ setProcessing(false);
67
+ });
68
+ }
69
+ }, [form.values]);
70
+ return { ...form, processing };
71
+ };
@@ -0,0 +1,17 @@
1
+ import type { AsyncItemRule } from '../async-rule';
2
+ import type { UseItemsCore } from './use-items';
3
+ /**
4
+ * Composes with useItemsCore / useItems to add async rules.
5
+ * One runner per (rule, itemIndex) pair — item rules are independent.
6
+ *
7
+ * Usage:
8
+ * const core = useItems({ initial, rules });
9
+ * useAsyncRules(core.items, asyncRules, core.update);
10
+ * return core;
11
+ */
12
+ export declare const useAsyncRules: <T extends object>(items: T[], asyncRules: readonly AsyncItemRule<T>[] | undefined, update: UseItemsCore<T>["update"], opts?: {
13
+ onError?: (err: unknown, rule: AsyncItemRule<T>, index: number) => void;
14
+ }) => {
15
+ processing: boolean;
16
+ };
17
+ //# sourceMappingURL=use-async-rules.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"use-async-rules.d.ts","sourceRoot":"","sources":["../../src/use-items/use-async-rules.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,aAAa,EAAe,MAAM,eAAe,CAAC;AAEhE,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,aAAa,CAAC;AAuBhD;;;;;;;;GAQG;AACH,eAAO,MAAM,aAAa,GAAI,CAAC,SAAS,MAAM,EAC7C,OAAO,CAAC,EAAE,EACV,YAAY,SAAS,aAAa,CAAC,CAAC,CAAC,EAAE,GAAG,SAAS,EACnD,QAAQ,YAAY,CAAC,CAAC,CAAC,CAAC,QAAQ,CAAC,EACjC,OAAO;IACN,OAAO,CAAC,EAAE,CAAC,GAAG,EAAE,OAAO,EAAE,IAAI,EAAE,aAAa,CAAC,CAAC,CAAC,EAAE,KAAK,EAAE,MAAM,KAAK,IAAI,CAAC;CACxE,KACC;IAAE,UAAU,EAAE,OAAO,CAAA;CA0EvB,CAAC"}
@@ -0,0 +1,78 @@
1
+ import { useEffect, useRef, useState } from '@pionjs/pion';
2
+ import { makeTakeLatestRunner } from '../make-take-latest-runner';
3
+ const changed = (a, b) => a.length !== b.length || a.some((v, i) => !Object.is(v, b[i]));
4
+ const DEFAULT_ON_ERROR = (err) => {
5
+ // eslint-disable-next-line no-console
6
+ console.error('[cosmoz-form] async rule error:', err);
7
+ };
8
+ const ensureRuleTracking = (rule, runnersRef, prevDepsRef) => {
9
+ if (runnersRef.current.has(rule))
10
+ return;
11
+ runnersRef.current.set(rule, new Map());
12
+ prevDepsRef.current.set(rule, new Map());
13
+ };
14
+ /**
15
+ * Composes with useItemsCore / useItems to add async rules.
16
+ * One runner per (rule, itemIndex) pair — item rules are independent.
17
+ *
18
+ * Usage:
19
+ * const core = useItems({ initial, rules });
20
+ * useAsyncRules(core.items, asyncRules, core.update);
21
+ * return core;
22
+ */
23
+ export const useAsyncRules = (items, asyncRules, update, opts) => {
24
+ const onError = opts?.onError ?? DEFAULT_ON_ERROR;
25
+ const pendingCount = useRef(0);
26
+ const [processing, setProcessing] = useState(false);
27
+ const runnersRef = useRef(new Map());
28
+ const prevDepsRef = useRef(new Map());
29
+ // Cleanup on unmount
30
+ useEffect(() => () => {
31
+ for (const perItem of runnersRef.current.values()) {
32
+ for (const runner of perItem.values()) {
33
+ runner.cancel();
34
+ }
35
+ }
36
+ }, []);
37
+ // Dep-check + rule dispatch: runs after every items change
38
+ useEffect(() => {
39
+ if (!asyncRules?.length) {
40
+ return;
41
+ }
42
+ for (const rule of asyncRules) {
43
+ const [ruleFn, depsFn, runnerFactory = makeTakeLatestRunner] = rule;
44
+ ensureRuleTracking(rule, runnersRef, prevDepsRef);
45
+ for (const [idx, item] of items.entries()) {
46
+ const runnersForRule = runnersRef.current.get(rule);
47
+ if (!runnersForRule.has(idx)) {
48
+ runnersForRule.set(idx, runnerFactory());
49
+ }
50
+ const deps = depsFn(item, idx);
51
+ const prev = prevDepsRef.current.get(rule).get(idx);
52
+ if (prev != null && !changed(deps, prev)) {
53
+ continue;
54
+ }
55
+ prevDepsRef.current.get(rule).set(idx, deps);
56
+ const runner = runnersForRule.get(idx);
57
+ pendingCount.current++;
58
+ if (pendingCount.current === 1)
59
+ setProcessing(true);
60
+ runner
61
+ .run(ruleFn, item, (patch) => update(idx, patch), // intermediate: no touch
62
+ { index: idx })
63
+ .then((result) => {
64
+ if (result !== null) {
65
+ update(idx, result);
66
+ }
67
+ })
68
+ .catch((err) => onError(err, rule, idx))
69
+ .finally(() => {
70
+ pendingCount.current--;
71
+ if (pendingCount.current === 0)
72
+ setProcessing(false);
73
+ });
74
+ }
75
+ }
76
+ }, [items]);
77
+ return { processing };
78
+ };