Расширения могут зависеть друг от друга: кеш-расширение требует, чтобы валидатор работал первым, а логгер — последним. Управление этими зависимостями предотвращает конфликты и гарантирует правильный порядок инициализации.
// Неправильный порядок — логгер установлен до валидатора
registry
.use(LogExtension) // Логирует до валидации — включая невалидные вызовы
.use(ValidationExtension) // Валидирует слишком поздно
.use(CacheExtension); // Кеширует невалидные результаты
Правильный порядок: сначала валидация, потом кеш, потом лог. Если расширений много, порядок нужно описать явно.
interface Extension {
name: string;
version: string;
deps?: string[]; // Имена расширений, которые должны быть установлены раньше
after?: string[]; // Должен быть установлен после этих
install: (reg: Registry) => void;
}
const CacheExtension: Extension = {
name: 'cache',
version: '1.0.0',
deps: ['validation'], // Требует validation
after: ['validation'], // Устанавливается после
install: (reg) => { /* ... */ },
};
const LogExtension: Extension = {
name: 'log',
version: '1.0.0',
after: ['cache', 'validation'],
install: (reg) => { /* ... */ },
};
function topoSort(extensions: Extension[]): Extension[] {
const nameMap = new Map(extensions.map(e => [e.name, e]));
const visited = new Set<string>();
const result: Extension[] = [];
function visit(name: string): void {
if (visited.has(name)) return;
visited.add(name);
const ext = nameMap.get(name);
if (!ext) throw new Error(`Extension "${name}" not found`);
(ext.after ?? []).forEach(dep => {
if (!nameMap.has(dep)) throw new Error(`Extension "${name}" requires "${dep}"`);
visit(dep);
});
result.push(ext);
}
extensions.forEach(e => visit(e.name));
return result;
}
// Использование
const sorted = topoSort([LogExtension, ValidationExtension, CacheExtension]);
// Порядок: ValidationExtension → CacheExtension → LogExtension
class Registry {
private pending: Extension[] = [];
private installed = new Map<string, Extension>();
add(extension: Extension): this {
this.pending.push(extension);
return this;
}
resolve(): this {
const sorted = topoSort(this.pending);
sorted.forEach(ext => {
// Проверить зависимости
(ext.deps ?? []).forEach(dep => {
if (!this.installed.has(dep)) {
throw new Error(
`Extension "${ext.name}" requires "${dep}" to be installed`
);
}
});
ext.install(this);
this.installed.set(ext.name, ext);
});
this.pending = [];
return this;
}
isInstalled(name: string): boolean {
return this.installed.has(name);
}
}
const registry = new Registry();
registry
.add(LogExtension) // Добавить в любом порядке
.add(ValidationExtension)
.add(CacheExtension)
.resolve(); // Установить в правильном порядке
interface VersionedExtension extends Extension {
version: string;
peerDeps?: Record<string, string>; // имя → минимальная версия
}
function checkPeerDeps(
ext: VersionedExtension,
installed: Map<string, VersionedExtension>
): void {
const peers = ext.peerDeps ?? {};
Object.entries(peers).forEach(([name, minVersion]) => {
const dep = installed.get(name);
if (!dep) {
throw new Error(`"${ext.name}" requires peer "${name}" >= ${minVersion}`);
}
if (!satisfiesVersion(dep.version, minVersion)) {
throw new Error(
`"${ext.name}" requires "${name}" >= ${minVersion}, got ${dep.version}`
);
}
});
}
function satisfiesVersion(actual: string, required: string): boolean {
const [aMaj, aMin] = actual.split('.').map(Number);
const [rMaj, rMin] = required.split('.').map(Number);
return aMaj > rMaj || (aMaj === rMaj && aMin >= rMin);
}
const AnalyticsExtension: Extension = {
name: 'analytics',
version: '1.0.0',
install: (reg) => {
reg.wrapFormat((original) => (date, locale, opts) => {
const result = original(date, locale, opts);
// Опциональная зависимость от log-расширения
if (reg.isInstalled('log')) {
// Дополнительное поведение при наличии log
}
return result;
});
},
};
const ConflictingExtension: Extension = {
name: 'format-override',
version: '1.0.0',
install: (reg) => {
if (reg.isInstalled('cache')) {
throw new Error(
'"format-override" conflicts with "cache". Install one or the other.'
);
}
reg.wrapFormat((original) => (date, locale, opts) => {
return 'overridden!';
});
},
};
validation ───────┐
▼
cache ──┐
▼
log
▼
analytics
Каждое расширение знает своё место в цепочке. Ниже — правила для примера выше:
const EXTENSIONS = [
{ name: 'validation', after: [] },
{ name: 'cache', after: ['validation'] },
{ name: 'log', after: ['cache'] },
{ name: 'analytics', after: ['log'], deps: ['log'] },
];
// Диагностика: вывести установленные расширения и порядок
registry.getInstalled().forEach((ext, name) => {
console.log(`[${name}] v${ext.version}`);
});