Skip to content

Interceptors ​

Interceptors allow you to intercept and modify the dependency resolution process. They can be used for caching, logging, validation, retry logic, and more.

Two Styles of Interceptors ​

You can define interceptors in two ways: class-based (traditional) or functional (modern, like Angular 15+).

Class-based Interceptor ​

typescript
import type { Interceptor, InjectionContext } from '@noneforge/ioc';

class LoggingInterceptor implements Interceptor {
  intercept<T>(context: InjectionContext, next: () => T): T {
    console.log(`Resolving: ${String(context.token)}`);
    
    return next();
  }
}

// Usage
container.addProvider({
  provide: SERVICE,
  useClass: MyService,
  interceptors: [new LoggingInterceptor()],
});

Functional Interceptor ​

typescript
import type { InterceptorFn } from '@noneforge/ioc';

const loggingInterceptor: InterceptorFn = (context, next) => {
  console.log(`Resolving: ${String(context.token)}`);
  
  return next();
};

// Usage
container.addProvider({
  provide: SERVICE,
  useClass: MyService,
  interceptors: [loggingInterceptor],
});

Mixing Both Styles ​

You can freely mix class-based and functional interceptors:

typescript
container.addProvider({
  provide: SERVICE,
  useClass: MyService,
  interceptors: [
    new CachingInterceptor(),              // class
    loggingInterceptor,                    // function variable
    (ctx, next) => {                       // inline function
      console.log('Resolving:', ctx.token);
      
      return next();
    },
  ],
});

Interceptor Interface ​

typescript
// Class-based (with generics for type safety)
interface Interceptor {
  intercept<T>(
    context: InjectionContext,
    next: () => T | Promise<T>
  ): T | Promise<T>;
}

// Functional (simpler, uses unknown)
type InterceptorFn = (
  context: InjectionContext,
  next: () => unknown
) => unknown;

// Union type (accepts both)
type InterceptorLike = Interceptor | InterceptorFn;

The intercept method/function receives:

  • context - Information about the current resolution (token, container, metadata)
  • next - Function to call the next interceptor or resolve the dependency

Built-in Interceptors ​

CachingInterceptor ​

Caches resolved instances with optional TTL (Time To Live):

typescript
import { Container, CachingInterceptor, InjectionToken } from '@noneforge/ioc';

const EXPENSIVE_SERVICE = new InjectionToken<ExpensiveService>('EXPENSIVE');

const container = new Container();

container.addProvider({
  provide: EXPENSIVE_SERVICE,
  useFactory: () => {
    console.log('Creating expensive service...');
    
    return new ExpensiveService();
  },
  interceptors: [new CachingInterceptor()], // Default: no expiration
});

// First call creates the instance
const service1 = container.get(EXPENSIVE_SERVICE);
// "Creating expensive service..."

// Second call uses cached instance
const service2 = container.get(EXPENSIVE_SERVICE);
// No log - cached!

console.log(service1 === service2); // true

With TTL ​

typescript
container.addProvider({
  provide: EXPENSIVE_SERVICE,
  useFactory: () => new ExpensiveService(),
  interceptors: [
    new CachingInterceptor(60000), // Cache for 60 seconds
  ],
});

LoggingInterceptor ​

Logs resolution events with timing information:

typescript
import { Container, LoggingInterceptor, InjectionToken } from '@noneforge/ioc';

// Default logger (console)
const loggingInterceptor = new LoggingInterceptor();

// Custom logger
interface Logger {
  debug?(message: string, ...args: unknown[]): void;
  error?(message: string, error?: Error): void;
}

const customLogger: Logger = {
  debug: (msg, ...args) => console.log(`[DEBUG] ${msg}`, ...args),
  error: (msg, err) => console.error(`[ERROR] ${msg}`, err),
};

const customLoggingInterceptor = new LoggingInterceptor(customLogger);

container.addProvider({
  provide: UserService,
  useClass: UserService,
  interceptors: [new LoggingInterceptor()],
});

container.get(UserService);
// Logs:
// Resolving: UserService
// Resolved UserService in 2ms

RetryInterceptor ​

Automatically retries failed resolutions with exponential backoff:

typescript
import { Container, RetryInterceptor, InjectionToken } from '@noneforge/ioc';

const REMOTE_CONFIG = new InjectionToken<RemoteConfig>('REMOTE_CONFIG');

container.addProvider({
  provide: REMOTE_CONFIG,
  useAsync: async () => {
    const response = await fetch('https://config.example.com');
    if (!response.ok) {
      throw new Error('Failed to fetch config');
    }
    
    return response.json();
  },
  interceptors: [
    new RetryInterceptor(3, 1000), // 3 retries, 1 second initial delay
  ],
});

// Will retry up to 3 times with exponential backoff
const config = await container.getAsync(REMOTE_CONFIG);

Retry Parameters ​

typescript
new RetryInterceptor(
  maxRetries,  // Maximum number of retries (default: 3)
  delay        // Initial delay in ms (default: 1000)
);
// Delay doubles after each retry (exponential backoff)
// 1s -> 2s -> 4s -> ...

ValidationInterceptor ​

Validates resolved values against a custom validator:

typescript
import { Container, ValidationInterceptor, InjectionToken } from '@noneforge/ioc';

interface User {
  id: number;
  email: string;
}

const USER = new InjectionToken<User>('USER');

container.addProvider({
  provide: USER,
  useValue: { id: 1, email: 'test@example.com' },
  interceptors: [
    new ValidationInterceptor(
      (value) => {
        const user = value as User;
        
        return user.id > 0 && user.email.includes('@');
      },
      'User must have valid id and email'
    ),
  ],
});

container.get(USER); // Works

// Invalid value throws ValidationError
container.addProvider({
  provide: USER,
  useValue: { id: -1, email: 'invalid' },
  interceptors: [
    new ValidationInterceptor(
      (value) => (value as User).id > 0,
      'User id must be positive'
    ),
  ],
});

container.get(USER); // Throws ValidationError!

Creating Custom Interceptors ​

Basic Custom Interceptor ​

typescript
import type { Interceptor, InjectionContext } from '@noneforge/ioc';

class TimingInterceptor implements Interceptor {
  intercept<T>(context: InjectionContext, next: () => T): T {
    const start = performance.now();
    const result = next();
    const duration = performance.now() - start;

    console.log(`Resolved ${String(context.token)} in ${duration.toFixed(2)}ms`);

    return result;
  }
}

Async Custom Interceptor ​

typescript
class AsyncTimingInterceptor implements Interceptor {
  async intercept<T>(
    context: InjectionContext,
    next: () => T | Promise<T>
  ): Promise<T> {
    const start = performance.now();
    const result = await next();
    const duration = performance.now() - start;

    console.log(`Resolved ${String(context.token)} in ${duration.toFixed(2)}ms`);

    return result;
  }
}

Conditional Interceptor ​

typescript
class DebugInterceptor implements Interceptor {
  constructor(private debug: boolean) {}

  intercept<T>(context: InjectionContext, next: () => T): T {
    if (this.debug) {
      console.log(`Resolving: ${String(context.token)}`);
      console.log(`Depth: ${context.depth}`);
      console.log(`Path: ${context.path.map(String).join(' -> ')}`);
    }

    return next();
  }
}

Result-Modifying Interceptor ​

typescript
class WrappingInterceptor implements Interceptor {
  intercept<T>(context: InjectionContext, next: () => T): T {
    const result = next();

    // Wrap the result
    if (typeof result === 'object' && result !== null) {
      return new Proxy(result as object, {
        get(target, prop) {
          console.log(`Accessing ${String(prop)}`);
          
          return Reflect.get(target, prop);
        },
      }) as T;
    }

    return result;
  }
}

Error-Handling Interceptor ​

typescript
class FallbackInterceptor<T> implements Interceptor {
  constructor(private fallback: T) {}

  intercept<U>(context: InjectionContext, next: () => U): U {
    try {
      return next();
    } catch (error) {
      console.warn(`Failed to resolve ${String(context.token)}, using fallback`);
      
      return this.fallback as unknown as U;
    }
  }
}

Using inject() in Functional Interceptors ​

Functional interceptors run within an injection context, so you can use inject() to resolve dependencies:

typescript
import { inject, type InterceptorFn } from '@noneforge/ioc';

const authInterceptor: InterceptorFn = (context, next) => {
  // inject() works because we're in an injection context
  const authService = inject(AuthService);

  if (!authService.isAuthenticated()) {
    throw new Error('Not authenticated');
  }

  return next();
};

const loggingInterceptor: InterceptorFn = (context, next) => {
  const logger = inject(Logger);
  logger.debug(`Resolving: ${String(context.token)}`);
  
  return next();
};

This is similar to how Angular's functional interceptors work with inject().

Helper Functions ​

Helper functions make it easier to create common interceptor patterns.

createInterceptor ​

Create an interceptor from pre/post hooks:

typescript
import { createInterceptor } from '@noneforge/ioc';

const timingInterceptor = createInterceptor({
  pre: (context) => {
    context.metadata.set('startTime', Date.now());
  },
  post: (context, result) => {
    const duration = Date.now() - (context.metadata.get('startTime') as number);
    console.log(`Resolution took ${duration}ms`);
    
    return result;
  },
});

// Only pre hook
const preOnlyInterceptor = createInterceptor({
  pre: (context) => console.log('Before resolution'),
});

// Only post hook
const transformInterceptor = createInterceptor({
  post: (context, result: string) => result.toUpperCase(),
});

composeInterceptors ​

Combine multiple interceptors into one:

typescript
import { composeInterceptors, LoggingInterceptor, CachingInterceptor } from '@noneforge/ioc';

const combined = composeInterceptors(
  new LoggingInterceptor(),
  new CachingInterceptor(),
  (ctx, next) => {
    console.log('Custom logic');
    
    return next();
  },
);

// Use as single interceptor
container.addProvider({
  provide: SERVICE,
  useClass: MyService,
  interceptors: [combined],
});

when ​

Create a conditional interceptor that only runs when predicate returns true:

typescript
import { when } from '@noneforge/ioc';

// Only log in debug mode
const debugInterceptor = when(
  (ctx) => ctx.metadata.get('debug') === true,
  (ctx, next) => {
    console.log('Debug:', ctx.token);
    
    return next();
  },
);

// Only intercept specific tokens
const specificTokenInterceptor = when(
  (ctx) => ctx.token === UserService,
  (ctx, next) => {
    console.log('Intercepting UserService');
    return next();
  },
);

// Combine with class interceptors
const conditionalCache = when(
  (ctx) => ctx.metadata.get('useCache') !== false,
  new CachingInterceptor(),
);

Using Interceptors ​

Provider-Level Interceptors ​

Apply interceptors to specific providers:

typescript
container.addProvider({
  provide: UserService,
  useClass: UserService,
  interceptors: [
    new LoggingInterceptor(),
    new CachingInterceptor(60000),
  ],
});

Multiple Interceptors ​

Interceptors are executed in order:

typescript
container.addProvider({
  provide: SERVICE,
  useClass: Service,
  interceptors: [
    new LoggingInterceptor(),    // 1st: logs start
    new CachingInterceptor(),    // 2nd: checks cache
    new ValidationInterceptor(), // 3rd: validates result
  ],
});

// Execution order:
// 1. LoggingInterceptor.intercept() -> calls next()
// 2.   CachingInterceptor.intercept() -> calls next()
// 3.     ValidationInterceptor.intercept() -> calls next()
// 4.       Actual resolution
// 3.     ValidationInterceptor validates result
// 2.   CachingInterceptor caches result
// 1. LoggingInterceptor logs completion

InjectionContext ​

The context object provides information about the current resolution:

typescript
interface InjectionContext {
  // The container performing resolution
  container: ContainerLike;

  // The token being resolved
  token?: Token;

  // Request ID for request-scoped services
  requestId?: string | symbol;

  // Custom metadata
  metadata: Map<string, unknown>;

  // Resolution depth (for nested dependencies)
  depth: number;

  // Resolution path (for cycle detection)
  path: Token[];

  // Resolution strategy
  strategy: ResolutionStrategy;
}

Using Context in Interceptors ​

typescript
class ContextAwareInterceptor implements Interceptor {
  intercept<T>(context: InjectionContext, next: () => T): T {
    // Check resolution depth
    if (context.depth > 10) {
      console.warn('Deep dependency tree detected');
    }

    // Check for specific tokens
    if (context.token === SensitiveService) {
      console.log('Accessing sensitive service');
    }

    // Access custom metadata
    const userId = context.metadata.get('userId');
    if (userId) {
      console.log(`Resolution for user: ${userId}`);
    }

    return next();
  }
}

InterceptorChain ​

For advanced use cases, you can use InterceptorChain directly:

typescript
import { InterceptorChain } from '@noneforge/ioc';

const chain = new InterceptorChain();
chain.add(new LoggingInterceptor());
chain.add(new CachingInterceptor());

// Synchronous execution
const result = chain.execute(context, () => createInstance());

// Asynchronous execution
const asyncResult = await chain.executeAsync(context, async () => {
  return await createInstanceAsync();
});

Best Practices ​

  1. Keep interceptors focused - Each interceptor should do one thing
  2. Order matters - Put logging first, caching second, validation last
  3. Handle errors gracefully - Don't let interceptor errors crash the app
  4. Use async when needed - Return promises for async operations
  5. Avoid side effects - Interceptors should be predictable

Common Interceptor Patterns ​

typescript
// Logging + Caching + Validation
const standardInterceptors = [
  new LoggingInterceptor(logger),
  new CachingInterceptor(TTL),
  new ValidationInterceptor(validator),
];

// Retry + Logging for external services
const externalServiceInterceptors = [
  new RetryInterceptor(3, 1000),
  new LoggingInterceptor(logger),
];

// Debug mode
const debugInterceptors = process.env.DEBUG
  ? [new LoggingInterceptor(), new TimingInterceptor()]
  : [];

Runnable Example ​

See examples/interceptors for a complete runnable example demonstrating all interceptor patterns.

Next Steps ​

Released under the MIT License.