feat(monorepo): migrate vue packages and apply oxlint refactors
This commit is contained in:
@@ -0,0 +1,22 @@
|
||||
import { getCurrentInstance } from 'vue';
|
||||
import type { ComponentInternalInstance } from 'vue';
|
||||
|
||||
/**
|
||||
* @name getLifeCycleTarger
|
||||
* @category Utils
|
||||
* @description Function to get the target instance of the lifecycle hook
|
||||
*
|
||||
* @param {ComponentInternalInstance} target The target instance of the lifecycle hook
|
||||
* @returns {ComponentInternalInstance | null} Instance of the lifecycle hook or null
|
||||
*
|
||||
* @example
|
||||
* const target = getLifeCycleTarger();
|
||||
*
|
||||
* @example
|
||||
* const target = getLifeCycleTarger(instance);
|
||||
*
|
||||
* @since 0.0.1
|
||||
*/
|
||||
export function getLifeCycleTarger(target?: ComponentInternalInstance) {
|
||||
return target || getCurrentInstance();
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
/**
|
||||
* @name VueToolsError
|
||||
* @category Error
|
||||
* @description VueToolsError is a custom error class that represents an error in Vue Tools
|
||||
*
|
||||
* @since 0.0.1
|
||||
*/
|
||||
export class VueToolsError extends Error {
|
||||
constructor(message: string) {
|
||||
super(message);
|
||||
this.name = 'VueToolsError';
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,171 @@
|
||||
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
|
||||
import { ref } from 'vue';
|
||||
import { bypassFilter, debounceFilter, throttleFilter, pausableFilter } from './filters';
|
||||
|
||||
describe(bypassFilter, () => {
|
||||
it('invokes callback immediately', () => {
|
||||
const fn = vi.fn();
|
||||
bypassFilter(fn);
|
||||
expect(fn).toHaveBeenCalledOnce();
|
||||
});
|
||||
});
|
||||
|
||||
describe(debounceFilter, () => {
|
||||
beforeEach(() => {
|
||||
vi.useFakeTimers();
|
||||
});
|
||||
afterEach(() => {
|
||||
vi.useRealTimers();
|
||||
});
|
||||
|
||||
it('delays invocation by the specified ms', () => {
|
||||
const filter = debounceFilter(100);
|
||||
const fn = vi.fn();
|
||||
|
||||
filter(fn);
|
||||
expect(fn).not.toHaveBeenCalled();
|
||||
|
||||
vi.advanceTimersByTime(50);
|
||||
expect(fn).not.toHaveBeenCalled();
|
||||
|
||||
vi.advanceTimersByTime(50);
|
||||
expect(fn).toHaveBeenCalledOnce();
|
||||
});
|
||||
|
||||
it('resets timer on repeated calls', () => {
|
||||
const filter = debounceFilter(100);
|
||||
const fn = vi.fn();
|
||||
|
||||
filter(fn);
|
||||
vi.advanceTimersByTime(80);
|
||||
filter(fn);
|
||||
vi.advanceTimersByTime(80);
|
||||
|
||||
expect(fn).not.toHaveBeenCalled();
|
||||
|
||||
vi.advanceTimersByTime(20);
|
||||
expect(fn).toHaveBeenCalledOnce();
|
||||
});
|
||||
|
||||
it('supports reactive ms via ref', () => {
|
||||
const delay = ref(100);
|
||||
const filter = debounceFilter(delay);
|
||||
const fn = vi.fn();
|
||||
|
||||
filter(fn);
|
||||
vi.advanceTimersByTime(100);
|
||||
expect(fn).toHaveBeenCalledOnce();
|
||||
|
||||
delay.value = 200;
|
||||
filter(fn);
|
||||
vi.advanceTimersByTime(100);
|
||||
expect(fn).toHaveBeenCalledOnce(); // still 1
|
||||
|
||||
vi.advanceTimersByTime(100);
|
||||
expect(fn).toHaveBeenCalledTimes(2);
|
||||
});
|
||||
});
|
||||
|
||||
describe(throttleFilter, () => {
|
||||
beforeEach(() => {
|
||||
vi.useFakeTimers();
|
||||
});
|
||||
afterEach(() => {
|
||||
vi.useRealTimers();
|
||||
});
|
||||
|
||||
it('invokes immediately on first call (leading)', () => {
|
||||
const filter = throttleFilter(100);
|
||||
const fn = vi.fn();
|
||||
|
||||
filter(fn);
|
||||
expect(fn).toHaveBeenCalledOnce();
|
||||
});
|
||||
|
||||
it('throttles subsequent calls', () => {
|
||||
const filter = throttleFilter(100);
|
||||
const fn1 = vi.fn();
|
||||
const fn2 = vi.fn();
|
||||
|
||||
filter(fn1);
|
||||
expect(fn1).toHaveBeenCalledOnce();
|
||||
|
||||
// Within throttle window
|
||||
filter(fn2);
|
||||
expect(fn2).not.toHaveBeenCalled();
|
||||
|
||||
// After throttle window, trailing fires
|
||||
vi.advanceTimersByTime(100);
|
||||
expect(fn2).toHaveBeenCalledOnce();
|
||||
});
|
||||
|
||||
it('does not invoke trailing when trailing=false', () => {
|
||||
const filter = throttleFilter(100, false, true);
|
||||
const fn1 = vi.fn();
|
||||
const fn2 = vi.fn();
|
||||
|
||||
filter(fn1);
|
||||
expect(fn1).toHaveBeenCalledOnce();
|
||||
|
||||
filter(fn2);
|
||||
vi.advanceTimersByTime(200);
|
||||
expect(fn2).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('does not invoke leading when leading=false', () => {
|
||||
const filter = throttleFilter(100, true, false);
|
||||
const fn = vi.fn();
|
||||
|
||||
filter(fn);
|
||||
expect(fn).not.toHaveBeenCalled();
|
||||
|
||||
vi.advanceTimersByTime(100);
|
||||
expect(fn).toHaveBeenCalledOnce();
|
||||
});
|
||||
});
|
||||
|
||||
describe(pausableFilter, () => {
|
||||
it('invokes immediately when active', () => {
|
||||
const { filter, isActive } = pausableFilter();
|
||||
const fn = vi.fn();
|
||||
|
||||
expect(isActive.value).toBeTruthy();
|
||||
filter(fn);
|
||||
expect(fn).toHaveBeenCalledOnce();
|
||||
});
|
||||
|
||||
it('queues invocations when paused', () => {
|
||||
const { filter, pause } = pausableFilter();
|
||||
const fn = vi.fn();
|
||||
|
||||
pause();
|
||||
filter(fn);
|
||||
expect(fn).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('replays queued invocations on resume', () => {
|
||||
const { filter, pause, resume } = pausableFilter();
|
||||
const fn1 = vi.fn();
|
||||
const fn2 = vi.fn();
|
||||
|
||||
pause();
|
||||
filter(fn1);
|
||||
filter(fn2);
|
||||
expect(fn1).not.toHaveBeenCalled();
|
||||
expect(fn2).not.toHaveBeenCalled();
|
||||
|
||||
resume();
|
||||
expect(fn1).toHaveBeenCalledOnce();
|
||||
expect(fn2).toHaveBeenCalledOnce();
|
||||
});
|
||||
|
||||
it('isActive reflects the paused state', () => {
|
||||
const { isActive, pause, resume } = pausableFilter();
|
||||
|
||||
expect(isActive.value).toBeTruthy();
|
||||
pause();
|
||||
expect(isActive.value).toBeFalsy();
|
||||
resume();
|
||||
expect(isActive.value).toBeTruthy();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,138 @@
|
||||
import { ref, toValue } from 'vue';
|
||||
import type { MaybeRefOrGetter, Ref } from 'vue';
|
||||
|
||||
export type EventFilter = (invoke: () => void) => void;
|
||||
|
||||
export interface ConfigurableEventFilter {
|
||||
/**
|
||||
* Event filter for controlling how frequently writes propagate
|
||||
*
|
||||
* @example debounceFilter(500) — debounce writes by 500ms
|
||||
* @example throttleFilter(1000) — throttle writes to once per 1000ms
|
||||
*/
|
||||
eventFilter?: EventFilter;
|
||||
}
|
||||
|
||||
/**
|
||||
* A no-op filter that invokes the callback immediately
|
||||
*/
|
||||
export const bypassFilter: EventFilter = (invoke) => {
|
||||
invoke();
|
||||
};
|
||||
|
||||
/**
|
||||
* Create a debounce event filter
|
||||
*
|
||||
* @param ms Delay in milliseconds (can be reactive)
|
||||
* @returns EventFilter
|
||||
*/
|
||||
export function debounceFilter(ms: MaybeRefOrGetter<number>): EventFilter {
|
||||
let timer: ReturnType<typeof setTimeout> | undefined;
|
||||
|
||||
const filter: EventFilter = (invoke) => {
|
||||
if (timer !== undefined)
|
||||
clearTimeout(timer);
|
||||
|
||||
timer = setTimeout(() => {
|
||||
timer = undefined;
|
||||
invoke();
|
||||
}, toValue(ms));
|
||||
};
|
||||
|
||||
return filter;
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a throttle event filter
|
||||
*
|
||||
* @param ms Interval in milliseconds (can be reactive)
|
||||
* @param trailing Whether to invoke on trailing edge (default: true)
|
||||
* @param leading Whether to invoke on leading edge (default: true)
|
||||
* @returns EventFilter
|
||||
*/
|
||||
export function throttleFilter(
|
||||
ms: MaybeRefOrGetter<number>,
|
||||
trailing = true,
|
||||
leading = true,
|
||||
): EventFilter {
|
||||
let lastExec = 0;
|
||||
let timer: ReturnType<typeof setTimeout> | undefined;
|
||||
let lastInvoke: (() => void) | undefined;
|
||||
let isLeading = true;
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
export interface PausableEventFilterReturn {
|
||||
filter: EventFilter;
|
||||
isActive: Ref<boolean>;
|
||||
pause: () => void;
|
||||
resume: () => void;
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a pausable event filter
|
||||
*
|
||||
* When paused, invocations are queued and replayed on resume.
|
||||
*
|
||||
* @returns PausableEventFilterReturn
|
||||
*/
|
||||
export function pausableFilter(): PausableEventFilterReturn {
|
||||
const isActive = ref(true);
|
||||
let pendingInvocations: Array<() => void> = [];
|
||||
|
||||
const filter: EventFilter = (invoke) => {
|
||||
if (isActive.value) {
|
||||
invoke();
|
||||
}
|
||||
else {
|
||||
pendingInvocations.push(invoke);
|
||||
}
|
||||
};
|
||||
|
||||
return {
|
||||
filter,
|
||||
isActive: isActive as Ref<boolean>,
|
||||
pause: () => { isActive.value = false; },
|
||||
resume: () => {
|
||||
isActive.value = true;
|
||||
const pending = pendingInvocations;
|
||||
pendingInvocations = [];
|
||||
for (const fn of pending) fn();
|
||||
},
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
export * from './components';
|
||||
export * from './error';
|
||||
export * from './filters';
|
||||
Reference in New Issue
Block a user