Home Reference Source

scripts/core/safeobservable.js

/*
*
* Kind of wish JS had weak references.
*
* This little library tries to do something similar.
*
* The safe way to do this is probably to pass event messges through the object tree,
* instead of having objects store direct messages (or the weird analog I'm doing here)
* to each other directly.
*
* Though in that case most event "callbacks" would do nothing more then pass the event along to their children.
*
* */
let idgen = 0;

//you needn't subclass this directly,
//simply implementing its interface is enough
export class AbstractObservable {
  //note that child classes inherit event types from their parents
  static observeDefine() {return {
    events : {
      "mousedown" : MouseEvent,
      "some_integer" : IntProperty
    }
  }}

  stillAlive() {
    //returns if we're still alive, note that listeners can implement this too
    throw new Error("implement me");
  }
}

function _valid(obj) {
  return obj && (typeof obj === "object" || typeof obj === "function");
}

export class ObserveManger {
  constructor() {
    this.subscriberMap = new Map();
    this.subscribeeMap = new Map();
    this.idmap = new WeakMap();
  }

  getId(obj) {
    if (!this.idmap.has(obj)) {
      this.idmap.set(obj, idgen++);
    }

    return this.idmap.get(obj);
  }

  _getEvents(owner) {
    let keys = new Set();

    let p = owner;
    while (p) {
      if (p.observeDefine) {
        let def = p.observeDefine();

        for (let item of def.events) {
          keys.add(item);
        }
      }
      p = p.prototype;
    }

    return keys;
  }

  dispatch(owner, type, data) {
    if (!_valid(owner)) {
      throw new Error("invalid argument to ObserveManger.dispathc");
    }

    let oid = this.getId(owner);
    let list = this.subscribeeMap.get(oid);

    for (let i=0; i<list.length; i++) {
      let item = list[i];
      let cid = item.id, cb = item.callback, isValid = item.isValid;

      if (isValid && !isValid()) {
        if (this._unsubscribe(oid, type, cid, cb)) {
          i--;
        }

        continue;
      }

      try {
        cb(data);
      } catch (error) {
        util.print_stack(error);
        console.log("event dispatch error for " + type);
      }
    }
  }

  update() {
    for (let cid of this.subscriberMap.keys()) {
      let list = this.subscriberMap.get(cid);

      for (let item of list) {
        let bad = false;
        let oid = item.id;

        try {
          bad = item.isValid && !item.isValid();
        } catch (error) {
          bad = true;
          console.log("error in event callback");
        }

        if (bad) {
          this._unsubscribe(oid, item.type, cid, item.callback);
        }
      }
    }

    for (let oid of this.subscribeeMap.keys()) {
      let list = this.subscriberMap.get(oid);

      for (let item of list) {
        let bad = false;
        let cid = item.id;

        try {
          bad = item.isValid && !item.isValid();
        } catch (error) {
          bad = true;
          console.log("error in event callback");
        }

        if (bad) {
          this._unsubscribe(oid, item.type, cid, item.callback);
        }
      }
    }
  }

  subscribe(owner, type, child, callback, customIsValid=undefined) {
    if (!_valid(owner) || !_valid(child)) {
      throw new Error("invalid arguments to ObserveManager.subscribe");
    }

    if (!owner.constructor.observeDefine) {
      throw new Error("owner is not an observable; no observeDefine");
    }

    let validkeys = this._getEvents(owner);
    if (!validkeys.has(type)) {
      throw new Error("unknown event type " + type);
    }

    let oid = this.getId(owner);
    let cid = this.getId(child);
    let cbid = thi.getId(callback);

    if (!(cid in this.subscriberMap)) {
      this.subscriberMap.set(cid, []);
    }
    if (!(oid in this.subscribeeMap)) {
      this.subscribeeMap.set(oid, []);
    }

    let ref;
    if (typeof child.isValid === "function") {
      ref = child.isValid;
      if (ref === child.prototype.isValid) {
        ref = ref.bind(child);
      }
    }

    if (customIsValid && ref) {
      let ref2 = ref;
      ref = () => {
        return ref2 && customIsValid();
      }
    } else if (customIsValid) {
      ref = customIsValid;
    }

    this.subscriberMap.get(cid).push({
      id : oid,
      type : type,
      isValid : ref,
      callback : callback});


    ref = undefined;
    if (owner.isValid) {
      ref = owner.isValid;

      if (ref === owner.prototype.isValid) {
        ref = ref.bind(owner);
      }
    }

    this.subscribeeMap.get(oid).push({
      id       : cid,
      type     : type,
      callback : callback,
      isValid  : ref
    });


  } has(owner, type, child, callback) {
    if (!_valid(owner) || !_valid(child)) {
      throw new Error("invalid arguments to ObserveManager.has");
    }

  }

  //if callback is undefined, all callbacks owned by child will be unsubscribed
  unsubscribe(owner, type, child, callback) {
    if (!_valid(owner) || !_valid(child)) {
      throw new Error("invalid arguments to ObserveManager.unsubscribe");
    }
    let oid = this.getId(owner);
    let cid = this.getId(child);

    return this._unsubscribe(oid, type, cid, callback);
  }

  _unsubscribe(oid, type, cid, callback) {
    let cbid = this.getId(callback);

    if (!this.subscribeeMap.has(oid) || !this.subscriberMap.has(cid)) {
      console.warn("Warning, bad call to ObserveManager.unsubscribe");
    }

    let list = this.subscriberMap.get(cid);
    let found = false;

    for (let item of list.concat([])) {
      if (item.type === type && item.id === oid && (!callback || item.callback === callback)) {
        list.remove(item);
        found = true;
      }
    }

    list = this.subscribeeMap.get(oid);
    for (let item of list.concat([])) {
      if (item.type === type && item.id === cid && (!callback || item.callback === callback)) {
        list.remove(item);
        found = true;
      }
    }

    return found;
  }
}

export default manager = new ObserveManger();

//Utility class that hides details of ObserveManage

export class Observable extends AbstractObservable {
  static observeDefine() {
    throw new Error("implement me; see AbstractObservable");
  }

  isValid() {
    return true;
  }

  on(type, child, callback) {
    manager.subscribe(this, type, child, callback);
    return this;
  }

  /**if callback is undefined, then all callbacks associated with child will be
  removed*/
  off(type, child, callback) {
    manager.subscribe(this, type, child, callback);
    return this;
  }

  once(type, child, callback) {
    let i = 0;

    manager.subscribe(this, type, child, callback, () => {
      return i++ > 0;
    });

    return this;
  }

  static mixin(cls) {
    function set(p, key, val) {
      if (p[key] === undefined) {
        p[key] = val;
      }
    }

    set(cls,prototype, "on", this.prototype.on);
    set(cls,prototype, "off", this.prototype.off);
    set(cls,prototype, "once", this.prototype.once);
    set(cls,prototype, "isValid", this.prototype.isValid);
    set(cls, "observeDefine", this.observeDefine);
  }
}