Source

decorators.ts

import { TransactionalKeys } from "./constants";
import { Decoration, Metadata, method } from "@decaf-ts/decoration";
import { Transaction } from "./Transaction";
import { InternalError } from "@decaf-ts/db-decorators";

/**
 * @description Method decorator that enables transactional behavior
 * @summary Sets a class async method as transactional, wrapping it in a transaction context that can be managed by the transaction system. This decorator handles transaction creation, binding, and error handling.
 * @param {any[]} [data] - Optional metadata available to the {@link TransactionLock} implementation
 * @return {Function} A decorator function that wraps the original method with transactional behavior
 * @function transactional
 * @category Method Decorators
 * @mermaid
 * sequenceDiagram
 *   participant C as Client Code
 *   participant D as Decorator
 *   participant T as Transaction
 *   participant O as Original Method
 *
 *   C->>D: Call decorated method
 *   D->>D: Check if transaction exists in args
 *
 *   alt Transaction exists in args
 *     D->>T: Create updated transaction
 *     T->>T: Bind to original transaction
 *     T->>T: Fire transaction
 *   else No transaction
 *     D->>T: Create new transaction
 *     T->>T: Submit transaction
 *   end
 *
 *   T->>O: Execute original method
 *   O-->>T: Return result/error
 *   T->>T: Release transaction
 *   T-->>C: Return result/error
 * @category Decorators
 */
export function transactional(...data: any[]) {
  function innerTransactional(...data: any[]) {
    return function (target: any, propertyKey?: any, descriptor?: any) {
      if (!descriptor)
        throw new InternalError("This decorator only applies to methods");
      method()(target, propertyKey, descriptor);
      Metadata.set(
        target.constructor,
        Metadata.key(TransactionalKeys.TRANSACTIONAL, propertyKey),
        {
          data: data,
        }
      );
      descriptor.value = new Proxy(descriptor.value, {
        async apply<R>(obj: any, thisArg: any, argArray: any[]): Promise<R> {
          return new Promise<R>((resolve, reject) => {
            async function exitFunction(
              transaction: Transaction<R>,
              err?: Error | R,
              result?: R
            ): Promise<R> {
              if (err && !(err instanceof Error) && !result) {
                result = err;
                err = undefined;
              }
              await transaction.release(err as Error | undefined);
              return err
                ? (reject(err) as unknown as R)
                : (resolve(result as R) as unknown as R);
            }

            const candidate = argArray[0];
            const transactionPrefixLength = (() => {
              let count = 0;
              while (
                count < argArray.length &&
                argArray[count] instanceof Transaction
              ) {
                count++;
              }
              return count;
            })();
            const invocationArgs =
              transactionPrefixLength > 0
                ? argArray.slice(transactionPrefixLength)
                : argArray;

            const activeTransaction =
              candidate instanceof Transaction
                ? candidate
                : Transaction.contextTransaction(thisArg);

            if (activeTransaction) {
              const updatedTransaction: Transaction<any> = new Transaction(
                target.name,
                propertyKey,
                async () => {
                  try {
                    return resolve(
                      await Reflect.apply(
                        obj,
                        updatedTransaction.bindToTransaction(thisArg),
                        invocationArgs
                      )
                    );
                  } catch (e: unknown) {
                    return reject(e);
                  }
                },
                data.length ? data : undefined
              );
              activeTransaction.bindTransaction(updatedTransaction);
              activeTransaction.fire();
            } else {
              const newTransaction: Transaction<R> = new Transaction(
                target.name,
                propertyKey,
                async () => {
                  try {
                    return exitFunction(
                      newTransaction,
                      undefined,
                      await Reflect.apply(
                        obj,
                        newTransaction.bindToTransaction(thisArg),
                        invocationArgs
                      )
                    );
                  } catch (e: unknown) {
                    return exitFunction(newTransaction, e as Error);
                  }
                },
                data.length ? data : undefined
              );
              Transaction.submit(newTransaction);
            }
          });
        },
      });

      return descriptor;
    };
  }

  return Decoration.for(TransactionalKeys.TRANSACTIONAL)
    .define({
      decorator: innerTransactional,
      args: data,
    })
    .apply();
}