import { given } from "@nivinjoseph/n-defensive"; import { Duration } from "./duration.js"; import { Delay } from "./delay.js"; import { Mutex } from "./mutex.js"; import { ApplicationException } from "@nivinjoseph/n-exception"; /** * Creates a synchronize decorator that ensures only one instance of a method runs at a time. * Uses a mutex to prevent concurrent execution of the decorated method. * * @param delay - Optional delay between executions after the mutex is released * @returns A method decorator that implements synchronization behavior * @throws ArgumentException if delay is not a positive Duration * * @example * ```typescript * class Example { * @synchronize(Duration.fromSeconds(1)) * async updateResource(): Promise { * // This will ensure only one update happens at a time * // Other calls will wait for the current one to complete * await this.database.update(data); * } * } * ``` */ export function synchronize< This, Args extends Array >( delay: Duration ): SynchronizeMethodDecorator; export function synchronize< This, Args extends Array >( target: SynchronizeDecoratorTargetMethod, context: SynchronizeDecoratorContext ): SynchronizeDecoratorReplacementMethod; export function synchronize< This, Args extends Array >( delayOrTarget: Duration | SynchronizeDecoratorTargetMethod, context?: SynchronizeDecoratorContext ): SynchronizeMethodDecorator | SynchronizeDecoratorReplacementMethod { if (delayOrTarget instanceof Duration) { const delay = delayOrTarget; given(delay, "delay").ensureIsObject().ensureIsInstanceOf(Duration) .ensure(t => t.toMilliSeconds() > 0, "delay should be greater than 0ms"); const decorator: SynchronizeMethodDecorator = function (target, context) { return createReplacementMethod(target, context, delay); }; return decorator; } const target = delayOrTarget; if (context == null) throw new ApplicationException("Context should not be null or undefined"); return createReplacementMethod(target, context, null); } function createReplacementMethod< This, Args extends Array >( target: SynchronizeDecoratorTargetMethod, context: SynchronizeDecoratorContext, delay: Duration | null ): SynchronizeDecoratorReplacementMethod { const { name, kind } = context; given(kind, "kind").ensureHasValue().ensureIsString().ensure(t => t === "method", "synchronize decorated can only be used on a method"); const mutexKey = Symbol.for(`@nivinjoseph/n-util/synchronize/${String(name)}/mutex`); context.addInitializer(function (this) { (this as any)[mutexKey] = new Mutex(); }); return async function (this: This, ...args: Args): Promise { const mutex: Mutex = (this as any)[mutexKey]; await mutex.lock(); try { const result = await target.call(this, ...args); return result; } finally { if (delay != null) await Delay.milliseconds(delay.toMilliSeconds()); mutex.release(); } }; } export type SynchronizeDecoratorTargetMethod< This, Args extends Array > = (this: This, ...args: Args) => any; export type SynchronizeDecoratorReplacementMethod< This, Args extends Array > = (this: This, ...args: Args) => Promise; export type SynchronizeDecoratorContext< This, Args extends Array > = ClassMethodDecoratorContext>; export type SynchronizeMethodDecorator< This, Args extends Array > = ( value: SynchronizeDecoratorTargetMethod, context: SynchronizeDecoratorContext ) => SynchronizeDecoratorReplacementMethod;