/* --------------------------------------------------------------------------------------------- * Copyright (c) Microsoft Corporation. All rights reserved. * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ // Some code copied and modified from https://github.com/microsoft/vscode/blob/1.44.0/src/vs/base/common/decorators.ts import { isPromise } from './types'; import { randomString } from './uuid'; export function createDecorator(mapFn: (fn: Function, key: string) => Function): Function { return (target: any, key: string, descriptor: any) => { let fnKey: string | null = null; let fn: Function | null = null; if (typeof descriptor.value === 'function') { fnKey = 'value'; fn = descriptor.value; } else if (typeof descriptor.get === 'function') { fnKey = 'get'; fn = descriptor.get; } if (!fn) { throw new Error('not supported'); } descriptor[fnKey!] = mapFn(fn, key); }; } let memoizeId = 0; export function createMemoizer() { const memoizeKeyPrefix = `$memoize${memoizeId++}`; let self: any; const result = function memoize(target: any, key: string, descriptor: any) { let fnKey: string | null = null; let fn: Function | null = null; if (typeof descriptor.value === 'function') { fnKey = 'value'; fn = descriptor.value; if (fn!.length !== 0) { // eslint-disable-next-line no-console console.warn('Memoize should only be used in functions with zero parameters'); } } else if (typeof descriptor.get === 'function') { fnKey = 'get'; fn = descriptor.get; } if (!fn) { throw new Error('not supported'); } const memoizeKey = `${memoizeKeyPrefix}:${key}`; descriptor[fnKey!] = function (...args: any[]) { self = this; if (!this.hasOwnProperty(memoizeKey)) { Object.defineProperty(this, memoizeKey, { configurable: true, enumerable: false, writable: true, value: fn!.apply(this, args), }); } return this[memoizeKey]; }; }; result.clear = () => { if (typeof self === 'undefined') { return; } Object.getOwnPropertyNames(self).forEach((property) => { if (property.indexOf(memoizeKeyPrefix) === 0) { delete self[property]; } }); }; return result; } export function memoize(target: any, key: string, descriptor: any) { return createMemoizer()(target, key, descriptor); } export type IDebounceReducer = (previousValue: T, ...args: any[]) => T; export function debounce(delay: number, reducer?: IDebounceReducer, initialValueProvider?: () => T): Function { return createDecorator((fn, key) => { const timerKey = `$debounce$${key}`; const resultKey = `$debounce$result$${key}`; return function (this: any, ...args: any[]) { if (!this[resultKey]) { this[resultKey] = initialValueProvider ? initialValueProvider() : undefined; } clearTimeout(this[timerKey]); if (reducer) { this[resultKey] = reducer(this[resultKey], ...args); args = [this[resultKey]]; } this[timerKey] = setTimeout(() => { fn.apply(this, args); this[resultKey] = initialValueProvider ? initialValueProvider() : undefined; }, delay); }; }); } export function throttle(delay: number, reducer?: IDebounceReducer, initialValueProvider?: () => T): Function { return createDecorator((fn, key) => { const timerKey = `$throttle$timer$${key}`; const resultKey = `$throttle$result$${key}`; const lastRunKey = `$throttle$lastRun$${key}`; const pendingKey = `$throttle$pending$${key}`; return function (this: any, ...args: any[]) { if (!this[resultKey]) { this[resultKey] = initialValueProvider ? initialValueProvider() : undefined; } if (this[lastRunKey] === null || this[lastRunKey] === undefined) { this[lastRunKey] = -Number.MAX_VALUE; } if (reducer) { this[resultKey] = reducer(this[resultKey], ...args); } if (this[pendingKey]) { return; } const nextTime = this[lastRunKey] + delay; if (nextTime <= Date.now()) { this[lastRunKey] = Date.now(); fn.apply(this, [this[resultKey]]); this[resultKey] = initialValueProvider ? initialValueProvider() : undefined; } else { this[pendingKey] = true; this[timerKey] = setTimeout(() => { this[pendingKey] = false; this[lastRunKey] = Date.now(); fn.apply(this, [this[resultKey]]); this[resultKey] = initialValueProvider ? initialValueProvider() : undefined; }, nextTime - Date.now()); } }; }); } export function es5ClassCompat(target: any): any { function _() { // @ts-ignore return Reflect.construct(target, arguments, this.constructor); } Object.defineProperty(_, 'name', Object.getOwnPropertyDescriptor(target, 'name')!); Object.setPrototypeOf(_, target); Object.setPrototypeOf(_.prototype, target.prototype); return _; } /** * Store promises so that only one promise exists at a time */ export function pMemoize(hashFn?: (...params: any[]) => string) { return function (target: any, key: string, descriptor: any) { let fnKey: string | null = null; let fn: (() => any) | null = null; if (typeof descriptor.value === 'function') { fnKey = 'value'; fn = descriptor.value; } else if (typeof descriptor.get === 'function') { fnKey = 'get'; fn = descriptor.get; } if (!fn) { throw new Error('not supported'); } const hashFnResult = hashFn || (() => randomString(4)); descriptor[fnKey!] = function (...args: any[]) { const self = this; const memoizeKey = `$memoizedPromise:${hashFnResult(...args)}:${key}`; if (!this.hasOwnProperty(memoizeKey)) { const promise = fn!.apply(this, args); if (!isPromise(promise)) { throw new Error(`return type of ${key} is not promise, please use memoize instead`); } promise.finally(() => { delete self[memoizeKey]; }); Object.defineProperty(this, memoizeKey, { configurable: true, enumerable: false, writable: true, value: promise, }); } return this[memoizeKey]; }; }; }