diff --git a/.boring b/.boring
--- a/.boring
+++ b/.boring
@@ -147,6 +147,7 @@
^show/dance2014/music
^stagesim/three.js-master
^tkdnd
+^node_modules
# temporary!
rgbled/build-nano328/
diff --git a/light9/web/graph.coffee b/light9/web/graph.coffee
--- a/light9/web/graph.coffee
+++ b/light9/web/graph.coffee
@@ -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
+
diff --git a/light9/web/graph_test.coffee b/light9/web/graph_test.coffee
--- a/light9/web/graph_test.coffee
+++ b/light9/web/graph_test.coffee
@@ -1,4 +1,6 @@
+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 : .
+ :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 : .
+ :ctx { :x :y (:a1 :a2 :a3) } .
+ ", () ->
+ graph.runHandler(hand)
+ graph.clearGraph()
+ graph.loadTrig "
+ @prefix : .
+ :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()
diff --git a/makefile b/makefile
--- a/makefile
+++ b/makefile
@@ -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: