UNPKG

@proxydi/react

Version:

React wrapper for the ProxyDi library

560 lines (545 loc) 20.2 kB
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