@evyweb/ioctopus
Version:
A simple IoC container for JavaScript and TypeScript for classes and functions.
159 lines (157 loc) • 5.49 kB
JavaScript
// src/module.ts
function createModule() {
const bindings = /* @__PURE__ */ new Map();
const resolveDependenciesArray = (dependencies, resolve) => dependencies.map(resolve);
const resolveDependenciesObject = (dependencies, resolve) => {
const entries = Object.entries(dependencies);
return Object.fromEntries(entries.map(([key, dependency]) => [key, resolve(dependency)]));
};
const isDependencyArray = (dependencies) => Array.isArray(dependencies);
const isDependencyObject = (dependencies) => dependencies !== null && typeof dependencies === "object" && !Array.isArray(dependencies);
const bind = (key) => {
const toValue = (value) => {
bindings.set(key, { factory: () => value, scope: "singleton" });
};
const toFunction = (fn) => {
bindings.set(key, { factory: () => fn, scope: "singleton" });
};
const toHigherOrderFunction = (fn, dependencies, scope = "singleton") => {
if (dependencies && !isDependencyArray(dependencies) && !isDependencyObject(dependencies)) {
throw new Error("Invalid dependencies type");
}
const factory = (resolve) => {
if (!dependencies) {
return fn();
}
if (isDependencyArray(dependencies)) {
return fn(...resolveDependenciesArray(dependencies, resolve));
}
return fn({ ...resolveDependenciesObject(dependencies, resolve) });
};
bindings.set(key, { factory, scope });
};
const toCurry = toHigherOrderFunction;
const toFactory = (factory, scope = "singleton") => {
bindings.set(key, { factory: (resolve) => factory(resolve), scope });
};
const toClass = (AnyClass, dependencies, scope = "singleton") => {
if (dependencies && !isDependencyArray(dependencies) && !isDependencyObject(dependencies)) {
throw new Error("Invalid dependencies type");
}
const factory = (resolve) => {
if (!dependencies) {
return new AnyClass();
}
if (isDependencyArray(dependencies)) {
const resolvedDeps = resolveDependenciesArray(dependencies, resolve);
return new AnyClass(...resolvedDeps);
}
if (isDependencyObject(dependencies)) {
const resolvedDeps = resolveDependenciesObject(dependencies, resolve);
return new AnyClass({ ...resolvedDeps });
}
};
bindings.set(key, { factory, scope });
};
return {
toValue,
toFunction,
toFactory,
toClass,
toHigherOrderFunction,
toCurry
};
};
return { bind, bindings };
}
// src/container.ts
function createContainer() {
const modules = /* @__PURE__ */ new Map();
const singletonInstances = /* @__PURE__ */ new Map();
const scopedInstances = /* @__PURE__ */ new Map();
const resolutionStack = [];
let currentScopeId;
const DEFAULT_MODULE_KEY = Symbol("DEFAULT");
const defaultModule = createModule();
modules.set(DEFAULT_MODULE_KEY, defaultModule);
const bind = (key) => defaultModule.bind(key);
const load = (moduleKey, module) => modules.set(moduleKey, module);
const unload = (moduleKey) => {
singletonInstances.clear();
modules.delete(moduleKey);
};
const findLastBinding = (key) => {
const modulesArray = Array.from(modules.values());
for (let i = modulesArray.length - 1; i >= 0; i--) {
const module = modulesArray[i];
const binding = module.bindings.get(key);
if (binding) {
return binding;
}
}
return null;
};
const getLastBinding = (key) => {
const binding = findLastBinding(key);
if (!binding) {
throw new Error(`No binding found for key: ${key.toString()}`);
}
return binding;
};
const verifyCircularDependencies = (key) => {
if (resolutionStack.includes(key)) {
const cycle = [...resolutionStack, key].map((k) => k.toString()).join(" -> ");
throw new Error(`Circular dependency detected: ${cycle}`);
}
};
const get = (key) => {
verifyCircularDependencies(key);
resolutionStack.push(key);
try {
const binding = getLastBinding(key);
const { factory, scope } = binding;
if (scope === "singleton") {
if (!singletonInstances.has(key)) {
singletonInstances.set(key, factory(resolveDependency));
}
return singletonInstances.get(key);
}
if (scope === "transient") {
return factory(resolveDependency);
}
if (scope === "scoped") {
if (!currentScopeId)
throw new Error(`Cannot resolve scoped binding outside of a scope: ${key.toString()}`);
if (!scopedInstances.has(currentScopeId)) {
scopedInstances.set(currentScopeId, /* @__PURE__ */ new Map());
}
const scopeMap = scopedInstances.get(currentScopeId);
if (!scopeMap.has(key)) {
scopeMap.set(key, factory(resolveDependency));
}
return scopeMap.get(key);
}
throw new Error(`Unknown scope: ${scope}`);
} finally {
resolutionStack.pop();
}
};
const resolveDependency = (depKey) => {
return get(depKey);
};
const runInScope = (callback) => {
const previousScopeId = currentScopeId;
currentScopeId = Symbol("scope");
try {
return callback();
} finally {
scopedInstances.delete(currentScopeId);
currentScopeId = previousScopeId;
}
};
return { bind, load, get, unload, runInScope };
}
export {
createContainer,
createModule
};