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 = 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); } } }