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'; import { isString } from '../../utils/isString'; import referenceCounter from '../../ReferenceCounter'; import { Reflect } from '../../utils/reflect'; export class GraphRegistry { private readonly constructorToInstance = new Map, Set>(); private readonly instanceToConstructor = new Map>(); private readonly injectionTokenToInstance = new Map(); private readonly instanceToInjectionToken = new Map(); private readonly nameToInstance = new Map(); private readonly graphToSubgraphs = new WeakMap, Set>>(); private readonly graphToPrivateSubgraphs = new WeakMap, Set>>(); private readonly graphMiddlewares = new GraphMiddlewareChain(); private readonly keyToGenerator = new Map Constructable>(); private readonly keyToGraph = new Map>(); private readonly onClearListeners = new Map void>>(); register( constructor: Constructable, subgraphs: Constructable[] = [], privateSubgraphs: Constructable[] = [], ) { this.graphToSubgraphs.set(constructor, new Set(subgraphs)); this.graphToPrivateSubgraphs.set(constructor, new Set(privateSubgraphs)); } registerGraphGenerator(key: string, generator: () => Constructable) { if (this.keyToGenerator.has(key)) throw new Error(`Attempted to register a graph generator for key "${key}" that is already registered.`); this.keyToGenerator.set(key, generator); } ensureRegistered(keyOrGraph: string | Graph) { if (isString(keyOrGraph)) return; const graph = keyOrGraph; if (this.instanceToConstructor.get(graph)) return; this.set(graph.constructor as any, graph); } public isInstantiated(G: Constructable): boolean { return (this.constructorToInstance.get(G)?.size ?? 0) > 0; } getSubgraphs(graph: Graph): Graph[] { // Fall back to the instance's own constructor so that a graph that was // cleared from the registry while components still hold it (e.g. an // Activity pause misdetected as an unmount) can still resolve // dependencies provided by its subgraphs. const Graph = this.instanceToConstructor.get(graph) ?? (graph.constructor as any); const subgraphs = this.graphToSubgraphs.get(Graph) ?? new Set(); return Array.from(subgraphs).map((G) => this.resolve(G)); } getPrivateSubgraphs(graph: Graph): Graph[] { const Graph = this.instanceToConstructor.get(graph) ?? (graph.constructor as any); const privateSubgraphs = this.graphToPrivateSubgraphs.get(Graph) ?? new Set(); return Array.from(privateSubgraphs).map((G) => this.resolve(G)); } getGraphInstance(name: string): Graph { return this.nameToInstance.get(name)!; } resolve( keyOrGraph: String | Constructable, source: 'lifecycleOwner' | 'classInjection' | 'serviceLocator' = 'lifecycleOwner', props: any = undefined, injectionToken?: string, ): T { const Graph = isString(keyOrGraph) ? this.getGraphConstructorByKey(keyOrGraph) : keyOrGraph as Constructable; 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); this.instantiateCustomScopedSubgraphs(graph, props); return graph as T; } private instantiateCustomScopedSubgraphs(graph: Graph, props: any) { this.assertInstantiatingCustomScopedSubgraphFromSameScope(graph); if (!this.isCustomScopedLifecycleBound(this.instanceToConstructor.get(graph)!)) return; const customScope = Reflect.getMetadata('lifecycleScope', this.instanceToConstructor.get(graph)!); const subgraphs = this.getSubgraphsConstructors(graph); const sameScopeSubgraphs = subgraphs.filter( subgraph => Reflect.getMetadata('lifecycleScope', subgraph) === customScope, ); const instantiatedSubgraphs = sameScopeSubgraphs.map( subgraph => { return this.resolve(subgraph, 'lifecycleOwner', props); }, ); instantiatedSubgraphs.forEach((subgraph) => referenceCounter.retain(subgraph)); this.registerOnClearListener(graph, () => { instantiatedSubgraphs.forEach((subgraph) => referenceCounter.release(subgraph, () => this.clear(subgraph))); }); } private assertInstantiatingCustomScopedSubgraphFromSameScope(graph: Graph) { const graphScope = Reflect.getMetadata('lifecycleScope', this.instanceToConstructor.get(graph)!); const subgraphs = this.getSubgraphsConstructors(graph); subgraphs.forEach(subgraph => { const subgraphScope = Reflect.getMetadata('lifecycleScope', subgraph); if ( !this.isInstantiated(subgraph) && this.isCustomScopedLifecycleBound(subgraph) && graphScope !== subgraphScope ) { throw new Error(`Cannot instantiate the scoped graph '${subgraph.name}' as a subgraph of '${graph.constructor.name}' because the scopes do not match. ${graphScope} !== ${subgraphScope}`); } }); } private getSubgraphsConstructors(graph: Graph | Constructable): Constructable[] { const Graph = typeof graph === 'function' ? graph : this.instanceToConstructor.get(graph)!; const directSubgraphs = Array.from(this.graphToSubgraphs.get(Graph) ?? new Set>()); const directPrivateSubgraphs = Array.from( this.graphToPrivateSubgraphs.get(Graph) ?? new Set>(), ); const allDirectSubgraphs = [...directSubgraphs, ...directPrivateSubgraphs]; if (allDirectSubgraphs.length === 0) return []; return [ ...allDirectSubgraphs, ...new Set( allDirectSubgraphs .map(subgraph => this.getSubgraphsConstructors(subgraph)) .flat(), ), ]; } private getGraphConstructorByKey(key: string): Constructable { if (this.keyToGraph.has(key)) return this.keyToGraph.get(key) as Constructable; const generator = this.keyToGenerator.get(key); if (!generator) throw new Error(`Attempted to resolve a graph by key "${key}" that is not registered. Did you forget to call Obsidian.registerGraph?`); const constructor = generator(); this.keyToGraph.set(key, constructor); return constructor as Constructable; } private has(Graph: Constructable, 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(Graph: Constructable): T { return this.constructorToInstance.get(Graph)!.values().next().value as T; } private getByInjectionToken(Graph: Constructable, 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, 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): boolean { return Reflect.getMetadata('isSingleton', Graph) ?? false; } private isBoundToReactLifecycle(Graph: Constructable): boolean { return Reflect.getMetadata('isLifecycleBound', Graph) ?? false; } private isComponentScopedLifecycleBound(Graph: Constructable): boolean { return Reflect.getMetadata('lifecycleScope', Graph) === 'component'; } private isCustomScopedLifecycleBound(Graph: Constructable): boolean { const scope = Reflect.getMetadata('lifecycleScope', Graph); return typeof scope === 'string' && scope !== 'component' && scope !== 'feature'; } 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); } this.invokeOnClearListeners(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); } this.clearGraphsRegisteredByKey(Graph); this.invokeOnClearListeners(graph); } private registerOnClearListener(graph: Graph, callback: () => void) { const listeners = this.onClearListeners.get(graph) ?? new Set(); listeners.add(callback); this.onClearListeners.set(graph, listeners); } private invokeOnClearListeners(graph: Graph) { const listeners = this.onClearListeners.get(graph); if (!listeners) return; listeners.forEach((listener) => listener()); this.onClearListeners.delete(graph); } private clearGraphsRegisteredByKey(Graph: Constructable) { [...this.keyToGraph.keys()] .map((key) => [key, this.keyToGraph.get(key)!] as [string, Constructable]) .filter(([_, $Graph]) => $Graph === Graph) .forEach(([key, _]) => { this.keyToGraph.delete(key); this.keyToGenerator.delete(key); }); } addGraphMiddleware(middleware: Middleware) { this.graphMiddlewares.add(middleware); } clearGraphMiddlewares() { this.graphMiddlewares.clear(); } clearAll() { this.instanceToConstructor.clear(); this.constructorToInstance.clear(); this.nameToInstance.clear(); this.injectionTokenToInstance.clear(); this.instanceToInjectionToken.clear(); this.keyToGenerator.clear(); this.keyToGraph.clear(); this.onClearListeners.clear(); } } const globalObject = getGlobal(); globalObject.graphRegistry = globalObject.graphRegistry || new GraphRegistry(); export default globalObject.graphRegistry as GraphRegistry;