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:
2026-06-07 16:29:39 +07:00
parent e6919de29e
commit c7644ade69
203 changed files with 23016 additions and 141 deletions
+2 -2
View File
@@ -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', () => {
+80 -51
View File
@@ -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;
}