diff --git a/packages/js-lib/src/decorators/asyncMemo.decorator.ts b/packages/js-lib/src/decorators/asyncMemo.decorator.ts index 720a16fe..c077eff3 100644 --- a/packages/js-lib/src/decorators/asyncMemo.decorator.ts +++ b/packages/js-lib/src/decorators/asyncMemo.decorator.ts @@ -2,8 +2,9 @@ import { _assert, _assertTypeOf } from '../error/assert.js' import type { CommonLogger } from '../log/commonLogger.js' import type { AnyAsyncFunction, AnyFunction, AnyObject, MaybeParameters } from '../types.js' import { _objectAssign, MISS } from '../types.js' +import type { MethodDecorator } from './decorator.util.js' import { _getTargetMethodSignature } from './decorator.util.js' -import type { AsyncMemoCache, MethodDecorator } from './memo.util.js' +import type { AsyncMemoCache } from './memo.util.js' import { jsonMemoSerializer } from './memo.util.js' export interface AsyncMemoOptions { diff --git a/packages/js-lib/src/decorators/debounce.decorator.test.ts b/packages/js-lib/src/decorators/debounce.decorator.test.ts index 732edc7b..4e1dcd70 100644 --- a/packages/js-lib/src/decorators/debounce.decorator.test.ts +++ b/packages/js-lib/src/decorators/debounce.decorator.test.ts @@ -1,8 +1,8 @@ -import { test } from 'vitest' +import { describe, expect, test } from 'vitest' import { _since } from '../datetime/index.js' import { pDelay } from '../promise/index.js' import type { AnyFunction, UnixTimestampMillis } from '../types.js' -import { _Debounce } from './debounce.decorator.js' +import { _AsyncDebounce, _AsyncThrottle, _Debounce } from './debounce.decorator.js' class C { // @debounce(200, {leading: true, trailing: true}) @@ -27,6 +27,63 @@ async function startTimer(fn: AnyFunction, interval: number, count: number): Pro await pDelay(1000) // extra wait } -test('@debounce', async () => { - await startTimer(fn, 10, 10) +describe('@_Debounce', () => { + test('should debounce decorated method calls', async () => { + await startTimer(fn, 10, 10) + }) +}) + +describe('@_AsyncDebounce', () => { + test('should coalesce calls and resolve every caller with a real result (never undefined)', async () => { + class C { + calls = 0 + + @_AsyncDebounce(20) + async save(n: number): Promise { + this.calls++ + return n + } + } + + const inst = new C() + const results = await Promise.all([inst.save(1), inst.save(2), inst.save(3)]) + + expect(results).toEqual([3, 3, 3]) + expect(inst.calls).toBe(1) + }) +}) + +describe('@_AsyncThrottle', () => { + test('should invoke on the leading edge immediately', async () => { + class C { + calls = 0 + + @_AsyncThrottle(50) + async ping(n: number): Promise { + this.calls++ + return n + } + } + + const inst = new C() + expect(await inst.ping(7)).toBe(7) + expect(inst.calls).toBe(1) + }) + + test('should resolve in-window calls with undefined when trailing is disabled', async () => { + class C { + calls = 0 + + @_AsyncThrottle(50, { trailing: false }) + async ping(n: number): Promise { + this.calls++ + return n + } + } + + const inst = new C() + expect(await inst.ping(1)).toBe(1) + expect(await inst.ping(2)).toBeUndefined() + expect(inst.calls).toBe(1) + }) }) diff --git a/packages/js-lib/src/decorators/debounce.decorator.ts b/packages/js-lib/src/decorators/debounce.decorator.ts index 3a402bdb..9c8b3d36 100644 --- a/packages/js-lib/src/decorators/debounce.decorator.ts +++ b/packages/js-lib/src/decorators/debounce.decorator.ts @@ -1,18 +1,69 @@ -import type { DebounceOptions, ThrottleOptions } from './debounce.js' -import { _debounce, _throttle } from './debounce.js' +import type { AnyAsyncFunction, AnyFunction } from '../types.js' +import type { + AsyncDebounceOptions, + AsyncThrottleOptions, + DebounceOptions, + ThrottleOptions, +} from './debounce.js' +import { _asyncDebounce, _asyncThrottle, _debounce, _throttle } from './debounce.js' +import type { MethodDecorator } from './decorator.util.js' -export function _Debounce(wait: number, opt: DebounceOptions = {}): MethodDecorator { +export function _Debounce( + wait: number, + opt: DebounceOptions = {}, +): MethodDecorator { return (_target, _key, descriptor) => { - const originalFn = descriptor.value - descriptor.value = _debounce(originalFn as any, wait, opt) + const originalFn = descriptor.value! + descriptor.value = _debounce(originalFn, wait, opt) return descriptor } } -export function _Throttle(wait: number, opt: ThrottleOptions = {}): MethodDecorator { +export function _Throttle( + wait: number, + opt: ThrottleOptions = {}, +): MethodDecorator { return (_target, _key, descriptor) => { - const originalFn = descriptor.value - descriptor.value = _throttle(originalFn as any, wait, opt) + const originalFn = descriptor.value! + descriptor.value = _throttle(originalFn, wait, opt) + return descriptor + } +} + +/** + * Like `@_Debounce`, but for async methods: every call returns a real Promise resolving with the + * coalesced invocation's result, so `await`-ing never yields a stale value or a non-promise + * + * Be aware that the decorated method may resolve `undefined` if the config yields no invocation, + * which the method's `T` type can't express. + * + * @experimental + */ +export function _AsyncDebounce( + wait: number, + opt: AsyncDebounceOptions = {}, +): MethodDecorator { + return (_target, _key, descriptor) => { + const originalFn = descriptor.value! + descriptor.value = _asyncDebounce(originalFn, wait, opt) as any + return descriptor + } +} + +/** + * Like `@_Throttle`, but for async methods. + * + * @see {@link _AsyncDebounce} + * + * @experimental + */ +export function _AsyncThrottle( + wait: number, + opt: AsyncThrottleOptions = {}, +): MethodDecorator { + return (_target, _key, descriptor) => { + const originalFn = descriptor.value! + descriptor.value = _asyncThrottle(originalFn, wait, opt) as any return descriptor } } diff --git a/packages/js-lib/src/decorators/debounce.test.ts b/packages/js-lib/src/decorators/debounce.test.ts index a6c1db8b..1b16b867 100644 --- a/packages/js-lib/src/decorators/debounce.test.ts +++ b/packages/js-lib/src/decorators/debounce.test.ts @@ -1,8 +1,8 @@ -import { afterEach, test, vi } from 'vitest' +import { afterEach, describe, expect, test, vi } from 'vitest' import { _since } from '../datetime/index.js' import { pDelay } from '../promise/index.js' import type { AnyFunction, UnixTimestampMillis } from '../types.js' -import { _debounce } from './debounce.js' +import { _asyncDebounce, _asyncThrottle, _debounce } from './debounce.js' afterEach(() => { vi.useRealTimers() @@ -22,22 +22,98 @@ async function startTimer(fn: AnyFunction, interval: number, count: number): Pro await pDelay(2000) // extra wait } -test('_debounce', async () => { - vi.useFakeTimers() +describe('_debounce', () => { + test('should debounce calls with leading/trailing/maxWait', async () => { + vi.useFakeTimers() - const fn = _debounce(originalFn, 20, { leading: true, trailing: true, maxWait: 300 }) + const fn = _debounce(originalFn, 20, { leading: true, trailing: true, maxWait: 300 }) - const promise = startTimer(fn, 10, 10) - await vi.runAllTimersAsync() - await promise + const promise = startTimer(fn, 10, 10) + await vi.runAllTimersAsync() + await promise + }) + + // Test cases: + // _debounce leading=1 trailing=0 (default) + // _debounce leading=1 trailing=1 + // _debounce leading=0 trailing=1 + // _debounce leading=0 trailing=0 + // _throttle leading=1 trailing=1 (default) + // _throttle leading=1 trailing=0 + // _throttle leading=0 trailing=1 + // _throttle leading=0 trailing=0 +}) + +describe('_asyncDebounce', () => { + test('should return a real promise (never undefined) resolving with the invocation result', async () => { + let calls = 0 + const fn = _asyncDebounce(async (n: number) => { + calls++ + return n * 2 + }, 20) + + const p = fn(5) + expect(p).toBeInstanceOf(Promise) + expect(await p).toBe(10) + expect(calls).toBe(1) + }) + + test('should coalesce rapid calls into one invocation and resolve all callers with that result', async () => { + let calls = 0 + const fn = _asyncDebounce(async (n: number) => { + calls++ + return n + }, 20) + + const results = await Promise.all([fn(1), fn(2), fn(3)]) + expect(results).toEqual([3, 3, 3]) + expect(calls).toBe(1) + }) + + test('should reject all coalesced callers when the invocation throws', async () => { + const fn = _asyncDebounce(async () => { + throw new Error('boom') + }, 20) + + const p1 = fn() + const p2 = fn() + + await expect(p1).rejects.toThrow('boom') + await expect(p2).rejects.toThrow('boom') + }) + + test('should reject pending promises on cancel()', async () => { + const fn = _asyncDebounce(async (n: number) => n, 50) + + const p = fn(1) + fn.cancel() + + await expect(p).rejects.toThrow('asyncDebounce cancelled') + }) + + test('should resolve dropped calls with undefined when the config yields no invocation (trailing: false)', async () => { + const fn = _asyncDebounce(async (n: number) => n, 20, { trailing: false }) + + expect(await fn(1)).toBeUndefined() + }) }) -// Test cases: -// _debounce leading=1 trailing=0 (default) -// _debounce leading=1 trailing=1 -// _debounce leading=0 trailing=1 -// _debounce leading=0 trailing=0 -// _throttle leading=1 trailing=1 (default) -// _throttle leading=1 trailing=0 -// _throttle leading=0 trailing=1 -// _throttle leading=0 trailing=0 +describe('_asyncThrottle', () => { + test('should invoke on the leading edge immediately', async () => { + let calls = 0 + const fn = _asyncThrottle(async (n: number) => { + calls++ + return n + }, 50) + + expect(await fn(7)).toBe(7) + expect(calls).toBe(1) + }) + + test('should resolve the leading caller but resolve in-window callers with undefined when trailing is disabled', async () => { + const fn = _asyncThrottle(async (n: number) => n, 50, { trailing: false }) + + expect(await fn(1)).toBe(1) + expect(await fn(2)).toBeUndefined() + }) +}) diff --git a/packages/js-lib/src/decorators/debounce.ts b/packages/js-lib/src/decorators/debounce.ts index d7a0f529..88006c7a 100644 --- a/packages/js-lib/src/decorators/debounce.ts +++ b/packages/js-lib/src/decorators/debounce.ts @@ -1,116 +1,58 @@ -import type { AnyFunction } from '../types.js' - -export interface Cancelable { - cancel: () => void - flush: () => void -} - -export interface ThrottleOptions { - /** - * @default true - */ - leading?: boolean - - /** - * @default true - */ - trailing?: boolean -} - -export interface DebounceOptions { - /** - * @default false - */ - leading?: boolean - - /** - * @default true - */ - trailing?: boolean - - /** - * - */ - maxWait?: number -} +import { _anyToError } from '../error/error.util.js' +import { pDefer } from '../promise/pDefer.js' +import type { DeferredPromise } from '../promise/pDefer.js' +import type { AnyAsyncFunction, AnyFunction } from '../types.js' export function _debounce( func: T, wait: number, opt: DebounceOptions = {}, ): T & Cancelable { - let lastArgs: any - let lastThis: any - let result: any + let lastArgs: Parameters | undefined + let lastThis: ThisParameterType | undefined + let result: ReturnType let timerId: number | undefined - let lastCallTime: number | undefined - let lastInvokeTime = 0 const maxing = 'maxWait' in opt - const { leading = false, trailing = true } = opt - const maxWait = maxing ? Math.max(Number(opt.maxWait) || 0, wait) : opt.maxWait + const state: DebounceTimerState = { + lastCallTime: undefined, + lastInvokeTime: 0, + wait, + maxing, + maxWait: maxing ? Math.max(Number(opt.maxWait) || 0, wait) : opt.maxWait, + } - function invokeFunc(time: number): any { + function invokeFunc(time: number): ReturnType { const args = lastArgs const thisArg = lastThis lastArgs = lastThis = undefined - lastInvokeTime = time - result = func.apply(thisArg, args) + state.lastInvokeTime = time + result = func.apply(thisArg, args!) return result } - function startTimer(pendingFunc: AnyFunction, wait: number): number { - return setTimeout(pendingFunc, wait) - } - - function cancelTimer(id: number): void { - clearTimeout(id) - } - - function leadingEdge(time: number): any { + function leadingEdge(time: number): ReturnType { // Reset any `maxWait` timer. - lastInvokeTime = time + state.lastInvokeTime = time // Start the timer for the trailing edge. - timerId = startTimer(timerExpired, wait) + timerId = startTimer(timerExpired, state.wait) // Invoke the leading edge. return leading ? invokeFunc(time) : result } - function remainingWait(time: number): number { - const timeSinceLastCall = time - lastCallTime! - const timeSinceLastInvoke = time - lastInvokeTime - const timeWaiting = wait - timeSinceLastCall - - return maxing ? Math.min(timeWaiting, maxWait! - timeSinceLastInvoke) : timeWaiting - } - - function shouldInvoke(time: number): boolean { - const timeSinceLastCall = time - lastCallTime! - const timeSinceLastInvoke = time - lastInvokeTime - - // Either this is the first call, activity has stopped and we're at the - // trailing edge, the system time has gone backwards and we're treating - // it as the trailing edge, or we've hit the `maxWait` limit. - return ( - lastCallTime === undefined || - timeSinceLastCall >= wait || - timeSinceLastCall < 0 || - (maxing && timeSinceLastInvoke >= maxWait!) - ) - } - - function timerExpired(): any { + function timerExpired(): void { const time = Date.now() - if (shouldInvoke(time)) { - return trailingEdge(time) + if (shouldInvoke(time, state)) { + trailingEdge(time) + return } // Restart the timer. - timerId = startTimer(timerExpired, remainingWait(time)) + timerId = startTimer(timerExpired, remainingWait(time, state)) } - function trailingEdge(time: number): any { + function trailingEdge(time: number): ReturnType { timerId = undefined // Only invoke if we have `lastArgs` which means `func` has been @@ -126,11 +68,12 @@ export function _debounce( if (timerId !== undefined) { cancelTimer(timerId) } - lastInvokeTime = 0 - lastArgs = lastCallTime = lastThis = timerId = undefined + state.lastInvokeTime = 0 + state.lastCallTime = undefined + lastArgs = lastThis = timerId = undefined } - function flush(): any { + function flush(): ReturnType { return timerId === undefined ? result : trailingEdge(Date.now()) } @@ -138,26 +81,26 @@ export function _debounce( return timerId !== undefined } - function debounced(this: any, ...args: any[]): any { + function debounced(this: ThisParameterType, ...args: Parameters): ReturnType { const time = Date.now() - const isInvoking = shouldInvoke(time) + const isInvoking = shouldInvoke(time, state) lastArgs = args lastThis = this - lastCallTime = time + state.lastCallTime = time if (isInvoking) { if (timerId === undefined) { - return leadingEdge(lastCallTime) + return leadingEdge(state.lastCallTime) } - if (maxing) { + if (state.maxing) { // Handle invocations in a tight loop. - timerId = startTimer(timerExpired, wait) - return invokeFunc(lastCallTime) + timerId = startTimer(timerExpired, state.wait) + return invokeFunc(state.lastCallTime) } } if (timerId === undefined) { - timerId = startTimer(timerExpired, wait) + timerId = startTimer(timerExpired, state.wait) } return result } @@ -180,3 +123,319 @@ export function _throttle( maxWait: wait, }) } + +/** + * Like `_debounce`, but for async functions. + * + * Unlike `_debounce` (which returns a stale `result`/`undefined` for suppressed calls), + * `_asyncDebounce` always returns a real Promise, resolving with the coalesced invocation's result. + * + * @experimental + */ +export function _asyncDebounce( + func: T, + wait: number, + opt: AsyncDebounceOptions = {}, +): AsyncDebounced { + let lastArgs: Parameters | undefined + let lastThis: ThisParameterType | undefined + let timerId: number | undefined + // Promise shared by all calls coalesced into the next invocation (undefined when none pending). + let deferred: DeferredPromise> | undefined> | undefined + + const { leading = false, trailing = true } = opt + + const maxing = 'maxWait' in opt + const maxWait = maxing ? Math.max(Number(opt.maxWait) || 0, wait) : opt.maxWait + + const state: DebounceTimerState = { + lastCallTime: undefined, + lastInvokeTime: 0, + wait, + maxing, + maxWait, + } + + async function invokeFunc(time: number): Promise { + const args = lastArgs + const thisArg = lastThis + + lastArgs = lastThis = undefined + state.lastInvokeTime = time + + // Detach and settle the current batch. + const d = deferred! + deferred = undefined + try { + const result = await func.apply(thisArg, args!) + d.resolve(result) + } catch (err) { + d.reject(_anyToError(err)) + } + } + + async function leadingEdge(time: number): Promise> | undefined> { + // Reset any `maxWait` timer. + state.lastInvokeTime = time + // Start the timer for the trailing edge. + timerId = startTimer(timerExpired, state.wait) + // Capture the batch before `invokeFunc` detaches it. + const d = deferred! + // Invoke the leading edge. + if (leading) { + await invokeFunc(time) + } + return d + } + + function timerExpired(): void { + const time = Date.now() + if (shouldInvoke(time, state)) { + void trailingEdge(time) + return + } + // Restart the timer. + timerId = startTimer(timerExpired, remainingWait(time, state)) + } + + async function trailingEdge(time: number): Promise { + timerId = undefined + + // Only invoke if we have `lastArgs` which means `func` has been + // debounced at least once. + if (trailing && lastArgs && deferred) { + await invokeFunc(time) + return + } + lastArgs = lastThis = undefined + // Dropped batch (only reachable with `trailing: false`): no invocation, so resolve `undefined`. + if (deferred) { + const d = deferred + deferred = undefined + d.resolve(undefined) + } + } + + function cancel(): void { + if (timerId !== undefined) { + cancelTimer(timerId) + } + state.lastInvokeTime = 0 + state.lastCallTime = undefined + lastArgs = lastThis = timerId = undefined + // Reject pending awaiters. + if (deferred) { + const d = deferred + deferred = undefined + d.reject(new Error('asyncDebounce cancelled')) + } + } + + async function flush(): Promise> | undefined> { + if (timerId === undefined || !deferred) { + return undefined + } + const d = deferred + cancelTimer(timerId) + await trailingEdge(Date.now()) + return d + } + + function pending(): boolean { + return timerId !== undefined + } + + async function debounced( + this: ThisParameterType, + ...args: Parameters + ): Promise> | undefined> { + const time = Date.now() + const isInvoking = shouldInvoke(time, state) + + lastArgs = args + lastThis = this + state.lastCallTime = time + + deferred ||= pDefer> | undefined>() + const currentDeferred = deferred + + if (isInvoking) { + if (timerId === undefined) { + return leadingEdge(state.lastCallTime) + } + if (state.maxing) { + // Handle invocations in a tight loop. + timerId = startTimer(timerExpired, state.wait) + await invokeFunc(state.lastCallTime) + return currentDeferred + } + } + if (timerId === undefined) { + timerId = startTimer(timerExpired, state.wait) + } + return currentDeferred + } + + debounced.cancel = cancel + debounced.flush = flush + debounced.pending = pending + return debounced as any +} + +/** + * Like `_throttle`, but for async functions. See `_asyncDebounce` for the Promise semantics. + * + * @experimental + */ +export function _asyncThrottle( + func: T, + wait: number, + opt: AsyncThrottleOptions = {}, +): AsyncDebounced { + return _asyncDebounce(func, wait, { + leading: true, + trailing: true, + ...opt, + maxWait: wait, + }) +} + +function remainingWait(time: number, state: DebounceTimerState): number { + const { lastCallTime, lastInvokeTime, wait, maxing, maxWait } = state + const timeSinceLastCall = time - lastCallTime! + const timeSinceLastInvoke = time - lastInvokeTime + const timeWaiting = wait - timeSinceLastCall + + return maxing ? Math.min(timeWaiting, maxWait! - timeSinceLastInvoke) : timeWaiting +} + +function shouldInvoke(time: number, state: DebounceTimerState): boolean { + const { lastCallTime, lastInvokeTime, wait, maxing, maxWait } = state + const timeSinceLastCall = time - lastCallTime! + const timeSinceLastInvoke = time - lastInvokeTime + + // Either this is the first call, activity has stopped and we're at the + // trailing edge, the system time has gone backwards and we're treating + // it as the trailing edge, or we've hit the `maxWait` limit. + return ( + lastCallTime === undefined || + timeSinceLastCall >= wait || + timeSinceLastCall < 0 || + (maxing && timeSinceLastInvoke >= maxWait!) + ) +} + +function startTimer(pendingFunc: AnyFunction, wait: number): number { + return setTimeout(pendingFunc, wait) +} + +function cancelTimer(id: number): void { + clearTimeout(id) +} + +interface DebounceTimerState { + lastCallTime: number | undefined + lastInvokeTime: number + wait: number + maxing: boolean + maxWait: number | undefined +} + +export interface Cancelable { + cancel: () => void + flush: () => void +} + +/** + * Like `Cancelable`, but for the async debounced functions: `flush()` resolves with the flushed + * invocation's result (or `undefined` if nothing was pending), and `pending()` is exposed. + */ +export interface AsyncCancelable { + cancel: () => void + flush: () => Promise + pending: () => boolean +} + +export interface ThrottleOptions { + /** + * Invoke on the leading edge of the window (immediately, on the first call). + * + * @default true + */ + leading?: boolean + + /** + * Invoke on the trailing edge of the window (after `wait` has elapsed). + * + * @default true + */ + trailing?: boolean +} + +export interface DebounceOptions { + /** + * Invoke on the leading edge of the window (immediately, on the first call). + * + * @default false + */ + leading?: boolean + + /** + * Invoke on the trailing edge of the window (after `wait` has elapsed). + * + * @default true + */ + trailing?: boolean + + /** + * Maximum time `func` is allowed to be delayed before it's forcibly invoked. + */ + maxWait?: number +} + +export interface AsyncThrottleOptions { + /** + * Invoke on the leading edge of the window (immediately, on the first call). + * + * @default true + */ + leading?: boolean + + /** + * Invoke on the trailing edge of the window (after `wait` has elapsed). + * + * When `false`, calls dropped within the window (not served by a leading invocation) resolve + * with `undefined`. There's no invocation for them to await. This is why the returned function + * can resolve with `undefined` even though the original function's return type doesn't express that. + * + * @default true + */ + trailing?: boolean +} + +export interface AsyncDebounceOptions extends AsyncThrottleOptions { + /** + * Invoke on the leading edge of the window (immediately, on the first call). + * + * @default false + */ + leading?: boolean + + /** + * Maximum time `func` is allowed to be delayed before it's forcibly invoked. + */ + maxWait?: number +} + +/** + * The function returned by `_asyncDebounce`/`_asyncThrottle`. Same signature as `T`, but resolves + * `Awaited> | undefined` + * + * This is because some configurations (e.g. `trailing: false`) may drop calls without invoking + * the original function, so there's no result to await. + */ +export type AsyncDebounced = (( + this: ThisParameterType, + ...args: Parameters +) => Promise> | undefined>) & + AsyncCancelable>> diff --git a/packages/js-lib/src/decorators/decorator.util.ts b/packages/js-lib/src/decorators/decorator.util.ts index dbe16a8e..9cc43623 100644 --- a/packages/js-lib/src/decorators/decorator.util.ts +++ b/packages/js-lib/src/decorators/decorator.util.ts @@ -1,5 +1,16 @@ import type { AnyObject, InstanceId } from '../types.js' +/** + * Generic override of TypeScript's built-in legacy MethodDecorator, that + * allows us to infer the parameters of the decorated method from the parameters + * of a decorator. + */ +export type MethodDecorator = ( + target: AnyObject, + propertyKey: string | symbol, + descriptor: TypedPropertyDescriptor, +) => TypedPropertyDescriptor | undefined + /** * @returns * e.g `NameOfYourClass.methodName` diff --git a/packages/js-lib/src/decorators/memo.decorator.ts b/packages/js-lib/src/decorators/memo.decorator.ts index ff71f313..fe6b24a7 100644 --- a/packages/js-lib/src/decorators/memo.decorator.ts +++ b/packages/js-lib/src/decorators/memo.decorator.ts @@ -2,8 +2,9 @@ import { _assert, _assertTypeOf } from '../error/assert.js' import type { CommonLogger } from '../log/commonLogger.js' import type { AnyFunction, AnyObject, MaybeParameters } from '../types.js' import { _objectAssign } from '../types.js' +import type { MethodDecorator } from './decorator.util.js' import { _getTargetMethodSignature } from './decorator.util.js' -import type { MemoCache, MethodDecorator } from './memo.util.js' +import type { MemoCache } from './memo.util.js' import { jsonMemoSerializer, MapMemoCache } from './memo.util.js' export interface MemoOptions { diff --git a/packages/js-lib/src/decorators/memo.util.ts b/packages/js-lib/src/decorators/memo.util.ts index 507d4d48..af04226a 100644 --- a/packages/js-lib/src/decorators/memo.util.ts +++ b/packages/js-lib/src/decorators/memo.util.ts @@ -1,6 +1,6 @@ import { _isPrimitive } from '../is.util.js' import { pDelay } from '../promise/pDelay.js' -import type { AnyObject, UnixTimestamp } from '../types.js' +import type { UnixTimestamp } from '../types.js' import { MISS } from '../types.js' export type MemoSerializer = (args: any[]) => any @@ -147,14 +147,3 @@ export class MapAsyncMemoCache implements AsyncMemoCache this.m.clear() } } - -/** - * Generic override of Typescript's built in legacy MethodDecorator, that - * allows us to infer the parameters of the decorated method from the parameters - * of a decorator. - */ -export type MethodDecorator = ( - target: AnyObject, - propertyKey: string | symbol, - descriptor: TypedPropertyDescriptor, -) => TypedPropertyDescriptor | undefined