di-tory
Version:
Compose applications with dependency injection
134 lines (133 loc) • 5.02 kB
JavaScript
import { Stack } from '../stack/Stack.js';
import { DependencyResolutionError as DRError, DependencyResolutionErrorCode as DRErrType, } from '../DependencyResolutionError.js';
export const scope = {
module: 'module',
singleton: 'singleton',
};
const createModule = (singletons, privateResolvers, publicResolvers, initializers, params) => {
const resolvers = { ...privateResolvers, ...publicResolvers };
const resolutionStack = new Stack();
const moduleInstances = new Map();
const self = {};
const module = {};
for (const name of Object.keys(privateResolvers)) {
Object.defineProperty(self, name, {
get: () => resolve(name),
});
Object.defineProperty(module, name, {
get: () => {
throw new DRError(DRErrType.PrivateMemberAccessFailure, resolutionStack.toStringArray(), String(name));
},
});
}
for (const name of Object.keys(publicResolvers)) {
Object.defineProperty(self, name, {
get: () => resolve(name),
});
Object.defineProperty(module, name, {
get: () => resolve(name),
});
}
const resolve = (name) => {
const resolver = resolvers[name];
const instances = resolver.scope === 'singleton' ? singletons : moduleInstances;
if (instances.has(name))
return instances.get(name);
const currentStack = resolutionStack.toStringArray();
try {
resolutionStack.push(name);
}
catch {
throw new DRError(DRErrType.CircularDependencyFailure, resolutionStack.toStringArray(), String(name));
}
let instance;
try {
instance = resolver(self, params);
}
catch (err) {
if (err instanceof DRError)
throw err;
throw new DRError(DRErrType.InstantiationFailure, currentStack, String(name), err);
}
resolutionStack.pop();
instances.set(name, instance);
if (resolutionStack.length === 0) {
initializers[name]?.call(instance, self, params);
}
return instance;
};
return module;
};
export const Module = () => {
const privateResolvers = {};
const publicResolvers = {};
let initializers = null;
let sealed = false;
const singletons = new Map();
const self = {
private(resolvers, scope) {
if (sealed) {
throw new Error('Cannot extend initialized module');
}
for (const name of Object.keys(resolvers)) {
const resolver = resolvers[name];
if (typeof resolver !== 'function') {
throw new Error(`Expected ${String(name)} to be a function, but got ${typeof resolver}`);
}
resolver.scope ?? (resolver.scope = scope);
privateResolvers[name] = resolver;
}
return self;
},
privateImpl(implementation) {
if (sealed) {
throw new Error('Cannot extend initialized module');
}
for (const name of Object.keys(implementation)) {
const method = implementation[name];
const resolver = (moduleInstance) => (...args) => method(moduleInstance, ...args);
resolver.scope = 'module';
privateResolvers[name] = resolver;
}
return self;
},
public(resolvers, scope) {
if (sealed) {
throw new Error('Cannot extend initialized module');
}
for (const name of Object.keys(resolvers)) {
const resolver = resolvers[name];
if (typeof resolver !== 'function') {
throw new Error(`Expected ${String(name)} to be a function, but got ${typeof resolver}`);
}
resolver.scope ?? (resolver.scope = scope);
publicResolvers[name] = resolver;
}
return self;
},
publicImpl(implementation) {
if (sealed) {
throw new Error('Cannot extend initialized module');
}
for (const name of Object.keys(implementation)) {
const method = implementation[name];
const resolver = (moduleInstance) => (...args) => method(moduleInstance, ...args);
resolver.scope = 'module';
publicResolvers[name] = resolver;
}
return self;
},
init(initializer) {
initializers = initializer;
sealed = true;
return self;
},
create(params = {}) {
sealed = true;
return createModule(singletons, privateResolvers, publicResolvers, initializers ?? {}, params);
},
};
// eslint-disable-next-line @typescript-eslint/no-explicit-any
return self;
};
export default Module;