diff --git a/app/server.hoon b/app/server.hoon index 89adc2002..5a15fc45d 100644 --- a/app/server.hoon +++ b/app/server.hoon @@ -36,17 +36,33 @@ ; Time is ;span#time; == - ;script:''' - var evtSource = new EventSource("/~server/stream", - { withCredentials: true } ); - - evtSource.onmessage = function(e) { - var message = document.getElementById("time"); - message.innerHTML = e.data; - } - ''' + ;script(type "module", src "/~server/hello.js"); == == +:: +++ hello-js + ^- octs + %- as-octs:mimes:html + ''' + import * as urb from '/~/channel/channel.js'; + + var c = urb.newChannel(); + c.poke("zod", "server", "json", 5, + function() { + console.log("Poke worked"); + }, + function(err) { + console.log("Poke failed: " + err); + }); + + var evtSource = new EventSource("/~server/stream", + { withCredentials: true } ); + + evtSource.onmessage = function(e) { + var message = document.getElementById("time"); + message.innerHTML = e.data; + } + ''' :: helper library that lets an app handle an EventSource. :: :: TODO: This doesn't even attempt to deal with sequence numbers. @@ -194,6 +210,15 @@ (handle-start-stream inbound-request) ~& [%name name] :: + ?: =(name 'hello') + :_ this + :~ ^- move + :- ost.bow + :* %http-response + [%start 200 ['content-type' 'application/javascript']~ [~ hello-js] %.y] + == + == + :: :_ this :~ ^- move :- ost.bow diff --git a/sys/vane/light.hoon b/sys/vane/light.hoon index 574b4e29c..a3cd48c3f 100644 --- a/sys/vane/light.hoon +++ b/sys/vane/light.hoon @@ -415,6 +415,107 @@ ~ == == +:: +channel-js: the urbit javascript interface +:: +++ channel-js + ^- octs + %- as-octs:mimes:html + ''' + class Channel { + constructor() { + // unique identifier: current time and random number + // + this.uid = + new Date().getTime().toString() + + "-" + + Math.random().toString(16).slice(-6); + + this.requestId = 1; + this.connection = null; + + // a registry of requestId to successFunc/failureFunc + // + // These functions are registered during a +poke and are executed + // in the onServerEvent()/onServerError() callbacks. Only one of + // the functions will be called, and the outstanding poke will be + // removed after calling the success or failure function. + // + this.outstandingPokes = new Map(); + + // a registry of requestId to eventFunc/disconnectFunc + // + // These functions are registered during a +subscribe and are + // executed in the onServerEvent()/onServerError() callbacks. The + // event function will be called whenever a new piece of data on this + // subscription is available, which may be 0, 1, or many times. The + // disconnect function may be called exactly once. + // + this.outstandingSubscriptions = new Map(); + } + + // sends a poke to an app on an urbit ship + // + poke(ship, app, mark, json, successFunc, failureFunc) { + var id = this.nextId(); + this.outstandingPokes.set( + id, {"success": successFunc, "fail": failureFunc}); + + var req = new XMLHttpRequest(); + req.open("PUT", this.channelURL()); + req.setRequestHeader("Content-Type", "application/json"); + + // TODO: Need to stuff an "ack" in here, too. + var x = JSON.stringify([{ + "id": id, + "action": "poke", + "ship": ship, + "app": app, + "mark": mark, + "json": json + }]); + req.send(x); + this.connectIfDisconnected(); + } + + connectIfDisconnected() { + if (this.connection) + return; + + this.eventSource = new EventSource(this.channelURL(), {withCredentials:true}); + this.eventSource.onmessage = e => { + var obj = JSON.parse(e.data); + if (obj.response == "poke") { + var funcs = this.outstandingPokes.get(obj.id); + if (obj.hasOwnProperty("ok")) + funcs["success"]() + else + funcs["fail"](obj.err) + this.outstandingPokes.delete(obj.id); + } else { + console.log("Unrecognized response: ", e); + } + } + + this.eventSource.onerror = e => { + // TODO: The server broke the connection. Call every poke cancel and every + // subscription disconnect. + console.log(e); + } + } + + channelURL() { + return "/~/channel/" + this.uid; + } + + nextId() { + return this.requestId++; + } + }; + + export function newChannel() { + return new Channel; + } + ''' :: +format-ud-as-integer: prints a number for consumption outside urbit :: ++ format-ud-as-integer @@ -651,7 +752,6 @@ :: if we are not a post, return an error :: ?. =('POST' method.http-request) - ~& [%something-other-than-get-post-on-login method.http-request] (return-static-data-on-duct 400 'text/html' (login-page ~)) :: we are a post, and must process the body type as form data :: @@ -690,7 +790,6 @@ ?~ redirect=(get-header 'redirect' u.parsed) '/' u.redirect - ~& [%minting http-request] :: :_ state :_ ~ @@ -770,6 +869,7 @@ :: page; issuing a redirect won't help. :: ?. authenticated + ~& %unauthenticated :: TODO: Real 400 page. :: %^ return-static-data-on-duct 400 'text/html' @@ -778,7 +878,8 @@ :: =+ request-line=(parse-request-line url.http-request) ?. ?=([@t @t @t ~] site.request-line) - :: url is not of the form '/~/subscription/' + ~& %bad-request-line + :: url is not of the form '/~/channel/' :: %^ return-static-data-on-duct 400 'text/html' (internal-server-error authenticated url.http-request ~) @@ -786,6 +887,13 @@ :: =+ channel-id=i.t.t.site.request-line :: + ?: ?& =('channel' channel-id) + =([~ ~.js] ext.request-line) + == + :: client is requesting the javascript shim + :: + (return-static-data-on-duct 200 'application/javascript' channel-js) + :: ?: =('PUT' method.http-request) :: PUT methods starts/modifies a channel, and returns a result immediately :: @@ -977,24 +1085,29 @@ ++ on-put-request |= [channel-id=@t =http-request] ^- [(list move) server-state] + ~& %on-put-request :: error when there's no body :: ?~ body.http-request + ~& %no-body %^ return-static-data-on-duct 400 'text/html' (internal-server-error %.y url.http-request ~) :: if the incoming body isn't json, this is a bad request, 400. :: ?~ maybe-json=(de-json:html q.u.body.http-request) + ~& %no-json %^ return-static-data-on-duct 400 'text/html' (internal-server-error %.y url.http-request ~) :: parse the json into an array of +channel-request items :: ?~ maybe-requests=(parse-channel-request u.maybe-json) + ~& %no-parse %^ return-static-data-on-duct 400 'text/html' (internal-server-error %.y url.http-request ~) :: while weird, the request list could be empty :: ?: =(~ u.maybe-requests) + ~& %empty-list %^ return-static-data-on-duct 400 'text/html' (internal-server-error %.y url.http-request ~) :: check for the existence of the channel-id @@ -1411,7 +1524,7 @@ :: ++ parse-request-line |= url=@t - ^- [[(unit @ta) site=(list @t)] args=(list [key=@t value=@t])] + ^- [[ext=(unit @ta) site=(list @t)] args=(list [key=@t value=@t])] (fall (rush url ;~(plug apat:de-purl:html yque:de-purl:html)) [[~ ~] ~]) -- :: end the =~