summaryrefslogtreecommitdiff
path: root/addons/point_of_sale/static/src/js/ClassRegistry.js
diff options
context:
space:
mode:
authorstephanchrst <stephanchrst@gmail.com>2022-05-10 21:51:50 +0700
committerstephanchrst <stephanchrst@gmail.com>2022-05-10 21:51:50 +0700
commit3751379f1e9a4c215fb6eb898b4ccc67659b9ace (patch)
treea44932296ef4a9b71d5f010906253d8c53727726 /addons/point_of_sale/static/src/js/ClassRegistry.js
parent0a15094050bfde69a06d6eff798e9a8ddf2b8c21 (diff)
initial commit 2
Diffstat (limited to 'addons/point_of_sale/static/src/js/ClassRegistry.js')
-rw-r--r--addons/point_of_sale/static/src/js/ClassRegistry.js262
1 files changed, 262 insertions, 0 deletions
diff --git a/addons/point_of_sale/static/src/js/ClassRegistry.js b/addons/point_of_sale/static/src/js/ClassRegistry.js
new file mode 100644
index 00000000..eed07fe3
--- /dev/null
+++ b/addons/point_of_sale/static/src/js/ClassRegistry.js
@@ -0,0 +1,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;
+});