fix(vue): eslint/tsconfig migration + resolve type errors
@robonen/vue (toolkit): migrate to eslint flat config + composite tsconfig; fix composable + test type errors (writable computed returns, null guards, overload-compatible signatures, typed test helpers) — all type-level.
This commit is contained in:
@@ -1,6 +1,6 @@
|
||||
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
|
||||
import { ref } from 'vue';
|
||||
import { bypassFilter, debounceFilter, throttleFilter, pausableFilter } from './filters';
|
||||
import { bypassFilter, debounceFilter, pausableFilter, throttleFilter } from './filters';
|
||||
|
||||
describe(bypassFilter, () => {
|
||||
it('invokes callback immediately', () => {
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
import { ref, toValue } from 'vue';
|
||||
import type { MaybeRefOrGetter, Ref } from 'vue';
|
||||
import { debounce, throttle } from '@robonen/stdlib';
|
||||
import type { AnyFunction } from '@robonen/stdlib';
|
||||
|
||||
export type EventFilter = (invoke: () => void) => void;
|
||||
|
||||
@@ -13,6 +15,14 @@ export interface ConfigurableEventFilter {
|
||||
eventFilter?: EventFilter;
|
||||
}
|
||||
|
||||
export interface DebounceFilterOptions {
|
||||
/**
|
||||
* The maximum time the callback may be delayed before it is forcibly invoked
|
||||
* under sustained input (can be reactive). When omitted there is no upper bound.
|
||||
*/
|
||||
maxWait?: MaybeRefOrGetter<number>;
|
||||
}
|
||||
|
||||
/**
|
||||
* A no-op filter that invokes the callback immediately
|
||||
*/
|
||||
@@ -21,29 +31,37 @@ export const bypassFilter: EventFilter = (invoke) => {
|
||||
};
|
||||
|
||||
/**
|
||||
* Create a debounce event filter
|
||||
* Create a debounce event filter — a reactive-aware wrapper around
|
||||
* `@robonen/stdlib`'s `debounce` (trailing edge).
|
||||
*
|
||||
* @param ms Delay in milliseconds (can be reactive)
|
||||
* @param options Optional `maxWait` ceiling (can be reactive)
|
||||
* @returns EventFilter
|
||||
*/
|
||||
export function debounceFilter(ms: MaybeRefOrGetter<number>): EventFilter {
|
||||
let timer: ReturnType<typeof setTimeout> | undefined;
|
||||
export function debounceFilter(ms: MaybeRefOrGetter<number>, options: DebounceFilterOptions = {}): EventFilter {
|
||||
const { maxWait } = options;
|
||||
|
||||
const filter: EventFilter = (invoke) => {
|
||||
if (timer !== undefined)
|
||||
clearTimeout(timer);
|
||||
const debounced = debounce(
|
||||
(invoke: () => void) => invoke(),
|
||||
() => toValue(ms),
|
||||
{ maxWait: maxWait === undefined ? undefined : () => toValue(maxWait) },
|
||||
);
|
||||
|
||||
timer = setTimeout(() => {
|
||||
timer = undefined;
|
||||
return (invoke) => {
|
||||
// Non-positive delay runs synchronously (behaves like an un-debounced call).
|
||||
if (toValue(ms) <= 0) {
|
||||
debounced.cancel();
|
||||
invoke();
|
||||
}, toValue(ms));
|
||||
};
|
||||
return;
|
||||
}
|
||||
|
||||
return filter;
|
||||
debounced(invoke);
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a throttle event filter
|
||||
* Create a throttle event filter — a reactive-aware wrapper around
|
||||
* `@robonen/stdlib`'s `throttle`.
|
||||
*
|
||||
* @param ms Interval in milliseconds (can be reactive)
|
||||
* @param trailing Whether to invoke on trailing edge (default: true)
|
||||
@@ -55,46 +73,13 @@ export function throttleFilter(
|
||||
trailing = true,
|
||||
leading = true,
|
||||
): EventFilter {
|
||||
let lastExec = 0;
|
||||
let timer: ReturnType<typeof setTimeout> | undefined;
|
||||
let lastInvoke: (() => void) | undefined;
|
||||
let isLeading = true;
|
||||
const throttled = throttle(
|
||||
(invoke: () => void) => invoke(),
|
||||
() => toValue(ms),
|
||||
{ trailing, leading },
|
||||
);
|
||||
|
||||
const clear = () => {
|
||||
if (timer !== undefined) {
|
||||
clearTimeout(timer);
|
||||
timer = undefined;
|
||||
}
|
||||
};
|
||||
|
||||
const filter: EventFilter = (invoke) => {
|
||||
const duration = toValue(ms);
|
||||
const elapsed = Date.now() - lastExec;
|
||||
|
||||
lastInvoke = invoke;
|
||||
|
||||
if (elapsed >= duration && (leading || !isLeading)) {
|
||||
lastExec = Date.now();
|
||||
isLeading = false;
|
||||
invoke();
|
||||
clear();
|
||||
return;
|
||||
}
|
||||
|
||||
isLeading = false;
|
||||
|
||||
if (trailing) {
|
||||
clear();
|
||||
timer = setTimeout(() => {
|
||||
lastExec = Date.now();
|
||||
isLeading = true;
|
||||
timer = undefined;
|
||||
lastInvoke?.();
|
||||
}, Math.max(0, duration - elapsed));
|
||||
}
|
||||
};
|
||||
|
||||
return filter;
|
||||
return invoke => throttled(invoke);
|
||||
}
|
||||
|
||||
export interface PausableEventFilterReturn {
|
||||
@@ -136,3 +121,47 @@ export function pausableFilter(): PausableEventFilterReturn {
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Wrap a function with an {@link EventFilter}, preserving arguments, `this`,
|
||||
* and the return value through a promise.
|
||||
*
|
||||
* The wrapper returns a promise that resolves with the result of the wrapped
|
||||
* function once the filter lets it through. When the filter coalesces calls
|
||||
* (e.g. debounce/throttle), every pending promise scheduled since the last
|
||||
* invocation resolves together with that invocation's result — so nothing is
|
||||
* left dangling.
|
||||
*
|
||||
* @param filter The event filter controlling invocation timing
|
||||
* @param fn The function to wrap
|
||||
* @returns A filtered wrapper returning a promise of the result
|
||||
*/
|
||||
export function createFilterWrapper<T extends AnyFunction>(
|
||||
filter: EventFilter,
|
||||
fn: T,
|
||||
): (...args: Parameters<T>) => Promise<Awaited<ReturnType<T>>> {
|
||||
// Promises scheduled but not yet resolved by an invocation. The filter may
|
||||
// drop intermediate invokes (debounce) — they all settle on the next real one.
|
||||
let pending: Array<{ resolve: (value: any) => void; reject: (reason?: unknown) => void }> = [];
|
||||
|
||||
function wrapper(this: unknown, ...args: Parameters<T>) {
|
||||
return new Promise<Awaited<ReturnType<T>>>((resolve, reject) => {
|
||||
pending.push({ resolve, reject });
|
||||
|
||||
filter(() => {
|
||||
const settled = pending;
|
||||
pending = [];
|
||||
|
||||
try {
|
||||
const result = fn.apply(this, args);
|
||||
for (const p of settled) p.resolve(result);
|
||||
}
|
||||
catch (error) {
|
||||
for (const p of settled) p.reject(error);
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
return wrapper;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user