Changeset - 233b81d9bd9d
[Not reviewed]
default
0 4 0
Drew Perttula - 9 years ago 2016-06-06 05:57:30
drewp@bigasterisk.com
simple first version of SyncedGraph.runHandler
Ignore-this: bbe6aa9fe4edd0de1564b57c1077e711
4 files changed with 338 insertions and 9 deletions:
0 comments (0 inline, 0 general)
.boring
Show inline comments
 
@@ -147,6 +147,7 @@
 
^show/dance2014/music
 
^stagesim/three.js-master
 
^tkdnd
 
^node_modules
 

	
 
# temporary!
 
rgbled/build-nano328/
light9/web/graph.coffee
Show inline comments
 
@@ -7,15 +7,20 @@ log = console.log
 
if require?
 
  `window = {}`
 
  `N3 = require('./lib/N3.js-1d2d975c10ad3252d38393c3ea97b36fd3ab986a/N3.js')`
 
  `d3 = require('./lib/d3/build/d3.min.js')`
 
  `RdfDbClient = require('./rdfdbclient.js').RdfDbClient`
 
  module.exports = window
 

	
 
# (sloppily shared to rdfdbclient.coffee too)
 
window.patchSizeSummary = (patch) ->
 
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
 
class GraphWatchers # throw this one away; use AutoDependencies
 
  constructor: ->
 
    @handlersSp = {} # {s: {p: [handlers]}}
 
  subscribe: (s, p, o, onChange) -> # return subscription handle
 
@@ -52,6 +57,49 @@ class GraphWatchers
 
      for cb in @matchingHandlers(quad)
 
        cb({delQuads: [], addQuads: [quad]})
 

	
 
class Handler
 
  # a function and the quad patterns it cared about
 
  constructor: (@func) ->
 
    patterns = [] # s,p,o,g quads that should trigger the next run
 
  
 
class AutoDependencies
 
  constructor: () ->
 
    @handlers = [] # all known Handlers (at least those with non-empty patterns)
 
    @handlerStack = [] # currently running
 
    
 
  runHandler: (func) ->
 
    # what if we have this func already? duplicate is safe?
 
    
 
    h = new Handler(func)
 
    @handlers.push(h)
 
    @_rerunHandler(h)
 
    
 
  _rerunHandler: (handler) ->
 
    handler.patterns = []
 
    @handlerStack.push(handler)
 
    try
 
      handler.func()
 
    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.
 
    for h in @handlers
 
      @_rerunHandler(h)
 

	
 
  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?
 
      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
 
@@ -64,15 +112,16 @@ class window.SyncedGraph
 
  constructor: (@patchSenderUrl, @prefixes, @setStatus) ->
 
    # patchSenderUrl is the /syncedGraph path of an rdfdb server.
 
    # prefixes can be used in Uri(curie) calls.
 
    @_watchers = new GraphWatchers()
 
    @_watchers = new GraphWatchers() # old
 
    @_autoDeps = new AutoDependencies() # replaces GraphWatchers
 
    @clearGraph()
 

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

	
 
@@ -112,9 +161,9 @@ class window.SyncedGraph
 
  quads: () -> # for debugging
 
    [q.subject, q.predicate, q.object, q.graph] for q in @graph.find()
 

	
 
  applyAndSendPatch: (patch, cb) ->
 
  applyAndSendPatch: (patch) ->
 
    @_applyPatch(patch)
 
    @_client.sendPatch(patch)
 
    @_client.sendPatch(patch) if @_client
 

	
 
  _applyPatch: (patch) ->
 
    # In most cases you want applyAndSendPatch.
 
@@ -124,8 +173,9 @@ class window.SyncedGraph
 
      @graph.removeTriple(quad)
 
    for quad in patch.addQuads
 
      @graph.addTriple(quad)
 
    log('applied patch locally', patchSizeSummary(patch))
 
    #log('applied patch locally', patchSizeSummary(patch))
 
    @_watchers.graphChanged(patch)
 
    @_autoDeps.graphChanged(patch)
 

	
 
  getObjectPatch: (s, p, newObject, g) ->
 
    # send a patch which removes existing values for (s,p,*,c) and
 
@@ -153,7 +203,14 @@ class window.SyncedGraph
 
  unsubscribe: (subscription) ->
 
    @_watchers.unsubscribe(subscription)
 

	
 
  runHandler: (func) ->
 
    # 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).
 
    @_autoDeps.runHandler(func)
 

	
 
  _singleValue: (s, p) ->
 
    @_autoDeps.askedFor(s, p, null, null)
 
    quads = @graph.findByIRI(s, p)
 
    switch quads.length
 
      when 0
 
@@ -174,12 +231,36 @@ class window.SyncedGraph
 
    @_singleValue(s, p)
 

	
 
  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
 

	
 

	
light9/web/graph_test.coffee
Show inline comments
 
log = console.log
 
assert = require('chai').assert
 
expect = require('chai').expect
 
SyncedGraph = require('./graph.js').SyncedGraph
 

	
 
describe 'SyncedGraph', ->
 
@@ -8,3 +10,244 @@ describe 'SyncedGraph', ->
 
      g.quads()
 
      assert.equal(g.quads().length, 0)
 

	
 
  describe 'auto dependencies', ->
 
    graph = new SyncedGraph()
 
    RDF = 'http://www.w3.org/1999/02/22-rdf-syntax-ns#'
 
    U = (tail) -> graph.Uri('http://example.com/' + tail)
 
    A1 = U('a1')
 
    A2 = U('a2')
 
    A3 = U('a3')
 
    A4 = U('a4')
 
    ctx = U('ctx')
 
    quad = (s, p, o) -> {subject: s, predicate: p, object: o, graph: ctx}
 

	
 
    beforeEach (done) ->
 
      graph = new SyncedGraph()
 
      graph.loadTrig("
 
        @prefix : <http://example.com/> .
 
        :ctx {
 
          :a1 :a2 :a3 .
 
          :a1 :someFloat 1.5 .
 
          :a1 :someString \"hello\" .
 
          :a1 :multipleObjects :a4, :a5 .
 
          :a2 a :Type1 .
 
          :a3 a :Type1 .
 
        }
 
      ", done)
 
    
 
    it 'calls a handler right away', ->
 
      called = 0
 
      hand = ->
 
        called++
 
      graph.runHandler(hand)
 
      assert.equal(1, called)
 
      
 
    it 'calls a handler a 2nd time if the graph is patched with relevant data', ->
 
      called = 0
 
      hand = ->
 
        called++
 
        graph.uriValue(A1, A2)
 
      graph.runHandler(hand)
 
      graph.applyAndSendPatch({
 
        delQuads: [quad(A1, A2, A3)], addQuads: [quad(A1, A2, A4)]})
 
      assert.equal(2, called) 
 

	
 
    it 'notices new queries a handler makes upon rerun', ->
 
      called = 0
 
      objsFound = []
 
      hand = ->
 
        called++
 
        graph.uriValue(A1, A2)
 
        if called > 1
 
          objsFound.push(graph.objects(A1, A3))
 
      graph.runHandler(hand)
 
      # first run looked up A1,A2,*
 
      graph.applyAndSendPatch({
 
        delQuads: [quad(A1, A2, A3)], addQuads: [quad(A1, A2, A4)]})
 
      # second run also looked up A1,A3,* (which matched none)
 
      graph.applyAndSendPatch({
 
        delQuads: [], addQuads: [quad(A1, A3, A4)]})
 
      # third run should happen here, noticing the new A1,A3,* match
 
      assert.equal(3, called)
 
      assert.deepEqual([[], [A4]], objsFound)
 

	
 
    it 'calls a handler again even if the handler throws an error', ->
 
      called = 0
 
      hand = ->
 
        called++
 
        graph.uriValue(A1, A2)
 
        throw new Error('this test handler throws an error')
 
      graph.runHandler(hand)
 
      graph.applyAndSendPatch({
 
        delQuads: [quad(A1, A2, A3)], addQuads: [quad(A1, A2, A4)]})
 
      assert.equal(2, called) 
 

	
 
    describe 'works with nested handlers', ->
 

	
 
      innerResults = []
 
      inner = ->
 
        innerResults.push(graph.uriValue(A1, A2))
 

	
 
      outerResults = []
 
      doRunInner = true
 
      outer = ->
 
        if doRunInner
 
          graph.runHandler(inner)
 
        outerResults.push(graph.floatValue(A1, U('someFloat')))
 

	
 
      beforeEach ->
 
        innerResults = []
 
        outerResults = []
 
        doRunInner = true
 

	
 
      affectInner = {
 
        delQuads: [quad(A1, A2, A3)], addQuads: [quad(A1, A2, A4)]
 
      }
 
      affectOuter = {
 
        delQuads: [
 
          quad(A1, U('someFloat'), '"1.5"^^http://www.w3.org/2001/XMLSchema#decimal')
 
        ], addQuads: [
 
          quad(A1, U('someFloat'), graph.LiteralRoundedFloat(2))
 
        ]}
 
      affectBoth = {
 
        delQuads: affectInner.delQuads.concat(affectOuter.delQuads),
 
        addQuads: affectInner.addQuads.concat(affectOuter.addQuads)
 
        }
 
                  
 
      it 'calls everything normally once', ->
 
        graph.runHandler(outer)
 
        assert.deepEqual([A3], innerResults)
 
        assert.deepEqual([1.5], outerResults)
 

	
 
      it '[performance] reruns just the inner if its dependencies change', ->
 
        graph.runHandler(outer)
 
        graph.applyAndSendPatch(affectInner)
 
        assert.deepEqual([A3, A4], innerResults)
 
        assert.deepEqual([1.5], outerResults)
 
        
 
      it '[performance] reruns the outer (and therefore inner) if its dependencies change', ->
 
        graph.runHandler(outer)
 
        graph.applyAndSendPatch(affectOuter)
 
        assert.deepEqual([A3, A3], innerResults)
 
        assert.deepEqual([1.5, 2], outerResults)
 
        
 
        
 
      it '[performance] does not send a redundant inner run if it is already rerunning outer', ->
 
        # Note that outer may or may not call inner each time, and we
 
        # don't want to redundantly call inner. We need to:
 
        #  1. build the set of handlers to rerun,
 
        #  2. call them from outside-in, and
 
        #  3. any runHandler calls that happen, they need to count as reruns.
 
        graph.runHandler(outer)
 
        graph.applyAndSendPatch(affectBoth)
 
        assert.deepEqual([A3, A4], innerResults)
 
        assert.deepEqual([1.5, 2], outerResults)
 

	
 
      it 'reruns the outer and the inner if all dependencies change, but outer omits calling inner this time', ->
 
        graph.runHandler(outer)
 
        doRunInner = false
 
        graph.applyAndSendPatch(affectBoth)
 
        assert.deepEqual([A3, A4], innerResults)
 
        assert.deepEqual([1.5, 2], outerResults)
 
        
 
    describe 'watches calls to:', ->
 
      it 'floatValue', ->
 
        values = []
 
        hand = -> values.push(graph.floatValue(A1, U('someFloat')))
 
        graph.runHandler(hand)
 
        graph.patchObject(A1, U('someFloat'), graph.LiteralRoundedFloat(2), ctx)
 
        assert.deepEqual([1.5, 2.0], values)
 
        
 
      it 'stringValue', ->
 
        values = []
 
        hand = -> values.push(graph.stringValue(A1, U('someString')))
 
        graph.runHandler(hand)
 
        graph.patchObject(A1, U('someString'), graph.Literal('world'), ctx)
 
        assert.deepEqual(['hello', 'world'], values)
 

	
 
      it 'uriValue', ->
 
        # covered above, but this one tests patchObject on a uri, too
 
        values = []
 
        hand = -> values.push(graph.uriValue(A1, A2))
 
        graph.runHandler(hand)
 
        graph.patchObject(A1, A2, A4, ctx)
 
        assert.deepEqual([A3, A4], values)
 

	
 
      it 'objects', ->
 
        values = []
 
        hand = -> values.push(graph.objects(A1, U('multipleObjects')))
 
        graph.runHandler(hand)
 
        graph.patchObject(A1, U('multipleObjects'), U('newOne'), ctx)
 
        expect(values[0]).to.deep.have.members([U('a4'), U('a5')])
 
        expect(values[1]).to.deep.have.members([U('newOne')])
 

	
 
      it 'subjects', ->
 
        values = []
 
        rdfType = graph.Uri(RDF + 'type')
 
        hand = -> values.push(graph.subjects(rdfType, U('Type1')))
 
        graph.runHandler(hand)
 
        graph.applyAndSendPatch(
 
          {delQuads: [], addQuads: [quad(A4, rdfType, U('Type1'))]})
 
        expect(values[0]).to.deep.have.members([A2, A3])
 
        expect(values[1]).to.deep.have.members([A2, A3, A4])
 

	
 
      describe 'items', ->
 
        it 'when the list order changes', (done) ->
 
          values = []
 
          successes = 0
 
          hand = ->
 
            try 
 
              head = graph.uriValue(U('x'), U('y'))
 
            catch
 
              # graph goes empty between clearGraph and loadTrig
 
              return
 
            values.push(graph.items(head))
 
            successes++
 
          graph.clearGraph()
 
          graph.loadTrig "
 
            @prefix : <http://example.com/> .
 
            :ctx { :x :y (:a1 :a2 :a3) } .
 
          ", () ->
 
            graph.runHandler(hand)
 
            graph.clearGraph()
 
            graph.loadTrig "
 
              @prefix : <http://example.com/> .
 
              :ctx { :x :y (:a1 :a3 :a2) } .
 
            ", () ->
 
              assert.deepEqual([[A1, A2, A3], [A1, A3, A2]], values)
 
              assert.equal(2, successes)
 
              done()
 
  
 
      describe 'contains', ->
 
        it 'when a new triple is added', ->
 
          values = []
 
          hand = -> values.push(graph.contains(A1, A1, A1))
 
          graph.runHandler(hand)
 
          graph.applyAndSendPatch(
 
            {delQuads: [], addQuads: [quad(A1, A1, A1)]})
 
          assert.deepEqual([false, true], values)
 

	
 
        it 'when a relevant triple is removed', ->
 
          values = []
 
          hand = -> values.push(graph.contains(A1, A2, A3))
 
          graph.runHandler(hand)
 
          graph.applyAndSendPatch(
 
            {delQuads: [quad(A1, A2, A3)], addQuads: []})
 
          assert.deepEqual([true, false], values)
 

	
 
    describe 'performs well', ->
 
      it "[performance] doesn't call handler a 2nd time if the graph gets an unrelated patch", ->
 
        called = 0
 
        hand = ->
 
          called++
 
          graph.uriValue(A1, A2)
 
        graph.runHandler(hand)
 
        graph.applyAndSendPatch({
 
          delQuads: [], addQuads: [quad(A2, A3, A4)]})
 
        assert.equal(1, called)
 

	
 
      it '[performance] calls a handler 2x but then not again if the handler stopped caring about the data', ->
 
        assert.fail()
 

	
 
      it "[performance] doesn't get slow if the handler makes tons of repetitive lookups", ->
 
        assert.fail()
makefile
Show inline comments
 
@@ -17,6 +17,10 @@ test_js:
 
	coffee -c light9/web/*.coffee
 
	node_modules/mocha/bin/mocha --compilers coffee:coffee-script/register --globals window,N3 light9/web/graph_test.coffee
 

	
 
test_js_watch:
 
	# have coffee continuously running
 
	watch -c node_modules/mocha/bin/mocha --compilers coffee:coffee-script/register --globals window,N3 light9/web/graph_test.coffee --colors
 

	
 
# needed packages: python-gtk2 python-imaging
 

	
 
binexec:
0 comments (0 inline, 0 general)