Files @ e61eb9bb36d3
Branch filter:

Location: light9/web/AutoDependencies.ts

drewp@bigasterisk.com
fiix pytest; stop using ./lib
import debug from "debug";
import { NamedNode, Quad_Graph, Quad_Object, Quad_Predicate, Quad_Subject, Term, Util } from "n3";
import { filter } from "underscore";
import { Patch, QuadPattern } from "./patch";
import { SubEvent } from "sub-events";
import { SyncedGraph } from "./SyncedGraph";

const log = debug("autodep");

// use patch as an optional optimization, but you can't count on it
export type HandlerFunc = (p?: Patch) => void;

class Handler {
  patterns: QuadPattern[];
  innerHandlers: Handler[];
  // a function and the quad patterns it cared about
  constructor(public func: HandlerFunc | null, public label: string) {
    this.patterns = []; // s,p,o,g quads that should trigger the next run
    this.innerHandlers = []; // Handlers requested while this one was running
  }
}

export class AutoDependencies {
  handlers: Handler;
  handlerStack: Handler[];
  graphError: SubEvent<string> = new SubEvent();
  constructor(private graph: SyncedGraph) {
    // tree of all known Handlers (at least those with non-empty
    // patterns). Top node is not a handler.
    this.handlers = new Handler(null, "root");
    this.handlerStack = [this.handlers]; // currently running
    log("window.ad");
    (window as any).ad = this;
  }

  runHandler(func: HandlerFunc, label: string) {
    // what if we have this func already? duplicate is safe?
    if (label == null) {
      throw new Error("missing label");
    }

    const h = new Handler(func, label);
    const tailChildren = this.handlerStack[this.handlerStack.length - 1].innerHandlers;
    const matchingLabel = filter(tailChildren, (c: Handler) => c.label === label).length;
    // ohno, something depends on some handlers getting run twice :(
    if (matchingLabel < 2) {
      tailChildren.push(h);
    }
    //console.time("handler #{label}")
    // todo: this may fire 1-2 times before the
    // graph is initially loaded, which is a waste. Try deferring it if we
    // haven't gotten the graph yet.
    this._rerunHandler(h, /*patch=*/ undefined);
    log(`new handler ${label} ran first time and requested ${h.patterns.length} pats`);
  }

  _rerunHandler(handler: Handler, patch?: Patch) {
    handler.patterns = [];
    this.handlerStack.push(handler);
    try {
      if (handler.func === null) {
        throw new Error("tried to rerun root");
      }
      handler.func(patch);
    } catch (e) {
      this.graphError.emit(String(e));
    } finally {
      // assuming here it didn't get to do all its queries, we could
      // add a *,*,*,* handler to call for sure the next time?
      // log('done. got: ', handler.patterns)
      this.handlerStack.pop();
    }
  }

  // handler might have no watches, in which case we could forget about it
  logHandlerTree() {
    log("handler tree:");
    const shorten = (x: Term | null) => {
      if (x === null) {
        return "null";
      }
      if (!Util.isNamedNode(x)) {
        return x.value;
      }
      return this.graph.shorten(x as NamedNode);
    };

    var prn = (h: Handler, indent: string) => {
      log(`${indent} 🤝 handler "${h.label}" ${h.patterns.length} pats`);
      for (let pat of h.patterns) {
        log(`${indent}   ⣝ s=${shorten(pat.subject)} p=${shorten(pat.predicate)} o=${shorten(pat.object)}`);
      }
      Array.from(h.innerHandlers).map((c: any) => prn(c, indent + "    "));
    };
    prn(this.handlers, "");
  }

  _handlerIsAffected(child: Handler, patch: Patch): boolean {
    // it should be correct but slow to always return true here
    for (let pat of child.patterns) {
      if (patch.matches(pat)) {
        return true;
      }
    }
    return false;
  }

  graphChanged(patch: Patch) {
    // SyncedGraph is telling us this patch just got applied to the graph.

    var rerunInners = (cur: Handler) => {
      const toRun = cur.innerHandlers.slice();
      for (let child of Array.from(toRun)) {
        const match = this._handlerIsAffected(child, patch);

        if (match) {
          log("match", child.label, match);
          child.innerHandlers = []; // let all children get called again
          this._rerunHandler(child, patch);
        } else {
          rerunInners(child);
        }
      }
    };
    rerunInners(this.handlers);
  }

  askedFor(s: Quad_Subject | null, p: Quad_Predicate | null, o: Quad_Object | null, g: Quad_Graph | null) {
    // SyncedGraph is telling us someone did a query that depended on
    // quads in the given pattern.
    // console.log(`  asked for s/${s?.id} p/${p?.id} o/${o?.id}`)
    const current = this.handlerStack[this.handlerStack.length - 1];
    if (current != null && current !== this.handlers) {
      current.patterns.push({ subject: s, predicate: p, object: o, graph: g } as QuadPattern);
    }
  }
}