From 451a9125f9195afa2e53be3a4cfd1561ed77eb6e Mon Sep 17 00:00:00 2001 From: Afshin Mehrabani Date: Sat, 30 Dec 2023 15:11:41 +0000 Subject: [PATCH 1/9] Throttle the container.tsx pipeline processing function --- src/config.ts | 3 +++ src/util/throttle.ts | 13 +++++++++++-- src/view/container.tsx | 35 ++++++++++++++++++----------------- 3 files changed, 32 insertions(+), 19 deletions(-) diff --git a/src/config.ts b/src/config.ts index 71e07a43..b3b20352 100644 --- a/src/config.ts +++ b/src/config.ts @@ -36,6 +36,8 @@ export interface Config { /** to parse a HTML table and load the data */ from: HTMLElement; storage: Storage; + /** Pipeline process throttle timeout in milliseconds */ + processingThrottleMs: number; pipeline: Pipeline; /** to automatically calculate the columns width */ autoWidth: boolean; @@ -128,6 +130,7 @@ export class Config { tableRef: createRef(), width: '100%', height: 'auto', + processingThrottleMs: 50, autoWidth: true, style: {}, className: {}, diff --git a/src/util/throttle.ts b/src/util/throttle.ts index 7a677609..c11da443 100644 --- a/src/util/throttle.ts +++ b/src/util/throttle.ts @@ -1,11 +1,20 @@ -export const throttle = (fn: (...args) => void, wait = 100) => { +/** + * Throttle a given function + * @param fn Function to be called + * @param wait Throttle timeout in milliseconds + * @param leading whether or not to call "fn" immediately + * @returns Throttled function + */ +export const throttle = (fn: (...args) => void, wait = 100, leading = true) => { let inThrottle: boolean; let lastFn: ReturnType; let lastTime: number; return (...args) => { if (!inThrottle) { - fn(...args); + if (leading) { + fn(...args); + } lastTime = Date.now(); inThrottle = true; } else { diff --git a/src/view/container.tsx b/src/view/container.tsx index 92b6dd58..35c8bde2 100644 --- a/src/view/container.tsx +++ b/src/view/container.tsx @@ -10,6 +10,7 @@ import * as actions from './actions'; import { useStore } from '../hooks/useStore'; import useSelector from '../../src/hooks/useSelector'; import { useConfig } from '../../src/hooks/useConfig'; +import { throttle } from 'src/util/throttle'; export function Container() { const config = useConfig(); @@ -19,6 +20,23 @@ export function Container() { const tableRef = useSelector((state) => state.tableRef); const tempRef = createRef(); + const processPipeline = throttle(async () => { + dispatch(actions.SetLoadingData()); + + try { + const data = await config.pipeline.process(); + dispatch(actions.SetData(data)); + + // TODO: do we need this setTimemout? + setTimeout(() => { + dispatch(actions.SetStatusToRendered()); + }, 0); + } catch (e) { + log.error(e); + dispatch(actions.SetDataErrored()); + } + }, config.processingThrottleMs, false); + useEffect(() => { // set the initial header object // we update the header width later when "data" @@ -41,23 +59,6 @@ export function Container() { } }, [data, config, tempRef]); - const processPipeline = async () => { - dispatch(actions.SetLoadingData()); - - try { - const data = await config.pipeline.process(); - dispatch(actions.SetData(data)); - - // TODO: do we need this setTimemout? - setTimeout(() => { - dispatch(actions.SetStatusToRendered()); - }, 0); - } catch (e) { - log.error(e); - dispatch(actions.SetDataErrored()); - } - }; - return (
Date: Sat, 30 Dec 2023 15:26:23 +0000 Subject: [PATCH 2/9] Throw error when duplicate processor is being registered + process props deep equal check --- src/pipeline/pipeline.ts | 37 ++++++++++++++++++++++++++++++++----- src/pipeline/processor.ts | 15 ++++++++++++--- 2 files changed, 44 insertions(+), 8 deletions(-) diff --git a/src/pipeline/pipeline.ts b/src/pipeline/pipeline.ts index aa6a6ded..5bc87ad5 100644 --- a/src/pipeline/pipeline.ts +++ b/src/pipeline/pipeline.ts @@ -47,7 +47,7 @@ class Pipeline extends EventEmitter> { // -1 means all new processors should be processed private lastProcessorIndexUpdated = -1; - constructor(steps?: PipelineProcessor[]) { + constructor(steps?: PipelineProcessor[]) { super(); if (steps) { @@ -70,20 +70,46 @@ class Pipeline extends EventEmitter> { * @param priority */ register( - processor: PipelineProcessor, + processor: PipelineProcessor, priority: number = null, - ): void { - if (!processor) return; + ): PipelineProcessor { + if (!processor) { + throw Error('Processor is not defined'); + } if (processor.type === null) { throw Error('Processor type is not defined'); } + if (this.findProcessorIndexByID(processor.id) > -1) { + throw Error(`Processor ID ${processor.id} is already defined`); + } + // binding the propsUpdated callback to the Pipeline processor.on('propsUpdated', this.processorPropsUpdated.bind(this)); this.addProcessorByPriority(processor, priority); this.afterRegistered(processor); + + return processor; + } + + /** + * Tries to register a new processor + * @param processor + * @param priority + */ + tryRegister( + processor: PipelineProcessor, + priority: number = null, + ): PipelineProcessor | undefined { + try { + return this.register(processor, priority); + } catch (_) { + // noop + } + + return undefined; } /** @@ -91,8 +117,9 @@ class Pipeline extends EventEmitter> { * * @param processor */ - unregister(processor: PipelineProcessor): void { + unregister(processor: PipelineProcessor): void { if (!processor) return; + if (this.findProcessorIndexByID(processor.id) === -1) return; const subSteps = this._steps.get(processor.type); diff --git a/src/pipeline/processor.ts b/src/pipeline/processor.ts index b3f2d2f7..01835cef 100644 --- a/src/pipeline/processor.ts +++ b/src/pipeline/processor.ts @@ -2,6 +2,7 @@ // e.g. Extractor = 0 will be processed before Transformer = 1 import { generateUUID, ID } from '../util/id'; import { EventEmitter } from '../util/eventEmitter'; +import { deepEqual } from '../util/deepEqual'; export enum ProcessorType { Initiator, @@ -29,7 +30,7 @@ export abstract class PipelineProcessor< P extends Partial, > extends EventEmitter> { public readonly id: ID; - private readonly _props: P; + private _props: P; abstract get type(): ProcessorType; protected abstract _process(...args): T | Promise; @@ -62,8 +63,16 @@ export abstract class PipelineProcessor< } setProps(props: Partial

): this { - Object.assign(this._props, props); - this.emit('propsUpdated', this); + const updatedProps = { + ...this._props, + ...props, + }; + + if (!deepEqual(updatedProps, this._props)) { + this._props = updatedProps; + this.emit('propsUpdated', this); + } + return this; } From 20b2787e9a971b0225da60c49a8976af56dc1091 Mon Sep 17 00:00:00 2001 From: Afshin Mehrabani Date: Sat, 30 Dec 2023 15:26:45 +0000 Subject: [PATCH 3/9] deepEqual --- src/util/deepEqual.ts | 9 +++++++++ 1 file changed, 9 insertions(+) create mode 100644 src/util/deepEqual.ts diff --git a/src/util/deepEqual.ts b/src/util/deepEqual.ts new file mode 100644 index 00000000..70a9be25 --- /dev/null +++ b/src/util/deepEqual.ts @@ -0,0 +1,9 @@ +/** + * Returns true if both objects are equal + * @param a left object + * @param b right object + * @returns + */ +export function deepEqual(a: A, b: B) { + return JSON.stringify(a) === JSON.stringify(b); +} From 1609c8c787b80c98943f47b14712b79e41c6494d Mon Sep 17 00:00:00 2001 From: Afshin Mehrabani Date: Sat, 30 Dec 2023 15:29:50 +0000 Subject: [PATCH 4/9] Fix sort.tsx to always register one processor --- src/hooks/useSelector.ts | 2 +- src/view/plugin/sort/actions.ts | 4 +- src/view/plugin/sort/sort.tsx | 85 ++++++++++++++++----------------- 3 files changed, 45 insertions(+), 46 deletions(-) diff --git a/src/hooks/useSelector.ts b/src/hooks/useSelector.ts index ed3d7b29..74ade663 100644 --- a/src/hooks/useSelector.ts +++ b/src/hooks/useSelector.ts @@ -15,7 +15,7 @@ export default function useSelector(selector: (state) => T) { }); return unsubscribe; - }, []); + }, [store, current]); return current; } diff --git a/src/view/plugin/sort/actions.ts b/src/view/plugin/sort/actions.ts index 722382f9..e642c0b7 100644 --- a/src/view/plugin/sort/actions.ts +++ b/src/view/plugin/sort/actions.ts @@ -8,7 +8,7 @@ export const SortColumn = compare?: Comparator, ) => (state) => { - let columns = state.sort ? [...state.sort.columns] : []; + let columns = state.sort ? structuredClone(state.sort.columns) : []; const count = columns.length; const column = columns.find((x) => x.index === index); const exists = column !== undefined; @@ -86,7 +86,7 @@ export const SortColumn = export const SortToggle = (index: number, multi: boolean, compare?: Comparator) => (state) => { - const columns = state.sort ? [...state.sort.columns] : []; + const columns = state.sort ? structuredClone(state.sort.columns) : []; const column = columns.find((x) => x.index === index); if (!column) { diff --git a/src/view/plugin/sort/sort.tsx b/src/view/plugin/sort/sort.tsx index 5b19779c..2598e3ed 100644 --- a/src/view/plugin/sort/sort.tsx +++ b/src/view/plugin/sort/sort.tsx @@ -1,7 +1,7 @@ import { h, JSX } from 'preact'; import { classJoin, className } from '../../../util/className'; -import { ProcessorType } from '../../../pipeline/processor'; +import { PipelineProcessor, ProcessorType } from '../../../pipeline/processor'; import NativeSort from '../../../pipeline/sort/native'; import { Comparator, TCell, TColumnSort } from '../../../types'; import * as actions from './actions'; @@ -38,25 +38,52 @@ export function Sort( } & SortConfig, ) { const config = useConfig(); + const { dispatch } = useStore(); const _ = useTranslator(); const [direction, setDirection] = useState(0); - const [processor, setProcessor] = useState( - undefined, - ); - const state = useSelector((state) => state.sort); - const { dispatch } = useStore(); const sortConfig = config.sort as GenericSortConfig; + const state = useSelector((state) => state.sort); + const processorType = + typeof sortConfig?.server === 'object' + ? ProcessorType.ServerSort + : ProcessorType.Sort; - useEffect(() => { - const processor = getOrCreateSortProcessor(); - if (processor) setProcessor(processor); - }, []); + const getSortProcessor = () => { + const processors = config.pipeline.getStepsByType(processorType); + if (processors.length) { + return processors[0]; + } + return undefined; + }; + + const createSortProcessor = () => { + if (processorType === ProcessorType.ServerSort) { + return new ServerSort({ + columns: state ? state.columns : [], + ...sortConfig.server, + }); + } + + return new NativeSort({ + columns: state ? state.columns : [], + }); + }; + const getOrCreateSortProcessor = (): PipelineProcessor => { + const existingSortProcessor = getSortProcessor(); + if (existingSortProcessor) { + return existingSortProcessor; + } + + return createSortProcessor(); + }; + useEffect(() => { - config.pipeline.register(processor); + const processor = getOrCreateSortProcessor(); + config.pipeline.tryRegister(processor); return () => config.pipeline.unregister(processor); - }, [config, processor]); + }, [config]); /** * Sets the internal state of component @@ -74,6 +101,8 @@ export function Sort( }, [state]); useEffect(() => { + const processor = getSortProcessor(); + if (!processor) return; if (!state) return; @@ -82,36 +111,6 @@ export function Sort( }); }, [state]); - const getOrCreateSortProcessor = (): NativeSort | null => { - let processorType = ProcessorType.Sort; - - if (sortConfig && typeof sortConfig.server === 'object') { - processorType = ProcessorType.ServerSort; - } - - const processors = config.pipeline.getStepsByType(processorType); - - if (processors.length === 0) { - // my assumption is that we only have ONE sorting processor in the - // entire pipeline and that's why I'm displaying a warning here - let processor; - - if (processorType === ProcessorType.ServerSort) { - processor = new ServerSort({ - columns: state ? state.columns : [], - ...sortConfig.server, - }); - } else { - processor = new NativeSort({ - columns: state ? state.columns : [], - }); - } - return processor; - } - - return null; - }; - const changeDirection = (e: JSX.TargetedMouseEvent) => { e.preventDefault(); e.stopPropagation(); @@ -125,7 +124,7 @@ export function Sort( ); }; - const getSortClassName = (direction) => { + const getSortClassName = (direction: number) => { if (direction === 1) { return 'asc'; } else if (direction === -1) { From e6ab5146266a3426d3a108108772bc6355098fb6 Mon Sep 17 00:00:00 2001 From: Afshin Mehrabani Date: Sun, 31 Dec 2023 23:06:35 +0000 Subject: [PATCH 5/9] fix types --- src/pipeline/extractor/storage.ts | 2 +- src/pipeline/pipeline.ts | 46 +++++++++++++++++-------------- src/pipeline/processor.ts | 8 ++++-- src/view/container.tsx | 6 ++-- src/view/plugin/pagination.tsx | 7 +++-- src/view/plugin/search/search.tsx | 6 ++-- 6 files changed, 43 insertions(+), 32 deletions(-) diff --git a/src/pipeline/extractor/storage.ts b/src/pipeline/extractor/storage.ts index 7efbad1c..4c5c529a 100644 --- a/src/pipeline/extractor/storage.ts +++ b/src/pipeline/extractor/storage.ts @@ -10,7 +10,7 @@ interface StorageExtractorProps extends PipelineProcessorProps { } class StorageExtractor extends PipelineProcessor< - Promise, + StorageResponse, StorageExtractorProps > { get type(): ProcessorType { diff --git a/src/pipeline/pipeline.ts b/src/pipeline/pipeline.ts index 5bc87ad5..050d2d4e 100644 --- a/src/pipeline/pipeline.ts +++ b/src/pipeline/pipeline.ts @@ -3,13 +3,13 @@ import { ID } from '../util/id'; import log from '../util/log'; import { EventEmitter } from '../util/eventEmitter'; -interface PipelineEvents { +interface PipelineEvents { /** * Generic updated event. Triggers the callback function when the pipeline * is updated, including when a new processor is registered, a processor's props * get updated, etc. */ - updated: (processor: PipelineProcessor) => void; + updated: (processor: PipelineProcessor) => void; /** * Triggers the callback function when a new * processor is registered successfully @@ -27,27 +27,29 @@ interface PipelineEvents { * afterProcess will not be called if there is an * error in the pipeline (i.e a step throw an Error) */ - afterProcess: (prev: T) => void; + afterProcess: (prev: R) => void; /** * Triggers the callback function when the pipeline * fails to process all steps or at least one step * throws an Error */ - error: (prev: T) => void; + error: (prev: T) => void; } -class Pipeline extends EventEmitter> { +class Pipeline extends EventEmitter> { // available steps for this pipeline - private readonly _steps: Map[]> = - new Map[]>(); + private readonly _steps: Map< + ProcessorType, + PipelineProcessor[] + > = new Map[]>(); // used to cache the results of processors using their id field - private cache: Map = new Map(); + private cache: Map = new Map(); // keeps the index of the last updated processor in the registered // processors list and will be used to invalidate the cache // -1 means all new processors should be processed private lastProcessorIndexUpdated = -1; - constructor(steps?: PipelineProcessor[]) { + constructor(steps?: PipelineProcessor[]) { super(); if (steps) { @@ -59,7 +61,7 @@ class Pipeline extends EventEmitter> { * Clears the `cache` array */ clearCache(): void { - this.cache = new Map(); + this.cache = new Map(); this.lastProcessorIndexUpdated = -1; } @@ -69,7 +71,7 @@ class Pipeline extends EventEmitter> { * @param processor * @param priority */ - register( + register( processor: PipelineProcessor, priority: number = null, ): PipelineProcessor { @@ -99,7 +101,7 @@ class Pipeline extends EventEmitter> { * @param processor * @param priority */ - tryRegister( + tryRegister( processor: PipelineProcessor, priority: number = null, ): PipelineProcessor | undefined { @@ -117,7 +119,7 @@ class Pipeline extends EventEmitter> { * * @param processor */ - unregister(processor: PipelineProcessor): void { + unregister(processor: PipelineProcessor): void { if (!processor) return; if (this.findProcessorIndexByID(processor.id) === -1) return; @@ -138,7 +140,7 @@ class Pipeline extends EventEmitter> { * @param processor * @param priority */ - private addProcessorByPriority( + private addProcessorByPriority( processor: PipelineProcessor, priority: number, ): void { @@ -169,8 +171,8 @@ class Pipeline extends EventEmitter> { /** * Flattens the _steps Map and returns a list of steps with their correct priorities */ - get steps(): PipelineProcessor[] { - let steps: PipelineProcessor[] = []; + get steps(): PipelineProcessor[] { + let steps: PipelineProcessor[] = []; for (const type of this.getSortedProcessorTypes()) { const subSteps = this._steps.get(type); @@ -190,7 +192,7 @@ class Pipeline extends EventEmitter> { * * @param type */ - getStepsByType(type: ProcessorType): PipelineProcessor[] { + getStepsByType(type: ProcessorType): PipelineProcessor[] { return this.steps.filter((process) => process.type === type); } @@ -209,7 +211,7 @@ class Pipeline extends EventEmitter> { * * @param data */ - async process(data?: T): Promise { + async process(data?: R): Promise { const lastProcessorIndexUpdated = this.lastProcessorIndexUpdated; const steps = this.steps; @@ -224,11 +226,11 @@ class Pipeline extends EventEmitter> { // updated processor was before "processor". // This is to ensure that we always have correct and up to date // data from processors and also to skip them when necessary - prev = await processor.process(prev); + prev = (await processor.process(prev)) as R; this.cache.set(processor.id, prev); } else { // cached results already exist - prev = this.cache.get(processor.id); + prev = this.cache.get(processor.id) as R; } } } catch (e) { @@ -263,7 +265,9 @@ class Pipeline extends EventEmitter> { * This is used to invalid or skip a processor in * the process() method */ - private setLastProcessorIndex(processor: PipelineProcessor): void { + private setLastProcessorIndex( + processor: PipelineProcessor, + ): void { const processorIndex = this.findProcessorIndexByID(processor.id); if (this.lastProcessorIndexUpdated > processorIndex) { diff --git a/src/pipeline/processor.ts b/src/pipeline/processor.ts index 01835cef..47882915 100644 --- a/src/pipeline/processor.ts +++ b/src/pipeline/processor.ts @@ -16,8 +16,10 @@ export enum ProcessorType { Limit, } -interface PipelineProcessorEvents { - propsUpdated: (processor: PipelineProcessor) => void; +interface PipelineProcessorEvents { + propsUpdated: ( + processor: PipelineProcessor, + ) => void; beforeProcess: (...args) => void; afterProcess: (...args) => void; } @@ -28,7 +30,7 @@ export interface PipelineProcessorProps {} export abstract class PipelineProcessor< T, P extends Partial, -> extends EventEmitter> { +> extends EventEmitter { public readonly id: ID; private _props: P; diff --git a/src/view/container.tsx b/src/view/container.tsx index 35c8bde2..041908a6 100644 --- a/src/view/container.tsx +++ b/src/view/container.tsx @@ -8,9 +8,9 @@ import log from '../util/log'; import { useEffect } from 'preact/hooks'; import * as actions from './actions'; import { useStore } from '../hooks/useStore'; -import useSelector from '../../src/hooks/useSelector'; -import { useConfig } from '../../src/hooks/useConfig'; -import { throttle } from 'src/util/throttle'; +import useSelector from '../hooks/useSelector'; +import { useConfig } from '../hooks/useConfig'; +import { throttle } from '../util/throttle'; export function Container() { const config = useConfig(); diff --git a/src/view/plugin/pagination.tsx b/src/view/plugin/pagination.tsx index d8feb1b5..3f5e9abb 100644 --- a/src/view/plugin/pagination.tsx +++ b/src/view/plugin/pagination.tsx @@ -46,11 +46,15 @@ export function Pagination() { url: server.url, body: server.body, }); + + config.pipeline.register(processor.current); } else { processor.current = new PaginationLimit({ limit: limit, page: currentPage, }); + + config.pipeline.register(processor.current); } if (processor.current instanceof ServerPaginationLimit) { @@ -65,7 +69,6 @@ export function Pagination() { } config.pipeline.on('updated', onUpdate); - config.pipeline.register(processor.current); // we need to make sure that the state is set // to the default props when an error happens @@ -75,7 +78,7 @@ export function Pagination() { }); return () => { - config.pipeline.unregister(processor.current); + config.pipeline.unregister(processor.current); config.pipeline.off('updated', onUpdate); }; }, []); diff --git a/src/view/plugin/search/search.tsx b/src/view/plugin/search/search.tsx index 89593af2..c449e68d 100644 --- a/src/view/plugin/search/search.tsx +++ b/src/view/plugin/search/search.tsx @@ -67,9 +67,11 @@ export function Search() { }, [props]); useEffect(() => { - config.pipeline.register(processor); + if (processor) { + config.pipeline.register(processor); + } - return () => config.pipeline.unregister(processor); + return () => config.pipeline.unregister(processor); }, [config, processor]); const debouncedOnInput = useCallback( From 905f15c08986f72428c11ac55877a4a2d65f517f Mon Sep 17 00:00:00 2001 From: Afshin Mehrabani Date: Mon, 1 Jan 2024 22:05:13 +0000 Subject: [PATCH 6/9] update throttle --- src/config.ts | 2 +- src/util/throttle.ts | 41 ++++++++++--------- src/view/container.tsx | 2 +- tests/jest/util/throttle.test.ts | 69 ++++++++++++++++++++++++++++++++ 4 files changed, 94 insertions(+), 20 deletions(-) create mode 100644 tests/jest/util/throttle.test.ts diff --git a/src/config.ts b/src/config.ts index b3b20352..9f8dc465 100644 --- a/src/config.ts +++ b/src/config.ts @@ -130,7 +130,7 @@ export class Config { tableRef: createRef(), width: '100%', height: 'auto', - processingThrottleMs: 50, + processingThrottleMs: 100, autoWidth: true, style: {}, className: {}, diff --git a/src/util/throttle.ts b/src/util/throttle.ts index c11da443..aae8e113 100644 --- a/src/util/throttle.ts +++ b/src/util/throttle.ts @@ -2,29 +2,34 @@ * Throttle a given function * @param fn Function to be called * @param wait Throttle timeout in milliseconds - * @param leading whether or not to call "fn" immediately * @returns Throttled function */ -export const throttle = (fn: (...args) => void, wait = 100, leading = true) => { - let inThrottle: boolean; - let lastFn: ReturnType; - let lastTime: number; +export const throttle = (fn: (...args) => void, wait = 100) => { + let timeoutId: ReturnType; + let lastTime = Date.now(); + + const execute = (...args) => { + lastTime = Date.now(); + fn(...args); + }; return (...args) => { - if (!inThrottle) { - if (leading) { - fn(...args); - } - lastTime = Date.now(); - inThrottle = true; + const currentTime = Date.now(); + const elapsed = currentTime - lastTime; + + if (elapsed >= wait) { + // If enough time has passed since the last call, execute the function immediately + execute(args); } else { - clearTimeout(lastFn); - lastFn = setTimeout(() => { - if (Date.now() - lastTime >= wait) { - fn(...args); - lastTime = Date.now(); - } - }, Math.max(wait - (Date.now() - lastTime), 0)); + // If not enough time has passed, schedule the function call after the remaining delay + if (timeoutId) { + clearTimeout(timeoutId); + } + + timeoutId = setTimeout(() => { + execute(args); + timeoutId = null; + }, wait - elapsed); } }; }; diff --git a/src/view/container.tsx b/src/view/container.tsx index 041908a6..40a5ef98 100644 --- a/src/view/container.tsx +++ b/src/view/container.tsx @@ -35,7 +35,7 @@ export function Container() { log.error(e); dispatch(actions.SetDataErrored()); } - }, config.processingThrottleMs, false); + }, config.processingThrottleMs); useEffect(() => { // set the initial header object diff --git a/tests/jest/util/throttle.test.ts b/tests/jest/util/throttle.test.ts new file mode 100644 index 00000000..eb183f5c --- /dev/null +++ b/tests/jest/util/throttle.test.ts @@ -0,0 +1,69 @@ +import { throttle } from '../../../src/util/throttle'; + +const sleep = (wait: number) => new Promise((r) => setTimeout(r, wait)); + +describe('throttle', () => { + it('should throttle calls', async () => { + const wait = 100; + const fn = jest.fn(); + const throttled = throttle(fn, wait); + + throttled('a'); + sleep(wait - 5) + throttled('b'); + sleep(wait - 10) + throttled('c'); + + await sleep(wait); + + expect(fn).toBeCalledTimes(1); + expect(fn).toBeCalledWith(['c']); + }); + + it('should execute the first call', async () => { + const wait = 100; + const fn = jest.fn(); + const throttled = throttle(fn, wait); + + throttled(); + + await sleep(wait); + + expect(fn).toBeCalledTimes(1); + }); + + it('should call at trailing edge of the timeout', async () => { + const wait = 100; + const fn = jest.fn(); + const throttled = throttle(fn, wait); + + throttled(); + + expect(fn).toBeCalledTimes(0); + + await sleep(wait); + + expect(fn).toBeCalledTimes(1); + }); + + it('should call after the timer', async () => { + const wait = 100; + const fn = jest.fn(); + const throttled = throttle(fn, wait); + + throttled(); + await sleep(wait); + + expect(fn).toBeCalledTimes(1); + + throttled(); + await sleep(wait); + + expect(fn).toBeCalledTimes(2); + + throttled(); + await sleep(wait); + + expect(fn).toBeCalledTimes(3); + }); +}); From 835b31ebf6d6fe490e423bbc4ae620d133280c61 Mon Sep 17 00:00:00 2001 From: Afshin Mehrabani Date: Tue, 9 Jan 2024 08:54:06 +0000 Subject: [PATCH 7/9] fix tests --- src/view/plugin/search/search.tsx | 6 +++--- tests/jest/grid.test.ts | 1 + tests/jest/view/container.test.tsx | 4 ++++ 3 files changed, 8 insertions(+), 3 deletions(-) diff --git a/src/view/plugin/search/search.tsx b/src/view/plugin/search/search.tsx index c449e68d..ab7a5984 100644 --- a/src/view/plugin/search/search.tsx +++ b/src/view/plugin/search/search.tsx @@ -67,9 +67,9 @@ export function Search() { }, [props]); useEffect(() => { - if (processor) { - config.pipeline.register(processor); - } + if (!processor) return undefined; + + config.pipeline.register(processor); return () => config.pipeline.unregister(processor); }, [config, processor]); diff --git a/tests/jest/grid.test.ts b/tests/jest/grid.test.ts index 440b0a10..bc3d542d 100644 --- a/tests/jest/grid.test.ts +++ b/tests/jest/grid.test.ts @@ -12,6 +12,7 @@ describe('Grid class', () => { it('should trigger the events in the correct order', async () => { const grid = new Grid({ + processingThrottleMs: 0, columns: ['a', 'b', 'c'], data: [[1, 2, 3]], }); diff --git a/tests/jest/view/container.test.tsx b/tests/jest/view/container.test.tsx index fae66f47..d3b548d0 100644 --- a/tests/jest/view/container.test.tsx +++ b/tests/jest/view/container.test.tsx @@ -20,6 +20,7 @@ describe('Container component', () => { beforeEach(() => { config = new Config().update({ + processingThrottleMs: 0, data: [ [1, 2, 3], ['a', 'b', 'c'], @@ -244,6 +245,7 @@ describe('Container component', () => { it('should render a container with array of objects without columns input', async () => { const config = Config.fromPartialConfig({ + processingThrottleMs: 0, data: [ [1, 2, 3], ['a', 'b', 'c'], @@ -285,6 +287,7 @@ describe('Container component', () => { it('should render a container with array of objects with object columns', async () => { const config = Config.fromPartialConfig({ + processingThrottleMs: 0, columns: [ { name: 'Name', @@ -341,6 +344,7 @@ describe('Container component', () => { it('should unregister the processors', async () => { const config = new Config().update({ + processingThrottleMs: 0, pagination: true, search: true, sort: true, From 231a7c1f53328ccc80ab40e7f97c5b290d32c5ff Mon Sep 17 00:00:00 2001 From: Afshin Mehrabani Date: Tue, 9 Jan 2024 08:56:53 +0000 Subject: [PATCH 8/9] prettier --- src/pipeline/processor.ts | 4 +--- src/view/plugin/sort/sort.tsx | 2 +- tests/jest/util/throttle.test.ts | 4 ++-- 3 files changed, 4 insertions(+), 6 deletions(-) diff --git a/src/pipeline/processor.ts b/src/pipeline/processor.ts index 47882915..19291c93 100644 --- a/src/pipeline/processor.ts +++ b/src/pipeline/processor.ts @@ -17,9 +17,7 @@ export enum ProcessorType { } interface PipelineProcessorEvents { - propsUpdated: ( - processor: PipelineProcessor, - ) => void; + propsUpdated: (processor: PipelineProcessor) => void; beforeProcess: (...args) => void; afterProcess: (...args) => void; } diff --git a/src/view/plugin/sort/sort.tsx b/src/view/plugin/sort/sort.tsx index 2598e3ed..3dd1e6d6 100644 --- a/src/view/plugin/sort/sort.tsx +++ b/src/view/plugin/sort/sort.tsx @@ -77,7 +77,7 @@ export function Sort( return createSortProcessor(); }; - + useEffect(() => { const processor = getOrCreateSortProcessor(); config.pipeline.tryRegister(processor); diff --git a/tests/jest/util/throttle.test.ts b/tests/jest/util/throttle.test.ts index eb183f5c..4e6ed5ea 100644 --- a/tests/jest/util/throttle.test.ts +++ b/tests/jest/util/throttle.test.ts @@ -9,9 +9,9 @@ describe('throttle', () => { const throttled = throttle(fn, wait); throttled('a'); - sleep(wait - 5) + sleep(wait - 5); throttled('b'); - sleep(wait - 10) + sleep(wait - 10); throttled('c'); await sleep(wait); From da64c760fc70c3ffba30d55e04425ea79c5dd3a0 Mon Sep 17 00:00:00 2001 From: Afshin Mehrabani Date: Tue, 9 Jan 2024 09:06:45 +0000 Subject: [PATCH 9/9] fix tests --- tests/jest/view/container.test.tsx | 1 + 1 file changed, 1 insertion(+) diff --git a/tests/jest/view/container.test.tsx b/tests/jest/view/container.test.tsx index d3b548d0..43b788e5 100644 --- a/tests/jest/view/container.test.tsx +++ b/tests/jest/view/container.test.tsx @@ -265,6 +265,7 @@ describe('Container component', () => { it('should render a container with array of objects with string columns', async () => { const config = Config.fromPartialConfig({ + processingThrottleMs: 0, columns: ['Name', 'Phone Number'], data: [ { name: 'boo', phoneNumber: '123' },