1
0
mirror of https://github.com/bitwarden/browser synced 2026-02-09 05:00:10 +00:00
Files
browser/libs/angular/src/platform/services/default-decentralized-init.service.ts
2026-01-21 09:59:59 +01:00

114 lines
3.9 KiB
TypeScript

import { Inject, Injectable, Injector } from "@angular/core";
import { Dependency, Initializable } from "@bitwarden/common/platform/abstractions/initializable";
import {
DecentralizedInitService,
INIT_SERVICES,
} from "../abstractions/decentralized-init.service";
/**
* Default implementation of DecentralizedInitService that uses topological sort
* to execute initialization in dependency order.
*
* This service:
* - Collects registered service tokens via the INIT_SERVICES token
* - Resolves tokens to instances using Angular's Injector
* - Builds a dependency graph from each service's dependencies property
* - Performs topological sort to determine execution order
* - Detects circular dependencies and throws clear errors
* - Executes init() methods sequentially in dependency order
*/
@Injectable()
export class DefaultDecentralizedInitService implements DecentralizedInitService {
constructor(
@Inject(INIT_SERVICES) private initServiceTokens: Dependency[],
private injector: Injector,
) {}
async init(): Promise<void> {
if (!this.initServiceTokens || this.initServiceTokens.length === 0) {
return;
}
// Resolve all tokens to instances using Angular's Injector
const services: Initializable[] = this.initServiceTokens.map((token) =>
this.injector.get(token),
);
const sorted = this.topologicalSort(services, this.initServiceTokens);
for (const service of sorted) {
try {
await service.init();
} catch (error) {
throw new Error(`Failed to initialize ${service.constructor.name}: ${error}`);
}
}
}
/**
* Performs topological sort on services based on their declared dependencies.
* Returns services in an order where all dependencies come before dependents.
*
* @param services The resolved service instances
* @param tokens The tokens used to register these services (parallel array)
* @throws Error if circular dependencies are detected
* @throws Error if a dependency is declared but not registered
*/
private topologicalSort(services: Initializable[], tokens: Dependency[]): Initializable[] {
// Build a map from token to instance
// This uses the exact tokens that were registered, so abstract classes work correctly
const instanceMap = new Map<Dependency, Initializable>();
for (let i = 0; i < services.length; i++) {
instanceMap.set(tokens[i], services[i]);
}
const sorted: Initializable[] = [];
const visiting = new Set<Initializable>();
const visited = new Set<Initializable>();
const visit = (service: Initializable, path: string[] = []) => {
if (visited.has(service)) {
return;
}
if (visiting.has(service)) {
// Circular dependency detected - build a clear error message
const cycle = [...path, service.constructor.name].join(" -> ");
throw new Error(`Circular dependency detected: ${cycle}`);
}
visiting.add(service);
const currentPath = [...path, service.constructor.name];
// Visit all dependencies first
for (const depClass of service.dependencies ?? []) {
const depInstance = instanceMap.get(depClass);
if (!depInstance) {
// Dependency declared but not registered - this is likely a configuration error
throw new Error(
`${service.constructor.name} depends on ${depClass.name}, but ${depClass.name} is not registered in INIT_SERVICES. ` +
`Make sure to add it to your providers array:\n` +
`{ provide: INIT_SERVICES, useValue: ${depClass.name}, multi: true }`,
);
}
visit(depInstance, currentPath);
}
visiting.delete(service);
visited.add(service);
sorted.push(service);
};
// Visit all services (handles disconnected components in the graph)
for (const service of services) {
visit(service);
}
return sorted;
}
}