UNPKG

snpk-cycle

Version:

> High Precision Adaptive Timer for Node.js

483 lines (417 loc) 14.9 kB
import { performance } from "perf_hooks"; /** * @author SNIPPIK * @description Базовый класс цикла * @class BaseCycle * @extends Set * @abstract * @private */ abstract class BaseCycle<T = unknown> extends Set<T> { /** * @description Последний записанное значение performance.now(), нужно для улавливания event loop lags * @private */ private performance: number; /** * @description Последний записанное значение performance.now(), нужно для сглаживания лага * @private */ private prevEventLoopLag: number; /** * @description Последний сохраненный временной интервал * @private */ private lastDelay: number; /** * @description Следующее запланированное время запуска (в ms, с плавающей точкой) * @private */ private startTime: number = 0; /** * @description Время для высчитывания * @private */ private tickTime: number = 0; /** * @description Временное число отставания цикла в миллисекундах * @private */ private drift: number = 0; /** * @description Последний зафиксированный разбег во времени * @returns number * @public */ public get drifting(): number { return this.drift + this.prevEventLoopLag; }; /** * @description Время циклической системы изнутри * @returns number * @public */ public get insideTime(): number { return this.startTime + this.tickTime; }; /** * @description Последний зафиксированный промежуток выполнения * @returns number * @public */ public get delay(): number { return this.lastDelay; }; /** * @description Высчитываем задержку шага * @param duration - Истинное время шага * @private */ private set delay(duration: number) { // Получаем следующее время const expectedTime = this.startTime + this.tickTime + duration; // Корректируем шаги, для точности цикла const step = Math.max(1, (this.time - expectedTime) / duration); // Делаем шаг const timeCorrection = step * duration; this.tickTime += timeCorrection; this.lastDelay = timeCorrection; }; /** * @description Метод получения времени для обновления времени цикла * @default Date.now * @returns number * @protected */ protected get time(): number { return Date.now(); }; /** * @description Добавляем элемент в очередь * @param item - Объект T * @public */ public add(item: T): this { const existing = this.has(item); // Если добавляется уже существующий объект if (existing) this.delete(item); super.add(item); // Запускаем цикл, если добавлен первый объект if (this.size === 1 && this.startTime === 0) { this.startTime = this.time; setImmediate(this._stepCycle); } return this; }; /** * @description Чистка цикла от всего * @returns void * @public */ public reset(): void { this.clear(); // Удаляем все объекты this.startTime = 0; this.tickTime = 0; this.lastDelay = 0; // Чистимся от drift составляющих this.drift = 0; // Чистим performance.now this.performance = 0; this.prevEventLoopLag = 0; }; /** * @description Выполняет шаг цикла с учётом точного времени следующего запуска * @returns void * @protected * @abstract */ protected abstract _stepCycle: () => void; /** * @description Проверяем время для запуска цикла повторно, без учета дрифта * @returns void * @protected * @readonly */ protected _stepCheckTimeCycle = (duration: number): void => { // Проверяем цикл на наличие объектов if (this.size === 0) return this.reset(); // Высчитываем время шага this.delay = duration; // Запускаем таймер return this._runTimeout(this.insideTime, this._stepCycle); }; /** * @description Проверяем время для запуска цикла повторно с учетом дрифта цикла * @returns void * @protected * @readonly */ protected _stepCheckTimeCycleDrift = (duration: number): void => { if (this.size === 0) return this.reset(); // Для компенсации дрейфа const tickStart = this.time; // Высчитываем время шага this.delay = duration; // Получение задержки event loop const lags = this._calculateLags(this.lastDelay); // Следующее время шага с вычетом упущенного времени const nextTargetTime = this.insideTime - this.drift - lags; // Запускаем шаг this._runTimeout(nextTargetTime, () => { this._stepCycle(); const tickEnd = this.time; // Компенсируем дрейф this.drift = this._compensator(0.9, this.drift, tickEnd - tickStart); }); }; /** * @description Функция запуска timeout или immediate функции * @param actualTime - Внутренне время с учетом прошлого тика * @param callback - Функция для высчитывания * @returns void * @protected * @readonly */ protected _runTimeout = (actualTime: number, callback: () => void): void => { const delay = Math.max(0, actualTime - this.time); (delay < 1 ? process.nextTick : setTimeout)(callback, delay); }; /** * @description Высчитываем задержки event loop * @param duration - Размер шага * @protected * @readonly */ protected _calculateLags = (duration: number): number => { // Коррекция event loop lag const performanceNow = performance.now(); const driftEvent = this.performance ? Math.max(0, (performanceNow - this.performance) - duration) : 0; this.performance = performanceNow; // Смягчение event loop lag return this.prevEventLoopLag = this.prevEventLoopLag !== undefined ? this._compensator(0.9, this.prevEventLoopLag, driftEvent): driftEvent; }; /** * @description Сглаживание дрифта времени, смягчает новый по сравнению со старым * @param alpha - Значение для сглаживания * @param old - Старое время * @param current - Новое время * @private */ private _compensator = (alpha: number, old: number, current: number): number => { return alpha * old + (1 - alpha) * current; }; } /** * @author SNIPPIK * @description Класс для удобного управления циклами * @class TaskCycle * @abstract * @public */ export abstract class TaskCycle<T = unknown> extends BaseCycle<T> { /** * @description Создаем класс и добавляем параметры * @param options - Параметры для работы класса * @constructor * @protected */ protected constructor(public readonly options: TaskCycleConfig<T>) { super(); }; /** * @description Добавляем элемент в очередь * @param item - Объект T * @returns this * @public */ public add = (item: T): this => { if (this.options.custom?.push) this.options.custom?.push(item); else if (this.has(item)) this.delete(item); super.add(item); return this; }; /** * @description Удаляем элемент из очереди * @param item - Объект T * @returns boolean * @public */ public delete = (item: T) => { const index = this.has(item); // Если есть объект в базе if (index) { if (this.options.custom?.remove) this.options.custom.remove(item); super.delete(item); } return true; }; /** * @description Здесь будет выполнен прогон объектов для выполнения execute * @returns Promise<void> * @readonly * @private */ protected _stepCycle = async (): Promise<void> => { this.options?.custom?.step?.(); // Запускаем цикл for (const item of this) { // Если объект не готов if (!this.options.filter(item)) continue; try { this.options.execute(item); } catch (error) { this.delete(item); console.log(error); } } // Запускаем цикл повторно if (this.options.drift) return this._stepCheckTimeCycle(this.options.duration); return this._stepCheckTimeCycleDrift(this.options.duration); }; } /** * @author SNIPPIK * @description Класс для удобного управления promise циклами * @class PromiseCycle * @abstract * @public */ export abstract class PromiseCycle<T = unknown> extends BaseCycle<T> { /** * @description Создаем класс и добавляем параметры * @param options - Параметры для работы класса * @constructor * @protected */ protected constructor(public readonly options: PromiseCycleConfig<T>) { super(); }; /** * @description Добавляем элемент в очередь * @param item - Объект T * @returns this * @public */ public add = (item: T): this => { if (this.options.custom?.push) this.options.custom?.push(item); else if (this.has(item)) this.delete(item); super.add(item); return this; }; /** * @description Удаляем элемент из очереди * @param item - Объект T * @returns boolean * @public */ public delete = (item: T) => { const index = this.has(item); // Если есть объект в базе if (index) { if (this.options.custom?.remove) this.options.custom.remove(item); super.delete(item); } return true; }; /** * @description Здесь будет выполнен прогон объектов для выполнения execute * @returns Promise<void> * @readonly * @private */ protected _stepCycle = async (): Promise<void> => { for (const item of this) { // Если объект не готов if (!this.options.filter(item)) continue; try { const bool = await this.options.execute(item); // Если ответ был получен if (!bool) this.delete(item); } catch (error) { this.delete(item); console.log(error); } } // Запускаем цикл повторно if (this.options.drift) return this._stepCheckTimeCycle(30e3); return this._stepCheckTimeCycleDrift(30e3); }; } /** * @author SNIPPIK * @description Интерфейс для опций BaseCycle * @private */ interface BaseCycleConfig<T> { /** * @description Допустим ли drift, если требуется учитывать дрифт для стабилизации цикла * @readonly * @public */ readonly drift: boolean; /** * @description Как фильтровать объекты, вдруг объект еще не готов * @readonly * @public */ readonly filter: (item: T) => boolean; /** * @description Кастомные функции, необходимы для модификации или правильного удаления * @readonly * @public */ readonly custom?: { /** * @description Данная функция расширяет функционал добавления, выполняется перед добавлением * @param item - объект * @readonly * @public */ readonly push?: (item: T) => void; /** * @description Данная функция расширяет функционал удаления, выполняется перед удалением * @param item - объект * @readonly * @public */ readonly remove?: (item: T) => void; /** * @description Данная функция расширяет функционал шага, выполняется перед шагом * @readonly * @public */ readonly step?: () => void; } } /** * @author SNIPPIK * @description Интерфейс для опций SyncCycle * @private */ interface TaskCycleConfig<T> extends BaseCycleConfig<T> { /** * @description Функция для выполнения * @readonly * @public */ readonly execute: (item: T) => Promise<void> | void; /** * @description Время прогона цикла, через n времени будет запущен цикл по новой * @readonly * @public */ duration: number; } /** * @author SNIPPIK * @description Интерфейс для опций AsyncCycle * @private */ interface PromiseCycleConfig<T> extends BaseCycleConfig<T> { /** * @description Функция для выполнения * @readonly * @public */ readonly execute: (item: T) => Promise<boolean>; }