-
Notifications
You must be signed in to change notification settings - Fork 3
/
component.ts
145 lines (133 loc) · 4.13 KB
/
component.ts
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
import { Collection, CollectionListener } from './collection';
import { ComponentClass } from './types';
/**
* The component interface, every component has to implement.
*
* If you want your system to treat different Components the same way,
* you may define a static string variable named `type` in your components.
*/
// eslint-disable-next-line @typescript-eslint/no-explicit-any
export interface Component extends Record<string, any> {
/**
* An optional id for the component.
*/
id?: string;
/**
* An optional type for the component.
*/
type?: string;
}
/**
* A collection for components.
* Supports accessing components by their class.
*
*/
export class ComponentCollection<C extends Component = Component>
extends Collection<C>
implements CollectionListener<C>
{
/**
* Internal map for faster component access, by class or type.
*/
protected cache = new Map<ComponentClass<C> | string, readonly C[]>();
/**
* Internal state for updating the components access memory.
*
*/
protected dirty = new Map<ComponentClass<C> | string, boolean>();
constructor(initial: C[] = []) {
super(initial);
this.addListener(this, true);
}
/**
* @inheritdoc
* Update the internal cache.
*/
onAdded(...elements: C[]): void {
this.markForCacheUpdate(...elements);
}
/**
* @inheritdoc
* Update the internal cache.
*/
onRemoved(...elements: C[]): void {
this.markForCacheUpdate(...elements);
}
/**
* @inheritdoc
* Update the internal cache.
*/
onCleared() {
this.dirty.clear();
this.cache.clear();
}
/**
* Searches for the first component matching the given class or type.
*
* @param classOrType The class or type a component has to match.
* @return The found component or `null`.
*/
get<T extends C>(classOrType: ComponentClass<T> | string): T {
return this.getAll(classOrType)[0];
}
/**
* Searches for the all components matching the given class or type.
*
* @param classOrType The class or type components have to match.
* @return A list of all components matching the given class.
*/
getAll<T extends C>(classOrType: ComponentClass<T> | string): readonly T[] {
if (this.dirty.get(classOrType)) this.updateCache(classOrType);
if (this.cache.has(classOrType)) return this.cache.get(classOrType) as T[];
this.updateCache(classOrType);
return this.cache.get(classOrType) as T[];
}
/**
* Updates the cache for the given class or type.
*
* @param classOrType The class or type to update the cache for.
*/
protected updateCache(classOrType: ComponentClass<C> | string): void {
const keys = this.cache.keys();
const type = typeof classOrType === 'string' ? classOrType : classOrType.type;
const filtered = this.filter(element => {
const clazz = element.constructor as ComponentClass<C>;
const typeVal = element.type ?? clazz.type;
return type && typeVal ? type === typeVal : clazz === classOrType;
});
if (typeof classOrType !== 'string' && classOrType.type) {
this.cache.set(classOrType.type, filtered);
this.dirty.delete(classOrType.type);
} else if (typeof classOrType === 'string') {
for (const key of keys) {
if (typeof key !== 'string' && key.type === classOrType) {
this.cache.set(key, filtered);
this.dirty.delete(key);
}
}
}
this.cache.set(classOrType, filtered);
this.dirty.delete(classOrType);
}
/**
* Marks the classes and types of the given elements as dirty,
* so their cache gets updated on the next request.
*
* @param elements
*
*/
protected markForCacheUpdate(...elements: C[]): void {
const keys = this.cache.keys();
elements.forEach(element => {
const clazz = element.constructor as ComponentClass<C>;
const classOrType = element.type ?? clazz.type ?? clazz;
if (this.dirty.get(classOrType)) return;
if (typeof classOrType === 'string') {
for (const key of keys) {
if (typeof key !== 'string' && key.type === classOrType) this.dirty.set(key, true);
}
}
this.dirty.set(classOrType, true);
});
}
}