UNPKG

4.71 kBPlain TextView Raw
1// Copyright IBM Corp. and LoopBack contributors 2020. All Rights Reserved.
2// Node module: @loopback/context
3// This file is licensed under the MIT License.
4// License text available at https://opensource.org/licenses/MIT
5
6import {Binding, BindingEventListener, BindingTag} from './binding';
7import {BindingFilter, filterByTag} from './binding-filter';
8import {Context} from './context';
9import {ContextEventListener} from './context-event';
10import {BoundValue} from './value-promise';
11
12/**
13 * Indexer for context bindings by tag
14 */
15export class ContextTagIndexer {
16 /**
17 * Index for bindings by tag names
18 */
19 readonly bindingsIndexedByTag: Map<string, Set<Readonly<Binding<unknown>>>> =
20 new Map();
21
22 /**
23 * A listener for binding events
24 */
25 private bindingEventListener: BindingEventListener;
26
27 /**
28 * A listener to maintain tag index for bindings
29 */
30 private tagIndexListener: ContextEventListener;
31
32 constructor(protected readonly context: Context) {
33 this.setupTagIndexForBindings();
34 }
35
36 /**
37 * Set up context/binding listeners and refresh index for bindings by tag
38 */
39 private setupTagIndexForBindings() {
40 this.bindingEventListener = ({binding, operation}) => {
41 if (operation === 'tag') {
42 this.updateTagIndexForBinding(binding);
43 }
44 };
45 this.tagIndexListener = event => {
46 const {binding, type} = event;
47 if (event.context !== this.context) return;
48 if (type === 'bind') {
49 this.updateTagIndexForBinding(binding);
50 binding.on('changed', this.bindingEventListener);
51 } else if (type === 'unbind') {
52 this.removeTagIndexForBinding(binding);
53 binding.removeListener('changed', this.bindingEventListener);
54 }
55 };
56 this.context.on('bind', this.tagIndexListener);
57 this.context.on('unbind', this.tagIndexListener);
58 }
59
60 /**
61 * Remove tag index for the given binding
62 * @param binding - Binding object
63 */
64 private removeTagIndexForBinding(binding: Readonly<Binding<unknown>>) {
65 for (const [, bindings] of this.bindingsIndexedByTag) {
66 bindings.delete(binding);
67 }
68 }
69
70 /**
71 * Update tag index for the given binding
72 * @param binding - Binding object
73 */
74 private updateTagIndexForBinding(binding: Readonly<Binding<unknown>>) {
75 this.removeTagIndexForBinding(binding);
76 for (const tag of binding.tagNames) {
77 let bindings = this.bindingsIndexedByTag.get(tag);
78 if (bindings == null) {
79 bindings = new Set();
80 this.bindingsIndexedByTag.set(tag, bindings);
81 }
82 bindings.add(binding);
83 }
84 }
85
86 /**
87 * Find bindings by tag leveraging indexes
88 * @param tag - Tag name pattern or name/value pairs
89 */
90 findByTagIndex<ValueType = BoundValue>(
91 tag: BindingTag | RegExp,
92 ): Readonly<Binding<ValueType>>[] {
93 let tagNames: string[];
94 // A flag to control if a union of matched bindings should be created
95 let union = false;
96 if (tag instanceof RegExp) {
97 // For wildcard/regexp, a union of matched bindings is desired
98 union = true;
99 // Find all matching tag names
100 tagNames = [];
101 for (const t of this.bindingsIndexedByTag.keys()) {
102 if (tag.test(t)) {
103 tagNames.push(t);
104 }
105 }
106 } else if (typeof tag === 'string') {
107 tagNames = [tag];
108 } else {
109 tagNames = Object.keys(tag);
110 }
111 let filter: BindingFilter | undefined;
112 let bindings: Set<Readonly<Binding<ValueType>>> | undefined;
113 for (const t of tagNames) {
114 const bindingsByTag = this.bindingsIndexedByTag.get(t);
115 if (bindingsByTag == null) break; // One of the tags is not found
116 filter = filter ?? filterByTag(tag);
117 const matched = new Set(Array.from(bindingsByTag).filter(filter)) as Set<
118 Readonly<Binding<ValueType>>
119 >;
120 if (!union && matched.size === 0) break; // One of the tag name/value is not found
121 if (bindings == null) {
122 // First set of bindings matching the tag
123 bindings = matched;
124 } else {
125 if (union) {
126 matched.forEach(b => bindings?.add(b));
127 } else {
128 // Now need to find intersected bindings against visited tags
129 const intersection = new Set<Readonly<Binding<ValueType>>>();
130 bindings.forEach(b => {
131 if (matched.has(b)) {
132 intersection.add(b);
133 }
134 });
135 bindings = intersection;
136 }
137 if (!union && bindings.size === 0) break;
138 }
139 }
140 return bindings == null ? [] : Array.from(bindings);
141 }
142
143 close() {
144 this.context.removeListener('bind', this.tagIndexListener);
145 this.context.removeListener('unbind', this.tagIndexListener);
146 }
147}