UNPKG

contexify

Version:

A TypeScript library providing a powerful dependency injection container with context-based IoC capabilities, inspired by LoopBack's Context system.

147 lines (137 loc) 4.58 kB
import type { Binding, BindingEventListener, BindingTag, } from '../binding/binding.js'; import { type BindingFilter, filterByTag } from '../binding/binding-filter.js'; import type { BoundValue } from '../utils/value-promise.js'; import type { Context } from './context.js'; import type { ContextEventListener } from './context-event.js'; /** * Indexer for context bindings by tag */ export class ContextTagIndexer { /** * Index for bindings by tag names */ readonly bindingsIndexedByTag: Map<string, Set<Readonly<Binding<unknown>>>> = new Map(); /** * A listener for binding events */ private bindingEventListener!: BindingEventListener; /** * A listener to maintain tag index for bindings */ private tagIndexListener!: ContextEventListener; constructor(protected readonly context: Context) { this.setupTagIndexForBindings(); } /** * Set up context/binding listeners and refresh index for bindings by tag */ private setupTagIndexForBindings() { this.bindingEventListener = ({ binding, operation }) => { if (operation === 'tag') { this.updateTagIndexForBinding(binding); } }; this.tagIndexListener = (event) => { const { binding, type } = event; if (event.context !== this.context) return; if (type === 'bind') { this.updateTagIndexForBinding(binding); binding.on('changed', this.bindingEventListener); } else if (type === 'unbind') { this.removeTagIndexForBinding(binding); binding.removeListener('changed', this.bindingEventListener); } }; this.context.on('bind', this.tagIndexListener); this.context.on('unbind', this.tagIndexListener); } /** * Remove tag index for the given binding * @param binding - Binding object */ private removeTagIndexForBinding(binding: Readonly<Binding<unknown>>) { for (const [, bindings] of this.bindingsIndexedByTag) { bindings.delete(binding); } } /** * Update tag index for the given binding * @param binding - Binding object */ private updateTagIndexForBinding(binding: Readonly<Binding<unknown>>) { this.removeTagIndexForBinding(binding); for (const tag of binding.tagNames) { let bindings = this.bindingsIndexedByTag.get(tag); if (bindings == null) { bindings = new Set(); this.bindingsIndexedByTag.set(tag, bindings); } bindings.add(binding); } } /** * Find bindings by tag leveraging indexes * @param tag - Tag name pattern or name/value pairs */ findByTagIndex<ValueType = BoundValue>( tag: BindingTag | RegExp ): Readonly<Binding<ValueType>>[] { let tagNames: string[]; // A flag to control if a union of matched bindings should be created let union = false; if (tag instanceof RegExp) { // For wildcard/regexp, a union of matched bindings is desired union = true; // Find all matching tag names tagNames = []; for (const t of this.bindingsIndexedByTag.keys()) { if (tag.test(t)) { tagNames.push(t); } } } else if (typeof tag === 'string') { tagNames = [tag]; } else { tagNames = Object.keys(tag); } let filter: BindingFilter | undefined; let bindings: Set<Readonly<Binding<ValueType>>> | undefined; for (const t of tagNames) { const bindingsByTag = this.bindingsIndexedByTag.get(t); if (bindingsByTag == null) break; // One of the tags is not found filter = filter ?? filterByTag(tag); const matched = new Set(Array.from(bindingsByTag).filter(filter)) as Set< Readonly<Binding<ValueType>> >; if (!union && matched.size === 0) break; // One of the tag name/value is not found if (bindings == null) { // First set of bindings matching the tag bindings = matched; } else { if (union) { matched.forEach((b) => bindings?.add(b)); } else { // Now need to find intersected bindings against visited tags const intersection = new Set<Readonly<Binding<ValueType>>>(); bindings.forEach((b) => { if (matched.has(b)) { intersection.add(b); } }); bindings = intersection; } if (!union && bindings.size === 0) break; } } return bindings == null ? [] : Array.from(bindings); } close() { this.context.removeListener('bind', this.tagIndexListener); this.context.removeListener('unbind', this.tagIndexListener); } }