Skip to content

Scope

Contract

@modularityjs/scope provides a universal scope hierarchy using Node.js AsyncLocalStorage for per-request context isolation.

typescript
interface ScopeLevel {
  level: string; // e.g. 'tenant', 'workspace'
  id: string; // e.g. 'acme', 'ws-123'
}

type ScopeChain = ScopeLevel[];

class ScopeService {
  runInScope<T>(leaf: ScopeLevel, fn: () => T | Promise<T>): Promise<T>;
  runInChain<T>(chain: ScopeChain, fn: () => T): T;
  runInDefaultScope<T>(fn: () => T): T;
  getCurrentScope(): ScopeContext;
  getLeafScope(): ScopeLevel | undefined;
}

Setup

typescript
import { ScopeModule } from '@modularityjs/scope';

const modules = [ScopeModule];

Usage

typescript
@Injectable()
class TenantService {
  constructor(@Inject(ScopeService) private readonly scope: ScopeService) {}

  async handleRequest(tenantId: string) {
    await this.scope.runInScope({ level: 'tenant', id: tenantId }, async () => {
      // All code in this callback runs in the tenant's scope
      const current = this.scope.getCurrentScope();
      // current.chain === [{ level: 'tenant', id: tenantId }]
    });
  }
}

Chain Resolver

For hierarchical scopes, implement ScopeChainResolver to expand a leaf scope into a full chain:

resolve is async — implementations that don't need I/O simply return the chain directly (TypeScript auto-wraps in a Promise):

typescript
@Injectable()
class MyChainResolver extends ScopeChainResolver {
  async resolve(leaf: ScopeLevel): Promise<ScopeChain> {
    if (leaf.level === 'workspace') {
      const tenant = await this.lookupTenant(leaf.id);
      return [leaf, { level: 'tenant', id: tenant.id }];
    }
    return [leaf];
  }
}

@Module({
  name: 'my-scope',
  imports: [ScopeModule],
  providers: [MyChainResolver],
  preferences: [{ provide: ScopeChainResolver, useClass: MyChainResolver }],
})
class MyScopeModule {}

Config Integration

Bridge PackageWhat it does
@modularityjs/config-scopeMakes ConfigService scope-aware via ScopedConfigService preference
@modularityjs/config-env-scopeMakes EnvConfigSource scope-aware, reading MODULARITYJS__SCOPES__<LEVEL>__<ID>__<PATH> env vars

See Configuration — Scope-Aware Resolution.

Multi-Tenant Example

A service can wrap any operation in a scope using runInScope. When the config-scope extension is active, ConfigService automatically resolves tenant-specific values within the callback:

typescript
import { ConfigService } from '@modularityjs/config';
import { Inject, Injectable } from '@modularityjs/di';
import { ScopeService } from '@modularityjs/scope';

@Injectable()
class TenantAwareService {
  constructor(
    @Inject(ScopeService) private readonly scope: ScopeService,
    @Inject(ConfigService) private readonly config: ConfigService,
  ) {}

  async handleForTenant(tenantId: string): Promise<string> {
    return this.scope.runInScope(
      { level: 'tenant', id: tenantId },
      async () => {
        // ConfigService resolves tenant-specific values when config-scope extension is active
        return this.config.resolve('app/theme');
      },
    );
  }
}

Scope Nesting

Inner runInScope calls create a nested context. The outer scope is restored after the inner callback completes:

typescript
await scope.runInScope({ level: 'tenant', id: 'acme' }, async () => {
  // chain: [{ level: 'tenant', id: 'acme' }]

  await scope.runInScope({ level: 'workspace', id: 'ws-1' }, async () => {
    // chain: [{ level: 'workspace', id: 'ws-1' }]
    // (inner scope, not appended — it's a fresh context)
  });

  // Back to: [{ level: 'tenant', id: 'acme' }]
});

When a ScopeChainResolver is registered, runInScope expands a leaf into a full chain (e.g., workspace resolves to [workspace, tenant]).

Default Scope

typescript
scope.runInDefaultScope(() => {
  // Runs with an empty scope chain, regardless of the surrounding context.
  // Useful for background tasks that should not inherit a request scope.
});

Concurrency Isolation

AsyncLocalStorage guarantees isolation between concurrent requests. Two parallel runInScope calls never interfere with each other — each maintains its own scope chain.