Changeset - 56a9eaf5e882
[Not reviewed]
default
2 2 3
drewp@bigasterisk.com - 3 years ago 2022-05-22 10:00:37
drewp@bigasterisk.com
rough ports from coffee to ts. untested
5 files changed with 838 insertions and 532 deletions:
0 comments (0 inline, 0 general)
light9/web/graph.ts
Show inline comments
 
file renamed from light9/web/graph.coffee to light9/web/graph.ts
 
log = debug('graph')
 

	
 
# Patch is {addQuads: <quads>, delQuads: <quads>}
 
# <quads> are made with Quad(s,p,o,g)
 

	
 
# for mocha
 
if require?
 
  `window = {}`
 
  `_ = require('./lib/underscore/underscore-min.js')`
 
  `N3 = require('../../node_modules/n3/n3-browser.js')`
 
  `d3 = require('../../node_modules/d3/dist/d3.min.js')`
 
  `RdfDbClient = require('./rdfdbclient.js').RdfDbClient`
 
  module.exports = window
 
import * as d3 from "d3";
 
import debug from "debug";
 
import * as N3 from "n3";
 
import { Quad, Quad_Subject, Quad_Predicate, Quad_Object, Quad_Graph } from "n3";
 
import { filter, sortBy, unique } from "underscore";
 
import { allPatchSubjs, Patch } from "./patch";
 
import { RdfDbClient } from "./rdfdbclient";
 
const log = debug("graph");
 

	
 
RDF = 'http://www.w3.org/1999/02/22-rdf-syntax-ns#'
 

	
 
patchSizeSummary = (patch) ->
 
  '-' + patch.delQuads.length + ' +' + patch.addQuads.length
 
const RDF = "http://www.w3.org/1999/02/22-rdf-syntax-ns#";
 

	
 
# (sloppily shared to rdfdbclient.coffee too)
 
window.patchSizeSummary = patchSizeSummary
 

	
 
patchContainsPreds = (patch, preds) ->
 
  if patch._allPreds == undefined
 
    patch._allPreds = new Set()
 
    for qq in [patch.addQuads, patch.delQuads]
 
      for q in qq
 
        patch._allPreds.add(q.predicate.value)
 
interface QuadPattern {
 
  subject: Quad_Subject | null;
 
  predicate: Quad_Predicate | null;
 
  object: Quad_Object | null;
 
  graph: Quad_Graph | null;
 
}
 

	
 
  for p in preds
 
    if patch._allPreds.has(p.value)
 
      return true
 
  return false
 

	
 
allPatchSubjs = (patch) ->   # returns subjs as Set of strings
 
  out = new Set()
 
  if patch._allSubjs == undefined
 
    patch._allSubjs = new Set()
 
    for qq in [patch.addQuads, patch.delQuads]
 
      for q in qq
 
        patch._allSubjs.add(q.subject.value)
 

	
 
  return patch._allSubjs
 
class Handler {
 
  patterns: QuadPattern[];
 
  innerHandlers: Handler[];
 
  // a function and the quad patterns it cared about
 
  constructor(public func: ((p: Patch) => void) | 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
 
  }
 
}
 

	
 
class Handler
 
  # a function and the quad patterns it cared about
 
  constructor: (@func, @label) ->
 
    @patterns = [] # s,p,o,g quads that should trigger the next run
 
    @innerHandlers = [] # Handlers requested while this one was running
 
  
 
class AutoDependencies
 
  constructor: () ->
 
    # tree of all known Handlers (at least those with non-empty
 
    # patterns). Top node is not a handler.
 
    @handlers = new Handler(null)
 
    @handlerStack = [@handlers] # currently running
 
class AutoDependencies {
 
  handlers: Handler;
 
  handlerStack: Handler[];
 
  constructor() {
 
    // 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
 
  }
 

	
 
  runHandler: (func, label) ->
 
    # what if we have this func already? duplicate is safe?
 
  runHandler(func: any, label: any) {
 
    // what if we have this func already? duplicate is safe?
 

	
 
    if not label?
 
      throw new Error("missing label")
 
    if (label == null) {
 
      throw new Error("missing label");
 
    }
 

	
 
    h = new Handler(func, label)
 
    tailChildren = @handlerStack[@handlerStack.length - 1].innerHandlers
 
    matchingLabel = _.filter(tailChildren, ((c) -> c.label == label)).length
 
    # ohno, something depends on some handlers getting run twice :(
 
    if matchingLabel < 2
 
      tailChildren.push(h)
 
    #console.time("handler #{label}")
 
    @_rerunHandler(h, null)
 
    #console.timeEnd("handler #{label}")
 
    #@_logHandlerTree()
 
    
 
  _rerunHandler: (handler, patch) ->
 
    handler.patterns = []
 
    @handlerStack.push(handler)
 
    try
 
      handler.func(patch)
 
    catch e
 
      log('error running handler: ', e)
 
      # assuming here it didn't get to do all its queries, we could
 
      # add a *,*,*,* handler to call for sure the next time?
 
    finally
 
      #log('done. got: ', handler.patterns)
 
      @handlerStack.pop()
 
    # handler might have no watches, in which case we could forget about it
 
    const h = new Handler(func, label);
 
    const tailChildren = this.handlerStack[this.handlerStack.length - 1].innerHandlers;
 
    const matchingLabel = filter(tailChildren, (c: { label: any }) => c.label === label).length;
 
    // ohno, something depends on some handlers getting run twice :(
 
    if (matchingLabel < 2) {
 
      tailChildren.push(h);
 
    }
 
    //console.time("handler #{label}")
 
    return this._rerunHandler(h, null);
 
  }
 
  //console.timeEnd("handler #{label}")
 
  //@_logHandlerTree()
 

	
 
  _logHandlerTree: ->
 
    log('handler tree:')
 
    prn = (h, depth) ->
 
      indent = ''
 
      for i in [0...depth]
 
        indent += '  '
 
      log("#{indent} \"#{h.label}\" #{h.patterns.length} pats")
 
      for c in h.innerHandlers
 
        prn(c, depth + 1)
 
    prn(@handlers, 0)
 
    
 
  _handlerIsAffected: (child, patchSubjs) ->
 
    if patchSubjs == null
 
      return true
 
    if not child.patterns.length
 
      return false
 
      
 
    for stmt in child.patterns
 
      if stmt[0] == null # wildcard on subject
 
        return true
 
      if patchSubjs.has(stmt[0].value)
 
        return true
 
  _rerunHandler(handler: Handler, patch: any) {
 
    handler.patterns = [];
 
    this.handlerStack.push(handler);
 
    try {
 
      if (handler.func === null) {
 
        throw new Error("tried to rerun root");
 
      }
 
      return handler.func(patch);
 
    } catch (e) {
 
      return log("error running handler: ", 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
 

	
 
    return false
 
            
 
  graphChanged: (patch) ->
 
    # SyncedGraph is telling us this patch just got applied to the graph.
 

	
 
    subjs = allPatchSubjs(patch)
 
    
 
    rerunInners = (cur) =>
 
      toRun = cur.innerHandlers.slice()
 
      for child in toRun
 
        #match = @_handlerIsAffected(child, subjs)
 
        #continue if not match
 
        #log('match', child.label, match)
 
        #child.innerHandlers = [] # let all children get called again
 
        
 
        @_rerunHandler(child, patch)
 
        rerunInners(child)
 
    rerunInners(@handlers)
 
  _logHandlerTree() {
 
    log("handler tree:");
 
    var prn = function (h: Handler, depth: number) {
 
      let indent = "";
 
      for (let i = 0, end = depth, asc = 0 <= end; asc ? i < end : i > end; asc ? i++ : i--) {
 
        indent += "  ";
 
      }
 
      log(`${indent} \"${h.label}\" ${h.patterns.length} pats`);
 
      return Array.from(h.innerHandlers).map((c: any) => prn(c, depth + 1));
 
    };
 
    return prn(this.handlers, 0);
 
  }
 

	
 
  askedFor: (s, p, o, g) ->
 
    # SyncedGraph is telling us someone did a query that depended on
 
    # quads in the given pattern.
 
    current = @handlerStack[@handlerStack.length - 1]
 
    if current? and current != @handlers
 
      current.patterns.push([s, p, o, g])
 
      #log('push', s,p,o,g)
 
    #else
 
    #  console.trace('read outside runHandler')
 
  _handlerIsAffected(child: Handler, patchSubjs: Set<string>) {
 
    if (patchSubjs === null) {
 
      return true;
 
    }
 
    if (!child.patterns.length) {
 
      return false;
 
    }
 

	
 
class window.SyncedGraph
 
  # Main graph object for a browser to use. Syncs both ways with
 
  # rdfdb. Meant to hide the choice of RDF lib, so we can change it
 
  # later.
 
  #
 
  # Note that _applyPatch is the only method to write to the graph, so
 
  # it can fire subscriptions.
 

	
 
  constructor: (@patchSenderUrl, @prefixes, @setStatus, @clearCb) ->
 
    # patchSenderUrl is the /syncedGraph path of an rdfdb server.
 
    # prefixes can be used in Uri(curie) calls.
 
    @_autoDeps = new AutoDependencies() # replaces GraphWatchers
 
    @clearGraph()
 
    for (let stmt of Array.from(child.patterns)) {
 
      if (stmt.subject === null) {
 
        // wildcard on subject
 
        return true;
 
      }
 
      if (patchSubjs.has(stmt.subject.value)) {
 
        return true;
 
      }
 
    }
 

	
 
    if @patchSenderUrl
 
      @_client = new RdfDbClient(@patchSenderUrl,
 
                                 @_clearGraphOnNewConnection.bind(@),
 
                                 @_applyPatch.bind(@),
 
                                 @setStatus)
 
    
 
  clearGraph: ->
 
    # just deletes the statements; watchers are unaffected.
 
    if @graph?
 
      @_applyPatch({addQuads: [], delQuads: @graph.getQuads()})
 
    return false;
 
  }
 

	
 
    # if we had a Store already, this lets N3.Store free all its indices/etc
 
    @graph = N3.Store()
 
    @_addPrefixes(@prefixes)
 
    @cachedFloatValues = new Map() # s + '|' + p -> number
 
    @cachedUriValues = new Map() # s + '|' + p -> Uri
 
  graphChanged(patch: Patch) {
 
    // SyncedGraph is telling us this patch just got applied to the graph.
 

	
 
    const subjs = allPatchSubjs(patch);
 

	
 
  _clearGraphOnNewConnection: -> # must not send a patch to the server!
 
    log('graph: clearGraphOnNewConnection')
 
    @clearGraph()
 
    log('graph: clearGraphOnNewConnection done')
 
    @clearCb() if @clearCb?
 
      
 
  _addPrefixes: (prefixes) ->
 
    for k in (prefixes or {})
 
      @prefixes[k] = prefixes[k]
 
    @prefixFuncs = N3.Util.prefixes(@prefixes)
 
        
 
  Uri: (curie) ->
 
    if not curie?
 
      throw new Error("no uri")
 
    if curie.match(/^http/)
 
      return N3.DataFactory.namedNode(curie)
 
    part = curie.split(':')
 
    return @prefixFuncs(part[0])(part[1])
 
    var rerunInners = (cur: Handler) => {
 
      const toRun = cur.innerHandlers.slice();
 
      for (let child of Array.from(toRun)) {
 
        //match = @_handlerIsAffected(child, subjs)
 
        //continue if not match
 
        //log('match', child.label, match)
 
        //child.innerHandlers = [] # let all children get called again
 

	
 
  Literal: (jsValue) ->
 
    N3.DataFactory.literal(jsValue)
 

	
 
  LiteralRoundedFloat: (f) ->
 
    N3.DataFactory.literal(d3.format(".3f")(f),
 
                          @Uri("http://www.w3.org/2001/XMLSchema#double"))
 
        this._rerunHandler(child, patch);
 
        rerunInners(child);
 
      }
 
    };
 
    return rerunInners(this.handlers);
 
  }
 

	
 
  Quad: (s, p, o, g) -> N3.DataFactory.quad(s, p, o, g)
 

	
 
  toJs: (literal) ->
 
    # incomplete
 
    parseFloat(literal.value)
 

	
 
  loadTrig: (trig, cb) -> # for debugging
 
    patch = {delQuads: [], addQuads: []}
 
    parser = N3.Parser()
 
    parser.parse trig, (error, quad, prefixes) =>
 
      if error
 
        throw new Error(error)
 
      if (quad)
 
        patch.addQuads.push(quad)
 
      else
 
        @_applyPatch(patch)
 
        @_addPrefixes(prefixes)
 
        cb() if cb
 
                    
 
  quads: () -> # for debugging
 
    [q.subject, q.predicate, q.object, q.graph] for q in @graph.getQuads()
 

	
 
  applyAndSendPatch: (patch) ->
 
    console.time('applyAndSendPatch')
 
    if not @_client
 
      log('not connected-- dropping patch')
 
      return
 
    if !Array.isArray(patch.addQuads) || !Array.isArray(patch.delQuads)
 
      console.timeEnd('applyAndSendPatch')
 
      log('corrupt patch')
 
      throw new Error("corrupt patch: #{JSON.stringify(patch)}")
 

	
 
    @_validatePatch(patch)
 
  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.
 
    const current = this.handlerStack[this.handlerStack.length - 1];
 
    if (current != null && current !== this.handlers) {
 
      return current.patterns.push({ subject: s, predicate: p, object: o, graph: g } as QuadPattern);
 
    }
 
  }
 
}
 

	
 
    @_applyPatch(patch)
 
    @_client.sendPatch(patch) if @_client
 
    console.timeEnd('applyAndSendPatch')
 
export class SyncedGraph {
 
  _autoDeps: AutoDependencies;
 
  _client: any;
 
  graph: N3.Store;
 
  cachedFloatValues: any;
 
  cachedUriValues: any;
 
  prefixFuncs: (x: string) => string = (x) => x;
 
  serial: any;
 
  _nextNumber: any;
 
  // Main graph object for a browser to use. Syncs both ways with
 
  // rdfdb. Meant to hide the choice of RDF lib, so we can change it
 
  // later.
 
  //
 
  // Note that _applyPatch is the only method to write to the graph, so
 
  // it can fire subscriptions.
 

	
 
  _validatePatch: (patch) ->
 
    for qs in [patch.addQuads, patch.delQuads]
 
      for q in qs
 
        if not q.equals
 
          throw new Error("doesn't look like a proper Quad")
 
        if not q.subject.id or not q.graph.id? or not q.predicate.id?
 
          throw new Error("corrupt patch: #{JSON.stringify(q)}")
 
    
 
  _applyPatch: (patch) ->
 
    # In most cases you want applyAndSendPatch.
 
    #
 
    # This is the only method that writes to @graph!
 
    @cachedFloatValues.clear()
 
    @cachedUriValues.clear()
 
    for quad in patch.delQuads
 
      #log("remove #{JSON.stringify(quad)}")
 
      did = @graph.removeQuad(quad)
 
      #log("removed: #{did}")
 
    for quad in patch.addQuads
 
      @graph.addQuad(quad)
 
    #log('applied patch locally', patchSizeSummary(patch))
 
    @_autoDeps.graphChanged(patch)
 
  constructor(
 
    // patchSenderUrl is the /syncedGraph path of an rdfdb server.
 
    public patchSenderUrl: any,
 
    // prefixes can be used in Uri(curie) calls.
 
    public prefixes: { [short: string]: string },
 
    private setStatus: any,
 
    // called if we clear the graph
 
    private clearCb: any
 
  ) {
 
    this.graph = new N3.Store();
 
    this._autoDeps = new AutoDependencies(); // replaces GraphWatchers
 
    this.clearGraph();
 

	
 
  getObjectPatch: (s, p, newObject, g) ->
 
    # make a patch which removes existing values for (s,p,*,c) and
 
    # adds (s,p,newObject,c). Values in other graphs are not affected.
 
    existing = @graph.getQuads(s, p, null, g)
 
    return {
 
      delQuads: existing,
 
      addQuads: [@Quad(s, p, newObject, g)]
 
    if (this.patchSenderUrl) {
 
      this._client = new RdfDbClient(this.patchSenderUrl, this._clearGraphOnNewConnection.bind(this), this._applyPatch.bind(this), this.setStatus);
 
    }
 
  }
 

	
 
  clearGraph() {
 
    // just deletes the statements; watchers are unaffected.
 
    if (this.graph != null) {
 
      this._applyPatch({ adds: [], dels: this.graph.getQuads(null, null, null, null) });
 
    }
 

	
 
  patchObject: (s, p, newObject, g) ->
 
    @applyAndSendPatch(@getObjectPatch(s, p, newObject, g))
 
    // if we had a Store already, this lets N3.Store free all its indices/etc
 
    this.graph = new N3.Store();
 
    this._addPrefixes(this.prefixes);
 
    this.cachedFloatValues = new Map(); // s + '|' + p -> number
 
    return (this.cachedUriValues = new Map()); // s + '|' + p -> Uri
 
  }
 

	
 
  _clearGraphOnNewConnection() {
 
    // must not send a patch to the server!
 
    log("graph: clearGraphOnNewConnection");
 
    this.clearGraph();
 
    log("graph: clearGraphOnNewConnection done");
 
    if (this.clearCb != null) {
 
      return this.clearCb();
 
    }
 
  }
 

	
 
  clearObjects: (s, p, g) ->
 
    @applyAndSendPatch({
 
      delQuads: @graph.getQuads(s, p, null, g),
 
      addQuads: []
 
    })
 
  
 
  runHandler: (func, label) ->
 
    # runs your func once, tracking graph calls. if a future patch
 
    # matches what you queried, we runHandler your func again (and
 
    # forget your queries from the first time).
 
  _addPrefixes(prefixes: { [x: string]: string }) {
 
    for (let k of Array.from(prefixes || {})) {
 
      this.prefixes[k] = prefixes[k];
 
    }
 
    this.prefixFuncs = N3.Util.prefixes(this.prefixes);
 
  }
 

	
 
  Uri(curie: string) {
 
    if (curie == null) {
 
      throw new Error("no uri");
 
    }
 
    if (curie.match(/^http/)) {
 
      return N3.DataFactory.namedNode(curie);
 
    }
 
    const part = curie.split(":");
 
    return this.prefixFuncs(part[0])(part[1]);
 
  }
 

	
 
    # helps with memleak? not sure yet. The point was if two matching
 
    # labels get puushed on, we should run only one. So maybe
 
    # appending a serial number is backwards.
 
    @serial = 1 if not @serial
 
    @serial += 1
 
    #label = label + @serial
 
    
 
    @_autoDeps.runHandler(func, label)
 
  Literal(jsValue: any) {
 
    return N3.DataFactory.literal(jsValue);
 
  }
 

	
 
  LiteralRoundedFloat(f: number) {
 
    return N3.DataFactory.literal(d3.format(".3f")(f), this.Uri("http://www.w3.org/2001/XMLSchema#double"));
 
  }
 

	
 
  Quad(s: any, p: any, o: any, g: any) {
 
    return N3.DataFactory.quad(s, p, o, g);
 
  }
 

	
 
  toJs(literal: { value: any }) {
 
    // incomplete
 
    return parseFloat(literal.value);
 
  }
 

	
 
  _singleValue: (s, p) ->
 
    @_autoDeps.askedFor(s, p, null, null)
 
    quads = @graph.getQuads(s, p)
 
    objs = new Set(q.object for q in quads)
 
    
 
    switch objs.size
 
      when 0
 
        throw new Error("no value for "+s.value+" "+p.value)
 
      when 1
 
        obj = objs.values().next().value
 
        return obj
 
      else
 
        throw new Error("too many different values: " + JSON.stringify(quads))
 
  loadTrig(trig: any, cb: () => any) {
 
    // for debugging
 
    const patch: Patch = { dels: [], adds: [] };
 
    const parser = new N3.Parser();
 
    return parser.parse(trig, (error: any, quad: any, prefixes: any) => {
 
      if (error) {
 
        throw new Error(error);
 
      }
 
      if (quad) {
 
        return patch.adds.push(quad);
 
      } else {
 
        this._applyPatch(patch);
 
        this._addPrefixes(prefixes);
 
        if (cb) {
 
          return cb();
 
        }
 
      }
 
    });
 
  }
 

	
 
  quads(): any {
 
    // for debugging
 
    return Array.from(this.graph.getQuads(null, null, null, null)).map((q: Quad) => [q.subject, q.predicate, q.object, q.graph]);
 
  }
 

	
 
  floatValue: (s, p) ->
 
    key = s.value + '|' + p.value
 
    hit = @cachedFloatValues.get(key)
 
    return hit if hit != undefined
 
    #log('float miss', s, p)
 
  applyAndSendPatch(patch: Patch) {
 
    console.time("applyAndSendPatch");
 
    if (!this._client) {
 
      log("not connected-- dropping patch");
 
      return;
 
    }
 
    if (!Array.isArray(patch.adds) || !Array.isArray(patch.dels)) {
 
      console.timeEnd("applyAndSendPatch");
 
      log("corrupt patch");
 
      throw new Error(`corrupt patch: ${JSON.stringify(patch)}`);
 
    }
 

	
 
    this._validatePatch(patch);
 

	
 
    v = @_singleValue(s, p).value
 
    ret = parseFloat(v)
 
    if isNaN(ret)
 
      throw new Error("#{s.value} #{p.value} -> #{v} not a float")
 
    @cachedFloatValues.set(key, ret)
 
    return ret
 
    
 
  stringValue: (s, p) ->
 
    @_singleValue(s, p).value
 
    
 
  uriValue: (s, p) ->
 
    key = s.value + '|' + p.value
 
    hit = @cachedUriValues.get(key)
 
    return hit if hit != undefined
 
    this._applyPatch(patch);
 
    if (this._client) {
 
      this._client.sendPatch(patch);
 
    }
 
    return console.timeEnd("applyAndSendPatch");
 
  }
 

	
 
  _validatePatch(patch: Patch) {
 
    return [patch.adds, patch.dels].map((qs: Quad[]) =>
 
      (() => {
 
        const result = [];
 
        for (let q of Array.from(qs)) {
 
          if (!q.equals) {
 
            throw new Error("doesn't look like a proper Quad");
 
          }
 
          if (!q.subject.id || q.graph.id == null || q.predicate.id == null) {
 
            throw new Error(`corrupt patch: ${JSON.stringify(q)}`);
 
          } else {
 
            result.push(undefined);
 
          }
 
        }
 
        return result;
 
      })()
 
    );
 
  }
 

	
 
    ret = @_singleValue(s, p)
 
    @cachedUriValues.set(key, ret)
 
    return ret
 
  _applyPatch(patch: Patch) {
 
    // In most cases you want applyAndSendPatch.
 
    //
 
    // This is the only method that writes to @graph!
 
    let quad: any;
 
    this.cachedFloatValues.clear();
 
    this.cachedUriValues.clear();
 
    for (quad of Array.from(patch.dels)) {
 
      //log("remove #{JSON.stringify(quad)}")
 
      const did = this.graph.removeQuad(quad);
 
    }
 
    //log("removed: #{did}")
 
    for (quad of Array.from(patch.adds)) {
 
      this.graph.addQuad(quad);
 
    }
 
    //log('applied patch locally', patchSizeSummary(patch))
 
    return this._autoDeps.graphChanged(patch);
 
  }
 

	
 
  labelOrTail: (uri) ->
 
    try
 
      ret = @stringValue(uri, @Uri('rdfs:label'))
 
    catch
 
      words = uri.value.split('/')
 
      ret = words[words.length-1]
 
    if not ret
 
      ret = uri.value
 
    return ret
 
  getObjectPatch(s: N3.NamedNode, p: N3.NamedNode, newObject: N3.Quad_Object, g: N3.NamedNode): Patch {
 
    // make a patch which removes existing values for (s,p,*,c) and
 
    // adds (s,p,newObject,c). Values in other graphs are not affected.
 
    const existing = this.graph.getQuads(s, p, null, g);
 
    return {
 
      dels: existing,
 
      adds: [this.Quad(s, p, newObject, g)],
 
    };
 
  }
 

	
 
  patchObject(s: N3.NamedNode, p: N3.NamedNode, newObject: N3.Quad_Object, g: N3.NamedNode) {
 
    return this.applyAndSendPatch(this.getObjectPatch(s, p, newObject, g));
 
  }
 

	
 
  objects: (s, p) ->
 
    @_autoDeps.askedFor(s, p, null, null)
 
    quads = @graph.getQuads(s, p)
 
    return (q.object for q in quads)
 
  clearObjects(s: N3.NamedNode, p: N3.NamedNode, g: N3.NamedNode) {
 
    return this.applyAndSendPatch({
 
      dels: this.graph.getQuads(s, p, null, g),
 
      adds: [],
 
    });
 
  }
 

	
 
  runHandler(func: any, label: any) {
 
    // runs your func once, tracking graph calls. if a future patch
 
    // matches what you queried, we runHandler your func again (and
 
    // forget your queries from the first time).
 

	
 
  subjects: (p, o) ->
 
    @_autoDeps.askedFor(null, p, o, null)
 
    quads = @graph.getQuads(null, p, o)
 
    return (q.subject for q in quads)
 
    // helps with memleak? not sure yet. The point was if two matching
 
    // labels get puushed on, we should run only one. So maybe
 
    // appending a serial number is backwards.
 
    if (!this.serial) {
 
      this.serial = 1;
 
    }
 
    this.serial += 1;
 
    //label = label + @serial
 

	
 
    return this._autoDeps.runHandler(func, label);
 
  }
 

	
 
  items: (list) ->
 
    out = []
 
    current = list
 
    while true
 
      if current == RDF + 'nil'
 
        break
 
        
 
      @_autoDeps.askedFor(current, null, null, null) # a little loose
 
  _singleValue(s: Quad_Subject, p: Quad_Predicate) {
 
    this._autoDeps.askedFor(s, p, null, null);
 
    const quads = this.graph.getQuads(s, p, null, null);
 
    const objs = new Set(Array.from(quads).map((q: Quad) => q.object));
 

	
 
    switch (objs.size) {
 
      case 0:
 
        throw new Error("no value for " + s.value + " " + p.value);
 
      case 1:
 
        var obj = objs.values().next().value;
 
        return obj;
 
      default:
 
        throw new Error("too many different values: " + JSON.stringify(quads));
 
    }
 
  }
 

	
 
      firsts = @graph.getQuads(current, RDF + 'first', null)
 
      rests = @graph.getQuads(current, RDF + 'rest', null)
 
      if firsts.length != 1
 
        throw new Error(
 
          "list node #{current} has #{firsts.length} rdf:first edges")
 
      out.push(firsts[0].object)
 
  floatValue(s: Quad_Subject, p: Quad_Predicate) {
 
    const key = s.value + "|" + p.value;
 
    const hit = this.cachedFloatValues.get(key);
 
    if (hit !== undefined) {
 
      return hit;
 
    }
 
    //log('float miss', s, p)
 

	
 
    const v = this._singleValue(s, p).value;
 
    const ret = parseFloat(v);
 
    if (isNaN(ret)) {
 
      throw new Error(`${s.value} ${p.value} -> ${v} not a float`);
 
    }
 
    this.cachedFloatValues.set(key, ret);
 
    return ret;
 
  }
 

	
 
  stringValue(s: any, p: any) {
 
    return this._singleValue(s, p).value;
 
  }
 

	
 
      if rests.length != 1
 
        throw new Error(
 
          "list node #{current} has #{rests.length} rdf:rest edges")
 
      current = rests[0].object
 
    
 
    return out
 
  uriValue(s: Quad_Subject, p: Quad_Predicate) {
 
    const key = s.value + "|" + p.value;
 
    const hit = this.cachedUriValues.get(key);
 
    if (hit !== undefined) {
 
      return hit;
 
    }
 

	
 
    const ret = this._singleValue(s, p);
 
    this.cachedUriValues.set(key, ret);
 
    return ret;
 
  }
 

	
 
  contains: (s, p, o) ->
 
    @_autoDeps.askedFor(s, p, o, null)
 
    log('contains calling getQuads when graph has ', @graph.size)
 
    return @graph.getQuads(s, p, o).length > 0
 
  labelOrTail(uri: { value: { split: (arg0: string) => any } }) {
 
    let ret: any;
 
    try {
 
      ret = this.stringValue(uri, this.Uri("rdfs:label"));
 
    } catch (error) {
 
      const words = uri.value.split("/");
 
      ret = words[words.length - 1];
 
    }
 
    if (!ret) {
 
      ret = uri.value;
 
    }
 
    return ret;
 
  }
 

	
 
  nextNumberedResources: (base, howMany) ->
 
    # base is NamedNode or string
 
    # Note this is unsafe before we're synced with the graph. It'll
 
    # always return 'name0'.
 
    base = base.id if base.id
 
    results = []
 
  objects(s: any, p: any) {
 
    this._autoDeps.askedFor(s, p, null, null);
 
    const quads = this.graph.getQuads(s, p, null, null);
 
    return Array.from(quads).map((q: { object: any }) => q.object);
 
  }
 

	
 
  subjects(p: any, o: any) {
 
    this._autoDeps.askedFor(null, p, o, null);
 
    const quads = this.graph.getQuads(null, p, o, null);
 
    return Array.from(quads).map((q: { subject: any }) => q.subject);
 
  }
 

	
 
  items(list: any) {
 
    const out = [];
 
    let current = list;
 
    while (true) {
 
      if (current === RDF + "nil") {
 
        break;
 
      }
 

	
 
      this._autoDeps.askedFor(current, null, null, null); // a little loose
 

	
 
    # @contains is really slow.
 
    @_nextNumber = new Map() unless @_nextNumber?
 
    start = @_nextNumber.get(base)
 
    if start == undefined
 
      start = 0
 
      
 
    for serial in [start..1000]
 
      uri = @Uri("#{base}#{serial}")
 
      if not @contains(uri, null, null)
 
        results.push(uri)
 
        log('nextNumberedResources', "picked #{uri}")
 
        @_nextNumber.set(base, serial + 1)
 
        if results.length >= howMany
 
          return results
 
    throw new Error("can't make sequential uri with base #{base}")
 
      const firsts = this.graph.getQuads(current, RDF + "first", null, null);
 
      const rests = this.graph.getQuads(current, RDF + "rest", null, null);
 
      if (firsts.length !== 1) {
 
        throw new Error(`list node ${current} has ${firsts.length} rdf:first edges`);
 
      }
 
      out.push(firsts[0].object);
 

	
 
      if (rests.length !== 1) {
 
        throw new Error(`list node ${current} has ${rests.length} rdf:rest edges`);
 
      }
 
      current = rests[0].object;
 
    }
 

	
 
    return out;
 
  }
 

	
 
  nextNumberedResource: (base) ->
 
    @nextNumberedResources(base, 1)[0]
 
  contains(s: any, p: any, o: any) {
 
    this._autoDeps.askedFor(s, p, o, null);
 
    log("contains calling getQuads when graph has ", this.graph.size);
 
    return this.graph.getQuads(s, p, o, null).length > 0;
 
  }
 

	
 
  nextNumberedResources(base: { id: any }, howMany: number) {
 
    // base is NamedNode or string
 
    // Note this is unsafe before we're synced with the graph. It'll
 
    // always return 'name0'.
 
    if (base.id) {
 
      base = base.id;
 
    }
 
    const results = [];
 

	
 
    // @contains is really slow.
 
    if (this._nextNumber == null) {
 
      this._nextNumber = new Map();
 
    }
 
    let start = this._nextNumber.get(base);
 
    if (start === undefined) {
 
      start = 0;
 
    }
 

	
 
  contextsWithPattern: (s, p, o) ->
 
    @_autoDeps.askedFor(s, p, o, null)
 
    ctxs = []
 
    for q in @graph.getQuads(s, p, o)
 
      ctxs.push(q.graph)
 
    return _.unique(ctxs)
 
    for (let serial = start, asc = start <= 1000; asc ? serial <= 1000 : serial >= 1000; asc ? serial++ : serial--) {
 
      const uri = this.Uri(`${base}${serial}`);
 
      if (!this.contains(uri, null, null)) {
 
        results.push(uri);
 
        log("nextNumberedResources", `picked ${uri}`);
 
        this._nextNumber.set(base, serial + 1);
 
        if (results.length >= howMany) {
 
          return results;
 
        }
 
      }
 
    }
 
    throw new Error(`can't make sequential uri with base ${base}`);
 
  }
 

	
 
  nextNumberedResource(base: any) {
 
    return this.nextNumberedResources(base, 1)[0];
 
  }
 

	
 
  sortKey: (uri) ->
 
    parts = uri.value.split(/([0-9]+)/)
 
    expanded = parts.map (p) ->
 
      f = parseInt(p)
 
      return p if isNaN(f)
 
      return p.padStart(8, '0')
 
    return expanded.join('')
 
  contextsWithPattern(s: any, p: any, o: any) {
 
    this._autoDeps.askedFor(s, p, o, null);
 
    const ctxs = [];
 
    for (let q of Array.from(this.graph.getQuads(s, p, o, null))) {
 
      ctxs.push(q.graph);
 
    }
 
    return unique(ctxs);
 
  }
 

	
 
  sortedUris: (uris) ->
 
    _.sortBy uris, @sortKey
 
  sortKey(uri: N3.NamedNode) {
 
    const parts = uri.value.split(/([0-9]+)/);
 
    const expanded = parts.map(function (p: string) {
 
      const f = parseInt(p);
 
      if (isNaN(f)) {
 
        return p;
 
      }
 
      return p.padStart(8, "0");
 
    });
 
    return expanded.join("");
 
  }
 

	
 
  # temporary optimization since autodeps calls too often
 
  @patchContainsPreds: (patch, preds) ->
 
    patchContainsPreds(patch, preds)
 
  sortedUris(uris: any) {
 
    return sortBy(uris, this.sortKey);
 
  }
 

	
 
  prettyLiteral: (x) ->
 
    if typeof(x) == 'number'
 
      @LiteralRoundedFloat(x)
 
    else
 
      @Literal(x)
 
  prettyLiteral(x: any) {
 
    if (typeof x === "number") {
 
      return this.LiteralRoundedFloat(x);
 
    } else {
 
      return this.Literal(x);
 
    }
 
  }
 
}
light9/web/patch.ts
Show inline comments
 
new file 100644
 
import debug from "debug";
 
import * as async from "async";
 
import { Writer, Parser, Quad, NamedNode } from "n3";
 
const log = debug("patch");
 

	
 
export interface Patch {
 
  dels: Quad[];
 
  adds: Quad[];
 
  _allPredsCache?: Set<string>;
 
  _allSubjsCache?: Set<string>;
 
}
 

	
 
interface SyncgraphPatchMessage {
 
  patch: { adds: string; deletes: string };
 
}
 

	
 
export function patchSizeSummary(patch: Patch) {
 
  return "-" + patch.dels.length + " +" + patch.adds.length;
 
}
 

	
 
export function parseJsonPatch(input: SyncgraphPatchMessage, cb: (p: Patch) => void) {
 
  // note response cb doesn't have an error arg.
 
  const patch: Patch = { dels: [], adds: [] };
 

	
 
  const parseAdds = (cb: () => any) => {
 
    const parser = new Parser();
 
    return parser.parse(input.patch.adds, (error: any, quad: Quad, prefixes: any) => {
 
      if (quad) {
 
        return patch.adds.push(quad);
 
      } else {
 
        return cb();
 
      }
 
    });
 
  };
 
  const parseDels = (cb: () => any) => {
 
    const parser = new Parser();
 
    return parser.parse(input.patch.deletes, (error: any, quad: any, prefixes: any) => {
 
      if (quad) {
 
        return patch.dels.push(quad);
 
      } else {
 
        return cb();
 
      }
 
    });
 
  };
 

	
 
  return async.parallel([parseAdds, parseDels], (err: any) => cb(patch));
 
}
 

	
 
export function toJsonPatch(jsPatch: Patch, cb: { (json: any): any; (arg0: any): any }) {
 
  const out: SyncgraphPatchMessage = { patch: { adds: "", deletes: "" } };
 

	
 
  const writeDels = function (cb: () => any) {
 
    const writer = new Writer({ format: "N-Quads" });
 
    writer.addQuads(jsPatch.dels);
 
    return writer.end(function (err: any, result: string) {
 
      out.patch.deletes = result;
 
      return cb();
 
    });
 
  };
 

	
 
  const writeAdds = function (cb: () => any) {
 
    const writer = new Writer({ format: "N-Quads" });
 
    writer.addQuads(jsPatch.adds);
 
    return writer.end(function (err: any, result: string) {
 
      out.patch.adds = result;
 
      return cb();
 
    });
 
  };
 

	
 
  return async.parallel([writeDels, writeAdds], (err: any) => cb(JSON.stringify(out)));
 
}
 

	
 
export function patchContainsPreds(patch: Patch, preds: NamedNode[]): boolean {
 
  if (patch._allPredsCache === undefined) {
 
    patch._allPredsCache = new Set();
 
    for (let qq of [patch.adds, patch.dels]) {
 
      for (let q of Array.from(qq)) {
 
        patch._allPredsCache.add(q.predicate.value);
 
      }
 
    }
 
  }
 

	
 
  for (let p of Array.from(preds)) {
 
    if (patch._allPredsCache.has(p.value)) {
 
      return true;
 
    }
 
  }
 
  return false;
 
}
 

	
 
export function allPatchSubjs(patch: Patch): Set<string> {
 
  // returns subjs as Set of strings
 
  const out = new Set();
 
  if (patch._allSubjsCache === undefined) {
 
    patch._allSubjsCache = new Set();
 
    for (let qq of [patch.adds, patch.dels]) {
 
      for (let q of Array.from(qq)) {
 
        patch._allSubjsCache.add(q.subject.value);
 
      }
 
    }
 
  }
 

	
 
  return patch._allSubjsCache;
 
}
light9/web/rdfdbclient.ts
Show inline comments
 
file renamed from light9/web/rdfdbclient.coffee to light9/web/rdfdbclient.ts
 
log = debug('rdfdbclient')
 

	
 
# for mocha
 
if require?
 
  `window = {}`
 
  `N3 = require('../../node_modules/n3/n3-browser.js')`
 
  module.exports = window
 

	
 

	
 
toJsonPatch = (jsPatch, cb) ->
 
  out = {patch: {adds: '', deletes: ''}}
 
import debug from "debug";
 
import * as async from "async";
 
import { parseJsonPatch, Patch, patchSizeSummary, toJsonPatch } from "./patch";
 
const log = debug("rdfdbclient");
 

	
 
  writeDels = (cb) ->
 
    writer = N3.Writer({ format: 'N-Quads' })
 
    writer.addQuads(jsPatch.delQuads)
 
    writer.end((err, result) ->
 
      out.patch.deletes = result
 
      cb())
 
export class RdfDbClient {
 
  _patchesToSend: Patch[];
 
  _lastPingMs: number;
 
  _patchesReceived: number;
 
  _patchesSent: number;
 
  _connectionId: string;
 
  _reconnectionTimeout: number | null;
 
  ws: WebSocket | undefined;
 
  _pingLoopTimeout: any;
 
  // Send and receive patches from rdfdb
 
  //
 
  // What this should do, and does not yet, is keep the graph
 
  // 'coasting' over a reconnect, applying only the diffs from the old
 
  // contents to the new ones once they're in. Then, remove all the
 
  // clearGraph stuff in graph.coffee that doesn't even work right.
 
  //
 
  constructor(
 
    public patchSenderUrl: string,
 
    private clearGraphOnNewConnection: () => void,
 
    private applyPatch: (p: Patch) => void,
 
    private setStatus: (status: string) => void
 
  ) {
 
    this._patchesToSend = [];
 
    this._lastPingMs = -1;
 
    this._patchesReceived = 0;
 
    this._patchesSent = 0;
 
    this._connectionId = "??";
 
    this._reconnectionTimeout = null;
 
    this.ws = undefined;
 

	
 
  writeAdds = (cb) ->
 
    writer = N3.Writer({ format: 'N-Quads' })
 
    writer.addQuads(jsPatch.addQuads)
 
    writer.end((err, result) ->
 
      out.patch.adds = result
 
      cb())
 
    
 
  async.parallel([writeDels, writeAdds], (err) ->
 
    cb(JSON.stringify(out))
 
  )
 

	
 
parseJsonPatch = (input, cb) ->
 
  # note response cb doesn't have an error arg.
 
  patch = {delQuads: [], addQuads: []}
 
    this._newConnection();
 
  }
 

	
 
  parseAdds = (cb) =>
 
    parser = N3.Parser()
 
    parser.parse input.patch.adds, (error, quad, prefixes) =>
 
      if (quad)
 
        patch.addQuads.push(quad)
 
      else
 
        cb()
 
  parseDels = (cb) =>
 
    parser = N3.Parser()
 
    parser.parse input.patch.deletes, (error, quad, prefixes) =>
 
      if (quad)
 
        patch.delQuads.push(quad)
 
      else
 
        cb()
 

	
 
  async.parallel([parseAdds, parseDels], ((err) => cb(patch)))
 
  _updateStatus() {
 
    const conn = (() => {
 
      if (this.ws === undefined) {
 
        return "no";
 
      } else {
 
        switch (this.ws.readyState) {
 
          case this.ws.CONNECTING:
 
            return "connecting";
 
          case this.ws.OPEN:
 
            return `open as ${this._connectionId}`;
 
          case this.ws.CLOSING:
 
            return "closing";
 
          case this.ws.CLOSED:
 
            return "close";
 
        }
 
      }
 
    })();
 

	
 
class window.RdfDbClient
 
  # Send and receive patches from rdfdb
 
  #
 
  # What this should do, and does not yet, is keep the graph
 
  # 'coasting' over a reconnect, applying only the diffs from the old
 
  # contents to the new ones once they're in. Then, remove all the
 
  # clearGraph stuff in graph.coffee that doesn't even work right.
 
  # 
 
  constructor: (@patchSenderUrl, @clearGraphOnNewConnection, @applyPatch,
 
                @setStatus) ->
 
    @_patchesToSend = []
 
    @_lastPingMs = -1
 
    @_patchesReceived = 0
 
    @_patchesSent = 0
 
    @_connectionId = '??'
 
    @_reconnectionTimeout = null
 
    @_newConnection()
 
    const ping = this._lastPingMs > 0 ? this._lastPingMs : "...";
 
    return this.setStatus(`${conn}; \
 
${this._patchesReceived} recv; \
 
${this._patchesSent} sent; \
 
${this._patchesToSend.length} pending; \
 
${ping}ms`);
 
  }
 

	
 
  _updateStatus: ->
 
    ws = (if not @ws? then 'no' else switch @ws.readyState
 
      when @ws.CONNECTING then 'connecting'
 
      when @ws.OPEN then "open as #{@_connectionId}"
 
      when @ws.CLOSING then 'closing'
 
      when @ws.CLOSED then 'close'
 
      )
 
  sendPatch(patch: Patch) {
 
    log("rdfdbclient: queue patch to server ", patchSizeSummary(patch));
 
    this._patchesToSend.push(patch);
 
    this._updateStatus();
 
    this._continueSending();
 
  }
 

	
 
  _newConnection() {
 
    const wsOrWss = window.location.protocol.replace("http", "ws");
 
    const fullUrl = wsOrWss + "//" + window.location.host + this.patchSenderUrl;
 
    if (this.ws !== undefined) {
 
      this.ws.close();
 
    }
 
    this.ws = new WebSocket(fullUrl);
 

	
 
    ping = if @_lastPingMs > 0 then @_lastPingMs else '...'
 
    @setStatus("#{ws};
 
      #{@_patchesReceived} recv;
 
      #{@_patchesSent} sent;
 
      #{@_patchesToSend.length} pending;
 
      #{ping}ms")
 
 
 
  sendPatch: (patch) ->
 
    log('rdfdbclient: queue patch to server ', patchSizeSummary(patch))
 
    @_patchesToSend.push(patch)
 
    @_updateStatus()
 
    @_continueSending()
 
    this.ws.onopen = () => {
 
      log("rdfdbclient: new connection to", fullUrl);
 
      this._updateStatus();
 
      this.clearGraphOnNewConnection();
 
      return this._pingLoop();
 
    };
 

	
 
    this.ws.onerror = (e: Event) => {
 
      log("rdfdbclient: ws error " + e);
 
      if (this.ws !== undefined) {
 
        const closeHandler = this.ws.onclose?.bind(this.ws);
 
        if (!closeHandler) {
 
          throw new Error();
 
        }
 
        closeHandler(new CloseEvent("forced"));
 
      }
 
    };
 

	
 
  _newConnection: ->
 
    wsOrWss = window.location.protocol.replace('http', 'ws')
 
    fullUrl = wsOrWss + '//' + window.location.host + @patchSenderUrl
 
    @ws.close() if @ws?
 
    @ws = new WebSocket(fullUrl)
 
    this.ws.onclose = (ev: CloseEvent) => {
 
      log("rdfdbclient: ws close");
 
      this._updateStatus();
 
      if (this._reconnectionTimeout != null) {
 
        clearTimeout(this._reconnectionTimeout);
 
      }
 
      this._reconnectionTimeout = (setTimeout(this._newConnection.bind(this), 1000) as unknown) as number;
 
    };
 

	
 
    @ws.onopen = =>
 
      log('rdfdbclient: new connection to', fullUrl)
 
      @_updateStatus()
 
      @clearGraphOnNewConnection()
 
      @_pingLoop()
 
    this.ws.onmessage = this._onMessage.bind(this);
 
  }
 

	
 
    @ws.onerror = (e) =>
 
      log('rdfdbclient: ws error ' + e)
 
      @ws.onclose()
 
  _pingLoop() {
 
    if (this.ws && this.ws.readyState === this.ws.OPEN) {
 
      this.ws.send("PING");
 
      this._lastPingMs = -Date.now();
 

	
 
    @ws.onclose = =>
 
      log('rdfdbclient: ws close')
 
      @_updateStatus()
 
      clearTimeout(@_reconnectionTimeout) if @_reconnectionTimeout?
 
      @_reconnectionTimeout = setTimeout(@_newConnection.bind(@), 1000)
 

	
 
    @ws.onmessage = @_onMessage.bind(@)
 
      if (this._pingLoopTimeout != null) {
 
        clearTimeout(this._pingLoopTimeout);
 
      }
 
      this._pingLoopTimeout = setTimeout(this._pingLoop.bind(this), 10000);
 
    }
 
  }
 

	
 
  _pingLoop: () ->
 
    if @ws.readyState == @ws.OPEN
 
      @ws.send('PING')
 
      @_lastPingMs = -Date.now()
 
      
 
      clearTimeout(@_pingLoopTimeout) if @_pingLoopTimeout?
 
      @_pingLoopTimeout = setTimeout(@_pingLoop.bind(@), 10000)
 
  _onMessage(evt: { data: string }) {
 
    const msg = evt.data;
 
    if (msg === "PONG") {
 
      this._lastPingMs = Date.now() + this._lastPingMs;
 
      this._updateStatus();
 
      return;
 
    }
 

	
 
    const input = JSON.parse(msg);
 
    if (input.connectedAs) {
 
      this._connectionId = input.connectedAs;
 
    } else {
 
      parseJsonPatch(input, this.applyPatch.bind(this));
 
      this._patchesReceived++;
 
    }
 
    return this._updateStatus();
 
  }
 

	
 
  _onMessage: (evt) ->
 
    msg = evt.data
 
    if msg == 'PONG'
 
      @_lastPingMs = Date.now() + @_lastPingMs
 
      @_updateStatus()
 
      return
 
      
 
    input = JSON.parse(msg)
 
    if input.connectedAs
 
      @_connectionId = input.connectedAs
 
    else
 
      parseJsonPatch(input, @applyPatch.bind(@))
 
      @_patchesReceived++
 
    @_updateStatus()
 
  _continueSending() {
 
    if (this.ws && this.ws.readyState !== this.ws.OPEN) {
 
      setTimeout(this._continueSending.bind(this), 500);
 
      return;
 
    }
 

	
 
    // we could call this less often and coalesce patches together to optimize
 
    // the dragging cases.
 

	
 
  _continueSending: ->
 
    if @ws.readyState != @ws.OPEN
 
      setTimeout(@_continueSending.bind(@), 500)
 
      return
 

	
 
    # we could call this less often and coalesce patches together to optimize
 
    # the dragging cases.
 
    const sendOne = (patch: any, cb: (arg0: any) => any) => {
 
      return toJsonPatch(patch, (json: string) => {
 
        log("rdfdbclient: send patch to server, " + json.length + " bytes");
 
        if (!this.ws) {
 
          throw new Error("can't send");
 
        }
 
        this.ws.send(json);
 
        this._patchesSent++;
 
        this._updateStatus();
 
        return cb(null);
 
      });
 
    };
 

	
 
    sendOne = (patch, cb) =>
 
      toJsonPatch(patch, (json) =>
 
        log('rdfdbclient: send patch to server, ' + json.length + ' bytes')
 
        @ws.send(json)
 
        @_patchesSent++
 
        @_updateStatus()
 
        cb(null)
 
      )
 

	
 
    async.eachSeries(@_patchesToSend, sendOne, () =>
 
      @_patchesToSend = []
 
      @_updateStatus()
 
    )
 
    return async.eachSeries(this._patchesToSend, sendOne, () => {
 
      this._patchesToSend = [];
 
      return this._updateStatus();
 
    });
 
  }
 
}
package.json
Show inline comments
 
@@ -9,16 +9,22 @@
 
    "test": "test"
 
  },
 
  "dependencies": {
 
    "@types/async": "^3.2.13",
 
    "@types/d3": "^7.1.0",
 
    "@types/debug": "^4.1.7",
 
    "@types/n3": "^1.10.4",
 
    "@types/node": "^17.0.31",
 
    "@types/reconnectingwebsocket": "^1.0.7",
 
    "@types/sylvester": "^0.1.8",
 
    "@types/underscore": "^1.11.4",
 
    "async": "^3.2.3",
 
    "d3": "^7.4.4",
 
    "debug": "^4.3.4",
 
    "knockout": "^3.5.1",
 
    "lit": "^2.2.3",
 
    "n3": "^1.16.2",
 
    "parse-prometheus-text-format": "^1.1.1",
 
    "reconnectingwebsocket": "^1.0.0",
 
    "sylvester": "^0.0.21",
 
    "underscore": "^1.13.3",
 
    "vite": "^2.9.1",
pnpm-lock.yaml
Show inline comments
 
lockfileVersion: 5.3
 

	
 
specifiers:
 
  '@types/async': ^3.2.13
 
  '@types/d3': ^7.1.0
 
  '@types/debug': ^4.1.7
 
  '@types/n3': ^1.10.4
 
  '@types/node': ^17.0.31
 
  '@types/reconnectingwebsocket': ^1.0.7
 
  '@types/sylvester': ^0.1.8
 
  '@types/underscore': ^1.11.4
 
  async: ^3.2.3
 
  d3: ^7.4.4
 
  debug: ^4.3.4
 
  knockout: ^3.5.1
 
  lit: ^2.2.3
 
  n3: ^1.16.2
 
  parse-prometheus-text-format: ^1.1.1
 
  reconnectingwebsocket: ^1.0.0
 
  sylvester: ^0.0.21
 
  underscore: ^1.13.3
 
  vite: ^2.9.1
 
  vite-plugin-rewrite-all: ^0.1.2
 

	
 
dependencies:
 
  '@types/async': 3.2.13
 
  '@types/d3': 7.1.0
 
  '@types/debug': 4.1.7
 
  '@types/n3': 1.10.4
 
  '@types/node': 17.0.31
 
  '@types/reconnectingwebsocket': 1.0.7
 
  '@types/sylvester': 0.1.8
 
  '@types/underscore': 1.11.4
 
  async: 3.2.3
 
  d3: 7.4.4
 
  debug: 4.3.4
 
  knockout: 3.5.1
 
  lit: 2.2.3
 
  n3: 1.16.2
 
  parse-prometheus-text-format: 1.1.1
 
  reconnectingwebsocket: 1.0.0
 
  sylvester: 0.0.21
 
  underscore: 1.13.3
 
  vite: 2.9.1
 
@@ -38,6 +50,16 @@ packages:
 
    resolution: {integrity: sha512-A2e18XzPMrIh35nhIdE4uoqRzoIpEU5vZYuQN4S3Ee1zkGdYC27DP12pewbw/RLgPHzaE4kx/YqxMzebOpm0dA==}
 
    dev: false
 

	
 
  /@rdfjs/types/1.1.0:
 
    resolution: {integrity: sha512-5zm8bN2/CC634dTcn/0AhTRLaQRjXDZs3QfcAsQKNturHT7XVWcKy/8p3P5gXl+YkZTAmy7T5M/LyiT/jbkENw==}
 
    dependencies:
 
      '@types/node': 17.0.31
 
    dev: false
 

	
 
  /@types/async/3.2.13:
 
    resolution: {integrity: sha512-7Q3awrhnvm89OzfsmqeqRQh8mh+8Pxfgq1UvSAn2nWQ5y/F3+NrbIF0RbkWq8+5dY99ozgap2b3DNBNwjLVOxw==}
 
    dev: false
 

	
 
  /@types/d3-array/3.0.2:
 
    resolution: {integrity: sha512-5mjGjz6XOXKOCdTajXTZ/pMsg236RdiwKPrRPWAEf/2S/+PzwY+LLYShUpeysWaMvsdS7LArh6GdUefoxpchsQ==}
 
    dev: false
 
@@ -231,10 +253,21 @@ packages:
 
    resolution: {integrity: sha512-iiUgKzV9AuaEkZqkOLDIvlQiL6ltuZd9tGcW3gwpnX8JbuiuhFlEGmmFXEXkN50Cvq7Os88IY2v0dkDqXYWVgA==}
 
    dev: false
 

	
 
  /@types/n3/1.10.4:
 
    resolution: {integrity: sha512-FfRTwcbXcScVHuAjIASveRWL6Fi6fPALl1Ge8tMESYLqU7R42LJvtdBpUi+f9YK0oQPqIN+zFFgMDFJfLMx0bg==}
 
    dependencies:
 
      '@types/node': 17.0.31
 
      rdf-js: 4.0.2
 
    dev: false
 

	
 
  /@types/node/17.0.31:
 
    resolution: {integrity: sha512-AR0x5HbXGqkEx9CadRH3EBYx/VkiUgZIhP4wvPn/+5KIsgpNoyFaRlVe0Zlx9gRtg8fA06a9tskE2MSN7TcG4Q==}
 
    dev: false
 

	
 
  /@types/reconnectingwebsocket/1.0.7:
 
    resolution: {integrity: sha512-17pnIZsGi9P8YNp7c0ueY2WEauSxDivuFeGuMuPPJMA3qk34CnFgBHGqkxgun3HUifEuwNr8cf+9rU7vSd8i5g==}
 
    dev: false
 

	
 
  /@types/sylvester/0.1.8:
 
    resolution: {integrity: sha512-x1bzR4PCxvv1/9iPrbdQ15gWgP8Tp8EPjO4VLjhMijepB44BzJ/XvJavoPViSiHxlBX6NgzRgO0H+qa68lJFGA==}
 
    dev: false
 
@@ -247,6 +280,10 @@ packages:
 
    resolution: {integrity: sha512-uO4CD2ELOjw8tasUrAhvnn2W4A0ZECOvMjCivJr4gA9pGgjv+qxKWY9GLTMVEK8ej85BxQOocUyE7hImmSQYcg==}
 
    dev: false
 

	
 
  /async/3.2.3:
 
    resolution: {integrity: sha512-spZRyzKL5l5BZQrr/6m/SqFdBN0q3OCI0f9rjfBzCMBIP4p75P620rR3gTmaksNOhmzgdxcaxdNfMy6anrbM0g==}
 
    dev: false
 

	
 
  /commander/7.2.0:
 
    resolution: {integrity: sha512-QrWXB+ZQSVPmIWIhtEO9H+gwHaMGYiF5ChvoJ+K9ZGHG/sVsa6yiesAD1GC/x46sET00Xlwo1u49RVVVzvcSkw==}
 
    engines: {node: '>= 10'}
 
@@ -757,6 +794,10 @@ packages:
 
      safer-buffer: 2.1.2
 
    dev: false
 

	
 
  /inherits/2.0.4:
 
    resolution: {integrity: sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==}
 
    dev: false
 

	
 
  /internmap/2.0.3:
 
    resolution: {integrity: sha512-5Hh7Y1wQbvY5ooGgPbDaL5iYLAPzMTUrjMulskHLH6wnv/A+1q5rgEaiuqEjB+oxGXIVZs1FF+R/KPN3ZSQYYg==}
 
    engines: {node: '>=12'}
 
@@ -797,6 +838,14 @@ packages:
 
    resolution: {integrity: sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==}
 
    dev: false
 

	
 
  /n3/1.16.2:
 
    resolution: {integrity: sha512-5vYa2HuNEJ+a26FEs4FGgfFLgaPOODaZpJlc7FS0eUjDumc4uK0cvx216PjKXBkLzmAsSqGgQPwqztcLLvwDsw==}
 
    engines: {node: '>=8.0'}
 
    dependencies:
 
      queue-microtask: 1.2.3
 
      readable-stream: 3.6.0
 
    dev: false
 

	
 
  /nanoid/3.3.2:
 
    resolution: {integrity: sha512-CuHBogktKwpm5g2sRgv83jEy2ijFzBwMoYA60orPDR7ynsLijJDqgsi4RDGj3OJpy3Ieb+LYwiRmIOGyytgITA==}
 
    engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1}
 
@@ -826,6 +875,29 @@ packages:
 
      source-map-js: 1.0.2
 
    dev: false
 

	
 
  /queue-microtask/1.2.3:
 
    resolution: {integrity: sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==}
 
    dev: false
 

	
 
  /rdf-js/4.0.2:
 
    resolution: {integrity: sha512-ApvlFa/WsQh8LpPK/6hctQwG06Z9ztQQGWVtrcrf9L6+sejHNXLPOqL+w7q3hF+iL0C4sv3AX1PUtGkLNzyZ0Q==}
 
    dependencies:
 
      '@rdfjs/types': 1.1.0
 
    dev: false
 

	
 
  /readable-stream/3.6.0:
 
    resolution: {integrity: sha512-BViHy7LKeTz4oNnkcLJ+lVSL6vpiFeX6/d3oSH8zCW7UxP2onchk+vTGB143xuFjHS3deTgkKoXXymXqymiIdA==}
 
    engines: {node: '>= 6'}
 
    dependencies:
 
      inherits: 2.0.4
 
      string_decoder: 1.3.0
 
      util-deprecate: 1.0.2
 
    dev: false
 

	
 
  /reconnectingwebsocket/1.0.0:
 
    resolution: {integrity: sha1-C4Jbq7N7ZwRFxlqn0+2XgwAgVEQ=}
 
    dev: false
 

	
 
  /resolve/1.22.0:
 
    resolution: {integrity: sha512-Hhtrw0nLeSrFQ7phPp4OOcVjLPIeMnRlr5mcnVuMe7M/7eBn98A3hmFRLoFo3DLZkivSYwhRUJTyPyWAk56WLw==}
 
    hasBin: true
 
@@ -851,6 +923,10 @@ packages:
 
    resolution: {integrity: sha1-P4Yt+pGrdmsUiF700BEkv9oHT7Q=}
 
    dev: false
 

	
 
  /safe-buffer/5.2.1:
 
    resolution: {integrity: sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==}
 
    dev: false
 

	
 
  /safer-buffer/2.1.2:
 
    resolution: {integrity: sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==}
 
    dev: false
 
@@ -864,6 +940,12 @@ packages:
 
    engines: {node: '>=0.10.0'}
 
    dev: false
 

	
 
  /string_decoder/1.3.0:
 
    resolution: {integrity: sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==}
 
    dependencies:
 
      safe-buffer: 5.2.1
 
    dev: false
 

	
 
  /supports-preserve-symlinks-flag/1.0.0:
 
    resolution: {integrity: sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==}
 
    engines: {node: '>= 0.4'}
 
@@ -878,6 +960,10 @@ packages:
 
    resolution: {integrity: sha512-QvjkYpiD+dJJraRA8+dGAU4i7aBbb2s0S3jA45TFOvg2VgqvdCDd/3N6CqA8gluk1W91GLoXg5enMUx560QzuA==}
 
    dev: false
 

	
 
  /util-deprecate/1.0.2:
 
    resolution: {integrity: sha1-RQ1Nyfpw3nMnYvvS1KKJgUGaDM8=}
 
    dev: false
 

	
 
  /vite-plugin-rewrite-all/0.1.2_vite@2.9.1:
 
    resolution: {integrity: sha512-hBFuG043kbixgZ/ke9SzKhkO6P8a5ryxD0CmZTe+/Cz17RIKi7uSeNUJy79V4FgavZ7pWVRg0tqVwJ7lP/A2/Q==}
 
    engines: {node: '>=12.0.0'}
0 comments (0 inline, 0 general)