Transversals provide Aspect-Oriented Programming (AOP) capabilities in assembler.js, allowing you to implement cross-cutting concerns in a modular and reusable way. Cross-cutting concerns are functionalities that affect multiple parts of an application, such as logging, security, caching, performance monitoring, and validation.
A Transversal is a specialized type of Assemblage that:
- Intercepts method calls on other assemblages
- Executes additional logic before, after, or around the target method
- Is automatically injected and managed by the DI container
- Is singleton by default (shared across all contexts)
- Cannot have
injectoruseproperties (dependencies are resolved from parent context)
An advice is a piece of code that runs at a specific point during method execution. assembler.js supports three types of advices:
- @Before - Executes before the target method
- @After - Executes after the target method completes (receives result)
- @Around - Wraps the target method (can control execution)
A pointcut is an expression that determines which methods should be intercepted by an advice. The syntax is:
execution(ClassName.methodName)Wildcards are supported:
execution(UserService.*)- All methods in UserServiceexecution(*.create)- All create methods in any classexecution(*.*)- All methods in all classes
A join point represents a specific point in your program execution where an advice can be applied - typically a method call.
The AdviceContext provides information about the intercepted method:
target- The instance being calledmethodName- Name of the methodargs- Array of arguments passed to the methodresult- Return value (for @After advice)proceed()- Function to continue execution (for @Around advice)config- Optional configuration from @Affect decorator
import { Transversal, Before, After, Around, AbstractTransversal, type AdviceContext } from 'assemblerjs';
@Transversal()
class LoggingTransversal implements AbstractTransversal {
onInit() {
console.log('LoggingTransversal initialized');
}
@Before('execution(*.*)')
logBefore(context: AdviceContext) {
console.log(`[BEFORE] ${context.methodName}`, context.args);
}
@After('execution(*.*)')
logAfter(context: AdviceContext) {
console.log(`[AFTER] ${context.methodName} =>`, context.result);
}
@Around('execution(*.create)')
async measurePerformance(context: AdviceContext) {
const start = Date.now();
const result = await context.proceed!();
const duration = Date.now() - start;
console.log(`[PERF] ${context.methodName} took ${duration}ms`);
return result;
}
}Transversals can receive dependencies through constructor parameters:
@Transversal()
class ValidationTransversal implements AbstractTransversal {
constructor(
private logger: Logger,
@Configuration('validation') private config: any
) {}
@Before('execution(*.save)')
validate(context: AdviceContext) {
const [data] = context.args;
if (!data) {
throw new Error('Data is required');
}
this.logger.log('Validation passed');
}
}Transversals are registered using the engage property in an assemblage definition:
@Assemblage({
inject: [[UserService]],
engage: [[LoggingTransversal], [ValidationTransversal]]
})
class App implements AbstractAssemblage {
constructor(private userService: UserService) {}
}The transversals will automatically intercept methods on UserService and all its dependencies based on their pointcut expressions.
You can explicitly apply a transversal to specific methods using the @Affect decorator, regardless of pointcut matching:
@Assemblage()
class UserService {
// Only this method will be affected by LoggingTransversal
@Affect(LoggingTransversal)
create(name: string) {
return { id: '1', name };
}
// This method won't be logged (no pointcut match, no @Affect)
findAll() {
return [];
}
}You can apply multiple transversals to a single method:
@Assemblage()
class ProductService {
@Affect(LoggingTransversal)
@Affect(ValidationTransversal)
@Affect(PerformanceTransversal, { threshold: 100 })
async create(data: any) {
// Implementation
}
}You can use both automatic pointcut matching and explicit @Affect:
@Transversal()
class MixedTransversal {
// Automatically applied to all 'create' methods
@Before('execution(*.create)')
autoApplied(context: AdviceContext) {
console.log('[AUTO]', context.methodName);
}
// Only applied where explicitly marked with @Affect
@Before('execution(NothingMatches.*)')
manualApplied(context: AdviceContext) {
console.log('[MANUAL]', context.methodName);
}
}
@Assemblage()
class OrderService {
// Triggers autoApplied via pointcut
create(data: any) { }
// Triggers manualApplied via @Affect
@Affect(MixedTransversal)
update(id: string, data: any) { }
}When multiple advices match the same method, they execute in priority order (higher values first):
@Transversal()
class SecurityTransversal {
@Before('execution(*.*)', 100) // Executes first
checkAuth(context: AdviceContext) {
// Security check
}
}
@Transversal()
class ValidationTransversal {
@Before('execution(*.*)', 50) // Executes second
validate(context: AdviceContext) {
// Validation
}
}Default priority is 0. Same-priority advices execute in registration order.
@Around advice has full control over method execution:
@Around('execution(*.save)')
async interceptSave(context: AdviceContext) {
// Pre-processing
console.log('Before save');
try {
// Call the next advice or original method
const result = await context.proceed!();
// Post-processing
console.log('After save, result:', result);
// Can modify result
return { ...result, timestamp: Date.now() };
} catch (error) {
// Error handling
console.error('Save failed:', error);
throw error;
}
}Important: Always call context.proceed() to continue the chain, unless you intentionally want to block execution.
@Transversal()
class LoggingTransversal implements AbstractTransversal {
@Before('execution(*.*)')
logMethodCall(context: AdviceContext) {
console.log(`Calling ${context.methodName}`, context.args);
}
}@Transversal()
class PerformanceTransversal implements AbstractTransversal {
@Around('execution(*.*)')
async measureTime(context: AdviceContext) {
const start = performance.now();
const result = await context.proceed!();
const duration = performance.now() - start;
if (duration > 100) {
console.warn(`Slow method: ${context.methodName} (${duration}ms)`);
}
return result;
}
}@Transversal()
class CachingTransversal implements AbstractTransversal {
private cache = new Map<string, any>();
@Around('execution(*.find*)')
async cacheRead(context: AdviceContext) {
const key = `${context.methodName}:${JSON.stringify(context.args)}`;
if (this.cache.has(key)) {
return this.cache.get(key);
}
const result = await context.proceed!();
this.cache.set(key, result);
return result;
}
}@Transversal()
class SecurityTransversal implements AbstractTransversal {
constructor(private authService: AuthService) {}
@Before('execution(*.delete*)', 100)
checkPermission(context: AdviceContext) {
if (!this.authService.hasPermission('delete')) {
throw new Error('Access denied');
}
}
}@Transversal()
class ValidationTransversal implements AbstractTransversal {
@Before('execution(*.save)')
validateData(context: AdviceContext) {
const [data] = context.args;
if (!data || !data.name) {
throw new Error('Invalid data: name is required');
}
}
}-
Keep Transversals Focused - Each transversal should handle one concern (logging, security, etc.)
-
Use Specific Pointcuts - Avoid
execution(*.*)when possible; target specific methods or classes -
Respect Priorities - Use priorities to ensure proper execution order (security before validation, etc.)
-
Handle Errors in @Around - Always wrap
context.proceed()in try-catch when using @Around -
Avoid Side Effects in @Before - @Before advices should not modify arguments (use @Around for that)
-
Use @Affect for Fine-Grained Control - When pointcuts are too broad or complex, use explicit @Affect
-
Implement AbstractTransversal - Implement the interface for type safety and lifecycle hooks
-
Test Transversals Independently - Write unit tests for transversal logic separately from integration tests
Transversals follow the standard assemblage lifecycle:
- Class Registration -
static onRegister()called when class is registered - Instantiation - Constructor called with resolved dependencies
- Initialization -
onInit()called after all dependencies are resolved - Active - Advices intercept method calls
- Disposal -
onDispose()called on cleanup
@Transversal()
class LifecycleTransversal implements AbstractTransversal {
static onRegister(context: AssemblerContext) {
console.log('Transversal registered');
}
onInit() {
console.log('Transversal initialized');
}
onDispose() {
console.log('Transversal disposed');
}
}Transversals support caller tracking, allowing you to identify which assemblage or external component initiated a method call. This is useful for audit logging, permission checking, and request tracing.
The AdviceContext provides caller information:
caller- The class name of the callercallerIdentifier- Optional unique identifier (string or symbol) for the caller
@Transversal()
class AuditTransversal implements AbstractTransversal {
@Before('execution(*.*)')
auditCall(context: AdviceContext) {
console.log(
`[AUDIT] ${context.caller || 'Unknown'} called ${context.target.constructor.name}.${context.methodName}`
);
}
@Before('execution(*.delete)')
checkDeletePermission(context: AdviceContext) {
// Only allow deletions from specific callers
if (context.caller !== 'AdminService') {
throw new Error(`Access denied: ${context.caller} cannot delete`);
}
}
}For callers outside the DI container (e.g., Vue components, external scripts), use TransversalWeaver.withCaller() or TransversalWeaver.wrapCaller():
import { TransversalWeaver } from 'assemblerjs';
// In a Vue component
export default {
methods: {
async saveUser() {
await TransversalWeaver.withCaller('UserEditComponent', async () => {
await this.userService.save(userData);
// Advices will see caller = 'UserEditComponent'
});
}
}
};For functions you'll call multiple times, use wrapCaller to create a wrapped function:
import { TransversalWeaver } from 'assemblerjs';
// In a Vue component
export default {
setup() {
// Create wrapped function once
const mergeClasses = TransversalWeaver.wrapCaller(
'LeafletMap',
'LeafletMap.vue',
(...args) => tailwind.mergeClasses(...args)
);
return {
// Can be called multiple times, always maintains caller context
mergeClasses
};
}
};For more detailed tracking, provide an identifier alongside the caller:
@Transversal()
class RequestTracingTransversal implements AbstractTransversal {
@Before('execution(*.*)')
traceRequest(context: AdviceContext) {
const callerId = context.callerIdentifier
? ` (ID: ${String(context.callerIdentifier)})`
: '';
console.log(`Request from: ${context.caller}${callerId}`);
}
}
// Usage
const requestId = Symbol('request-123');
await TransversalWeaver.withCaller('APIController', requestId, async () => {
await service.processRequest();
});Access the current caller outside of advices using TransversalWeaver.getCurrentCaller():
class ServiceA {
someMethod() {
const caller = TransversalWeaver.getCurrentCaller();
if (caller) {
console.log(`Called by: ${caller.className} (ID: ${caller.identifier})`);
}
}
}Caller tracking works even when no transversals are engaged:
// No transversals needed for caller context to work
@Assemblage()
class App {
constructor(private service: ServiceA) {}
async run() {
await TransversalWeaver.withCaller('App', async () => {
const result = await this.service.someMethod();
// Even without advices, getCurrentCaller() will return 'App'
});
}
}-
Audit Logging - Track who accessed sensitive data
@Before('execution(*.findSensitiveData)') auditAccess(context: AdviceContext) { this.logger.info(`${context.caller} accessed sensitive data`); }
-
Permission Checking - Restrict operations by caller
@Before('execution(*.delete)', 100) checkAuthorization(context: AdviceContext) { if (!this.canDelete(context.caller)) { throw new Error(`${context.caller} not authorized to delete`); } }
-
Request Tracing - Track request flow across services
@Before('execution(*.*)') traceFlow(context: AdviceContext) { if (context.callerIdentifier) { console.log(`[Trace ${context.callerIdentifier}] ${context.caller} → ${context.methodName}`); } }
-
Conditional Behavior - Different behavior based on caller
@Around('execution(*.getData)') async getData(context: AdviceContext) { if (context.caller === 'AdminService') { return await context.proceed!(); // Full data } else { const data = await context.proceed!(); return this.filterSensitiveFields(data); // Filtered data } }
- Transversals add overhead to method calls
- Use specific pointcuts to minimize unnecessary interceptions
- @Around advice is slightly more expensive than @Before/@After
- Transversals are singleton by default (minimal instantiation overhead)
- Consider using @Affect for performance-critical paths that need selective interception
- Cannot intercept constructors - Only methods can be intercepted
- Cannot have inject/use - Transversals receive dependencies via constructor parameters only
- Singleton only - Transversals cannot be transient (by design)
- Pointcut syntax - AssemblerJS focuses on method execution join points. It does not intercept calls, property access, or assignments.
- No private methods - Can only intercept public methods
- Learn about AOP Decorators
- See Advanced Examples for real-world usage
- Check API Types for AdviceContext and related types