diff web/AutoDependencies.ts @ 2376:4556eebe5d73

topdir reorgs; let pdm have its src/ dir; separate vite area from light9/
author drewp@bigasterisk.com
date Sun, 12 May 2024 19:02:10 -0700
parents light9/web/AutoDependencies.ts@9ca3d356b950
children
line wrap: on
line diff
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/web/AutoDependencies.ts	Sun May 12 19:02:10 2024 -0700
@@ -0,0 +1,137 @@
+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);
+    }
+  }
+}