1 |
|
2 |
|
3 |
|
4 |
|
5 |
|
6 | import {Binding, BindingEventListener, BindingTag} from './binding';
|
7 | import {BindingFilter, filterByTag} from './binding-filter';
|
8 | import {Context} from './context';
|
9 | import {ContextEventListener} from './context-event';
|
10 | import {BoundValue} from './value-promise';
|
11 |
|
12 |
|
13 |
|
14 |
|
15 | export class ContextTagIndexer {
|
16 | |
17 |
|
18 |
|
19 | readonly bindingsIndexedByTag: Map<string, Set<Readonly<Binding<unknown>>>> =
|
20 | new Map();
|
21 |
|
22 | |
23 |
|
24 |
|
25 | private bindingEventListener: BindingEventListener;
|
26 |
|
27 | |
28 |
|
29 |
|
30 | private tagIndexListener: ContextEventListener;
|
31 |
|
32 | constructor(protected readonly context: Context) {
|
33 | this.setupTagIndexForBindings();
|
34 | }
|
35 |
|
36 | |
37 |
|
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 |
|
62 |
|
63 |
|
64 | private removeTagIndexForBinding(binding: Readonly<Binding<unknown>>) {
|
65 | for (const [, bindings] of this.bindingsIndexedByTag) {
|
66 | bindings.delete(binding);
|
67 | }
|
68 | }
|
69 |
|
70 | |
71 |
|
72 |
|
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 |
|
88 |
|
89 |
|
90 | findByTagIndex<ValueType = BoundValue>(
|
91 | tag: BindingTag | RegExp,
|
92 | ): Readonly<Binding<ValueType>>[] {
|
93 | let tagNames: string[];
|
94 |
|
95 | let union = false;
|
96 | if (tag instanceof RegExp) {
|
97 |
|
98 | union = true;
|
99 |
|
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;
|
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;
|
121 | if (bindings == null) {
|
122 |
|
123 | bindings = matched;
|
124 | } else {
|
125 | if (union) {
|
126 | matched.forEach(b => bindings?.add(b));
|
127 | } else {
|
128 |
|
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 | }
|