@proxydi/react
Version:
React wrapper for the ProxyDi library
560 lines (545 loc) • 20.2 kB
JavaScript
import React, { createContext, useContext, useMemo, useEffect } from 'react';
const injectableClasses = {};
const constructorInjections = {};
function findInjectableId(injectable) {
for (const [id, DependencyClass] of Object.entries(injectableClasses)) {
if (DependencyClass === injectable) {
return id;
}
}
throw new Error(`Class is not @injectable: ${injectable.name}`);
}
const INJECTIONS = Symbol('injections');
/**
* This symbol constant defines a property name.
* This property is present in each dependency instance that was registered in ProxyDiContainer.
* The property stores the dependency identifier that should be used to resolve dependency from the container where it was registered.
*/
const DEPENDENCY_ID = Symbol('DependencyId');
/**
* This symbol constant defines a property name.
* This property is present in each dependency instance that was registered in ProxyDiContainer.
* The property stores a reference to the ProxyDiContainer in which the dependency was registered.
*/
const PROXYDI_CONTAINER = Symbol('proxyDiContainer');
const IS_INJECTION_PROXY = Symbol('isInjectionProxy');
const INJECTION_OWNER = Symbol('injectionOwner');
const IS_INSTANCE_PROXY = Symbol('isInstanceProxy');
const DEFAULT_SETTINGS = {
allowRegisterAnything: false,
allowRewriteDependencies: false,
resolveInContainerContext: false,
};
var _a;
class InjectionProxy {
constructor(onwer, container) {
this[_a] = true;
this[INJECTION_OWNER] = onwer;
this[PROXYDI_CONTAINER] = container;
}
}
_a = IS_INJECTION_PROXY;
const makeInjectionProxy = (injection, injectionOwner, container) => {
function getDependency() {
if (container.isKnown(injection.dependencyId)) {
const dependency = container.resolve(injection.dependencyId);
if (!container.settings.allowRewriteDependencies) {
injection.set(injectionOwner, dependency);
}
return dependency;
}
else {
throw new Error(`Unknown dependency: ${String(injection.dependencyId)}`);
}
}
return new Proxy(new InjectionProxy(injectionOwner, container), {
get: function (target, prop, receiver) {
if (target[prop]) {
return target[prop];
}
const dependency = getDependency();
return Reflect.get(dependency, prop, receiver);
},
set: function (target, prop, value) {
const dependency = getDependency();
return Reflect.set(dependency, prop, value);
},
has: function (target, prop) {
const dependency = getDependency();
return Reflect.has(dependency, prop);
},
});
};
function makeDependencyProxy(dependency) {
const injectionValues = {};
return new Proxy(dependency, {
get: function (target, prop, receiver) {
if (prop === IS_INSTANCE_PROXY) {
return true;
}
if (injectionValues[prop]) {
return injectionValues[prop];
}
return Reflect.get(target, prop, receiver);
},
set: function (target, prop, value) {
injectionValues[prop] = value;
return Reflect.set(target, prop, value);
},
});
}
function makeConstructorDependencyProxy(container, dependencyId) {
function getDependency() {
if (container.isKnown(dependencyId)) {
const dependency = container.resolve(dependencyId);
return dependency;
}
else {
throw new Error(`Unknown dependency: ${String(dependencyId)}`);
}
}
return new Proxy({}, {
get: function (target, prop, receiver) {
const dependency = getDependency();
return Reflect.get(dependency, prop, receiver);
},
set: function (_target, prop, value) {
const dependency = getDependency();
return Reflect.set(dependency, prop, value);
},
has: function (_target, prop) {
const dependency = getDependency();
return Reflect.has(dependency, prop);
},
});
}
const middlewaresClasses = {};
class MiddlewareManager {
constructor(parent) {
this.parent = parent;
this.handlers = {
register: [],
remove: [],
resolve: [],
};
}
add(middleware) {
if (isRegistrator(middleware)) {
middleware.onRegister && this.on('register', middleware.onRegister);
}
if (isRemover(middleware)) {
middleware.onRemove && this.on('remove', middleware.onRemove);
}
if (isResolver(middleware)) {
middleware.onResolve && this.on('resolve', middleware.onResolve);
}
}
remove(middleware) {
if (isRegistrator(middleware)) {
middleware.onRegister &&
this.off('register', middleware.onRegister);
}
if (isRemover(middleware)) {
middleware.onRemove && this.off('remove', middleware.onRemove);
}
if (isResolver(middleware)) {
middleware.onResolve && this.off('resolve', middleware.onResolve);
}
}
on(event, listener) {
this.handlers[event].push(listener);
}
onRegister(context) {
var _a;
this.handlers.register.forEach((listener) => listener(context));
(_a = this.parent) === null || _a === undefined ? undefined : _a.onRegister(context);
}
onRemove(context) {
var _a;
this.handlers.remove.forEach((listener) => listener(context));
(_a = this.parent) === null || _a === undefined ? undefined : _a.onRemove(context);
}
onResolve(context) {
let result = context;
this.handlers.resolve.forEach((listener) => {
result = listener(result);
});
return result;
}
off(event, listener) {
const index = this.handlers[event].indexOf(listener);
if (index !== -1) {
this.handlers[event].splice(index, 1);
}
}
}
function isRegistrator(middleware) {
return !!middleware.onRegister;
}
function isRemover(middleware) {
return !!middleware.onRemove;
}
function isResolver(middleware) {
return !!middleware.onResolve;
}
/**
* A dependency injection container
*/
class ProxyDiContainer {
/**
* Creates a new instance of ProxyDiContainer.
* @param settings Optional container settings to override defaults.
* @param parent Optional parent container.
*/
constructor(settings, parent) {
this._children = {};
/**
* Holds dependency instances registered particular in this container.
*/
this.dependencies = {};
/**
* Holds proxies for dependencies registered in parent containers to provide for it dependencies from this container
*/
this.inContextProxies = {};
this.resolveImpl = (dependencyId) => {
const proxy = this.inContextProxies[dependencyId];
if (proxy) {
return proxy;
}
const instance = this.findDependency(dependencyId);
if (instance) {
if (instance[PROXYDI_CONTAINER] !== this &&
typeof instance === 'object' &&
this.settings.resolveInContainerContext) {
const proxy = makeDependencyProxy(instance);
this.injectDependenciesTo(proxy);
this.inContextProxies[dependencyId] = proxy;
return proxy;
}
return instance;
}
const InjectableClass = injectableClasses[dependencyId];
return this.register(InjectableClass, dependencyId);
};
this.id = ProxyDiContainer.idCounter++;
this.middlewareManager = new MiddlewareManager(parent === null || parent === undefined ? undefined : parent.middlewareManager);
if (parent) {
this.parent = parent;
this.parent.addChild(this);
}
this.settings = Object.assign(Object.assign({}, DEFAULT_SETTINGS), settings);
}
registerMiddleware(middleware) {
this.middlewareManager.add(middleware);
}
removeMiddleware(middleware) {
this.middlewareManager.remove(middleware);
}
register(dependency, dependecyId) {
var _a;
let id = dependecyId;
if (!id) {
if (typeof dependency === 'function') {
try {
id = findInjectableId(dependency);
}
catch (_b) {
id = dependency.name;
}
}
}
if (this.dependencies[id]) {
if (!this.settings.allowRewriteDependencies) {
throw new Error(`ProxyDi already has dependency for ${String(id)}`);
}
}
let instance;
const isClass = typeof dependency === 'function';
if (isClass) {
instance = this.createInstance(dependency, id);
}
else {
instance = dependency;
}
const isObject = typeof instance === 'object';
if (!isObject && !this.settings.allowRegisterAnything) {
throw new Error(`Can't register as dependency (allowRegisterAnything is off for this contatiner): ${instance}`);
}
if (isObject) {
instance[PROXYDI_CONTAINER] = this;
instance[DEPENDENCY_ID] = id;
}
this.injectDependenciesTo(instance);
this.dependencies[id] = instance;
const constructorName = (_a = instance.constructor) === null || _a === undefined ? undefined : _a.name;
if (constructorName && middlewaresClasses[constructorName]) {
this.middlewareManager.add(instance);
}
let context = {
container: this,
dependencyId: id,
dependency: instance,
};
this.middlewareManager.onRegister(context);
return instance;
}
createInstance(Dependency, dependencyId) {
const paramIds = constructorInjections[dependencyId] || [];
const params = [];
for (const id of paramIds) {
const param = makeConstructorDependencyProxy(this, id);
params.push(param);
}
return new Dependency(...params);
}
/**
* Checks if a dependency with the given ID is known to the container or its ancestors which means that it can be resolved by this container
* @param dependencyId The identifier of the dependency.
* @returns True if the dependency is known, false otherwise.
*/
isKnown(dependencyId) {
return !!(this.inContextProxies[dependencyId] ||
this.dependencies[dependencyId] ||
(this.parent && this.parent.isKnown(dependencyId)) ||
injectableClasses[dependencyId]);
}
resolve(dependency) {
if (typeof dependency === 'function') {
let id;
try {
id = findInjectableId(dependency);
}
catch (_a) {
id = dependency.name;
}
return this.resolve(id);
}
if (!this.isKnown(dependency)) {
throw new Error(`Can't resolve unknown dependency: ${String(dependency)}`);
}
let context = {
container: this,
dependencyId: dependency,
dependency: this.resolveImpl(dependency),
};
context = this.middlewareManager.onResolve(context);
return context.dependency;
}
/**
* Injects dependencies to the given object based on its defined injections metadata. Does not affect the container.
* @param injectionsOwner The object to inject dependencies into.
*/
injectDependenciesTo(injectionsOwner) {
const dependencyInjects = injectionsOwner[INJECTIONS] || {};
Object.values(dependencyInjects).forEach((injection) => {
const dependencyProxy = makeInjectionProxy(injection, injectionsOwner, this);
injection.set(injectionsOwner, dependencyProxy);
});
}
/**
* Creates instances for all injectable classes and registers them in this container.
* @returns This container to allow use along with constructor.
*/
registerInjectables() {
for (const [dependencyId, InjectableClass] of Object.entries(injectableClasses)) {
this.register(InjectableClass, dependencyId);
}
return this;
}
/**
* Finalizes dependency injections, prevents further rewriting of dependencies,
* and recursively bakes injections for child containers.
*/
bakeInjections() {
for (const dependency of Object.values(this.dependencies)) {
const dependencyInjects = dependency[INJECTIONS] || {};
Object.values(dependencyInjects).forEach((inject) => {
const value = this.resolve(inject.dependencyId);
inject.set(dependency, value);
});
}
this.settings.allowRewriteDependencies = false;
for (const child of Object.values(this._children)) {
child.bakeInjections();
}
}
/**
* Creates a child container that inherits settings and dependencies from this container.
* @returns A new child instance of ProxyDiContainer.
*/
createChildContainer() {
return new ProxyDiContainer(this.settings, this);
}
/**
* Removes a given dependency from the container using either the dependency instance or its ID.
* @param dependencyOrId The dependency instance or dependency identifier to remove.
*/
remove(dependencyOrId) {
var _a;
const id = isDependency(dependencyOrId)
? dependencyOrId[DEPENDENCY_ID]
: dependencyOrId;
const dependency = this.dependencies[id];
if (dependency) {
const constructorName = (_a = dependency.constructor) === null || _a === undefined ? undefined : _a.name;
if (constructorName && middlewaresClasses[constructorName]) {
this.middlewareManager.remove(dependency);
}
const dependencyInjects = dependency[INJECTIONS]
? dependency[INJECTIONS]
: {};
Object.values(dependencyInjects).forEach((inject) => {
inject.set(dependency, undefined);
});
delete dependency[DEPENDENCY_ID];
delete this.dependencies[id];
this.middlewareManager.onRemove({
container: this,
dependencyId: id,
dependency,
});
}
}
/**
* Destroys the container by removing all dependencies,
* recursively destroying child containers and removing itself from its parent.
*/
destroy() {
const allDependencies = Object.values(this.dependencies);
for (const dependency of allDependencies) {
this.remove(dependency);
}
this.dependencies = {};
for (const child of Object.values(this._children)) {
child.destroy();
}
this._children = {};
if (this.parent) {
this.parent.removeChild(this.id);
this.parent = undefined;
}
}
/**
* Recursively finds a dependency by its ID from this container or its parent.
* @param dependencyId The identifier of the dependency to find.
* @returns The dependency if found, otherwise undefined.
*/
findDependency(dependencyId) {
const dependency = this.dependencies[dependencyId];
if (!dependency && this.parent) {
const parentDependency = this.parent.findDependency(dependencyId);
return parentDependency;
}
return dependency;
}
/**
* All direct descendants of this container
*/
get children() {
return Object.values(this._children);
}
/**
*
* @param id Unique identifier of container
* @returns
*/
getChild(id) {
const child = this._children[id];
if (!child) {
throw new Error(`Unknown ProxyDiContainer child ID: ${id}`);
}
return child;
}
/**
* Registers a child container to this container.
* @param child The child container to add.
* @throws Error if a child with the same ID already exists.
*/
addChild(child) {
if (this._children[child.id]) {
throw new Error(`ProxyDi already has child with id ${child.id}`);
}
this._children[child.id] = child;
}
/**
* Removes a child container by its ID.
* @param id The identifier of the child container to remove.
*/
removeChild(id) {
const child = this._children[id];
if (child) {
delete this._children[id];
}
}
}
/**
* Static counter used to assign unique IDs to containers.
*/
ProxyDiContainer.idCounter = 0;
/**
* Helper function to determine if the provided argument is a dependency instance.
* @param dependencyOrId The dependency instance or dependency identifier.
* @returns True if the argument is a dependency instance, false otherwise.
*/
function isDependency(dependencyOrId) {
return (typeof dependencyOrId === 'object' &&
!!dependencyOrId[DEPENDENCY_ID]);
}
var ProxyDiContext = createContext(undefined);
/**
* Returns the ProxyDi container instance provided by the context if you need it by some reason
*
* @returns {ProxyDiContainer} The ProxyDi container instance provided by the context.
*/
var useProxyDiContainer = function () {
var container = useContext(ProxyDiContext);
return container;
};
/**
* Provides a ProxyDi container to its child components.
* If a container is not provided (the recommended usage), a new container will be created.
* If this provider is nested within another ProxyDiProvider,
* the newly created container will be a child of the container from the parent ProxyDiProvider.
*
* @param {Function} [props.init] - Function to initialize the container's dependencies. This is the recommended way to register dependencies in the container.
* @param {ProxyDiContainer} [props.container] - An alternative, though not recommended, way to provide an initialized container instance.
* @param {React.ReactNode} props.children - Child components that will have access to the container.
*/
var ProxyDiProvider = function (_a) {
var children = _a.children, container = _a.container, init = _a.init;
var parentContainer = useProxyDiContainer();
var proxyDiContainer = useMemo(function () {
var proxyDiContainer = container !== null && container !== void 0 ? container : (parentContainer
? parentContainer.createChildContainer()
: new ProxyDiContainer());
if (init) {
init(proxyDiContainer);
}
return proxyDiContainer;
}, [container, parentContainer]);
useEffect(function () {
return function () {
proxyDiContainer.destroy();
};
}, [proxyDiContainer]);
return (React.createElement(ProxyDiContext.Provider, { value: proxyDiContainer }, children));
};
/**
* Resolves a dependency from the ProxyDi container. A wrapper for the ProxyDiContainer.resolve() method.
*
* @template T - The type of the dependency to resolve.
* @param {DependencyId} dependencyId - Identifier for the dependency.
* @returns {T} The resolved dependency instance.
*
* @throws {Error} If dependency ID is unknown.
* @throws {Error} If the hook is used outside of a ProxyDiProvider.
*/
var useProxyDi = function (dependencyId) {
var container = useProxyDiContainer();
if (!container) {
throw new Error('useProxyDi must be used within a ProxyDiProvider');
}
return container.resolve(dependencyId);
};
export { ProxyDiProvider, useProxyDi, useProxyDiContainer };
//# sourceMappingURL=index.esm.js.map