2021-11-02 13:53:44 +01:00
|
|
|
import { ComponentNode, getCurrent } from "./component/component_node";
|
2021-10-26 14:51:57 +02:00
|
|
|
import { onWillUnmount } from "./component/lifecycle_hooks";
|
2020-11-26 16:45:25 +01:00
|
|
|
|
2021-11-02 13:53:44 +01:00
|
|
|
type Observer = ComponentNode | Function;
|
2021-10-26 14:51:57 +02:00
|
|
|
type Atom = any; // proxy linked to a unique observer and source
|
|
|
|
|
type Source = any; // trackable that is not an atom
|
2020-11-26 16:45:25 +01:00
|
|
|
|
2021-11-02 13:53:44 +01:00
|
|
|
const sourceAtoms: WeakMap<Source, Map<any, ObserverSet>> = new WeakMap();
|
|
|
|
|
const observerSourceAtom: WeakMap<Observer, Map<any, Atom>> = new WeakMap();
|
2020-11-26 16:45:25 +01:00
|
|
|
|
2021-10-26 14:51:57 +02:00
|
|
|
const SOURCE = Symbol("source");
|
|
|
|
|
const OBSERVER = Symbol("observer");
|
|
|
|
|
const KEYS = Symbol("keys");
|
2021-11-02 13:53:44 +01:00
|
|
|
const ROOT = Symbol("root");
|
2020-11-26 16:45:25 +01:00
|
|
|
|
2021-10-26 14:51:57 +02:00
|
|
|
export function atom(source: any, observer: Observer) {
|
2021-11-02 13:53:44 +01:00
|
|
|
if (isTrackable(source) && observerSourceAtom.has(observer)) {
|
2021-10-26 14:51:57 +02:00
|
|
|
source = source[SOURCE] || source;
|
2021-11-02 13:53:44 +01:00
|
|
|
const oldAtom = observerSourceAtom.get(observer)!.get(source);
|
2021-10-26 14:51:57 +02:00
|
|
|
if (oldAtom) {
|
|
|
|
|
return oldAtom;
|
|
|
|
|
}
|
2021-11-02 13:53:44 +01:00
|
|
|
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;
|
2020-11-26 16:45:25 +01:00
|
|
|
}
|
2021-10-26 14:51:57 +02:00
|
|
|
return source;
|
|
|
|
|
}
|
2020-11-26 16:45:25 +01:00
|
|
|
|
2021-10-26 14:51:57 +02:00
|
|
|
function createAtom(source: Source, observer: Observer): Atom {
|
2021-11-02 13:53:44 +01:00
|
|
|
const keys: Set<any> = new Set();
|
|
|
|
|
let self: Atom;
|
2021-10-26 14:51:57 +02:00
|
|
|
const newAtom: Atom = new Proxy(source as any, {
|
|
|
|
|
set(target: any, key: string, value: any): boolean {
|
|
|
|
|
if (!(key in target)) {
|
|
|
|
|
target[key] = value;
|
2021-11-02 13:53:44 +01:00
|
|
|
notify(sourceAtoms.get(source)!.get(ROOT)!);
|
2021-10-26 14:51:57 +02:00
|
|
|
return true;
|
|
|
|
|
}
|
|
|
|
|
const current = target[key];
|
|
|
|
|
if (current !== value) {
|
|
|
|
|
target[key] = value;
|
2021-11-02 13:53:44 +01:00
|
|
|
const observerSet = sourceAtoms.get(source)!.get(key);
|
|
|
|
|
if (observerSet) {
|
|
|
|
|
notify(observerSet);
|
|
|
|
|
}
|
2021-10-26 14:51:57 +02:00
|
|
|
}
|
2020-11-26 16:45:25 +01:00
|
|
|
return true;
|
2021-10-26 14:51:57 +02:00
|
|
|
},
|
|
|
|
|
deleteProperty(target: any, key: string): boolean {
|
|
|
|
|
if (key in target) {
|
|
|
|
|
delete target[key];
|
2021-11-02 13:53:44 +01:00
|
|
|
// 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);
|
|
|
|
|
}
|
2021-10-26 14:51:57 +02:00
|
|
|
}
|
|
|
|
|
return true;
|
|
|
|
|
},
|
|
|
|
|
get(target: any, key: any): any {
|
|
|
|
|
switch (key) {
|
|
|
|
|
case OBSERVER:
|
|
|
|
|
return observer;
|
|
|
|
|
case SOURCE:
|
|
|
|
|
return source;
|
|
|
|
|
case KEYS:
|
|
|
|
|
return keys;
|
|
|
|
|
default:
|
|
|
|
|
const value = target[key];
|
2021-11-02 13:53:44 +01:00
|
|
|
// 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);
|
|
|
|
|
}
|
|
|
|
|
//
|
2021-10-26 14:51:57 +02:00
|
|
|
return atom(value, observer);
|
|
|
|
|
}
|
|
|
|
|
},
|
|
|
|
|
});
|
2021-11-02 13:53:44 +01:00
|
|
|
self = newAtom;
|
2021-10-26 14:51:57 +02:00
|
|
|
return newAtom;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function isTrackable(value: any): boolean {
|
|
|
|
|
return (
|
|
|
|
|
value !== null &&
|
|
|
|
|
typeof value === "object" &&
|
|
|
|
|
!(value instanceof Date) &&
|
|
|
|
|
!(value instanceof Promise)
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
2021-11-02 13:53:44 +01:00
|
|
|
export function registerObserver(observer: Observer) {
|
|
|
|
|
if (!observerSourceAtom.get(observer)) {
|
|
|
|
|
observerSourceAtom.set(observer, new Map());
|
|
|
|
|
}
|
|
|
|
|
return unregisterObserver.bind(null, observer);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function unregisterObserver(observer: Observer) {
|
|
|
|
|
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);
|
2021-10-26 14:51:57 +02:00
|
|
|
}
|
2020-11-26 16:45:25 +01:00
|
|
|
}
|
2021-11-02 13:53:44 +01:00
|
|
|
observerSourceAtom.delete(observer);
|
2020-11-26 16:45:25 +01:00
|
|
|
}
|
|
|
|
|
|
2021-11-02 13:53:44 +01:00
|
|
|
export function useState(state: any): Atom {
|
|
|
|
|
if (!isTrackable(state)) {
|
|
|
|
|
throw new Error("Argument is not trackable");
|
2020-11-26 16:45:25 +01:00
|
|
|
}
|
2021-11-02 13:53:44 +01:00
|
|
|
const node = getCurrent()!;
|
|
|
|
|
const unregisterObserver = registerObserver(node);
|
|
|
|
|
onWillUnmount(() => unregisterObserver());
|
|
|
|
|
return atom(state, node);
|
2021-10-26 14:51:57 +02:00
|
|
|
}
|
|
|
|
|
|
2021-11-02 13:53:44 +01:00
|
|
|
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]();
|
|
|
|
|
}
|
2020-11-26 16:45:25 +01:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2021-11-02 13:53:44 +01:00
|
|
|
// Need to optimize this!
|
|
|
|
|
function isLessOrEqual(node1: ComponentNode, node2: ComponentNode) {
|
|
|
|
|
let current: any = node1;
|
|
|
|
|
if (current.level <= node2.level) {
|
|
|
|
|
return false;
|
2021-10-26 14:51:57 +02:00
|
|
|
}
|
2021-11-02 13:53:44 +01:00
|
|
|
do {
|
|
|
|
|
if (current === node2) {
|
|
|
|
|
return true;
|
|
|
|
|
}
|
|
|
|
|
current = current.parent;
|
|
|
|
|
} while (current);
|
|
|
|
|
return false;
|
2020-11-26 16:45:25 +01:00
|
|
|
}
|
|
|
|
|
|
2021-11-02 13:53:44 +01:00
|
|
|
// 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;
|
2021-10-26 14:51:57 +02:00
|
|
|
}
|
2020-11-26 16:45:25 +01:00
|
|
|
}
|
|
|
|
|
|
2021-11-02 13:53:44 +01:00
|
|
|
let toNotify: ObserverSet | null = null;
|
|
|
|
|
async function notify(observers: ObserverSet) {
|
|
|
|
|
if (toNotify) {
|
|
|
|
|
toNotify.union(observers);
|
|
|
|
|
return;
|
2020-11-26 16:45:25 +01:00
|
|
|
}
|
2021-11-02 13:53:44 +01:00
|
|
|
toNotify = new ObserverSet();
|
|
|
|
|
toNotify.union(observers);
|
|
|
|
|
await Promise.resolve();
|
|
|
|
|
toNotify.notify();
|
|
|
|
|
toNotify = null;
|
2020-11-26 16:45:25 +01:00
|
|
|
}
|