changeset 159:a613380ffdc2

RLGWebD: excise polling. WebSockets are supported nearly everywhere now. Listing current games and watching them are still broken.
author John "Elwin" Edwards
date Sat, 03 Jan 2015 15:23:04 -0500
parents 9961a538c00e
children ed837da65e5f
files rlgterm.js rlgwebd.js
diffstat 2 files changed, 36 insertions(+), 666 deletions(-) [+]
line wrap: on
line diff
--- a/rlgterm.js	Thu Jan 01 15:56:22 2015 -0500
+++ b/rlgterm.js	Sat Jan 03 15:23:04 2015 -0500
@@ -1,60 +1,5 @@
 /* rlgterm.js: Roguelike Gallery's driver for termemu.js */
 
-// A state machine that keeps track of polling the server.
-var ajaxstate = {
-  state: 0,
-  timerID: null,
-  clear: function () {
-    if (this.timerID != null) {
-      window.clearTimeout(this.timerID);
-      this.timerID = null;
-    }
-  },
-  set: function (ms) {
-    this.clear();
-    this.timerID = window.setTimeout(getData, ms);
-  },
-  gotdata: function () {
-    this.set(1000);
-    this.state = 1;
-  },
-  gotnothing: function () {
-    if (this.state == 0) {
-      this.set(1000);
-      this.state = 1;
-    }
-    else if (this.state < 4) {
-      this.set(4000);
-      this.state++;
-    }
-    else if (session.playing) {
-      if (this.state < 8) {
-        this.set(15000);
-        this.state++;
-      }
-      else {
-        /* It's been over a minute.  Stop polling. */
-        this.clear();
-      }
-    }
-    else {
-      /* If watching, it can't stop polling entirely, because there
-       * are no POST events to start it up again. */
-      this.set(10000);
-    }
-  },
-  posted: function (wasdata) {
-    if (wasdata) {
-      this.set(1000);
-      this.state = 1;
-    }
-    else {
-      this.set(200);
-      this.state = 0;
-    }
-  }
-};
-
 /* Data on the available games. */
 var games = {
   "rogue3": {
@@ -81,7 +26,7 @@
 
 var session = {
   /* The session id assigned by the server. */
-  id: null,
+  connect: false,
   /* Login name and key are now in sessionStorage. */
   /* Whether the game is being played or just watched. */
   playing: false,
@@ -165,119 +110,15 @@
   return;
 }
 
-/* State for sending and receiving messages. */
-var nsend = 0; // The number of the next packet to send.
-var nrecv = 0; // The next packet expected.
-var msgQ = []; // Queue for out-of-order messages.
-
-/* Processes a message from the server, returning true or false if it was a 
- * data message with or without data, null if not data.
- * All non-special responseTexts should be handed directly to this function.
- */
-function processMsg(msg) {
-  var msgDicts;
-  var havedata = null; // eventual return value
-  try {
-    msgDicts = JSON.parse(msg);
-  } catch (e) {
-    if (e instanceof SyntaxError)
-      return null;
-  }
-  if (msgDicts.length === 0)
-    return false;
-  for (var j = 0; j < msgDicts.length; j++) {
-    if (!msgDicts[j].t)
-      continue;
-    else if (msgDicts[j].t == "E") {
-      if (msgDicts[j].c == 1 || msgDicts[j].c == 6 || msgDicts[j].c == 7) {
-        gameover();
-        if (msgDicts[j].c == 1) {
-          logout();
-        }
-      }
-      debug(1, "Server error: " + msgDicts[j].s);
-    }
-    // A data message
-    else if (msgDicts[j].t == "d") {
-      if (msgDicts[j].n === nrecv) {
-        writeData(msgDicts[j].d);
-        nrecv++;
-        /* Process anything in the queue that's now ready. */
-        var next;
-        while ((next = msgQ.shift()) !== undefined) {
-          writeData(next.d);
-          nrecv++;
-        }
-      }
-      else if (msgDicts[j].n > nrecv) {
-        /* The current message comes after one still missing.  Queue this one
-         * for later use. */
-        debug(1, "Got packet " + msgDicts[j].n + ", expected " + nrecv);
-        msgQ[msgDicts[j].n - nrecv - 1] = msgDicts[j];
-      }
-      else {
-        /* This message's number was encountered previously. */
-        debug(1, "Discarding packet " + msgDicts[j].n + ", expected " + nrecv);
-      }
-      havedata = true;
-    }
-    else if (msgDicts[j].t == "T") {
-      setTitle(msgDicts[j].d);
-    }
-    else if (msgDicts[j].t == "q") {
-      gameover();
-    }
-    else {
-      debug(1, "Unrecognized server message " + msg);
-    }
-  }
-  return havedata;
-}
-
-function getData() {
-  if (session.id == null)
-    return;
-  var datareq = new XMLHttpRequest();
-  var msg = JSON.stringify({"id": session.id, "t": "n"});
-  datareq.onerror = errHandler;
-  datareq.onreadystatechange = function () {
-    if (datareq.readyState == 4 && datareq.status == 200) {
-      var wasdata = processMsg(datareq.responseText);
-      if (wasdata != null) {
-        if (wasdata)
-          ajaxstate.gotdata();
-        else
-          ajaxstate.gotnothing();
-      }
-      return;
-    }
-  };
-  datareq.open('POST', '/feed', true);
-  datareq.send(msg);
-  return;
-}
-
-function postResponseHandler() {
-  if (this.readyState == 4 && this.status == 200) {
-    // We might want to do something with wasdata someday.
-    var wasdata = processMsg(this.responseText);
-    ajaxstate.posted();
-    return;
-  }
-}
-
 function errHandler() {
   message("Unable to connect to the server.", "warn");
 }
 
 function sendback(str) {
   /* For responding to terminal queries. */
-  var msgDict = {"id": session.id, "t": "d", "n": nsend++, "d": str};
-  var datareq = new XMLHttpRequest();
-  datareq.onerror = errHandler;
-  datareq.onreadystatechange = postResponseHandler;
-  datareq.open('POST', '/feed', true);
-  datareq.send(JSON.stringify(msgDict));
+  if (session.sock) {
+    session.sock.send(JSON.stringify({"t": "d", "d": str}));
+  }
   return;
 }
 
@@ -323,14 +164,7 @@
   if (session.sock) {
     session.sock.send(JSON.stringify({"t": "d", "d": code}));
   }
-  else {
-    var datareq = new XMLHttpRequest();
-    var msgDict = {"id": session.id, "t": "d", "n": nsend++, "d": code};
-    datareq.onerror = errHandler;
-    datareq.onreadystatechange = postResponseHandler;
-    datareq.open('POST', '/feed', true);
-    datareq.send(JSON.stringify(msgDict));
-  }
+  /* Otherwise it is disconnected */
   return;
 }
 
@@ -386,14 +220,6 @@
   if (session.sock) {
     session.sock.send(JSON.stringify({"t": "d", "d": keystr}));
   }
-  else {
-    var datareq = new XMLHttpRequest();
-    var msgDict = {"id": session.id, "t": "d", "n": nsend++, "d": keystr};
-    datareq.onerror = errHandler;
-    datareq.onreadystatechange = postResponseHandler;
-    datareq.open('POST', '/feed', true);
-    datareq.send(JSON.stringify(msgDict));
-  }
   return;
 }
 
@@ -426,8 +252,8 @@
       break;
   }
   if (!window.WebSocket) {
-    message("Your browser does not support WebSockets. You can still play, " +
-            "but it will be slower, and may not work in the future.", "warn");
+    message("Your browser does not support WebSockets. " +
+            "This Web app will not work.", "warn");
   }
   return;
 }
@@ -454,8 +280,7 @@
 
 function formlogin(ev) {
   ev.preventDefault();
-  if (session.id != null)
-    return;
+  /* What to do if logged in already? */
   var loginmsg = {};
   loginmsg["name"] = document.getElementById("input_name").value;
   loginmsg["pw"] = document.getElementById("input_pw").value;
@@ -490,6 +315,7 @@
   return;
 }
 
+/* FIXME game list API has changed */
 function tableCurrent(gamelist) {
   var gamediv = document.getElementById("gametable");
   while (gamediv.children.length > 2)
@@ -533,7 +359,7 @@
 function wsCurrent() {
   if (!window.WebSocket)
     return;
-  if (session.id) {
+  if (session.connect) {
     /* Don't bother with status if already playing/watching. */
     if (statsock) {
       statsock.close();
@@ -569,11 +395,12 @@
   }
 }
 
+/* FIXME gamelist API has changed */
 function getcurrent(clear) {
   if (window.WebSocket) {
     return;
   }
-  if (session.id || clear) {
+  if (session.connect || clear) {
     if (statInterval) {
       window.clearInterval(statInterval);
       statInterval = null;
@@ -608,7 +435,7 @@
 }
 
 function getchoices() {
-  if (session.id != null || !("lcred" in sessionStorage))
+  if (session.connect || !("lcred" in sessionStorage))
     return;
   var req = new XMLHttpRequest();
   req.onerror = errHandler;
@@ -680,62 +507,6 @@
   return starter;
 }
 
-function startgame(game) {
-  if (session.id != null || !("lcred" in sessionStorage))
-    return;
-  if (window.WebSocket) {
-    wsStart(game);
-    return;
-  }
-  var smsg = {};
-  smsg["key"] = sessionStorage.getItem("lcred");
-  smsg["game"] = game.uname;
-  smsg["h"] = 24;
-  smsg["w"] = 80;
-  var req = new XMLHttpRequest();
-  req.onerror = errHandler;
-  req.onreadystatechange = function () {
-    if (req.readyState != 4 || req.status != 200) 
-      return;
-    var reply = JSON.parse(req.responseText);
-    if (reply.t == 's') {
-      /* Success */
-      session.id = reply.id;
-      session.playing = true;
-      termemu.resize(reply.h, reply.w);
-      message("You are now playing " + game.name + ".");
-      setmode("play");
-      getData();
-    }
-    else if (reply.t == 'E') {
-      if (reply.c == 1) {
-        logout();
-        message("The server forgot about you, please log in again.", "warn");
-      }
-      else if (reply.c == 4) {
-        if (reply.s == "dgamelaunch") {
-          message("You are already playing " + game.name + " over SSH.", 
-                  "warn");
-        }
-        else {
-          message("You are already playing " + game.name + 
-                  " in another browser window.", "warn");
-        }
-      }
-      else if (reply.c == 7) {
-        message("The game is being saved, try again in a few seconds.");
-      }
-      else {
-        message("The server says it can't start your game because \"" + 
-                reply.s + "\". This is probably a bug.", "warn");
-      }
-    }
-  };
-  req.open('POST', '/play', true);
-  req.send(JSON.stringify(smsg));
-  return;
-}
-
 function makeStopper(gname) {
   if (!(gname in games))
     return null;
@@ -774,12 +545,17 @@
   return;
 }
 
-function wsStart(game) {
+function startgame(game) {
+  if (!("lcred" in sessionStorage) || session.connect)
+    return;
+  if (!window.WebSocket) {
+    return;
+  }
   var sockurl = "ws://" + window.location.host + "/play/" + game.uname;
   sockurl += "?key=" + sessionStorage.getItem("lcred") + "&w=80&h=24";
   ws = new WebSocket(sockurl);
   ws.onopen = function (event) {
-    session.id = true;
+    session.connect = true;
     session.playing = true;
     session.sock = ws;
     setmode("play");
@@ -800,55 +576,20 @@
   };
 }
 
-function startwatching(gamenumber) {
-  if (session.id != null)
-    return;
-  if (window.WebSocket) {
-    wsWatch(gamenumber);
-    return;
-  }
-  var wmsg = {"n": Number(gamenumber)};
-  var req = new XMLHttpRequest();
-  req.onerror = errHandler;
-  req.onreadystatechange = function () {
-    if (req.readyState != 4 || req.status != 200) 
-      return;
-    var reply = JSON.parse(req.responseText);
-    if (reply.t == 'w') {
-      /* Success */
-      session.id = reply.id;
-      session.playing = false;
-      termemu.resize(reply.h, reply.w);
-      termemu.reset();
-      termemu.toAltBuf();
-      var pname = reply.p;
-      var gname = games[reply.g].name;
-      message("You are now watching " + pname + " play " + gname + ".");
-      setmode("watch");
-      getData();
-    }
-    else if (reply.t == 'E') {
-      message("The game could not be watched: " + reply.s, "warn");
-      getcurrent();
-    }
-  };
-  req.open('POST', '/watch', true);
-  req.send(JSON.stringify(wmsg));
-  return;
-}
-
-function makeWatcher(n) {
+function makeWatcher(t) {
   function watcher(ev) {
-    startwatching(n);
+    startwatching(t);
   }
   return watcher;
 }
 
-function wsWatch(gamenumber) {
-  var sockurl = "ws://" + window.location.host + "/watch/" + String(gamenumber);
+function startwatching(tag) {
+  if (session.connect)
+    return;
+  var sockurl = "ws://" + window.location.host + "/watch/" + tag;
   var ws = new WebSocket(sockurl);
   ws.onopen = function (event) {
-    session.id = true;
+    session.connect = true;
     session.sock = ws;
     setmode("watch");
   };
@@ -874,7 +615,8 @@
 
 function formreg(ev) {
   ev.preventDefault();
-  if (session.id != null)
+  /* This ought to check for being logged in instead. */
+  if (session.connect)
     return;
   var regmsg = {};
   regmsg["name"] = document.getElementById("regin_name").value;
@@ -925,20 +667,16 @@
 }
 
 function gameover() {
-  if (session.id == null)
+  if (!session.connect)
     return;
   /* TODO IFACE2 If the end was unexpected, tell player the game was saved. */
   if (session.playing)
     message("Finished playing.");
   else
     message("Finished watching.");
-  session.id = null;
+  session.connect = false;
   session.playing = false;
-  ajaxstate.clear();
   termemu.toNormBuf();
-  nsend = 0;
-  nrecv = 0;
-  msgQ = [];
   if ("lcred" in sessionStorage)
     setmode("choose");
   else
@@ -952,24 +690,14 @@
   setmode("login");
 }
 
+/* TODO determine whether this is needed */
 function stop() {
-  if (!session.id)
+  if (!session.connect)
     return;
   if (session.sock) {
     session.sock.close();
     return;
   }
-  var req = new XMLHttpRequest();
-  req.onerror = errHandler;
-  req.onreadystatechange = function () {
-    if (req.readyState == 4 && req.status == 200) {
-      processMsg(req.responseText);
-      return;
-    }
-  };
-  req.open('POST', '/feed', true);
-  req.send(JSON.stringify({"id": session.id, "t": "q"}));
-  return;
 }
 
 function setmode(mode, ev) {
--- a/rlgwebd.js	Thu Jan 01 15:56:22 2015 -0500
+++ b/rlgwebd.js	Sat Jan 03 15:23:04 2015 -0500
@@ -63,7 +63,6 @@
 /* Global state */
 var logins = {};
 var sessions = {};
-var clients = {};
 var dglgames = {};
 var allowlogin = true;
 var gamemux = new events.EventEmitter();
@@ -193,178 +192,6 @@
 }
 TermSession.prototype = new events.EventEmitter();
 
-function Watcher(session) {
-  var ss = this; // that
-  this.session = session;
-  this.alive = true;
-  /* State for messaging. */
-  this.nsend = 0;
-  this.sendQ = [];
-  /* Get a place in the table. */
-  this.id = randkey(2);
-  while (this.id in clients) {
-    this.id = randkey(2);
-  }
-  clients[this.id] = this;
-  /* Recreate the current screen state from the session's buffer. */
-  this.sendQ.push({"t": "d", "n": this.nsend++, 
-        "d": session.framebuf.toString("hex", 0, session.frameoff)});
-  function dataH(buf) {
-    var reply = {};
-    reply.t = "d";
-    reply.n = ss.nsend++;
-    reply.d = buf.toString("hex");
-    ss.sendQ.push(reply);
-  }
-  function exitH() {
-    ss.alive = false;
-    ss.sendQ.push({"t": "q"});
-  }
-  session.on('data', dataH);
-  session.on('exit', exitH);
-  this.read = function() {
-    /* Returns an array of all outstanding messages, empty if none. */
-    var temp = this.sendQ;
-    this.sendQ = [];
-    /* Clean up if finished. */
-    if (!this.alive) {
-      delete clients[this.id];
-    }
-    return temp;
-  };
-  this.quit = function() {
-    this.session.removeListener('data', dataH);
-    this.session.removeListener('exit', exitH);
-    delete clients[this.id];
-  };
-}
-
-function Player(gamename, lkey, dims, callback) {
-  var ss = this;
-  this.alive = false;
-  /* State for messaging. */
-  this.nsend = 0;
-  this.nrecv = 0;
-  this.sendQ = [];
-  this.recvQ = []
-  this.Qtimeout = null;
-  /* Get a place in the table. */
-  this.id = randkey(2);
-  while (this.id in clients) {
-    this.id = randkey(2);
-  }
-  clients[this.id] = this;
-
-  this.read = function() {
-    var temp = this.sendQ;
-    this.sendQ = [];
-    /* Clean up if finished. */
-    if (!this.alive) {
-      clearTimeout(this.Qtimeout);
-      delete clients[this.id];
-    }
-    return temp;
-  };
-  this.write = function (data, n) {
-    if (!this.alive || typeof (n) != "number") {
-      return;
-    }
-    var oindex = n - this.nrecv;
-    if (oindex === 0) {
-      this.session.write(data);
-      this.nrecv++;
-      var next;
-      while ((next = this.recvQ.shift()) !== undefined) {
-        this.session.write(next);
-        this.nrecv++;
-      }
-      if (this.recvQ.length == 0 && this.Qtimeout) {
-        clearTimeout(this.Qtimeout);
-        this.Qtimeout = null;
-      }
-    }
-    else if (oindex > 0 && oindex <= 1024) {
-      tslog("Client %s: Stashing message %d at %d", this.id,  n, oindex - 1);
-      this.recvQ[oindex - 1] = data;
-      if (!this.Qtimeout) {
-        var nextn = this.nrecv + this.recvQ.length + 1;
-        this.Qtimeout = setTimeout(this.flushQ, 30000, this, nextn);
-      }
-    }
-    /* Otherwise, discard it */
-    return;
-  };
-  this.flushQ = function (client, n) {
-    /* Callback for when an unreceived message times out.
-     * n is the first empty space that will not be given up on. */
-    if (!client.alive || client.nrecv >= n)
-      return;
-    client.nrecv++;
-    var next;
-    /* Clear the queue up to n */
-    while (client.nrecv < n) {
-      next = client.recvQ.shift();
-      if (next !== undefined)
-        client.session.write(next);
-      client.nrecv++;
-    }
-    /* Clear out anything that's ready. */
-    while ((next = client.recvQ.shift()) !== undefined) {
-      client.session.write(next);
-      client.nrecv++;
-    }
-    /* Now set another timeout if necessary. */
-    if (client.recvQ.length != 0) {
-      var nextn = client.nrecv + client.recvQ.length + 1;
-      client.Qtimeout = setTimeout(client.flushQ, 30000, client, nextn);
-    }
-    tslog("Flushing queue for player %s", player.id);
-  };
-  this.reset = function () {
-    /* To be called when the game is taken over. */
-    if (this.Qtimeout) {
-      clearTimeout(this.Qtimeout);
-      this.Qtimeout = null;
-    }
-    for (var i = 0; i < this.recvQ.length; i++) {
-      if (this.recvQ[i] !== undefined) {
-        this.session.write(this.recvQ[i]);
-      }
-    }
-    this.recvQ = [];
-    this.nrecv = 0;
-    this.nsend = 0;
-    this.sendQ = [{"t": "d", "n": this.nsend++, 
-        "d": this.session.framebuf.toString("hex", 0, this.session.frameoff)}];
-  };
-  this.quit = function() {
-    if (this.alive)
-      this.session.close();
-  };
-  function openH(success, tag) {
-    if (success) {
-      ss.alive = true;
-      ss.session = sessions[tag];
-      ss.h = sessions[tag].h;
-      ss.w = sessions[tag].w;
-    }
-    callback(ss, success);
-  }
-  function dataH(chunk) {
-    var reply = {};
-    reply.t = "d";
-    reply.n = ss.nsend++;
-    reply.d = chunk.toString("hex");
-    ss.sendQ.push(reply);
-  }
-  function exitH() {
-    ss.alive = false;
-    ss.sendQ.push({"t": "q"});
-  }
-  var handlers = {'open': openH, 'data': dataH, 'exit': exitH};
-  this.session = new TermSession(gamename, lkey, dims, handlers);
-}
-
 // Also known as WebSocketAndTermSessionClosureGlueFactory
 function wsWatcher(conn, session) {
   var ss = this; // is this even needed?
@@ -716,103 +543,6 @@
   return;
 }
 
-function startgame(req, res, formdata) {
-  if (!allowlogin) {
-    sendError(res, 6, null);
-    return;
-  }
-  if (!("key" in formdata)) {
-    sendError(res, 2, "No key given.");
-    return;
-  }
-  else if (!("game" in formdata)) {
-    sendError(res, 2, "No game specified.");
-    return;
-  }
-  var lkey = String(formdata["key"]);
-  if (!(lkey in logins)) {
-    sendError(res, 1, null);
-    return;
-  }
-  var username = logins[lkey].name;
-  var gname = formdata["game"];
-  // If dims are not given or invalid, the constructor will handle it.
-  var dims = [formdata["h"], formdata["w"]];
-  if (!(gname in games)) {
-    sendError(res, 2, "No such game: " + gname);
-    tslog("Request for nonexistant game \"%s\"", gname);
-    return;
-  }
-  // A callback to pass to the game-in-progress checker.
-  var launch = function(err, fname) {
-    var nodematch = new RegExp("^" + username + ":node:");
-    if (fname && (fname.match(nodematch) === null)) {
-      /* It's being played in dgamelaunch. */
-      sendError(res, 4, "dgamelaunch");
-      tslog("%s is already playing %s", username, gname);
-      return;
-    }
-    // Game starting has been approved.
-    var respondlaunch = function(nclient, success) {
-      if (success) {
-        res.writeHead(200, {'Content-Type': 'application/json'});
-        var reply = {"t": "s", "id": nclient.id, "w": nclient.w, "h": 
-                     nclient.h, "p": username, "g": gname};
-        res.write(JSON.stringify(reply));
-        res.end();
-      }
-      else {
-        sendError(res, 5, "Failed to open TTY");
-        tslog("Unable to allocate TTY for %s", gname);
-      }
-    };
-    if (fname) {
-      for (var cid in clients) {
-        cli = clients[cid];
-        if ((cli instanceof Player) && 
-             cli.session.pname == username &&
-             cli.session.game.uname == gname) {
-          cli.reset();
-          respondlaunch(cli, true);
-          tslog("Game %d has been taken over.", cli.session.sessid);
-          return;
-        }
-      }
-      /* If there's no player, it's a WebSocket game, and shouldn't be 
-       * seized. */
-      sendError(res, 4, "WebSocket");
-    }
-    else {
-      new Player(gname, lkey, dims, respondlaunch);
-    }
-  };
-  checkprogress(username, games[gname], launch, []);
-}
-
-function watch(req, res, formdata) {
-  if (!("g" in formdata) | !("p" in formdata)) {
-    sendError(res, 2, "Game or player not given");
-    return;
-  }
-  if (!(formdata.g in games)) {
-    sendError(res, 2, "No such game: " + formdata.g);
-    return;
-  }
-  var tag = formdata.g = "/" + formdata.p;
-  if (!(tag in sessions)) {
-    sendError(res, 7);
-    return;
-  }
-  var session = sessions[tag];
-  var watch = new Watcher(session);
-  var reply = {"t": "w", "w": session.w, "h": session.h, 
-               "p": session.pname, "g": session.game.uname};
-  res.writeHead(200, {'Content-Type': 'application/json'});
-  res.write(JSON.stringify(reply));
-  res.end();
-  tslog("Game %d is being watched", tag);
-}
-
 /* Sets things up for a new user, like dgamelaunch's commands[register] */
 function regsetup(username) {
   function regsetup_l2(err) {
@@ -876,24 +606,7 @@
   return;
 }
 
-/* Ends the game, obviously.  Less obviously, stops watching the game if
- * the client is a Watcher instead of a Player. */
-function endgame(client, res) {
-  if (!client.alive) {
-    sendError(res, 7, null, true);
-    return;
-  }
-  client.quit();
-  // Give things some time to happen.
-  if (client instanceof Player)
-    setTimeout(readFeed, 200, client, res);
-  else
-    readFeed(client, res);
-  return;
-}
-
 /* Stops a running game if the request has the proper key. */
-/* TODO does this still work? */
 function stopgame(res, formdata) {
   if (!("key" in formdata) || !(formdata["key"] in logins)) {
     sendError(res, 1);
@@ -940,19 +653,6 @@
   checkprogress(pname, games[gname], checkback, []);
 }
 
-/* TODO remove polling */
-function findClient(formdata, playersOnly) {
-  if (typeof(formdata) != "object")
-    return null;
-  if ("id" in formdata) {
-    var id = formdata["id"];
-    if (id in clients && (!playersOnly || clients[id] instanceof Player)) {
-      return clients[id];
-    }
-  }
-  return null;
-}
-
 function startProgressWatcher() {
   var watchdirs = [];
   for (var gname in games) {
@@ -1032,22 +732,6 @@
   return;
 }
 
-/* TODO remove polling */
-function readFeed(client, res) {
-  if (!client) {
-    sendError(res, 7, null, true);
-    return;
-  }
-  var msgs = client.read();
-  if (!allowlogin && !msgs.length) {
-    sendError(res, 6, null, true);
-    return;
-  }
-  res.writeHead(200, { "Content-Type": "application/json" });
-  res.write(JSON.stringify(msgs));
-  res.end();
-}
-
 /* TODO simplify by storing timestamps instead of callin stat() */
 function getStatus(callback) {
   var now = new Date();
@@ -1272,45 +956,12 @@
     var target = url.parse(req.url).pathname;
     /* First figure out if the client is POSTing to a command interface. */
     if (req.method == 'POST') {
-      if (target == '/feed') {
-        var client = findClient(formdata, false);
-        if (!client) {
-          sendError(res, 7, null, true);
-          return;
-        }
-        if (formdata.t == "q") {
-          /* The client wants to terminate the process. */
-          endgame(client, res);
-          return; // endgame() calls readFeed() itself.
-        }
-        else if (formdata.t == "d" && typeof(formdata.d) == "string") {
-          if (!(client instanceof Player)) {
-            sendError(res, 7, "Watching", true);
-            return;
-          }
-          /* process the keys */
-          var hexstr = formdata.d.replace(/[^0-9a-f]/gi, "");
-          if (hexstr.length % 2 != 0) {
-            sendError(res, 2, "incomplete byte", true);
-            return;
-          }
-          var keybuf = new Buffer(hexstr, "hex");
-          client.write(keybuf, formdata.n);
-        }
-        readFeed(client, res);
-      }
-      else if (target == "/login") {
+      if (target == "/login") {
         login(req, res, formdata);
       }
       else if (target == "/addacct") {
         register(req, res, formdata);
       }
-      else if (target == "/play") {
-        startgame(req, res, formdata);
-      }
-      else if (target == "/watch") {
-        watch(req, res, formdata);
-      }
       else if (target == "/quit") {
         stopgame(res, formdata);
       }
@@ -1323,16 +974,7 @@
       }
     }
     else if (req.method == 'GET' || req.method == 'HEAD') {
-      if (target == '/feed') {
-        if (req.method == 'HEAD') {
-          res.writeHead(200, {"Content-Type": "application/json"});
-          res.end();
-        }
-        else
-          sendError(res, 7, null, true);
-        return;
-      }
-      else if (target == '/status') {
+      if (target == '/status') {
         statusmsg(req, res);
       }
       else if (target.match(/^\/uinfo\//)) {
@@ -1352,7 +994,6 @@
     return;
   }
   req.on('end', respond);
-
 }
 
 function wsHandler(wsRequest) {
@@ -1403,6 +1044,7 @@
     wsRequest.reject(404, "No such resource.");
 }
 
+/* TODO use a list instead */
 function pushStatus() {
   getStatus(function(info) {
     info["t"] = "t";