@thaitype/ioctopus
Version:
A simple IoC container for JavaScript and TypeScript for classes and functions.
237 lines (232 loc) • 8.36 kB
JavaScript
;
var __defProp = Object.defineProperty;
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
var __getOwnPropNames = Object.getOwnPropertyNames;
var __hasOwnProp = Object.prototype.hasOwnProperty;
var __export = (target, all) => {
for (var name in all)
__defProp(target, name, { get: all[name], enumerable: true });
};
var __copyProps = (to, from, except, desc) => {
if (from && typeof from === "object" || typeof from === "function") {
for (let key of __getOwnPropNames(from))
if (!__hasOwnProp.call(to, key) && key !== except)
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
}
return to;
};
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
// src/index.ts
var src_exports = {};
__export(src_exports, {
ServiceRegistry: () => ServiceRegistry,
createContainer: () => createContainer,
createModule: () => createModule,
createServiceRegistry: () => createServiceRegistry
});
module.exports = __toCommonJS(src_exports);
// src/module.ts
function createModule(serviceRegistry) {
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 resovledKey = serviceRegistry.get(key);
const toValue = (value) => {
bindings.set(resovledKey, { factory: () => value, scope: "singleton" });
};
const toFunction = (fn) => {
bindings.set(resovledKey, { factory: () => fn, scope: "singleton" });
};
const toHigherOrderFunction = (fn, dependencies, scope = "singleton") => {
if (dependencies && !isDependencyArray(dependencies) && !isDependencyObject(dependencies)) {
throw new Error("Invalid dependencies type");
}
const arrayDependencies = [];
if (!isDependencyObject(dependencies) && isDependencyArray(dependencies)) {
for (const dependency of dependencies) {
arrayDependencies.push(serviceRegistry.get(dependency));
}
}
const factory = (resolve) => {
if (!dependencies) {
return fn();
}
if (isDependencyArray(dependencies)) {
return fn(...resolveDependenciesArray(arrayDependencies, resolve));
}
return fn({ ...resolveDependenciesObject(dependencies, resolve) });
};
bindings.set(resovledKey, { factory, scope });
};
const toCurry = toHigherOrderFunction;
const toFactory = (factory, scope = "singleton") => {
bindings.set(resovledKey, { 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(resovledKey, { factory, scope });
};
return {
toValue,
toFunction,
toFactory,
toClass,
toHigherOrderFunction,
toCurry
};
};
return { bind, bindings };
}
// src/container.ts
function createContainer(serviceRegistry) {
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(serviceRegistry);
modules.set(DEFAULT_MODULE_KEY, defaultModule);
const bind = (key) => defaultModule.bind(key);
const load = (moduleKey, module2) => modules.set(moduleKey, module2);
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 module2 = modulesArray[i];
const binding = module2.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 isCircularDependency = (key) => resolutionStack.includes(key);
const buildCycleOf = (key) => [...resolutionStack, key].map((k) => k.toString()).join(" -> ");
const startCircularDependencyDetectionFor = (dependencyKey) => resolutionStack.push(dependencyKey);
const endCircularDependencyDetection = () => resolutionStack.pop();
const resolveDependencyKey = (dependency) => {
let dependencyKey;
if (typeof dependency === "symbol") {
dependencyKey = dependency;
} else {
dependencyKey = serviceRegistry.get(dependency);
}
return dependencyKey;
};
const get = (dependency) => {
const dependencyKey = resolveDependencyKey(dependency);
if (!dependencyKey) {
throw new Error(`No key found for dependency: ${dependency.toString()}`);
}
if (isCircularDependency(dependencyKey)) {
const cycle = buildCycleOf(dependencyKey);
throw new Error(`Circular dependency detected: ${cycle}`);
}
startCircularDependencyDetectionFor(dependencyKey);
try {
const binding = getLastBinding(dependencyKey);
const { factory, scope } = binding;
if (scope === "singleton") {
if (!singletonInstances.has(dependencyKey)) {
singletonInstances.set(dependencyKey, factory(resolveDependency));
}
return singletonInstances.get(dependencyKey);
}
if (scope === "transient") {
return factory(resolveDependency);
}
if (scope === "scoped") {
if (!currentScopeId) {
throw new Error(`Cannot resolve scoped binding outside of a scope: ${dependencyKey.toString()}`);
}
if (!scopedInstances.has(currentScopeId)) {
scopedInstances.set(currentScopeId, /* @__PURE__ */ new Map());
}
const scopeMap = scopedInstances.get(currentScopeId);
if (!scopeMap.has(dependencyKey)) {
scopeMap.set(dependencyKey, factory(resolveDependency));
}
return scopeMap.get(dependencyKey);
}
throw new Error(`Unknown scope: ${scope}`);
} finally {
endCircularDependencyDetection();
}
};
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 };
}
// src/service-registry.ts
function createServiceRegistry() {
return new ServiceRegistry();
}
var ServiceRegistry = class {
keyMap = {};
define(key, _identifier) {
this.keyMap[key] = Symbol.for(key);
return new ServiceBinder(this, key);
}
get(key) {
return this.keyMap[key];
}
};
var ServiceBinder = class {
constructor(serviceRegistry, key) {
this.serviceRegistry = serviceRegistry;
this.key = key;
}
mapTo() {
return this.serviceRegistry;
}
};
// Annotate the CommonJS export names for ESM import in node:
0 && (module.exports = {
ServiceRegistry,
createContainer,
createModule,
createServiceRegistry
});