@neovici/cosmoz-form 2.1.1 → 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.
- package/dist/async-rule.d.ts +83 -0
- package/dist/async-rule.d.ts.map +1 -0
- package/dist/async-rule.js +20 -0
- package/dist/index.d.ts +13 -5
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +18 -5
- package/dist/make-debounce-runner.d.ts +4 -0
- package/dist/make-debounce-runner.d.ts.map +1 -0
- package/dist/make-debounce-runner.js +50 -0
- package/dist/make-take-latest-runner.d.ts +4 -0
- package/dist/make-take-latest-runner.d.ts.map +1 -0
- package/dist/make-take-latest-runner.js +27 -0
- package/dist/test/make-debounce-runner.test.d.ts +2 -0
- package/dist/test/make-debounce-runner.test.d.ts.map +1 -0
- package/dist/test/make-debounce-runner.test.js +123 -0
- package/dist/test/make-take-latest-runner.test.d.ts +2 -0
- package/dist/test/make-take-latest-runner.test.d.ts.map +1 -0
- package/dist/test/make-take-latest-runner.test.js +110 -0
- package/dist/test/use-async-form-core.test.d.ts +2 -0
- package/dist/test/use-async-form-core.test.d.ts.map +1 -0
- package/dist/test/use-async-form-core.test.js +238 -0
- package/dist/test/use-async-rules.test.d.ts +2 -0
- package/dist/test/use-async-rules.test.d.ts.map +1 -0
- package/dist/test/use-async-rules.test.js +180 -0
- package/dist/use-async-form-core.d.ts +21 -0
- package/dist/use-async-form-core.d.ts.map +1 -0
- package/dist/use-async-form-core.js +71 -0
- package/dist/use-items/use-async-rules.d.ts +17 -0
- package/dist/use-items/use-async-rules.d.ts.map +1 -0
- package/dist/use-items/use-async-rules.js +78 -0
- package/dist/use-items/use-items.d.ts.map +1 -1
- package/dist/use-items/use-items.js +2 -2
- package/dist/use-validated-form$.d.ts +4 -1
- package/dist/use-validated-form$.d.ts.map +1 -1
- package/dist/use-validated-form$.js +4 -1
- package/package.json +6 -5
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Options passed to every async rule function.
|
|
3
|
+
* @template T - the form/item value type
|
|
4
|
+
*/
|
|
5
|
+
export type AsyncOpts<T> = {
|
|
6
|
+
/** Emit an intermediate partial patch immediately (e.g. a loading state). */
|
|
7
|
+
update: (patch: Partial<T>) => void;
|
|
8
|
+
/**
|
|
9
|
+
* AbortSignal that fires when the runner cancels this invocation
|
|
10
|
+
* (takeLatest supersession, debounce discard, or unmount).
|
|
11
|
+
* Pass to `fetch()`, `delay()`, or any other cancellable operation.
|
|
12
|
+
*/
|
|
13
|
+
signal: AbortSignal;
|
|
14
|
+
/**
|
|
15
|
+
* Item index when running inside `useAsyncRules`.
|
|
16
|
+
* `undefined` when running inside `useAsyncFormCore`.
|
|
17
|
+
*/
|
|
18
|
+
index?: number;
|
|
19
|
+
};
|
|
20
|
+
/**
|
|
21
|
+
* Plain async function that computes a partial state update.
|
|
22
|
+
*
|
|
23
|
+
* `values` is a snapshot at invocation time.
|
|
24
|
+
*
|
|
25
|
+
* @template T - the form/item value type
|
|
26
|
+
* @param values - snapshot of current form/item values
|
|
27
|
+
* @param opts - cancellation signal, intermediate update callback, and item index
|
|
28
|
+
* @returns a partial patch to merge into state
|
|
29
|
+
* @example
|
|
30
|
+
* async function myRule(values, { update, signal }) {
|
|
31
|
+
* if (!values.supplierId) return { contactEmail: '' };
|
|
32
|
+
* update({ contactEmail: 'loading…' });
|
|
33
|
+
* const email = await fetchEmail(signal, values.supplierId);
|
|
34
|
+
* return { contactEmail: email };
|
|
35
|
+
* }
|
|
36
|
+
*/
|
|
37
|
+
export type AsyncRule<T> = (values: T, opts: AsyncOpts<T>) => Promise<Partial<T>>;
|
|
38
|
+
/**
|
|
39
|
+
* Callback invoked with intermediate partial patches during a rule run.
|
|
40
|
+
* @param patch - the intermediate partial state to apply immediately
|
|
41
|
+
*/
|
|
42
|
+
export type OnIntermediate<T> = (patch: Partial<T>) => void;
|
|
43
|
+
/**
|
|
44
|
+
* Shared interface for all async runner strategies.
|
|
45
|
+
* @template T - the form/item value type
|
|
46
|
+
*/
|
|
47
|
+
export type AsyncRunner<T> = {
|
|
48
|
+
/**
|
|
49
|
+
* Run `fn` with `values` as input.
|
|
50
|
+
*
|
|
51
|
+
* @param fn - the async rule to execute
|
|
52
|
+
* @param values - snapshot of current values passed to `fn`
|
|
53
|
+
* @param onIntermediate - wired to `opts.update` inside the rule
|
|
54
|
+
* @param opts - optional `index` forwarded to `opts.index` inside the rule
|
|
55
|
+
* @returns the resolved patch from `fn`, or `null` if cancelled
|
|
56
|
+
*/
|
|
57
|
+
run: (fn: AsyncRule<T>, values: T, onIntermediate: OnIntermediate<T>, opts?: {
|
|
58
|
+
index?: number;
|
|
59
|
+
}) => Promise<Partial<T> | null>;
|
|
60
|
+
/** Cancel the current in-flight run, resolving it as `null`. */
|
|
61
|
+
cancel: () => void;
|
|
62
|
+
};
|
|
63
|
+
/**
|
|
64
|
+
* Tuple describing a single async rule for use with `useAsyncRules`.
|
|
65
|
+
*
|
|
66
|
+
* @template T - the form/item value type
|
|
67
|
+
*/
|
|
68
|
+
export type AsyncItemRule<T> = readonly [
|
|
69
|
+
ruleFn: AsyncRule<T>,
|
|
70
|
+
depsFn: (current: T, index?: number) => unknown[],
|
|
71
|
+
runner?: () => AsyncRunner<T>
|
|
72
|
+
];
|
|
73
|
+
/**
|
|
74
|
+
* Cancellable delay. Rejects with `AbortError` if `signal` fires before `ms` elapses.
|
|
75
|
+
*
|
|
76
|
+
* @param signal - AbortSignal to cancel the delay early
|
|
77
|
+
* @param ms - milliseconds to wait
|
|
78
|
+
* @returns a Promise that resolves after `ms`, or rejects with `AbortError` if cancelled
|
|
79
|
+
* @example
|
|
80
|
+
* await delay(opts.signal, 400);
|
|
81
|
+
*/
|
|
82
|
+
export declare const delay: (signal: AbortSignal, ms: number) => Promise<void>;
|
|
83
|
+
//# sourceMappingURL=async-rule.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"async-rule.d.ts","sourceRoot":"","sources":["../src/async-rule.ts"],"names":[],"mappings":"AAAA;;;GAGG;AACH,MAAM,MAAM,SAAS,CAAC,CAAC,IAAI;IAC1B,6EAA6E;IAC7E,MAAM,EAAE,CAAC,KAAK,EAAE,OAAO,CAAC,CAAC,CAAC,KAAK,IAAI,CAAC;IACpC;;;;OAIG;IACH,MAAM,EAAE,WAAW,CAAC;IACpB;;;OAGG;IACH,KAAK,CAAC,EAAE,MAAM,CAAC;CACf,CAAC;AAEF;;;;;;;;;;;;;;;;GAgBG;AACH,MAAM,MAAM,SAAS,CAAC,CAAC,IAAI,CAC1B,MAAM,EAAE,CAAC,EACT,IAAI,EAAE,SAAS,CAAC,CAAC,CAAC,KACd,OAAO,CAAC,OAAO,CAAC,CAAC,CAAC,CAAC,CAAC;AAEzB;;;GAGG;AACH,MAAM,MAAM,cAAc,CAAC,CAAC,IAAI,CAAC,KAAK,EAAE,OAAO,CAAC,CAAC,CAAC,KAAK,IAAI,CAAC;AAE5D;;;GAGG;AACH,MAAM,MAAM,WAAW,CAAC,CAAC,IAAI;IAC5B;;;;;;;;OAQG;IACH,GAAG,EAAE,CACJ,EAAE,EAAE,SAAS,CAAC,CAAC,CAAC,EAChB,MAAM,EAAE,CAAC,EACT,cAAc,EAAE,cAAc,CAAC,CAAC,CAAC,EACjC,IAAI,CAAC,EAAE;QAAE,KAAK,CAAC,EAAE,MAAM,CAAA;KAAE,KACrB,OAAO,CAAC,OAAO,CAAC,CAAC,CAAC,GAAG,IAAI,CAAC,CAAC;IAChC,gEAAgE;IAChE,MAAM,EAAE,MAAM,IAAI,CAAC;CACnB,CAAC;AAEF;;;;GAIG;AACH,MAAM,MAAM,aAAa,CAAC,CAAC,IAAI,SAAS;IACvC,MAAM,EAAE,SAAS,CAAC,CAAC,CAAC;IACpB,MAAM,EAAE,CAAC,OAAO,EAAE,CAAC,EAAE,KAAK,CAAC,EAAE,MAAM,KAAK,OAAO,EAAE;IACjD,MAAM,CAAC,EAAE,MAAM,WAAW,CAAC,CAAC,CAAC;CAC7B,CAAC;AAEF;;;;;;;;GAQG;AACH,eAAO,MAAM,KAAK,GAAI,QAAQ,WAAW,EAAE,IAAI,MAAM,KAAG,OAAO,CAAC,IAAI,CAejE,CAAC"}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Cancellable delay. Rejects with `AbortError` if `signal` fires before `ms` elapses.
|
|
3
|
+
*
|
|
4
|
+
* @param signal - AbortSignal to cancel the delay early
|
|
5
|
+
* @param ms - milliseconds to wait
|
|
6
|
+
* @returns a Promise that resolves after `ms`, or rejects with `AbortError` if cancelled
|
|
7
|
+
* @example
|
|
8
|
+
* await delay(opts.signal, 400);
|
|
9
|
+
*/
|
|
10
|
+
export const delay = (signal, ms) => new Promise((resolve, reject) => {
|
|
11
|
+
if (signal.aborted) {
|
|
12
|
+
reject(new DOMException('Aborted', 'AbortError'));
|
|
13
|
+
return;
|
|
14
|
+
}
|
|
15
|
+
const t = setTimeout(resolve, ms);
|
|
16
|
+
signal.addEventListener('abort', () => {
|
|
17
|
+
clearTimeout(t);
|
|
18
|
+
reject(new DOMException('Aborted', 'AbortError'));
|
|
19
|
+
}, { once: true });
|
|
20
|
+
});
|
package/dist/index.d.ts
CHANGED
|
@@ -1,16 +1,24 @@
|
|
|
1
|
+
export * from './form-dialog';
|
|
1
2
|
export * from './helpers';
|
|
2
3
|
export * from './inputs';
|
|
3
4
|
export * from './render';
|
|
4
|
-
export * from './
|
|
5
|
+
export * from './touch';
|
|
5
6
|
export * from './use-form';
|
|
6
|
-
export * from './use-validated-form';
|
|
7
|
-
export * from './use-validated-form$';
|
|
8
7
|
export * from './use-items';
|
|
9
|
-
export * from './touch';
|
|
10
8
|
export * from './use-items-filter';
|
|
11
|
-
export * from './form
|
|
9
|
+
export * from './use-validated-form';
|
|
10
|
+
export * from './use-validated-form$';
|
|
11
|
+
export * from './validation';
|
|
12
12
|
export type * from './types';
|
|
13
13
|
export { useFormCore, type FormValues } from './use-form-core';
|
|
14
14
|
export { computeRules, useValidatedFormCore } from './use-validated-form-core';
|
|
15
15
|
export * from './add';
|
|
16
|
+
export { delay } from './async-rule';
|
|
17
|
+
export type { AsyncItemRule, AsyncOpts, AsyncRule, AsyncRunner, OnIntermediate, } from './async-rule';
|
|
18
|
+
export { makeDebounceRunner } from './make-debounce-runner';
|
|
19
|
+
export type { DebounceRunner } from './make-debounce-runner';
|
|
20
|
+
export { makeTakeLatestRunner } from './make-take-latest-runner';
|
|
21
|
+
export type { TakeLatestRunner } from './make-take-latest-runner';
|
|
22
|
+
export { useAsyncFormCore } from './use-async-form-core';
|
|
23
|
+
export { useAsyncRules } from './use-items/use-async-rules';
|
|
16
24
|
//# sourceMappingURL=index.d.ts.map
|
package/dist/index.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,cAAc,WAAW,CAAC;AAC1B,cAAc,UAAU,CAAC;AACzB,cAAc,UAAU,CAAC;AACzB,cAAc,
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,cAAc,eAAe,CAAC;AAC9B,cAAc,WAAW,CAAC;AAC1B,cAAc,UAAU,CAAC;AACzB,cAAc,UAAU,CAAC;AACzB,cAAc,SAAS,CAAC;AACxB,cAAc,YAAY,CAAC;AAC3B,cAAc,aAAa,CAAC;AAC5B,cAAc,oBAAoB,CAAC;AACnC,cAAc,sBAAsB,CAAC;AACrC,cAAc,uBAAuB,CAAC;AACtC,cAAc,cAAc,CAAC;AAE7B,mBAAmB,SAAS,CAAC;AAG7B,OAAO,EAAE,WAAW,EAAE,KAAK,UAAU,EAAE,MAAM,iBAAiB,CAAC;AAC/D,OAAO,EAAE,YAAY,EAAE,oBAAoB,EAAE,MAAM,2BAA2B,CAAC;AAG/E,cAAc,OAAO,CAAC;AAGtB,OAAO,EAAE,KAAK,EAAE,MAAM,cAAc,CAAC;AACrC,YAAY,EACX,aAAa,EACb,SAAS,EACT,SAAS,EACT,WAAW,EACX,cAAc,GACd,MAAM,cAAc,CAAC;AAQtB,OAAO,EAAE,kBAAkB,EAAE,MAAM,wBAAwB,CAAC;AAC5D,YAAY,EAAE,cAAc,EAAE,MAAM,wBAAwB,CAAC;AAC7D,OAAO,EAAE,oBAAoB,EAAE,MAAM,2BAA2B,CAAC;AACjE,YAAY,EAAE,gBAAgB,EAAE,MAAM,2BAA2B,CAAC;AAClE,OAAO,EAAE,gBAAgB,EAAE,MAAM,uBAAuB,CAAC;AACzD,OAAO,EAAE,aAAa,EAAE,MAAM,6BAA6B,CAAC"}
|
package/dist/index.js
CHANGED
|
@@ -1,16 +1,29 @@
|
|
|
1
|
+
export * from './form-dialog';
|
|
1
2
|
export * from './helpers';
|
|
2
3
|
export * from './inputs';
|
|
3
4
|
export * from './render';
|
|
4
|
-
export * from './
|
|
5
|
+
export * from './touch';
|
|
5
6
|
export * from './use-form';
|
|
6
|
-
export * from './use-validated-form';
|
|
7
|
-
export * from './use-validated-form$';
|
|
8
7
|
export * from './use-items';
|
|
9
|
-
export * from './touch';
|
|
10
8
|
export * from './use-items-filter';
|
|
11
|
-
export * from './form
|
|
9
|
+
export * from './use-validated-form';
|
|
10
|
+
export * from './use-validated-form$';
|
|
11
|
+
export * from './validation';
|
|
12
12
|
// Core hooks
|
|
13
13
|
export { useFormCore } from './use-form-core';
|
|
14
14
|
export { computeRules, useValidatedFormCore } from './use-validated-form-core';
|
|
15
15
|
// Add form utilities
|
|
16
16
|
export * from './add';
|
|
17
|
+
// Async rules
|
|
18
|
+
export { delay } from './async-rule';
|
|
19
|
+
// Async runners — choose the concurrency strategy that fits your use case:
|
|
20
|
+
// makeTakeLatestRunner cancels the previous run when a new one arrives (switchMap)
|
|
21
|
+
// makeDebounceRunner waits for silence before running; resets timer on each call (debounce + switchMap)
|
|
22
|
+
//
|
|
23
|
+
// Future ideas:
|
|
24
|
+
// makeThrottleRunner fires immediately, queues one trailing call (runs when in-flight finishes)
|
|
25
|
+
// makeExhaustRunner ignores new calls while one is in-flight (exhaustMap)
|
|
26
|
+
export { makeDebounceRunner } from './make-debounce-runner';
|
|
27
|
+
export { makeTakeLatestRunner } from './make-take-latest-runner';
|
|
28
|
+
export { useAsyncFormCore } from './use-async-form-core';
|
|
29
|
+
export { useAsyncRules } from './use-items/use-async-rules';
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"make-debounce-runner.d.ts","sourceRoot":"","sources":["../src/make-debounce-runner.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAa,WAAW,EAAE,MAAM,cAAc,CAAC;AAE3D,MAAM,MAAM,cAAc,CAAC,CAAC,IAAI,WAAW,CAAC,CAAC,CAAC,CAAC;AAE/C,eAAO,MAAM,kBAAkB,GAAI,CAAC,EAAE,IAAI,MAAM,KAAG,cAAc,CAAC,CAAC,CA4DlE,CAAC"}
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
export const makeDebounceRunner = (ms) => {
|
|
2
|
+
let timer = null;
|
|
3
|
+
let ac = null;
|
|
4
|
+
let pending = null;
|
|
5
|
+
return {
|
|
6
|
+
run: (fn, values, onIntermediate, opts) => new Promise((resolve, reject) => {
|
|
7
|
+
if (pending !== null) {
|
|
8
|
+
// Debounced-away call resolves null (same as cancelled in takeLatest)
|
|
9
|
+
pending.resolve(null);
|
|
10
|
+
clearTimeout(timer);
|
|
11
|
+
}
|
|
12
|
+
pending = { fn, values, index: opts?.index, resolve, reject };
|
|
13
|
+
timer = setTimeout(async () => {
|
|
14
|
+
const { fn: f, values: v, index, resolve: res, reject: rej, } = pending;
|
|
15
|
+
pending = null;
|
|
16
|
+
timer = null;
|
|
17
|
+
ac = new AbortController();
|
|
18
|
+
const asyncOpts = {
|
|
19
|
+
update: onIntermediate,
|
|
20
|
+
signal: ac.signal,
|
|
21
|
+
index,
|
|
22
|
+
};
|
|
23
|
+
try {
|
|
24
|
+
res(await f(v, asyncOpts));
|
|
25
|
+
}
|
|
26
|
+
catch (e) {
|
|
27
|
+
if (e instanceof DOMException && e.name === 'AbortError')
|
|
28
|
+
res(null);
|
|
29
|
+
else
|
|
30
|
+
rej(e);
|
|
31
|
+
}
|
|
32
|
+
finally {
|
|
33
|
+
ac = null;
|
|
34
|
+
}
|
|
35
|
+
}, ms);
|
|
36
|
+
}),
|
|
37
|
+
cancel: () => {
|
|
38
|
+
if (timer !== null) {
|
|
39
|
+
clearTimeout(timer);
|
|
40
|
+
timer = null;
|
|
41
|
+
}
|
|
42
|
+
if (pending !== null) {
|
|
43
|
+
pending.resolve(null);
|
|
44
|
+
pending = null;
|
|
45
|
+
}
|
|
46
|
+
ac?.abort();
|
|
47
|
+
ac = null;
|
|
48
|
+
},
|
|
49
|
+
};
|
|
50
|
+
};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"make-take-latest-runner.d.ts","sourceRoot":"","sources":["../src/make-take-latest-runner.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,cAAc,CAAC;AAEhD,MAAM,MAAM,gBAAgB,CAAC,CAAC,IAAI,WAAW,CAAC,CAAC,CAAC,CAAC;AAEjD,eAAO,MAAM,oBAAoB,GAAI,CAAC,OAAK,gBAAgB,CAAC,CAAC,CAyB5D,CAAC"}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
export const makeTakeLatestRunner = () => {
|
|
2
|
+
let ac = null;
|
|
3
|
+
return {
|
|
4
|
+
run: async (fn, values, onIntermediate, opts) => {
|
|
5
|
+
ac?.abort(); // cancel previous (takeLatest / switchMap)
|
|
6
|
+
ac = new AbortController();
|
|
7
|
+
const asyncOpts = {
|
|
8
|
+
update: onIntermediate,
|
|
9
|
+
signal: ac.signal,
|
|
10
|
+
index: opts?.index,
|
|
11
|
+
};
|
|
12
|
+
try {
|
|
13
|
+
return await fn(values, asyncOpts);
|
|
14
|
+
}
|
|
15
|
+
catch (e) {
|
|
16
|
+
// AbortError from delay() or fetch() when cancelled — not a real error
|
|
17
|
+
if (e instanceof DOMException && e.name === 'AbortError')
|
|
18
|
+
return null;
|
|
19
|
+
throw e;
|
|
20
|
+
}
|
|
21
|
+
},
|
|
22
|
+
cancel: () => {
|
|
23
|
+
ac?.abort();
|
|
24
|
+
ac = null;
|
|
25
|
+
},
|
|
26
|
+
};
|
|
27
|
+
};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"make-debounce-runner.test.d.ts","sourceRoot":"","sources":["../../src/test/make-debounce-runner.test.ts"],"names":[],"mappings":""}
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
import { assert } from '@open-wc/testing';
|
|
2
|
+
import { spy, useFakeTimers } from 'sinon';
|
|
3
|
+
import { delay } from '../async-rule';
|
|
4
|
+
import { makeDebounceRunner, } from '../make-debounce-runner';
|
|
5
|
+
const noop = () => {
|
|
6
|
+
/* intentionally empty */
|
|
7
|
+
};
|
|
8
|
+
const returns = (value) => async () => value;
|
|
9
|
+
// ── Suite ─────────────────────────────────────────────────────────────────────
|
|
10
|
+
suite('makeDebounceRunner', () => {
|
|
11
|
+
let clock;
|
|
12
|
+
let runner;
|
|
13
|
+
setup(() => {
|
|
14
|
+
clock = useFakeTimers();
|
|
15
|
+
runner = makeDebounceRunner(50);
|
|
16
|
+
});
|
|
17
|
+
teardown(() => {
|
|
18
|
+
runner.cancel();
|
|
19
|
+
clock.restore();
|
|
20
|
+
});
|
|
21
|
+
test('run() resolves with fn return value after debounce delay', async () => {
|
|
22
|
+
const promise = runner.run(returns({ name: 'Alice' }), {}, noop);
|
|
23
|
+
await clock.tickAsync(50);
|
|
24
|
+
const result = await promise;
|
|
25
|
+
assert.deepEqual(result, { name: 'Alice' });
|
|
26
|
+
});
|
|
27
|
+
test('second run() within window resolves first as null and resets timer', async () => {
|
|
28
|
+
const first = runner.run(returns({ from: 'first' }), {}, noop);
|
|
29
|
+
await clock.tickAsync(30);
|
|
30
|
+
const second = runner.run(returns({ from: 'second' }), {}, noop);
|
|
31
|
+
await clock.tickAsync(50);
|
|
32
|
+
const [firstResult, secondResult] = await Promise.all([first, second]);
|
|
33
|
+
assert.isNull(firstResult, 'debounced-away run() resolves null');
|
|
34
|
+
assert.deepEqual(secondResult, { from: 'second' });
|
|
35
|
+
});
|
|
36
|
+
test('only the last run() in a burst executes; earlier ones resolve null', async () => {
|
|
37
|
+
const bodySpy = spy();
|
|
38
|
+
const tracked = (tag) => async () => {
|
|
39
|
+
bodySpy(tag);
|
|
40
|
+
return { tag };
|
|
41
|
+
};
|
|
42
|
+
const p1 = runner.run(tracked('a'), {}, noop);
|
|
43
|
+
const p2 = runner.run(tracked('b'), {}, noop);
|
|
44
|
+
const p3 = runner.run(tracked('c'), {}, noop);
|
|
45
|
+
await clock.tickAsync(50);
|
|
46
|
+
const [r1, r2, r3] = await Promise.all([p1, p2, p3]);
|
|
47
|
+
assert.isNull(r1);
|
|
48
|
+
assert.isNull(r2);
|
|
49
|
+
assert.deepEqual(r3, { tag: 'c' });
|
|
50
|
+
assert.equal(bodySpy.callCount, 1);
|
|
51
|
+
assert.equal(bodySpy.firstCall.args[0], 'c');
|
|
52
|
+
});
|
|
53
|
+
test('fn body runs once when calls stop arriving', async () => {
|
|
54
|
+
const bodySpy = spy();
|
|
55
|
+
const counted = async (_, { signal }) => {
|
|
56
|
+
bodySpy();
|
|
57
|
+
await delay(signal, 10);
|
|
58
|
+
return { counted: true };
|
|
59
|
+
};
|
|
60
|
+
const p = runner.run(counted, {}, noop);
|
|
61
|
+
await clock.tickAsync(60);
|
|
62
|
+
const result = await p;
|
|
63
|
+
assert.deepEqual(result, { counted: true });
|
|
64
|
+
assert.equal(bodySpy.callCount, 1);
|
|
65
|
+
});
|
|
66
|
+
test('cancel() before timer fires resolves pending run() as null', async () => {
|
|
67
|
+
const promise = runner.run(returns({ x: 1 }), {}, noop);
|
|
68
|
+
runner.cancel();
|
|
69
|
+
await clock.tickAsync(50);
|
|
70
|
+
const result = await promise;
|
|
71
|
+
assert.isNull(result);
|
|
72
|
+
});
|
|
73
|
+
test('cancel() while fn is in-flight resolves run() as null', async () => {
|
|
74
|
+
const slow = async (_, { signal }) => {
|
|
75
|
+
await delay(signal, 5000);
|
|
76
|
+
return { slow: true };
|
|
77
|
+
};
|
|
78
|
+
const promise = runner.run(slow, {}, noop);
|
|
79
|
+
await clock.tickAsync(50); // let debounce fire so fn starts
|
|
80
|
+
runner.cancel();
|
|
81
|
+
await clock.tickAsync(5000);
|
|
82
|
+
const result = await promise;
|
|
83
|
+
assert.isNull(result);
|
|
84
|
+
});
|
|
85
|
+
test('non-abort errors from the fn are re-thrown', async () => {
|
|
86
|
+
const boom = async () => {
|
|
87
|
+
throw new Error('network error');
|
|
88
|
+
};
|
|
89
|
+
const promise = runner.run(boom, {}, noop);
|
|
90
|
+
await clock.tickAsync(50);
|
|
91
|
+
try {
|
|
92
|
+
await promise;
|
|
93
|
+
assert.fail('should have thrown');
|
|
94
|
+
}
|
|
95
|
+
catch (e) {
|
|
96
|
+
assert.instanceOf(e, Error);
|
|
97
|
+
assert.equal(e.message, 'network error');
|
|
98
|
+
}
|
|
99
|
+
});
|
|
100
|
+
test('opts.update calls onIntermediate during fn execution', async () => {
|
|
101
|
+
const patches = [];
|
|
102
|
+
const withUpdate = async (_, { update }) => {
|
|
103
|
+
update({ status: 'loading' });
|
|
104
|
+
return { status: 'done' };
|
|
105
|
+
};
|
|
106
|
+
const promise = runner.run(withUpdate, {}, (p) => patches.push(p));
|
|
107
|
+
await clock.tickAsync(50);
|
|
108
|
+
const result = await promise;
|
|
109
|
+
assert.deepEqual(patches, [{ status: 'loading' }]);
|
|
110
|
+
assert.deepEqual(result, { status: 'done' });
|
|
111
|
+
});
|
|
112
|
+
test('values snapshot is passed to fn', async () => {
|
|
113
|
+
let receivedValues;
|
|
114
|
+
const captureValues = async (values) => {
|
|
115
|
+
receivedValues = values;
|
|
116
|
+
return {};
|
|
117
|
+
};
|
|
118
|
+
const promise = runner.run(captureValues, { city: 'Oslo' }, noop);
|
|
119
|
+
await clock.tickAsync(50);
|
|
120
|
+
await promise;
|
|
121
|
+
assert.deepEqual(receivedValues, { city: 'Oslo' });
|
|
122
|
+
});
|
|
123
|
+
});
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"make-take-latest-runner.test.d.ts","sourceRoot":"","sources":["../../src/test/make-take-latest-runner.test.ts"],"names":[],"mappings":""}
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
import { assert } from '@open-wc/testing';
|
|
2
|
+
import { spy } from 'sinon';
|
|
3
|
+
import { delay } from '../async-rule';
|
|
4
|
+
import { makeTakeLatestRunner, } from '../make-take-latest-runner';
|
|
5
|
+
const noop = () => {
|
|
6
|
+
/* intentionally empty */
|
|
7
|
+
};
|
|
8
|
+
const returns = (value) => async () => value;
|
|
9
|
+
// ── Suite ─────────────────────────────────────────────────────────────────────
|
|
10
|
+
suite('makeTakeLatestRunner', () => {
|
|
11
|
+
let runner;
|
|
12
|
+
setup(() => {
|
|
13
|
+
runner = makeTakeLatestRunner();
|
|
14
|
+
});
|
|
15
|
+
teardown(() => {
|
|
16
|
+
runner.cancel();
|
|
17
|
+
});
|
|
18
|
+
test('run() resolves with fn return value', async () => {
|
|
19
|
+
const result = await runner.run(returns({ name: 'Alice' }), {}, noop);
|
|
20
|
+
assert.deepEqual(result, { name: 'Alice' });
|
|
21
|
+
});
|
|
22
|
+
test('second run() aborts the first before starting', async () => {
|
|
23
|
+
const firstCompleted = spy();
|
|
24
|
+
const slow = async (_, { signal }) => {
|
|
25
|
+
await delay(signal, 200);
|
|
26
|
+
firstCompleted();
|
|
27
|
+
return { from: 'first' };
|
|
28
|
+
};
|
|
29
|
+
const fast = async (_, { signal }) => {
|
|
30
|
+
await delay(signal, 10);
|
|
31
|
+
return { from: 'second' };
|
|
32
|
+
};
|
|
33
|
+
const first = runner.run(slow, {}, noop);
|
|
34
|
+
const second = runner.run(fast, {}, noop);
|
|
35
|
+
const [firstResult, secondResult] = await Promise.all([first, second]);
|
|
36
|
+
assert.isNull(firstResult, 'first run is cancelled');
|
|
37
|
+
assert.deepEqual(secondResult, { from: 'second' });
|
|
38
|
+
assert.isFalse(firstCompleted.called, 'first body should not complete');
|
|
39
|
+
});
|
|
40
|
+
test('cancel() aborts current run, resolves null', async () => {
|
|
41
|
+
const slow = async (_, { signal }) => {
|
|
42
|
+
await delay(signal, 5000);
|
|
43
|
+
return {};
|
|
44
|
+
};
|
|
45
|
+
const promise = runner.run(slow, {}, noop);
|
|
46
|
+
runner.cancel();
|
|
47
|
+
const result = await promise;
|
|
48
|
+
assert.isNull(result);
|
|
49
|
+
});
|
|
50
|
+
test('non-abort errors are re-thrown (not swallowed)', async () => {
|
|
51
|
+
const boom = async () => {
|
|
52
|
+
throw new Error('network error');
|
|
53
|
+
};
|
|
54
|
+
try {
|
|
55
|
+
await runner.run(boom, {}, noop);
|
|
56
|
+
assert.fail('should have thrown');
|
|
57
|
+
}
|
|
58
|
+
catch (e) {
|
|
59
|
+
assert.instanceOf(e, Error);
|
|
60
|
+
assert.equal(e.message, 'network error');
|
|
61
|
+
}
|
|
62
|
+
});
|
|
63
|
+
test('AbortErrors are swallowed and resolve null', async () => {
|
|
64
|
+
const aborting = async (_, { signal }) => {
|
|
65
|
+
await delay(signal, 5000);
|
|
66
|
+
return {};
|
|
67
|
+
};
|
|
68
|
+
const promise = runner.run(aborting, {}, noop);
|
|
69
|
+
runner.cancel();
|
|
70
|
+
const result = await promise;
|
|
71
|
+
assert.isNull(result);
|
|
72
|
+
});
|
|
73
|
+
test('opts.signal is the live AbortController signal', async () => {
|
|
74
|
+
let receivedSignal;
|
|
75
|
+
const captureSignal = async (_, { signal }) => {
|
|
76
|
+
receivedSignal = signal;
|
|
77
|
+
return {};
|
|
78
|
+
};
|
|
79
|
+
await runner.run(captureSignal, {}, noop);
|
|
80
|
+
assert.instanceOf(receivedSignal, AbortSignal);
|
|
81
|
+
});
|
|
82
|
+
test('opts.update calls onIntermediate immediately', async () => {
|
|
83
|
+
const patches = [];
|
|
84
|
+
const withUpdate = async (_, { update }) => {
|
|
85
|
+
update({ status: 'loading' });
|
|
86
|
+
return { status: 'done' };
|
|
87
|
+
};
|
|
88
|
+
const result = await runner.run(withUpdate, {}, (p) => patches.push(p));
|
|
89
|
+
assert.deepEqual(patches, [{ status: 'loading' }]);
|
|
90
|
+
assert.deepEqual(result, { status: 'done' });
|
|
91
|
+
});
|
|
92
|
+
test('opts.index is forwarded from run opts', async () => {
|
|
93
|
+
let receivedIndex;
|
|
94
|
+
const captureIndex = async (_, opts) => {
|
|
95
|
+
receivedIndex = opts.index;
|
|
96
|
+
return {};
|
|
97
|
+
};
|
|
98
|
+
await runner.run(captureIndex, {}, noop, { index: 3 });
|
|
99
|
+
assert.equal(receivedIndex, 3);
|
|
100
|
+
});
|
|
101
|
+
test('values snapshot is passed to fn', async () => {
|
|
102
|
+
let receivedValues;
|
|
103
|
+
const captureValues = async (values) => {
|
|
104
|
+
receivedValues = values;
|
|
105
|
+
return {};
|
|
106
|
+
};
|
|
107
|
+
await runner.run(captureValues, { name: 'Bob' }, noop);
|
|
108
|
+
assert.deepEqual(receivedValues, { name: 'Bob' });
|
|
109
|
+
});
|
|
110
|
+
});
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"use-async-form-core.test.d.ts","sourceRoot":"","sources":["../../src/test/use-async-form-core.test.ts"],"names":[],"mappings":""}
|