import { InternalError } from "@decaf-ts/db-decorators";
import { Decoration, Metadata, method } from "@decaf-ts/decoration";
import { TransactionalKeys } from "@decaf-ts/transactional-decorators";
import { ModelService } from "../services/ModelService";
import { Repository } from "../repository/Repository";
import { Adapter } from "../persistence/Adapter";
import { ContextLock } from "../persistence/ContextLock";
/**
* @description Resolves the transaction lock for a `@transactional`-decorated call
* @summary Finds the underlying adapter for the decorated object (Adapter, Repository, or
* ModelService) and asks it for a fresh `ContextLock` via `Adapter.transactionLock()`
*/
export function resolveTransactionLock(obj: any, ...args: any[]): ContextLock {
let adapter: Adapter<any, any, any, any> | undefined;
if (obj instanceof ModelService)
adapter = obj.repo.adapter as Adapter<any, any, any, any>;
if (obj instanceof Repository)
adapter = obj["adapter"] as Adapter<any, any, any, any>;
if (obj instanceof Adapter) adapter = obj;
if (!adapter)
throw new InternalError(`Could not find adapter to extract transaction`);
return adapter.transactionLock(...args);
}
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> {
const { log, ctx } = (
await thisArg["logCtx"](argArray, obj.name, true)
).for(obj);
// Reuse an in-flight transaction (nested @transactional call) instead of starting a new one
const existing: ContextLock | undefined =
ctx.getOrUndefined("transactionLock");
const lock = existing || resolveTransactionLock(thisArg);
lock.depth++;
if (!existing) {
// only cache the lock once begin() succeeds, so a rejected begin() (e.g.
// maxConcurrentTransactions=0) leaves no stale lock/depth on the context
await lock.begin(ctx);
ctx.cache.put("transactionLock", lock);
}
let results: any;
try {
results = await obj.call(thisArg, ...argArray, ctx);
} catch (e: unknown) {
// An inner @transactional frame may have already rolled back and ended
// the transaction (depth forced to 0); enclosing frames must not roll back again
const alreadyEnded = lock.depth === 0;
lock.depth = 0;
if (!alreadyEnded) {
try {
await lock.rollback(e as Error, ctx);
} catch (rollbackError: unknown) {
log.error(
`Failed to rollback transaction`,
rollbackError as Error
);
}
}
throw e;
}
lock.depth--;
if (lock.depth === 0) {
try {
await lock.commit(ctx);
} catch (e: unknown) {
throw new InternalError(`Failed to commit transaction: ${e}`);
}
}
return results;
},
});
return descriptor;
};
}
/**
* @description Method decorator that wraps a method in core's transaction-lock mechanism
* @summary `@decaf-ts/transactional-decorators` exports its own `transactional()` factory, and that
* factory re-registers its own (base) decorator under the same Decoration key every time it is called
* — so importing core does not make core's implementation "stick" if anything also calls the base
* package's factory. Consumers that want core's `ContextLock`/per-adapter transaction-lock behavior
* MUST import `transactional` from `@decaf-ts/core` (this function), not from
* `@decaf-ts/transactional-decorators`. Whichever factory is called last determines the active
* implementation for the shared key/flavour going forward.
* @param {...any[]} data - Optional metadata available to the transaction-lock implementation
* @function transactional
* @category Decorators
*/
export function transactional(...data: any[]) {
return Decoration.for(TransactionalKeys.TRANSACTIONAL)
.define({
decorator: innerTransactional,
args: data,
} as any)
.apply();
}
Source