Files @ 4a751aaaee52
Branch filter:

Location: light9/light9/web/graph.coffee - annotation

Drew Perttula
big timeline rewrites. hopefully it's faster and less leaky
Ignore-this: f11cbe9b5f0950af7edfb91682470e60
2809a8b732f6
2809a8b732f6
4c6d88aa9e26
4c6d88aa9e26
4c6d88aa9e26
168262618f2d
168262618f2d
168262618f2d
ca5b10d7ecd1
233b81d9bd9d
168262618f2d
168262618f2d
168262618f2d
233b81d9bd9d
233b81d9bd9d
233b81d9bd9d
36f58b2aa8ef
36f58b2aa8ef
233b81d9bd9d
233b81d9bd9d
233b81d9bd9d
4c6d88aa9e26
233b81d9bd9d
4c6d88aa9e26
4c6d88aa9e26
4c6d88aa9e26
4c6d88aa9e26
4c6d88aa9e26
4c6d88aa9e26
4c6d88aa9e26
4c6d88aa9e26
4c6d88aa9e26
49f80544a3fb
49f80544a3fb
4c6d88aa9e26
4c6d88aa9e26
49f80544a3fb
49f80544a3fb
49f80544a3fb
49f80544a3fb
49f80544a3fb
8406a9fbeb9d
8406a9fbeb9d
8406a9fbeb9d
8406a9fbeb9d
8406a9fbeb9d
8406a9fbeb9d
8406a9fbeb9d
4c6d88aa9e26
4c6d88aa9e26
4c6d88aa9e26
8406a9fbeb9d
4c6d88aa9e26
4c6d88aa9e26
4c6d88aa9e26
4c6d88aa9e26
8406a9fbeb9d
4c6d88aa9e26
4c6d88aa9e26
233b81d9bd9d
233b81d9bd9d
29a1382de5c8
29a1382de5c8
29a1382de5c8
233b81d9bd9d
233b81d9bd9d
233b81d9bd9d
29a1382de5c8
29a1382de5c8
233b81d9bd9d
29a1382de5c8
233b81d9bd9d
29a1382de5c8
29a1382de5c8
29a1382de5c8
b95b97177de6
ea9e8f581eea
b95b97177de6
233b81d9bd9d
ea9e8f581eea
233b81d9bd9d
233b81d9bd9d
233b81d9bd9d
ea9e8f581eea
233b81d9bd9d
233b81d9bd9d
233b81d9bd9d
233b81d9bd9d
233b81d9bd9d
233b81d9bd9d
233b81d9bd9d
233b81d9bd9d
233b81d9bd9d
233b81d9bd9d
233b81d9bd9d
29a1382de5c8
29a1382de5c8
29a1382de5c8
29a1382de5c8
edc17fdcaaf1
edc17fdcaaf1
ea9e8f581eea
edc17fdcaaf1
29a1382de5c8
233b81d9bd9d
233b81d9bd9d
233b81d9bd9d
233b81d9bd9d
233b81d9bd9d
29a1382de5c8
233b81d9bd9d
233b81d9bd9d
4c6d88aa9e26
59eab70254fa
59eab70254fa
59eab70254fa
59eab70254fa
59eab70254fa
59eab70254fa
59eab70254fa
59eab70254fa
9476e97b57fb
59eab70254fa
59eab70254fa
233b81d9bd9d
233b81d9bd9d
59eab70254fa
59eab70254fa
168262618f2d
168262618f2d
168262618f2d
59eab70254fa
233b81d9bd9d
233b81d9bd9d
59eab70254fa
59eab70254fa
59eab70254fa
59eab70254fa
59eab70254fa
59eab70254fa
3bb58b74c9c1
36f58b2aa8ef
5c54a1f94050
4c6d88aa9e26
4c6d88aa9e26
4c6d88aa9e26
4c6d88aa9e26
4c6d88aa9e26
4c6d88aa9e26
4c6d88aa9e26
4c6d88aa9e26
4c6d88aa9e26
36f58b2aa8ef
36f58b2aa8ef
36f58b2aa8ef
36f58b2aa8ef
4c6d88aa9e26
4c6d88aa9e26
4c6d88aa9e26
4c6d88aa9e26
4c6d88aa9e26
4c6d88aa9e26
4c6d88aa9e26
4c6d88aa9e26
4c6d88aa9e26
4c6d88aa9e26
4c6d88aa9e26
59eab70254fa
4c6d88aa9e26
4c6d88aa9e26
4c6d88aa9e26
4c6d88aa9e26
4c6d88aa9e26
4c6d88aa9e26
233b81d9bd9d
73762e14ee98
73762e14ee98
59eab70254fa
233b81d9bd9d
4c6d88aa9e26
59eab70254fa
4c6d88aa9e26
4c6d88aa9e26
59eab70254fa
3bb58b74c9c1
4c6d88aa9e26
4c6d88aa9e26
4c6d88aa9e26
4c6d88aa9e26
233b81d9bd9d
4c6d88aa9e26
233b81d9bd9d
4c6d88aa9e26
4c6d88aa9e26
2713d2f7a0fc
4c6d88aa9e26
4c6d88aa9e26
4c6d88aa9e26
4c6d88aa9e26
4c6d88aa9e26
4c6d88aa9e26
4c6d88aa9e26
4c6d88aa9e26
4c6d88aa9e26
4c6d88aa9e26
4c6d88aa9e26
4c6d88aa9e26
29a1382de5c8
4c6d88aa9e26
4c6d88aa9e26
4c6d88aa9e26
49f80544a3fb
4c6d88aa9e26
4c6d88aa9e26
4c6d88aa9e26
49f80544a3fb
4c6d88aa9e26
4c6d88aa9e26
4c6d88aa9e26
4c6d88aa9e26
29a1382de5c8
233b81d9bd9d
233b81d9bd9d
233b81d9bd9d
4a751aaaee52
4a751aaaee52
4a751aaaee52
4a751aaaee52
4a751aaaee52
4a751aaaee52
4a751aaaee52
4a751aaaee52
29a1382de5c8
233b81d9bd9d
16aa26b7d685
233b81d9bd9d
4c6d88aa9e26
d4de13e83b7f
d4de13e83b7f
d4de13e83b7f
16aa26b7d685
16aa26b7d685
4c6d88aa9e26
d4de13e83b7f
6ea2e1aa5070
4c6d88aa9e26
d4de13e83b7f
16aa26b7d685
16aa26b7d685
3bb58b74c9c1
3bb58b74c9c1
3bb58b74c9c1
4a751aaaee52
3bb58b74c9c1
3bb58b74c9c1
3bb58b74c9c1
3bb58b74c9c1
4c6d88aa9e26
4c6d88aa9e26
6ea2e1aa5070
16aa26b7d685
4c6d88aa9e26
6ea2e1aa5070
4c6d88aa9e26
013cbd7a0f08
013cbd7a0f08
013cbd7a0f08
013cbd7a0f08
013cbd7a0f08
013cbd7a0f08
013cbd7a0f08
4c6d88aa9e26
233b81d9bd9d
6ea2e1aa5070
6ea2e1aa5070
4c6d88aa9e26
4c6d88aa9e26
233b81d9bd9d
233b81d9bd9d
233b81d9bd9d
4c6d88aa9e26
4c6d88aa9e26
233b81d9bd9d
233b81d9bd9d
233b81d9bd9d
233b81d9bd9d
233b81d9bd9d
233b81d9bd9d
233b81d9bd9d
233b81d9bd9d
233b81d9bd9d
233b81d9bd9d
233b81d9bd9d
233b81d9bd9d
233b81d9bd9d
233b81d9bd9d
233b81d9bd9d
233b81d9bd9d
233b81d9bd9d
4c6d88aa9e26
4c6d88aa9e26
233b81d9bd9d
233b81d9bd9d
4c6d88aa9e26
26fabcf3a0a8
26fabcf3a0a8
26fabcf3a0a8
26fabcf3a0a8
26fabcf3a0a8
26fabcf3a0a8
26fabcf3a0a8
26fabcf3a0a8
26fabcf3a0a8
26fabcf3a0a8
233b81d9bd9d
26fabcf3a0a8
26fabcf3a0a8
26fabcf3a0a8
2713d2f7a0fc
2713d2f7a0fc
2713d2f7a0fc
2713d2f7a0fc
2713d2f7a0fc
2713d2f7a0fc
log = console.log

# Patch is {addQuads: <quads>, delQuads: <quads>}
# <quads> is [{subject: s, ...}, ...]

# for mocha
if require?
  `window = {}`
  `N3 = require('./lib/N3.js-pull61/N3.js')`
  `d3 = require('./lib/d3/build/d3.min.js')`
  `RdfDbClient = require('./rdfdbclient.js').RdfDbClient`
  module.exports = window

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

patchSizeSummary = (patch) ->
  '-' + patch.delQuads.length + ' +' + patch.addQuads.length

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

# partial port of autodepgraphapi.py
class GraphWatchers # throw this one away; use AutoDependencies
  constructor: ->
    @handlersSp = {} # {s: {p: [handlers]}}
  subscribe: (s, p, o, onChange) -> # return subscription handle
    if o? then throw Error('not implemented')
    if not @handlersSp[s]
      @handlersSp[s] = {}
    if not @handlersSp[s][p]
      @handlersSp[s][p] = []
    @handlersSp[s][p].push(onChange)
    handle = {s: s, p: p, func: onChange}
    return handle
    
  unsubscribe: (subscription) ->
    spList = @handlersSp[subscription.s][subscription.p]
    i = spList.indexOf(subscription.func)
    if i == -1
      throw new Error('subscription not found')
    spList.splice(i, 1)

  matchingHandlers: (quad) ->
    matches = []
    for subjDict in [@handlersSp[quad.subject] || {}, @handlersSp[null] || {}]
      for subjPredMatches in [subjDict[quad.predicate] || [], subjDict[null] || []]
        matches = matches.concat(subjPredMatches)
    return matches
    
  graphChanged: (patch) ->
    for quad in patch.delQuads
      for cb in @matchingHandlers(quad)
        # currently calls multiple times, which is ok, but we might
        # group things into fewer patches
        cb({delQuads: [quad], addQuads: []})
    for quad in patch.addQuads
      for cb in @matchingHandlers(quad)
        cb({delQuads: [], addQuads: [quad]})

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: () ->
    @handlers = new Handler(null) # tree of all known Handlers (at least those with non-empty patterns). Top node is not a handler.
    @handlerStack = [@handlers] # currently running
    
  runHandler: (func, label) ->
    # what if we have this func already? duplicate is safe?

    h = new Handler(func, label)
    @handlerStack[@handlerStack.length - 1].innerHandlers.push(h)
    console.time("handler #{label}")
    @_rerunHandler(h, null)
    console.timeEnd("handler #{label}")
    
  _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
    
  graphChanged: (patch) ->
    # SyncedGraph is telling us this patch just got applied to the graph.

    rerunInners = (cur) =>
      toRun = cur.innerHandlers.slice()
      for child in toRun

        #child.innerHandlers = [] # let all children get called again
        @_rerunHandler(child, patch)
        rerunInners(child)
    rerunInners(@handlers)

  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)

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) ->
    # patchSenderUrl is the /syncedGraph path of an rdfdb server.
    # prefixes can be used in Uri(curie) calls.
    @_watchers = new GraphWatchers() # old
    @_autoDeps = new AutoDependencies() # replaces GraphWatchers
    @clearGraph()

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

    # if we had a Store already, this lets N3.Store free all its indices/etc
    @graph = N3.Store()
    @_addPrefixes(@prefixes)
    @cachedFloatValues = new Map();
    
      
  _addPrefixes: (prefixes) ->
    @graph.addPrefixes(prefixes)
        
  Uri: (curie) ->
    N3.Util.expandPrefixedName(curie, @graph._prefixes)

  Literal: (jsValue) ->
    N3.Util.createLiteral(jsValue)

  LiteralRoundedFloat: (f) ->
    N3.Util.createLiteral(d3.format(".3f")(f),
                          "http://www.w3.org/2001/XMLSchema#decimal")

  toJs: (literal) ->
    # incomplete
    parseFloat(N3.Util.getLiteralValue(literal))

  loadTrig: (trig, cb) -> # for debugging
    patch = {delQuads: [], addQuads: []}
    parser = N3.Parser()
    parser.parse trig, (error, quad, prefixes) =>
                  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.find()

  applyAndSendPatch: (patch) ->
    if !Array.isArray(patch.addQuads) || !Array.isArray(patch.delQuads)
      throw new Error("corrupt patch: #{patch}")
    @_applyPatch(patch)
    @_client.sendPatch(patch) if @_client

  _applyPatch: (patch) ->
    # In most cases you want applyAndSendPatch.
    # 
    # This is the only method that writes to @graph!
    @cachedFloatValues.clear()
    for quad in patch.delQuads
      @graph.removeTriple(quad)
    for quad in patch.addQuads
      @graph.addTriple(quad)
    #log('applied patch locally', patchSizeSummary(patch))
    @_watchers.graphChanged(patch)
    @_autoDeps.graphChanged(patch)

  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.findByIRI(s, p, null, g)
    return {
      delQuads: existing,
      addQuads: [{subject: s, predicate: p, object: newObject, graph: g}]
    }

  patchObject: (s, p, newObject, g) ->
    @applyAndSendPatch(@getObjectPatch(s, p, newObject, g))
  

  subscribe: (s, p, o, onChange) -> # return subscription handle
    throw
    # onChange is called with a patch that's limited to the quads
    # that match your request.
    # We call you immediately on existing triples.
    handle = @_watchers.subscribe(s, p, o, onChange)
    immediatePatch = {delQuads: [], addQuads: @graph.findByIRI(s, p, o)}
    if immediatePatch.addQuads.length
      onChange(immediatePatch)
    return handle

  unsubscribe: (subscription) ->
    @_watchers.unsubscribe(subscription)

  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).

    # 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)

  _singleValue: (s, p) ->
    @_autoDeps.askedFor(s, p, null, null)
    quads = @graph.findByIRI(s, p)
    objs = new Set(q.object for q in quads)
    
    switch objs.size
      when 0
        throw new Error("no value for "+s+" "+p)
      when 1
        obj = objs.values().next().value
        return obj
      else
        throw new Error("too many different values: " + JSON.stringify(quads))

  floatValue: (s, p) ->
    key = s + '|' + p
    hit = @cachedFloatValues.get(key)
    return hit if hit != undefined
    #log('float miss', s, p)

    ret = parseFloat(N3.Util.getLiteralValue(@_singleValue(s, p)))
    @cachedFloatValues.set(key, ret)
    return ret
    
  stringValue: (s, p) ->
    N3.Util.getLiteralValue(@_singleValue(s, p))
    
  uriValue: (s, p) ->
    @_singleValue(s, p)

  labelOrTail: (uri) ->
    try
      @graph.stringValue(uri, @graph.Uri('rdfs:label'))
    catch
      words = uri.split('/')
      words[words.length-1]

  objects: (s, p) ->
    @_autoDeps.askedFor(s, p, null, null)
    quads = @graph.findByIRI(s, p)
    return (q.object for q in quads)

  subjects: (p, o) ->
    @_autoDeps.askedFor(null, p, o, null)
    quads = @graph.findByIRI(null, p, o)
    return (q.subject for q in quads)

  items: (list) ->
    out = []
    current = list
    while true
      if current == RDF + 'nil'
        break
        
      firsts = @graph.findByIRI(current, RDF + 'first', null)
      rests = @graph.findByIRI(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)

      if rests.length != 1
        throw new Error("list node #{current} has #{rests.length} rdf:rest edges")
      current = rests[0].object
    
    return out

  contains: (s, p, o) ->
    @_autoDeps.askedFor(s, p, o, null)
    return @graph.findByIRI(s, p, o).length > 0

  nextNumberedResources: (base, howMany) ->
    results = []
    # we could cache [base,lastSerial]
    for serial in [0..1000]
      uri = @Uri("#{base}#{serial}")
      if not @contains(uri, null, null)
        results.push(uri)
        if results.length >= howMany
          return results
    throw new Error("can't make sequential uri with base #{base}")

  nextNumberedResource: (base) ->
    @nextNumberedResources(base, 1)[0]       

  contextsWithPattern: (s, p, o) ->
    ctxs = []
    for q in @graph.find(s, p, o)
      ctxs.push(q.graph)
    return _.unique(ctxs)