Scope
Contract
@modularityjs/scope provides a universal scope hierarchy using Node.js AsyncLocalStorage for per-request context isolation.
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
import { ScopeModule } from '@modularityjs/scope';
const modules = [ScopeModule];Usage
@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):
@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 Package | What it does |
|---|---|
@modularityjs/config-scope | Makes ConfigService scope-aware via ScopedConfigService preference |
@modularityjs/config-env-scope | Makes 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:
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:
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
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.