diff --git a/bin/rdfdb b/bin/rdfdb --- a/bin/rdfdb +++ b/bin/rdfdb @@ -107,9 +107,10 @@ Our web ui: to this, focused on that resource """ -from twisted.internet import reactor +from twisted.internet import reactor, defer import twisted.internet.error from twisted.python.filepath import FilePath +from twisted.python.failure import Failure from twisted.internet.inotify import humanReadableMask, IN_CREATE import sys, optparse, logging, json, os import cyclone.web, cyclone.httpclient, cyclone.websocket @@ -129,10 +130,12 @@ log.setLevel(logging.DEBUG) from lib.cycloneerr import PrettyErrorHandler +class WebsocketDisconnect(ValueError): + pass + def sendGraphToClient(graph, client): """send the client the whole graph contents""" - log.info("sending all graphs to %s at %s" % - (client.label, client.updateUri)) + log.info("sending all graphs to %r" % client) client.sendPatch(Patch( addQuads=graph.quads(ALLSTMTS), delQuads=[])) @@ -144,14 +147,33 @@ class Client(object): """ def __init__(self, updateUri, label): self.label = label + # todo: updateUri is used publicly to compare clients. Replace + # it with Client.__eq__ so WsClient doesn't have to fake an + # updateUri. self.updateUri = updateUri def __repr__(self): return "<%s client at %s>" % (self.label, self.updateUri) def sendPatch(self, p): + """ + returns deferred. error will be interpreted as the client being + broken. + """ return sendPatch(self.updateUri, p) + +class WsClient(object): + def __init__(self, connectionId, sendMessage): + self.updateUri = connectionId + self.sendMessage = sendMessage + def __repr__(self): + return "" % self.updateUri + + def sendPatch(self, p): + self.sendMessage(p.makeJsonRepr()) + return defer.succeed(None) + class WatchedFiles(object): """ find files, notice new files. @@ -304,8 +326,8 @@ class Db(object): d.addErrback(self.clientErrored, c) def clientErrored(self, err, c): - err.trap(twisted.internet.error.ConnectError) - log.info("connection error- dropping client %r" % c) + err.trap(twisted.internet.error.ConnectError, WebsocketDisconnect) + log.info("%r %r - dropping client", c, err.getErrorMessage()) self.clients.remove(c) self.sendClientsToAllLivePages() @@ -335,14 +357,14 @@ class Db(object): [self.clients.remove(c) for c in self.clients if c.updateUri == newClient.updateUri] - log.info("new client %s at %s" % (newClient.label, newClient.updateUri)) + log.info("new client %r" % newClient) sendGraphToClient(self.graph, newClient) self.clients.append(newClient) self.sendClientsToAllLivePages() def sendClientsToAllLivePages(self): sendToLiveClients({"clients":[ - dict(updateUri=c.updateUri, label=c.label) + dict(updateUri=c.updateUri, label=repr(c)) for c in self.clients]}) class GraphResource(PrettyErrorHandler, cyclone.web.RequestHandler): @@ -376,6 +398,28 @@ class GraphClients(PrettyErrorHandler, c import traceback traceback.print_exc() raise + +_wsClientSerial = 0 +class WebsocketClient(cyclone.websocket.WebSocketHandler): + + def connectionMade(self, *args, **kwargs): + global _wsClientSerial + self.connectionId = 'connection-%s' % _wsClientSerial + log.info("new ws client %r", self.connectionId) + _wsClientSerial += 1 + + self.wsClient = WsClient(self.connectionId, self.sendMessage) + self.settings.db.addClient(self.wsClient) + + def connectionLost(self, reason): + log.info("bye ws client %r", self.connectionId) + self.settings.db.clientErrored( + Failure(WebsocketDisconnect(reason)), self.wsClient) + + def messageReceived(self, message): + log.info("got message from %s: %s", self.connectionId, message) + # how + self.sendMessage(message) liveClients = set() def sendToLiveClients(d=None, asJson=None): @@ -427,6 +471,7 @@ if __name__ == "__main__": (r'/graph', GraphResource), (r'/patches', Patches), (r'/graphClients', GraphClients), + (r'/syncedGraph', WebsocketClient), (r'/(.*)', NoExts, {"path" : "light9/rdfdb/web", diff --git a/light9/rdfdb/web/syncedgraph.js b/light9/rdfdb/web/syncedgraph.js --- a/light9/rdfdb/web/syncedgraph.js +++ b/light9/rdfdb/web/syncedgraph.js @@ -3,6 +3,8 @@ function SyncedGraph(label) { like python SyncedGraph but talks over a websocket to rdfdb. This one has an API more conducive to reading and querying. + + light9/web/graph.coffee is the newer attempt */ var self = this; diff --git a/light9/web/graph.coffee b/light9/web/graph.coffee --- a/light9/web/graph.coffee +++ b/light9/web/graph.coffee @@ -40,11 +40,50 @@ class window.SyncedGraph # Note that applyPatch is the only method to write to the graph, so # it can fire subscriptions. - constructor: (patchSenderUrl, prefixes) -> + constructor: (@patchSenderUrl, prefixes) -> @graph = N3.Store() @_addPrefixes(prefixes) @_watchers = new GraphWatchers() + @newConnection() + newConnection: -> + fullUrl = 'ws://' + window.location.host + @patchSenderUrl + @ws = new WebSocket(fullUrl) + + @ws.onopen = => + log('connected to', fullUrl) + + @ws.onerror = (e) => + log('ws error ' + e) + + @ws.onclose = => + log('ws close') + + @ws.onmessage = (evt) => + @onMessage(JSON.parse(evt.data)) + + onMessage: (msg) -> + log('from rdfdb: ', msg) + + patch = {delQuads: [], addQuads: []} + + parseAdds = (cb) => + parser = N3.Parser() + parser.parse msg.patch.adds, (error, quad, prefixes) => + if (quad) + patch.addQuads.push(quad) + else + cb() + parseDels = (cb) => + parser = N3.Parser() + parser.parse msg.patch.deletes, (error, quad, prefixes) => + if (quad) + patch.delQuads.push(quad) + else + cb() + + async.parallel([parseAdds, parseDels], ((err) => @applyPatch(patch))) + _addPrefixes: (prefixes) -> @graph.addPrefixes(prefixes) @@ -87,6 +126,7 @@ class window.SyncedGraph @graph.removeTriple(quad) for quad in patch.addQuads @graph.addTriple(quad) + log('applied patch -' + patch.delQuads.length + ' +' + patch.addQuads.length) @_watchers.graphChanged(patch) getObjectPatch: (s, p, newObject, g) -> diff --git a/light9/web/lib/bower.json b/light9/web/lib/bower.json --- a/light9/web/lib/bower.json +++ b/light9/web/lib/bower.json @@ -1,5 +1,5 @@ { - "name": "3rd-party polymer elements", + "name": "3rd-party libs", "dependencies": { "polymer": "~1.4.0", "paper-slider": "PolymerElements/paper-slider#~1.0.11", @@ -14,7 +14,8 @@ "rdflib.js": "https://github.com/linkeddata/rdflib.js.git#920e59fe37", "rdfstore": "https://github.com/antoniogarrote/rdfstore-js.git#b3f7c0c9c1da9b26261af0d4858722fa982411bb", "N3.js": "https://github.com/RubenVerborgh/N3.js.git#04f4e21f4ccb351587dc00a3f26340b28d4bb10f", - "shortcut": "http://www.openjs.com/scripts/events/keyboard_shortcuts/shortcut.js" + "shortcut": "http://www.openjs.com/scripts/events/keyboard_shortcuts/shortcut.js", + "async": "https://github.com/caolan/async.git#^1.5.2" }, "resolutions": { "paper-styles": "^1.1.4", diff --git a/light9/web/rdfdb-synced-graph.html b/light9/web/rdfdb-synced-graph.html --- a/light9/web/rdfdb-synced-graph.html +++ b/light9/web/rdfdb-synced-graph.html @@ -13,23 +13,11 @@ graph: {type: Object, notify: true} }, ready: function() { - this.graph = new SyncedGraph('noServerYet', { + this.graph = new SyncedGraph('/rdfdb/syncedGraph', { '': 'http://light9.bigasterisk.com/', 'xsd': 'http://www.w3.org/2001/XMLSchema#', }); - this.graph.loadTrig( - ' @prefix : .'+ - ' @prefix dev: .'+ - ' {'+ - ' :demoResource0 :startTime 1; :endTime 120 .'+ - ' :demoResource1 :startTime 13; :endTime 16 .'+ - ' :demoResource2 :startTime 38; :endTime 60 .'+ - ' :demoResource3 :startTime 56; :endTime 60 .'+ - ' :demoResource4 :startTime 73; :endTime 74 .'+ - ' :demoResource5 :startTime 91; :endTime 105 .'+ - ' :demoResource6 :startTime 110; :endTime 120 .'+ - ' :demoResource7 :startTime 133; :endTime 140 .'+ - ' }'); + window.graph = this.graph; } }); diff --git a/light9/web/timeline-elements.html b/light9/web/timeline-elements.html --- a/light9/web/timeline-elements.html +++ b/light9/web/timeline-elements.html @@ -307,8 +307,9 @@ - - + + + diff --git a/light9/web/timeline.coffee b/light9/web/timeline.coffee --- a/light9/web/timeline.coffee +++ b/light9/web/timeline.coffee @@ -131,7 +131,7 @@ Polymer newCenter + visSeconds / 2, zoomAnimSec) persistDemo: -> - ctx = @graph.Uri('http://example.com/') + ctx = @graph.Uri('http://light9.bigasterisk.com/show/dance2016/song1') adjs = [] for n in [0..7] subj = @graph.Uri(':demoResource'+n) @@ -225,11 +225,14 @@ Polymer @graph.subscribe("http://light9.bigasterisk.com/demoResource6", null, null, @_onIronResize.bind(@)) _onIronResize: -> return if !@zoomInX - subj = "http://light9.bigasterisk.com/demoResource6" - setNote(subj, - @zoomInX(@graph.floatValue(subj, @graph.Uri(':startTime'))), - @zoomInX(@graph.floatValue(subj, @graph.Uri(':endTime'))), - @offsetTop, @offsetTop + @offsetHeight) + try + subj = "http://light9.bigasterisk.com/demoResource6" + setNote(subj, + @zoomInX(@graph.floatValue(subj, @graph.Uri(':startTime'))), + @zoomInX(@graph.floatValue(subj, @graph.Uri(':endTime'))), + @offsetTop, @offsetTop + @offsetHeight) + catch e + log('during resize, ', e) Polymer is: "light9-timeline-adjusters" diff --git a/show/dance2016/song1.n3 b/show/dance2016/song1.n3 new file mode 100644 --- /dev/null +++ b/show/dance2016/song1.n3 @@ -0,0 +1,11 @@ +@prefix : . +@prefix dev: . + + :demoResource0 :startTime 1; :endTime 120.3 . + :demoResource1 :startTime 13; :endTime 16 . + :demoResource2 :startTime 38; :endTime 60 . + :demoResource3 :startTime 56; :endTime 60 . + :demoResource4 :startTime 73; :endTime 74 . + :demoResource5 :startTime 91.88; :endTime 105 . + :demoResource6 :startTime 110; :endTime 120 . + :demoResource7 :startTime 133; :endTime 140 .