mirror of
https://github.com/n8n-io/n8n.git
synced 2026-10-12 02:19:43 +00:00
refactor(editor): Move useDebounce and durations into @n8n/composables (no-changelog) (#34143)
Co-authored-by: Claude Opus 4.8 <[email protected]>
This commit is contained in:
co-authored by
Claude Opus 4.8
parent
d6dcf3b749
commit
16b5ee2ff1
@@ -29,6 +29,7 @@
|
||||
},
|
||||
"dependencies": {
|
||||
"@vueuse/core": "catalog:frontend",
|
||||
"lodash": "catalog:",
|
||||
"vue": "catalog:frontend"
|
||||
},
|
||||
"devDependencies": {
|
||||
@@ -39,6 +40,7 @@
|
||||
"@testing-library/vue": "catalog:frontend",
|
||||
"@testing-library/jest-dom": "catalog:frontend",
|
||||
"@testing-library/user-event": "catalog:frontend",
|
||||
"@types/lodash": "catalog:",
|
||||
"@vitejs/plugin-vue": "catalog:frontend",
|
||||
"@vue/tsconfig": "catalog:frontend",
|
||||
"tsdown": "catalog:",
|
||||
|
||||
+44
-5
@@ -1,6 +1,7 @@
|
||||
import { vi, describe, it, expect } from 'vitest';
|
||||
import { useDebounce } from './useDebounce';
|
||||
import { render, screen } from '@testing-library/vue';
|
||||
import { vi, describe, it, expect, beforeEach } from 'vitest';
|
||||
|
||||
import { getDebounceTime, useDebounce } from './useDebounce';
|
||||
|
||||
describe('useDebounce()', () => {
|
||||
const debounceTime = 500;
|
||||
@@ -32,7 +33,7 @@ describe('useDebounce()', () => {
|
||||
},
|
||||
};
|
||||
|
||||
it('debounces a function call', async () => {
|
||||
it('debounces a function call', () => {
|
||||
const mockFn = vi.fn();
|
||||
render(TestComponent, { props: { mockFn } });
|
||||
const button = screen.getByText('Click me');
|
||||
@@ -43,7 +44,7 @@ describe('useDebounce()', () => {
|
||||
expect(mockFn).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it('supports trailing option', async () => {
|
||||
it('supports trailing option', () => {
|
||||
const mockFn = vi.fn();
|
||||
render(TestComponent, { props: { mockFn } });
|
||||
const button = screen.getByText('Click me trailing');
|
||||
@@ -58,7 +59,7 @@ describe('useDebounce()', () => {
|
||||
expect(mockFn).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it('works with async functions', async () => {
|
||||
it('works with async functions', () => {
|
||||
const mockAsyncFn = vi.fn(async () => {
|
||||
await new Promise((resolve) => setTimeout(resolve, 100));
|
||||
});
|
||||
@@ -71,3 +72,41 @@ describe('useDebounce()', () => {
|
||||
expect(mockAsyncFn).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
});
|
||||
|
||||
describe('getDebounceTime()', () => {
|
||||
beforeEach(() => {
|
||||
sessionStorage.clear();
|
||||
});
|
||||
|
||||
it('returns original time when no multiplier is set', () => {
|
||||
expect(getDebounceTime(100)).toBe(100);
|
||||
expect(getDebounceTime(1500)).toBe(1500);
|
||||
});
|
||||
|
||||
it('applies multiplier from sessionStorage', () => {
|
||||
sessionStorage.setItem('N8N_DEBOUNCE_MULTIPLIER', '0.5');
|
||||
expect(getDebounceTime(100)).toBe(50);
|
||||
expect(getDebounceTime(1500)).toBe(750);
|
||||
});
|
||||
|
||||
it('returns 0 when multiplier is 0', () => {
|
||||
sessionStorage.setItem('N8N_DEBOUNCE_MULTIPLIER', '0');
|
||||
expect(getDebounceTime(100)).toBe(0);
|
||||
expect(getDebounceTime(1500)).toBe(0);
|
||||
});
|
||||
|
||||
it('handles multiplier greater than 1', () => {
|
||||
sessionStorage.setItem('N8N_DEBOUNCE_MULTIPLIER', '2');
|
||||
expect(getDebounceTime(100)).toBe(200);
|
||||
});
|
||||
|
||||
it('defaults to 1 for invalid multiplier values', () => {
|
||||
sessionStorage.setItem('N8N_DEBOUNCE_MULTIPLIER', 'invalid');
|
||||
expect(getDebounceTime(100)).toBe(100);
|
||||
});
|
||||
|
||||
it('rounds to nearest integer', () => {
|
||||
sessionStorage.setItem('N8N_DEBOUNCE_MULTIPLIER', '0.33');
|
||||
expect(getDebounceTime(100)).toBe(33);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,68 @@
|
||||
import lodashDebounce from 'lodash/debounce';
|
||||
import { ref } from 'vue';
|
||||
|
||||
/**
|
||||
* Get debounce time with optional multiplier from sessionStorage.
|
||||
* Reads 'N8N_DEBOUNCE_MULTIPLIER' - defaults to 1 if not set.
|
||||
*/
|
||||
export function getDebounceTime(time: number): number {
|
||||
const stored = sessionStorage.getItem('N8N_DEBOUNCE_MULTIPLIER');
|
||||
const multiplier = stored !== null ? parseFloat(stored) : 1;
|
||||
return Math.round(time * (Number.isNaN(multiplier) ? 1 : multiplier));
|
||||
}
|
||||
|
||||
export interface DebounceOptions {
|
||||
debounceTime: number;
|
||||
trailing?: boolean;
|
||||
}
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
type AnyFunction = (...args: any) => any;
|
||||
type DebouncedFunc<T extends AnyFunction> = ReturnType<typeof lodashDebounce<T>>;
|
||||
|
||||
export function useDebounce() {
|
||||
// Create a ref for the WeakMap to store debounced functions.
|
||||
const debounceCache = ref(new WeakMap<AnyFunction, DebouncedFunc<AnyFunction>>());
|
||||
|
||||
const debounce = <T extends AnyFunction>(fn: T, options: DebounceOptions): DebouncedFunc<T> => {
|
||||
const { trailing, debounceTime } = options;
|
||||
|
||||
const effectiveDebounceTime = getDebounceTime(debounceTime);
|
||||
|
||||
// Check if a debounced version of the function is already stored in the WeakMap.
|
||||
let debouncedFn = debounceCache.value.get(fn);
|
||||
|
||||
// If a debounced version is not found, create one and store it in the WeakMap.
|
||||
if (debouncedFn === undefined) {
|
||||
debouncedFn = lodashDebounce(
|
||||
// eslint-disable-next-line @typescript-eslint/require-await -- kept async so the debounced wrapper always returns a promise
|
||||
async (...args: Parameters<T>) => {
|
||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-return -- AnyFunction return is intentionally `any`
|
||||
return fn(...args);
|
||||
},
|
||||
effectiveDebounceTime,
|
||||
trailing ? { trailing } : { leading: true },
|
||||
);
|
||||
|
||||
debounceCache.value.set(fn, debouncedFn);
|
||||
}
|
||||
|
||||
return debouncedFn;
|
||||
};
|
||||
|
||||
const callDebounced = <T extends AnyFunction>(
|
||||
fn: T,
|
||||
options: DebounceOptions,
|
||||
...inputParameters: Parameters<T>
|
||||
): ReturnType<T> | undefined => {
|
||||
const debouncedFn = debounce(fn, options);
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-return -- AnyFunction return is intentionally `any`
|
||||
return debouncedFn(...inputParameters);
|
||||
};
|
||||
|
||||
return {
|
||||
debounce,
|
||||
callDebounced,
|
||||
};
|
||||
}
|
||||
@@ -1,57 +1,8 @@
|
||||
import { ref } from 'vue';
|
||||
import _debounce from 'lodash/debounce';
|
||||
|
||||
import { getDebounceTime } from '@/app/constants/durations';
|
||||
|
||||
export interface DebounceOptions {
|
||||
debounceTime: number;
|
||||
trailing?: boolean;
|
||||
}
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
type AnyFunction = (...args: any) => any;
|
||||
type DebouncedFunc<T extends AnyFunction> = ReturnType<typeof _debounce<T>>;
|
||||
|
||||
export function useDebounce() {
|
||||
// Create a ref for the WeakMap to store debounced functions.
|
||||
const debounceCache = ref(new WeakMap<AnyFunction, DebouncedFunc<AnyFunction>>());
|
||||
|
||||
const debounce = <T extends AnyFunction>(fn: T, options: DebounceOptions): DebouncedFunc<T> => {
|
||||
const { trailing, debounceTime } = options;
|
||||
|
||||
const effectiveDebounceTime = getDebounceTime(debounceTime);
|
||||
|
||||
// Check if a debounced version of the function is already stored in the WeakMap.
|
||||
let debouncedFn = debounceCache.value.get(fn);
|
||||
|
||||
// If a debounced version is not found, create one and store it in the WeakMap.
|
||||
if (debouncedFn === undefined) {
|
||||
debouncedFn = _debounce(
|
||||
async (...args: Parameters<T>) => {
|
||||
return fn(...args);
|
||||
},
|
||||
effectiveDebounceTime,
|
||||
trailing ? { trailing } : { leading: true },
|
||||
);
|
||||
|
||||
debounceCache.value.set(fn, debouncedFn);
|
||||
}
|
||||
|
||||
return debouncedFn;
|
||||
};
|
||||
|
||||
const callDebounced = <T extends AnyFunction>(
|
||||
fn: T,
|
||||
options: DebounceOptions,
|
||||
...inputParameters: Parameters<T>
|
||||
): ReturnType<T> | undefined => {
|
||||
const debouncedFn = debounce(fn, options);
|
||||
|
||||
return debouncedFn(...inputParameters);
|
||||
};
|
||||
|
||||
return {
|
||||
debounce,
|
||||
callDebounced,
|
||||
};
|
||||
}
|
||||
/**
|
||||
* @deprecated Import from `@n8n/composables/useDebounce` instead.
|
||||
*
|
||||
* Temporary re-export shim for the frontend modularization effort (CAT-3686).
|
||||
* `useDebounce` now lives in `@n8n/composables`; call sites are migrated off
|
||||
* this path per-directory before it is removed.
|
||||
*/
|
||||
export * from '@n8n/composables/useDebounce';
|
||||
|
||||
@@ -1,39 +0,0 @@
|
||||
import { getDebounceTime } from './durations';
|
||||
|
||||
describe('getDebounceTime', () => {
|
||||
beforeEach(() => {
|
||||
sessionStorage.clear();
|
||||
});
|
||||
|
||||
it('returns original time when no multiplier is set', () => {
|
||||
expect(getDebounceTime(100)).toBe(100);
|
||||
expect(getDebounceTime(1500)).toBe(1500);
|
||||
});
|
||||
|
||||
it('applies multiplier from sessionStorage', () => {
|
||||
sessionStorage.setItem('N8N_DEBOUNCE_MULTIPLIER', '0.5');
|
||||
expect(getDebounceTime(100)).toBe(50);
|
||||
expect(getDebounceTime(1500)).toBe(750);
|
||||
});
|
||||
|
||||
it('returns 0 when multiplier is 0', () => {
|
||||
sessionStorage.setItem('N8N_DEBOUNCE_MULTIPLIER', '0');
|
||||
expect(getDebounceTime(100)).toBe(0);
|
||||
expect(getDebounceTime(1500)).toBe(0);
|
||||
});
|
||||
|
||||
it('handles multiplier greater than 1', () => {
|
||||
sessionStorage.setItem('N8N_DEBOUNCE_MULTIPLIER', '2');
|
||||
expect(getDebounceTime(100)).toBe(200);
|
||||
});
|
||||
|
||||
it('defaults to 1 for invalid multiplier values', () => {
|
||||
sessionStorage.setItem('N8N_DEBOUNCE_MULTIPLIER', 'invalid');
|
||||
expect(getDebounceTime(100)).toBe(100);
|
||||
});
|
||||
|
||||
it('rounds to nearest integer', () => {
|
||||
sessionStorage.setItem('N8N_DEBOUNCE_MULTIPLIER', '0.33');
|
||||
expect(getDebounceTime(100)).toBe(33);
|
||||
});
|
||||
});
|
||||
@@ -26,16 +26,6 @@ export const SIX_MONTHS_IN_MILLIS = 6 * 30 * TIME.DAY;
|
||||
|
||||
export const LOADING_ANIMATION_MIN_DURATION = 1000;
|
||||
|
||||
/**
|
||||
* Get debounce time with optional multiplier from sessionStorage.
|
||||
* Reads 'N8N_DEBOUNCE_MULTIPLIER' - defaults to 1 if not set.
|
||||
*/
|
||||
export function getDebounceTime(time: number): number {
|
||||
const stored = sessionStorage.getItem('N8N_DEBOUNCE_MULTIPLIER');
|
||||
const multiplier = stored !== null ? parseFloat(stored) : 1;
|
||||
return Math.round(time * (Number.isNaN(multiplier) ? 1 : multiplier));
|
||||
}
|
||||
|
||||
/** Centralized debounce timing constants. Use with getDebounceTime(). */
|
||||
export const DEBOUNCE_TIME = {
|
||||
/** UI responsiveness - very fast feedback */
|
||||
@@ -88,3 +78,10 @@ export const DEBOUNCE_TIME = {
|
||||
WEBSOCKET_DISCONNECT: 500,
|
||||
},
|
||||
} as const;
|
||||
|
||||
/**
|
||||
* `getDebounceTime` lives in `@n8n/composables`, colocated with `useDebounce`
|
||||
* (its only consumer). Re-exported here so existing `@/app/constants/durations`
|
||||
* and `@/app/constants` call sites keep working unchanged.
|
||||
*/
|
||||
export { getDebounceTime } from '@n8n/composables/useDebounce';
|
||||
|
||||
Generated
+6
@@ -4789,6 +4789,9 @@ importers:
|
||||
'@vueuse/core':
|
||||
specifier: catalog:frontend
|
||||
version: 10.11.0([email protected]([email protected]))
|
||||
lodash:
|
||||
specifier: 4.18.1
|
||||
version: 4.18.1
|
||||
vue:
|
||||
specifier: catalog:frontend
|
||||
version: 3.5.26([email protected])
|
||||
@@ -4814,6 +4817,9 @@ importers:
|
||||
'@testing-library/vue':
|
||||
specifier: catalog:frontend
|
||||
version: 8.1.0(@vue/[email protected])([email protected]([email protected]))
|
||||
'@types/lodash':
|
||||
specifier: 'catalog:'
|
||||
version: 4.17.17
|
||||
'@vitejs/plugin-vue':
|
||||
specifier: catalog:frontend
|
||||
version: 5.2.4([email protected](@types/[email protected])([email protected])([email protected])([email protected])([email protected])([email protected])([email protected])([email protected]))([email protected]([email protected]))
|
||||
|
||||
Reference in New Issue
Block a user