react-obsidian
Version:
Dependency injection framework for React and React Native applications
158 lines (135 loc) • 5.9 kB
text/typescript
import { Constructable } from '../../types';
import { Graph } from '../Graph';
import { Middleware } from './Middleware';
import GraphMiddlewareChain from './GraphMiddlewareChain';
import { ObtainLifecycleBoundGraphException } from './ObtainLifecycleBoundGraphException';
import { getGlobal } from '../../utils/getGlobal';
export class GraphRegistry {
private readonly constructorToInstance = new Map<Constructable<Graph>, Set<Graph>>();
private readonly instanceToConstructor = new Map<Graph, Constructable<Graph>>();
private readonly injectionTokenToInstance = new Map<string, Graph>();
private readonly instanceToInjectionToken = new Map<Graph, string>();
private readonly nameToInstance = new Map<string, Graph>();
private readonly graphToSubgraphs = new Map<Constructable<Graph>, Set<Constructable<Graph>>>();
private readonly graphMiddlewares = new GraphMiddlewareChain();
register(constructor: Constructable<Graph>, subgraphs: Constructable<Graph>[] = []) {
this.graphToSubgraphs.set(constructor, new Set(subgraphs));
}
ensureRegistered(graph: Graph) {
if (this.instanceToConstructor.get(graph)) return;
this.set(graph.constructor as any, graph);
}
getSubgraphs(graph: Graph): Graph[] {
const Graph = this.instanceToConstructor.get(graph)!;
const subgraphs = this.graphToSubgraphs.get(Graph) ?? new Set();
return Array.from(subgraphs).map((G) => this.resolve(G));
}
getGraphInstance(name: string): Graph {
return this.nameToInstance.get(name)!;
}
resolve<T extends Graph>(
Graph: Constructable<T>,
source: 'lifecycleOwner' | 'classInjection' | 'serviceLocator' = 'lifecycleOwner',
props: any = undefined,
injectionToken?: string,
): T {
if ((this.isSingleton(Graph) || this.isBoundToReactLifecycle(Graph)) && this.has(Graph, injectionToken)) {
return this.isComponentScopedLifecycleBound(Graph) ?
this.getByInjectionToken(Graph, injectionToken) :
this.getFirst(Graph);
}
if (this.isBoundToReactLifecycle(Graph) && source !== 'lifecycleOwner') {
throw new ObtainLifecycleBoundGraphException(Graph);
}
const graph = this.graphMiddlewares.resolve(Graph, props);
this.set(Graph, graph, injectionToken);
return graph as T;
}
private has(Graph: Constructable<Graph>, injectionToken?: string): boolean {
const instances = this.constructorToInstance.get(Graph);
if (!instances) return false;
if (this.isComponentScopedLifecycleBound(Graph)) {
return Array
.from(instances)
.some((graph) => this.instanceToInjectionToken.get(graph) === injectionToken);
}
return (this.constructorToInstance.get(Graph)?.size ?? 0) > 0;
}
private getFirst<T extends Graph>(Graph: Constructable<T>): T {
return this.constructorToInstance.get(Graph)!.values().next().value as T;
}
private getByInjectionToken<T extends Graph>(Graph: Constructable<T>, injectionToken?: string): T {
return Array
.from(this.constructorToInstance.get(Graph)!)
.find((graph) => {
return this.instanceToInjectionToken.get(graph) === injectionToken;
}) as T;
}
private set(Graph: Constructable<Graph>, graph: Graph, injectionToken?: string) {
const graphs = this.constructorToInstance.get(Graph) ?? new Set();
if (injectionToken && this.isComponentScopedLifecycleBound(Graph)) {
this.injectionTokenToInstance.set(injectionToken, graph);
this.instanceToInjectionToken.set(graph, injectionToken);
}
graphs.add(graph);
this.constructorToInstance.set(Graph, graphs);
this.instanceToConstructor.set(graph, Graph);
this.nameToInstance.set(graph.name, graph);
}
private isSingleton(Graph: Constructable<Graph>): boolean {
return Reflect.getMetadata('isSingleton', Graph) ?? false;
}
private isBoundToReactLifecycle(Graph: Constructable<Graph>): boolean {
return Reflect.getMetadata('isLifecycleBound', Graph) ?? false;
}
private isComponentScopedLifecycleBound(Graph: Constructable<Graph>): boolean {
return Reflect.getMetadata('lifecycleScope', Graph) === 'component';
}
clearGraphAfterItWasMockedInTests(graphName: string) {
const graphNames = this.nameToInstance.keys();
for (const name of graphNames) {
if (name.match(graphName)) {
const graph = this.nameToInstance.get(name);
if (!graph) return;
const Graph = this.instanceToConstructor.get(graph);
if (!Graph) return;
this.instanceToConstructor.delete(graph);
this.constructorToInstance.get(Graph)!.delete(graph);
this.nameToInstance.delete(graph.name);
const token = this.instanceToInjectionToken.get(graph);
if (token) {
this.injectionTokenToInstance.delete(token);
this.instanceToInjectionToken.delete(graph);
}
}
}
}
clear(graph: Graph) {
const Graph = this.instanceToConstructor.get(graph);
if (!Graph || this.isSingleton(Graph)) return;
this.instanceToConstructor.delete(graph);
this.constructorToInstance.get(Graph)!.delete(graph);
this.nameToInstance.delete(graph.name);
const token = this.instanceToInjectionToken.get(graph);
if (token) {
this.injectionTokenToInstance.delete(token);
this.instanceToInjectionToken.delete(graph);
}
}
addGraphMiddleware(middleware: Middleware<Graph>) {
this.graphMiddlewares.add(middleware);
}
clearGraphMiddlewares() {
this.graphMiddlewares.clear();
}
clearAll() {
this.instanceToConstructor.clear();
this.constructorToInstance.clear();
this.nameToInstance.clear();
this.injectionTokenToInstance.clear();
this.instanceToInjectionToken.clear();
}
}
const globalObject = getGlobal();
globalObject.graphRegistry = globalObject.graphRegistry || new GraphRegistry();
export default globalObject.graphRegistry as GraphRegistry;