@boost/decorators
Version:
Experimental decorators for common patterns.
227 lines (203 loc) • 9.21 kB
JavaScript
function ownKeys(e, r) { var t = Object.keys(e); if (Object.getOwnPropertySymbols) { var o = Object.getOwnPropertySymbols(e); r && (o = o.filter(function (r) { return Object.getOwnPropertyDescriptor(e, r).enumerable; })), t.push.apply(t, o); } return t; }
function _objectSpread(e) { for (var r = 1; r < arguments.length; r++) { var t = null != arguments[r] ? arguments[r] : {}; r % 2 ? ownKeys(Object(t), !0).forEach(function (r) { _defineProperty(e, r, t[r]); }) : Object.getOwnPropertyDescriptors ? Object.defineProperties(e, Object.getOwnPropertyDescriptors(t)) : ownKeys(Object(t)).forEach(function (r) { Object.defineProperty(e, r, Object.getOwnPropertyDescriptor(t, r)); }); } return e; }
function _defineProperty(obj, key, value) { key = _toPropertyKey(key); if (key in obj) { Object.defineProperty(obj, key, { value: value, enumerable: true, configurable: true, writable: true }); } else { obj[key] = value; } return obj; }
function _toPropertyKey(t) { var i = _toPrimitive(t, "string"); return "symbol" == typeof i ? i : String(i); }
function _toPrimitive(t, r) { if ("object" != typeof t || !t) return t; var e = t[Symbol.toPrimitive]; if (void 0 !== e) { var i = e.call(t, r || "default"); if ("object" != typeof i) return i; throw new TypeError("@@toPrimitive must return a primitive value."); } return ("string" === r ? String : Number)(t); }
// Bundled with Packemon: https://packemon.dev
// Platform: browser, Support: stable, Format: esm
function isMethod(target, property, descriptor) {
return Boolean(property && descriptor && typeof descriptor === 'object' && !('initializer' in descriptor) && ('value' in descriptor || 'get' in descriptor));
}
/**
* A method decorator that automatically binds a class method's
* `this` context to its current instance.
*/
function Bind() {
return (target, property, descriptor) => {
if (process.env.NODE_ENV !== "production" && (!isMethod(target, property, descriptor) || !('value' in descriptor && typeof descriptor.value === 'function'))) {
throw new TypeError(`\`@Bind\` may only be applied to class methods.`);
}
const func = descriptor.value;
return {
configurable: true,
get() {
const bound = func.bind(this);
// Only cache the bound function when in the deepest sub-class,
// otherwise any `super` calls will overwrite each other.
if (target.constructor.name === this.constructor.name) {
Object.defineProperty(this, property, {
configurable: true,
value: bound,
writable: true
});
}
// eslint-disable-next-line @typescript-eslint/no-unsafe-return
return bound;
}
};
};
}
/**
* A method decorator that delays the execution of the class method
* by the provided time in milliseconds.
*/
function Debounce(delay) {
return (target, property, descriptor) => {
if (process.env.NODE_ENV !== "production" && (!isMethod(target, property, descriptor) || !('value' in descriptor && typeof descriptor.value === 'function'))) {
throw new TypeError(`\`@Debounce\` may only be applied to class methods.`);
}
// We must use a map as all class instances would share the
// same timer value otherwise.
const timers = new WeakMap();
// Overwrite the value function with a new debounced function
const func = descriptor.value;
// @ts-expect-error Override generic
descriptor.value = function debounce(...args) {
const timer = timers.get(this);
if (timer) {
clearTimeout(timer);
timers.delete(this);
}
timers.set(this, setTimeout(() => {
func.apply(this, args);
}, delay));
};
};
}
function isClass(target, property, descriptor) {
return typeof target === 'function' && !property && !descriptor;
}
function isParam(target, property, index) {
return Boolean(property && typeof index === 'number');
}
function isProperty(target, property, descriptor) {
return Boolean(property && (descriptor && typeof descriptor === 'object' && 'initializer' in descriptor || descriptor === undefined));
}
/* eslint-disable no-console */
/**
* A decorator that marks a class declaration, class method,
* class property, or method parameter as deprecated by
* logging a deprecation message to the console.
*/
function Deprecate(message) {
return (target, property, descriptor) => {
const isProtoOrStatic = typeof target === 'function';
const className = isProtoOrStatic ? target.name : target.constructor.name;
const accessSymbol = isProtoOrStatic ? `.${String(property)}` : `#${String(property)}`;
// Class
if (isClass(target, property, descriptor)) {
console.debug(message ?? `Class \`${className}\` has been deprecated.`);
// Method
} else if (isMethod(target, property, descriptor)) {
console.debug(message ?? `Method \`${className + accessSymbol}()\` has been deprecated.`);
// Property
} else if (isProperty(target, property, descriptor)) {
console.debug(message ?? `Property \`${className + accessSymbol}\` has been deprecated.`);
// Param
} else if (isParam(target, property, descriptor)) {
console.debug(message ?? `Parameter ${descriptor} for \`${className + accessSymbol}()\` has been deprecated.`);
}
};
}
function hasher(...args) {
return JSON.stringify(args);
}
function createMemoizer(method, rootCache, options) {
// Must be a normal function as we require `this`
return function memoizer(...args) {
// Extract the cache for this specific instance
let cache = rootCache.get(this);
if (!cache) {
cache = options.cache ? new Map(options.cache) : new Map();
rootCache.set(this, cache);
}
// Hash the key and check the cache
const key = options.hasher(...args);
const item = cache.get(key);
if (item && (!item.time || typeof item.time === 'number' && item.time > Date.now())) {
return item.value;
}
// No cache so execute and cache the result
const value = method.apply(this, args);
const time = options.expires > 0 ? Date.now() + options.expires : null;
cache.set(key, {
time,
value
});
// Only cache if successful
if (value instanceof Promise) {
// eslint-disable-next-line promise/prefer-await-to-then
value.catch(() => cache?.delete(key));
}
return value;
};
}
/**
* A method decorator that caches the return value of a class method or
* getter to consistently and efficiently return the same value.
*/
function Memoize(options = {}) {
// eslint-disable-next-line complexity
return (target, property, descriptor) => {
if (process.env.NODE_ENV !== "production" && (!isMethod(target, property, descriptor) || !('value' in descriptor && typeof descriptor.value === 'function') && !('get' in descriptor && typeof descriptor.get === 'function'))) {
throw new TypeError(`\`@Memoize\` may only be applied to class methods or getters.`);
}
const config = _objectSpread({
cache: null,
expires: 0,
hasher
}, typeof options === 'function' ? {
hasher: options
} : options);
if (process.env.NODE_ENV !== "production") {
if (config.cache && !(config.cache instanceof Map)) {
throw new Error('`cache` must be an instance of `Map`.');
}
if (typeof config.expires !== 'number' || config.expires < 0) {
throw new Error('`expires` must be a number greater than or equal to 0.');
}
if (typeof config.hasher !== 'function') {
throw new TypeError('`hasher` must be a function.');
}
}
// We must use a map as all class instances would share the
// same cache otherwise. Probability of collision is high.
const rootCache = new WeakMap();
if (descriptor.get) {
// @ts-expect-error Override generic
descriptor.get = createMemoizer(descriptor.get, rootCache, config);
} else if (descriptor.value) {
// @ts-expect-error Override generic
descriptor.value = createMemoizer(descriptor.value, rootCache, config);
}
};
}
/**
* A method decorator that throttles the execution of a class method to
* only fire once within every delay timeframe (in milliseconds).
*/
function Throttle(delay) {
return (target, property, descriptor) => {
if (process.env.NODE_ENV !== "production" && (!isMethod(target, property, descriptor) || !('value' in descriptor && typeof descriptor.value === 'function'))) {
throw new TypeError(`\`@Throttle\` may only be applied to class methods.`);
}
// We must use a map as all class instances would share the
// same boolean value otherwise.
const throttling = new WeakMap();
// Overwrite the value function with a new throttled function
const func = descriptor.value;
// @ts-expect-error Override generic
descriptor.value = function throttle(...args) {
if (throttling.get(this)) {
return;
}
func.apply(this, args);
throttling.set(this, true);
setTimeout(() => {
throttling.delete(this);
}, delay);
};
};
}
export { Bind, Debounce, Deprecate, Memoize, Throttle };
//# sourceMappingURL=index.js.map