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
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
|
odoo.define('point_of_sale.ClassRegistry', function (require) {
'use strict';
/**
* **Usage:**
* ```
* const Registry = new ClassRegistry();
*
* class A {}
* Registry.add(A);
*
* const AExt1 = A => class extends A {}
* Registry.extend(A, AExt1)
*
* const B = A => class extends A {}
* Registry.addByExtending(B, A)
*
* const AExt2 = A => class extends A {}
* Registry.extend(A, AExt2)
*
* Registry.get(A)
* // above returns: AExt2 -> AExt1 -> A
* // Basically, 'A' in the registry points to
* // the inheritance chain above.
*
* Registry.get(B)
* // above returns: B -> AExt2 -> AExt1 -> A
* // Even though B extends A before applying all
* // the extensions of A, when getting it from the
* // registry, the return points to a class with
* // inheritance chain that includes all the extensions
* // of 'A'.
*
* Registry.freeze()
* // Example 'B' above is lazy. Basically, it is only
* // computed when we call `get` from the registry.
* // If we know that no more dynamic inheritances will happen,
* // we can freeze the registry and cache the final form
* // of each class in the registry.
* ```
*
* IMPROVEMENT:
* * So far, mixin can be accomplished by creating a method
* the takes a class and returns a class expression. This is then
* used before the extends keyword like so:
*
* ```js
* class A {}
* Registry.add(A)
* const Mixin = x => class extends x {}
* // apply mixin
* // |
* // v
* const B = x => class extends Mixin(x) {}
* Registry.addByExtending(B, A)
* ```
*
* In the example, `|B| => B -> Mixin -> A`, and this is pretty convenient
* already. However, this can still be improved since classes are only
* compiled after `Registry.freeze()`. Perhaps, we can make the
* Mixins extensible as well, such as so:
*
* ```
* class A {}
* Registry.add(A)
* const Mixin = x => class extends x {}
* Registry.add(Mixin)
* const OtherMixin = x => class extends x {}
* Registry.add(OtherMixin)
* const B = x => class extends x {}
* Registry.addByExtending(B, A, [Mixin, OtherMixin])
* const ExtendMixin = x => class extends x {}
* Registry.extend(Mixin, ExtendMixin)
* ```
*
* In the above, after `Registry.freeze()`,
* `|B| => B -> OtherMixin -> ExtendMixin -> Mixin -> A`
*/
class ClassRegistry {
constructor() {
// base name map
this.baseNameMap = {};
// Object that maps `baseClass` to the class implementation extended in-place.
this.includedMap = new Map();
// Object that maps `baseClassCB` to the array of callbacks to generate the extended class.
this.extendedCBMap = new Map();
// Object that maps `baseClassCB` extended class to the `baseClass` of its super in the includedMap.
this.extendedSuperMap = new Map();
// For faster access, we can `freeze` the registry so that instead of dynamically generating
// the extended classes, it is taken from the cache instead.
this.cache = new Map();
}
/**
* Add a new class in the Registry.
* @param {Function} baseClass `class`
*/
add(baseClass) {
this.includedMap.set(baseClass, []);
this.baseNameMap[baseClass.name] = baseClass;
}
/**
* Add a new class in the Registry based on other class
* in the registry.
* @param {Function} baseClassCB `class -> class`
* @param {Function} base `class | class -> class`
*/
addByExtending(baseClassCB, base) {
this.extendedCBMap.set(baseClassCB, [baseClassCB]);
this.extendedSuperMap.set(baseClassCB, base);
this.baseNameMap[baseClassCB.name] = baseClassCB;
}
/**
* Extend in-place a class in the registry. E.g.
* ```
* // Using the following notation:
* // * |A| - compiled class in the registry
* // * A - class or an extension callback
* // * |A| => A2 -> A1 -> A
* // - the above means, compiled class A
* // points to the class inheritance derived from
* // A2(A1(A))
*
* class A {};
* Registry.add(A);
* // |A| => A
*
* let A1 = x => class extends x {};
* Registry.extend(A, A1);
* // |A| => A1 -> A
*
* let B = x => class extends x {};
* Registry.addByExtending(B, A);
* // |B| => B -> |A|
* // |B| => B -> A1 -> A
*
* let B1 = x => class extends x {};
* Registry.extend(B, B1);
* // |B| => B1 -> B -> |A|
*
* let C = x => class extends x {};
* Registry.addByExtending(C, B);
* // |C| => C -> |B|
*
* let B2 = x => class extends x {};
* Registry.extend(B, B2);
* // |B| => B2 -> B1 -> B -> |A|
*
* // Overall:
* // |A| => A1 -> A
* // |B| => B2 -> B1 -> B -> A1 -> A
* // |C| => C -> B2 -> B1 -> B -> A1 -> A
* ```
* @param {Function} base `class | class -> class`
* @param {Function} extensionCB `class -> class`
*/
extend(base, extensionCB) {
if (typeof base === 'string') {
base = this.baseNameMap[base];
}
let extensionArray;
if (this.includedMap.get(base)) {
extensionArray = this.includedMap.get(base);
} else if (this.extendedCBMap.get(base)) {
extensionArray = this.extendedCBMap.get(base);
} else {
throw new Error(
`'${base.name}' is not in the Registry. Add it to Registry before extending.`
);
}
extensionArray.push(extensionCB);
const locOfNewExtension = extensionArray.length - 1;
const self = this;
const oldCompiled = this.isFrozen ? this.cache.get(base) : null;
return {
remove() {
extensionArray.splice(locOfNewExtension, 1);
self._recompute(base, oldCompiled);
},
compile() {
self._recompute(base);
}
};
}
_compile(base) {
let res;
if (this.includedMap.has(base)) {
res = this.includedMap.get(base).reduce((acc, ext) => ext(acc), base);
} else {
const superClass = this.extendedSuperMap.get(base);
const extensionCBs = this.extendedCBMap.get(base);
res = extensionCBs.reduce((acc, ext) => ext(acc), this._compile(superClass));
}
Object.defineProperty(res, 'name', { value: base.name });
return res;
}
/**
* Return the compiled class (containing all the extensions) of the base class.
* @param {Function} base `class | class -> class` function used in adding the class
*/
get(base) {
if (typeof base === 'string') {
base = this.baseNameMap[base];
}
if (this.isFrozen) {
return this.cache.get(base);
}
return this._compile(base);
}
/**
* Uses the callbacks registered in the registry to compile the classes.
*/
freeze() {
// Step: Compile the `included classes`.
for (let [baseClass, extensionCBs] of this.includedMap.entries()) {
const compiled = extensionCBs.reduce((acc, ext) => ext(acc), baseClass);
this.cache.set(baseClass, compiled);
}
// Step: Compile the `extended classes` based on `included classes`.
// Also gather those the are based on `extended classes`.
const remaining = [];
for (let [baseClassCB, extensionCBArray] of this.extendedCBMap.entries()) {
const compiled = this.cache.get(this.extendedSuperMap.get(baseClassCB));
if (!compiled) {
remaining.push([baseClassCB, extensionCBArray]);
continue;
}
const extendedClass = extensionCBArray.reduce(
(acc, extensionCB) => extensionCB(acc),
compiled
);
this.cache.set(baseClassCB, extendedClass);
}
// Step: Compile the `extended classes` based on `extended classes`.
for (let [baseClassCB, extensionCBArray] of remaining) {
const compiled = this.cache.get(this.extendedSuperMap.get(baseClassCB));
const extendedClass = extensionCBArray.reduce(
(acc, extensionCB) => extensionCB(acc),
compiled
);
this.cache.set(baseClassCB, extendedClass);
}
// Step: Set the name of the compiled classess
for (let [base, compiledClass] of this.cache.entries()) {
Object.defineProperty(compiledClass, 'name', { value: base.name });
}
// Step: Set the flag to true;
this.isFrozen = true;
}
_recompute(base, old) {
if (typeof base === 'string') {
base = this.baseNameMap[base];
}
return old ? old : this._compile(base);
}
}
return ClassRegistry;
});
|