[IMP] reactivity: Context replacement

Aim to replace the abstraction "Context" from OWL 1 with the new primitives
"atom" and "useState":

- notification is done only after a batch of modifications.
- observers are notified at most once for a batch.
- an observer of type component is notified (and rerendered)
  only if it does not have an ancestor that has to be notified for the
  same batch of operations (anywhere in the web of references!).
- notification of components is done on all levels "simultaneously".

Co-authored-by: Aaron Bohy <aab@odoo.com>
Co-authored-by: Géry Debongnie <ged@odoo.com>
Co-authored-by: Mathieu Duckerts-Antoine <dam@odoo.com>
This commit is contained in:
Mathieu Duckerts-Antoine
2021-11-02 13:53:44 +01:00
committed by Géry Debongnie
parent d47dcf6be2
commit d60a5a414e
5 changed files with 1237 additions and 135 deletions
+149 -70
View File
@@ -1,52 +1,67 @@
import { getCurrent } from "./component/component_node";
import { ComponentNode, getCurrent } from "./component/component_node";
import { onWillUnmount } from "./component/lifecycle_hooks";
type Observer = ComponentNode | Function;
type Atom = any; // proxy linked to a unique observer and source
type Source = any; // trackable that is not an atom
type Observer = () => void;
type Keys = Set<any>;
const sourceAtoms: WeakMap<Source, Set<Atom>> = new WeakMap();
const observerAtoms: WeakMap<Observer, Set<Atom>> = new WeakMap();
const sourceAtoms: WeakMap<Source, Map<any, ObserverSet>> = new WeakMap();
const observerSourceAtom: WeakMap<Observer, Map<any, Atom>> = new WeakMap();
const SOURCE = Symbol("source");
const OBSERVER = Symbol("observer");
const KEYS = Symbol("keys");
const ROOT = Symbol("root");
export function atom(source: any, observer: Observer) {
if (isTrackable(source) && observerAtoms.has(observer)) {
if (isTrackable(source) && observerSourceAtom.has(observer)) {
source = source[SOURCE] || source;
const oldAtom = getObserverSourceAtom(observer, source);
const oldAtom = observerSourceAtom.get(observer)!.get(source);
if (oldAtom) {
return oldAtom;
}
registerSource(source);
return createAtom(source, observer);
if (!sourceAtoms.get(source)) {
sourceAtoms.set(source, new Map([[ROOT, new ObserverSet()]]));
}
const newAtom = createAtom(source, observer);
observerSourceAtom.get(observer)!.set(source, newAtom);
sourceAtoms.get(source)!.get(ROOT)!.add(newAtom);
return newAtom;
}
return source;
}
function createAtom(source: Source, observer: Observer): Atom {
const keys: Keys = new Set();
const keys: Set<any> = new Set();
let self: Atom;
const newAtom: Atom = new Proxy(source as any, {
set(target: any, key: string, value: any): boolean {
if (!(key in target)) {
target[key] = value;
notifySourceObservers(source);
notify(sourceAtoms.get(source)!.get(ROOT)!);
return true;
}
const current = target[key];
if (current !== value) {
target[key] = value;
notifySourceKeyOBservers(source, key);
const observerSet = sourceAtoms.get(source)!.get(key);
if (observerSet) {
notify(observerSet);
}
}
return true;
},
deleteProperty(target: any, key: string): boolean {
if (key in target) {
delete target[key];
notifySourceObservers(source);
deleteKeyFromKeys(source, key);
// notify source observers
notify(sourceAtoms.get(source)!.get(ROOT)!);
const atoms = sourceAtoms.get(source)!;
if (atoms.has(key)) {
// clear source-key observers
atoms.get(key)!.deleteKey(key);
atoms.delete(key);
}
}
return true;
},
@@ -60,39 +75,24 @@ function createAtom(source: Source, observer: Observer): Atom {
return keys;
default:
const value = target[key];
keys.add(key);
// register observer to source-key
if (!keys.has(key) && observerSourceAtom.has(observer)) {
const atoms = sourceAtoms.get(source)!;
if (!atoms.has(key)) {
atoms.set(key, new ObserverSet());
}
atoms.get(key)!.add(self);
keys.add(key);
}
//
return atom(value, observer);
}
},
});
getObserverAtoms(observer).add(newAtom);
getSourceAtoms(source).add(newAtom);
self = newAtom;
return newAtom;
}
function deleteKeyFromKeys(source: Source, key: any) {
for (const atom of getSourceAtoms(source)) {
atom[KEYS].delete(key);
}
}
function getObserverAtoms(observer: Observer): Set<Atom> {
return observerAtoms.get(observer)!;
}
function getObserverSourceAtom(observer: Observer, source: Source): Atom | null {
for (const atom of getObserverAtoms(observer)) {
if (atom[SOURCE] === source) {
return atom;
}
}
return null;
}
function getSourceAtoms(source: Source): Set<Atom> {
return sourceAtoms.get(source)!;
}
function isTrackable(value: any): boolean {
return (
value !== null &&
@@ -102,40 +102,22 @@ function isTrackable(value: any): boolean {
);
}
function notifySourceKeyOBservers(source: Source, key: any) {
for (const atom of getSourceAtoms(source)) {
if (atom[KEYS].has(key)) {
atom[OBSERVER]();
}
}
}
function notifySourceObservers(source: Source) {
for (const atom of getSourceAtoms(source)) {
atom[OBSERVER]();
}
}
export function registerObserver(observer: Observer) {
if (!observerAtoms.get(observer)) {
observerAtoms.set(observer, new Set());
if (!observerSourceAtom.get(observer)) {
observerSourceAtom.set(observer, new Map());
}
return unregisterObserver.bind(null, observer);
}
function registerSource(source: Source) {
if (!sourceAtoms.get(source)) {
sourceAtoms.set(source, new Set());
}
}
function unregisterObserver(observer: Observer) {
for (const atom of getObserverAtoms(observer)) {
const source = atom[SOURCE];
const sourceAtoms = getSourceAtoms(source);
sourceAtoms.delete(atom);
for (const [source, atom] of observerSourceAtom.get(observer)!) {
const atoms = sourceAtoms.get(source)!;
atoms.get(ROOT)!.delete(atom);
for (const key of atom[KEYS]) {
atoms.get(key)!.delete(atom);
}
}
observerAtoms.delete(observer);
observerSourceAtom.delete(observer);
}
export function useState(state: any): Atom {
@@ -143,8 +125,105 @@ export function useState(state: any): Atom {
throw new Error("Argument is not trackable");
}
const node = getCurrent()!;
const observer = () => node.render();
const unregisterObserver = registerObserver(observer);
const unregisterObserver = registerObserver(node);
onWillUnmount(() => unregisterObserver());
return atom(state, observer);
return atom(state, node);
}
class ObserverSet {
nodeAntichain: Antichain = new Antichain();
callbackSet: Set<Atom> = new Set();
add(atom: Atom) {
if (atom[OBSERVER] instanceof ComponentNode) {
this.nodeAntichain.add(atom);
} else {
this.callbackSet.add(atom);
}
return this;
}
delete(atom: Atom) {
if (atom[OBSERVER] instanceof ComponentNode) {
return this.nodeAntichain.delete(atom);
} else {
return this.callbackSet.delete(atom);
}
}
deleteKey(key: any) {
for (const atom of this.nodeAntichain) {
atom[KEYS].delete(key);
}
for (const atom of this.callbackSet) {
atom[KEYS].delete(key);
}
}
union(other: ObserverSet) {
for (const atom of other.nodeAntichain) {
this.nodeAntichain.add(atom);
}
for (const callback of other.callbackSet) {
this.callbackSet.add(callback);
}
}
notify() {
for (const atom of this.nodeAntichain) {
atom[OBSERVER].render();
}
for (const atom of this.callbackSet) {
atom[OBSERVER]();
}
}
}
// Need to optimize this!
function isLessOrEqual(node1: ComponentNode, node2: ComponentNode) {
let current: any = node1;
if (current.level <= node2.level) {
return false;
}
do {
if (current === node2) {
return true;
}
current = current.parent;
} while (current);
return false;
}
// set of atoms linked to observers of type ComponentNode
class Antichain extends Set<Atom> {
level?: number;
add(atom: Atom) {
const node = atom[OBSERVER];
if (this.level === node.level || this.level === undefined) {
super.add(atom);
this.level = node.level;
return this;
}
let willAdd = false;
for (const atom2 of this) {
const node2 = atom2[OBSERVER];
if (!willAdd && isLessOrEqual(node, node2)) {
return this;
} else if (isLessOrEqual(node2, node)) {
super.delete(atom2);
willAdd = true;
}
}
super.add(atom);
this.level = NaN;
return this;
}
}
let toNotify: ObserverSet | null = null;
async function notify(observers: ObserverSet) {
if (toNotify) {
toNotify.union(observers);
return;
}
toNotify = new ObserverSet();
toNotify.union(observers);
await Promise.resolve();
toNotify.notify();
toNotify = null;
}