tor-browser

The Tor Browser
git clone https://git.dasho.dev/tor-browser.git
Log | Files | Refs | README | LICENSE

wave-service.js (23978B)


      1 function sendRequest(method, uri, headers, data, onSuccess, onError) {
      2  var xhr = new XMLHttpRequest();
      3  xhr.onload = function () {
      4    if (xhr.status === 200) {
      5      onSuccess(xhr.response);
      6    } else {
      7      if (onError) onError(xhr.status, xhr.response);
      8    }
      9  };
     10  xhr.onerror = function () {
     11    if (onError) onError();
     12  };
     13  xhr.open(method, WaveService.uriPrefix + uri, true);
     14  for (var header in headers) {
     15    xhr.setRequestHeader(header, headers[header]);
     16  }
     17  xhr.send(data);
     18  return xhr;
     19 }
     20 
     21 var WEB_ROOT = "{{WEB_ROOT}}";
     22 var HTTP_PORT = "{{HTTP_PORT}}";
     23 var HTTPS_PORT = "{{HTTPS_PORT}}";
     24 var OPEN = "open";
     25 var CLOSED = "closed";
     26 
     27 var WaveService = {
     28  uriPrefix: WEB_ROOT,
     29  socket: {
     30    state: CLOSED,
     31    onMessage: function () {},
     32    onOpen: function () {},
     33    onClose: function () {},
     34    send: function () {},
     35    close: function () {},
     36    onStateChange: function () {},
     37  },
     38  // SESSIONS API
     39  createSession: function (configuration, onSuccess, onError) {
     40    var data = JSON.stringify({
     41      tests: configuration.tests,
     42      types: configuration.types,
     43      timeouts: configuration.timeouts,
     44      reference_tokens: configuration.referenceTokens,
     45      expiration_date: configuration.expirationDate,
     46      labels: configuration.labels,
     47    });
     48    sendRequest(
     49      "POST",
     50      "api/sessions",
     51      { "Content-Type": "application/json" },
     52      data,
     53      function (response) {
     54        var jsonObject = JSON.parse(response);
     55        onSuccess(jsonObject.token);
     56      },
     57      onError
     58    );
     59  },
     60  readSession: function (token, onSuccess, onError) {
     61    sendRequest(
     62      "GET",
     63      "api/sessions/" + token,
     64      null,
     65      null,
     66      function (response) {
     67        var jsonObject = JSON.parse(response);
     68        onSuccess({
     69          token: jsonObject.token,
     70          tests: jsonObject.tests,
     71          types: jsonObject.types,
     72          userAgent: jsonObject.user_agent,
     73          labels: jsonObject.labels,
     74          timeouts: jsonObject.timeouts,
     75          browser: jsonObject.browser,
     76          isPublic: jsonObject.is_public,
     77          referenceTokens: jsonObject.reference_tokens,
     78        });
     79      },
     80      onError
     81    );
     82  },
     83  readMultipleSessions: function (tokens, onSuccess, onError) {
     84    var requestsLeft = tokens.length;
     85    if (requestsLeft === 0) onSuccess([]);
     86    var configurations = [];
     87    for (var i = 0; i < tokens.length; i++) {
     88      var token = tokens[i];
     89      WaveService.readSession(
     90        token,
     91        function (configuration) {
     92          requestsLeft--;
     93          configurations.push(configuration);
     94          if (requestsLeft === 0) onSuccess(configurations);
     95        },
     96        function (status) {
     97          if (status === 404) requestsLeft--;
     98          if (status !== 404 && onError) onError();
     99          if (requestsLeft === 0) onSuccess(configurations);
    100        }
    101      );
    102    }
    103  },
    104  readSessionStatus: function (token, onSuccess, onError) {
    105    sendRequest(
    106      "GET",
    107      "api/sessions/" + token + "/status",
    108      null,
    109      null,
    110      function (response) {
    111        var jsonObject = JSON.parse(response);
    112        var dateStarted = null;
    113        if (jsonObject.date_started) {
    114          dateStarted = new Date(jsonObject.date_started);
    115        }
    116        var dateFinished = null;
    117        if (jsonObject.date_finished) {
    118          dateFinished = new Date(jsonObject.date_finished);
    119        }
    120        var expirationDate = null;
    121        if (jsonObject.expiration_date) {
    122          expirationDate = new Date(jsonObject.expiration_date);
    123        }
    124        onSuccess({
    125          token: jsonObject.token,
    126          dateStarted: dateStarted,
    127          dateFinished: dateFinished,
    128          testFilesCount: jsonObject.test_files_count,
    129          testFilesCompleted: jsonObject.test_files_completed,
    130          status: jsonObject.status,
    131          expirationDate: expirationDate,
    132        });
    133      },
    134      function () {
    135        if (onError) onError();
    136      }
    137    );
    138  },
    139  readMultipleSessionStatuses: function (tokens, onSuccess, onError) {
    140    var requestsLeft = tokens.length;
    141    if (requestsLeft === 0) onSuccess([]);
    142    var statuses = [];
    143    for (var i = 0; i < tokens.length; i++) {
    144      var token = tokens[i];
    145      WaveService.readSessionStatus(
    146        token,
    147        function (status) {
    148          requestsLeft--;
    149          statuses.push(status);
    150          if (requestsLeft === 0) onSuccess(statuses);
    151        },
    152        function () {
    153          requestsLeft--;
    154          if (requestsLeft === 0) onSuccess(statuses);
    155        }
    156      );
    157    }
    158  },
    159  readPublicSessions: function (onSuccess, onError) {
    160    sendRequest(
    161      "GET",
    162      "api/sessions/public",
    163      null,
    164      null,
    165      function (response) {
    166        var jsonObject = JSON.parse(response);
    167        onSuccess(jsonObject);
    168      },
    169      onError
    170    );
    171  },
    172  updateSession: function (token, configuration, onSuccess, onError) {
    173    var data = JSON.stringify({
    174      tests: configuration.tests,
    175      types: configuration.types,
    176      timeouts: configuration.timeouts,
    177      reference_tokens: configuration.referenceTokens,
    178      expiration_date: configuration.expirationDate,
    179      type: configuration.type,
    180    });
    181    sendRequest(
    182      "PUT",
    183      "api/sessions/" + token,
    184      { "Content-Type": "application/json" },
    185      data,
    186      function () {
    187        onSuccess();
    188      },
    189      onError
    190    );
    191  },
    192  updateLabels: function (token, labels, onSuccess, onError) {
    193    var data = JSON.stringify({ labels: labels });
    194    sendRequest(
    195      "PUT",
    196      "api/sessions/" + token + "/labels",
    197      { "Content-Type": "application/json" },
    198      data,
    199      function () {
    200        if (onSuccess) onSuccess();
    201      },
    202      onError
    203    );
    204  },
    205  findToken: function (fragment, onSuccess, onError) {
    206    sendRequest(
    207      "GET",
    208      "api/sessions/" + fragment,
    209      null,
    210      null,
    211      function (response) {
    212        var jsonObject = JSON.parse(response);
    213        onSuccess(jsonObject.token);
    214      },
    215      onError
    216    );
    217  },
    218  startSession: function (token, onSuccess, onError) {
    219    sendRequest(
    220      "POST",
    221      "api/sessions/" + token + "/start",
    222      null,
    223      null,
    224      function () {
    225        onSuccess();
    226      },
    227      onError
    228    );
    229  },
    230  pauseSession: function (token, onSuccess, onError) {
    231    sendRequest(
    232      "POST",
    233      "api/sessions/" + token + "/pause",
    234      null,
    235      null,
    236      function () {
    237        onSuccess();
    238      },
    239      onError
    240    );
    241  },
    242  stopSession: function (token, onSuccess, onError) {
    243    sendRequest(
    244      "POST",
    245      "api/sessions/" + token + "/stop",
    246      null,
    247      null,
    248      function () {
    249        onSuccess();
    250      },
    251      onError
    252    );
    253  },
    254  resumeSession: function (token, resumeToken, onSuccess, onError) {
    255    var data = JSON.stringify({ resume_token: resumeToken });
    256    sendRequest(
    257      "POST",
    258      "api/sessions/" + token + "/resume",
    259      { "Content-Type": "application/json" },
    260      data,
    261      function () {
    262        if (onSuccess) onSuccess();
    263      },
    264      function (response) {
    265        if (onError) onError(response);
    266      }
    267    );
    268  },
    269  deleteSession: function (token, onSuccess, onError) {
    270    sendRequest(
    271      "DELETE",
    272      "api/sessions/" + token,
    273      null,
    274      null,
    275      function () {
    276        onSuccess();
    277      },
    278      onError
    279    );
    280  },
    281 
    282  // TESTS API
    283  readTestList: function (onSuccess, onError) {
    284    sendRequest(
    285      "GET",
    286      "api/tests",
    287      null,
    288      null,
    289      function (response) {
    290        var jsonObject = JSON.parse(response);
    291        onSuccess(jsonObject);
    292      },
    293      onError
    294    );
    295  },
    296  readNextTest: function (token, onSuccess, onError) {
    297    sendRequest(
    298      "GET",
    299      "api/tests/" + token + "/next",
    300      null,
    301      null,
    302      function (response) {
    303        var jsonObject = JSON.parse(response);
    304        onSuccess(jsonObject.next_test);
    305      },
    306      onError
    307    );
    308  },
    309  readLastCompletedTests: function (token, resultTypes, onSuccess, onError) {
    310    var status = "";
    311    if (resultTypes) {
    312      for (var i = 0; i < resultTypes.length; i++) {
    313        var type = resultTypes[i];
    314        status += type + ",";
    315      }
    316    }
    317    sendRequest(
    318      "GET",
    319      "api/tests/" + token + "/last_completed?status=" + status,
    320      null,
    321      null,
    322      function (response) {
    323        var tests = JSON.parse(response);
    324        var parsedTests = [];
    325        for (var status in tests) {
    326          for (var i = 0; i < tests[status].length; i++) {
    327            var path = tests[status][i];
    328            parsedTests.push({ path: path, status: status });
    329          }
    330        }
    331        onSuccess(parsedTests);
    332      },
    333      onError
    334    );
    335  },
    336  readMalfunctioningTests: function (token, onSuccess, onError) {
    337    sendRequest(
    338      "GET",
    339      "api/tests/" + token + "/malfunctioning",
    340      null,
    341      null,
    342      function (response) {
    343        var tests = JSON.parse(response);
    344        onSuccess(tests);
    345      },
    346      function (response) {
    347        var errorMessage = JSON.parse(response).error;
    348        onError(errorMessage);
    349      }
    350    );
    351  },
    352  updateMalfunctioningTests: function (
    353    token,
    354    malfunctioningTests,
    355    onSuccess,
    356    onError
    357  ) {
    358    var data = JSON.stringify(malfunctioningTests);
    359    sendRequest(
    360      "PUT",
    361      "api/tests/" + token + "/malfunctioning",
    362      { "Content-Type": "application/json" },
    363      data,
    364      function () {
    365        onSuccess();
    366      },
    367      function (response) {
    368        var errorMessage = JSON.parse(response).error;
    369        onError(errorMessage);
    370      }
    371    );
    372  },
    373  readAvailableApis: function (onSuccess, onError) {
    374    sendRequest(
    375      "GET",
    376      "api/tests/apis",
    377      null,
    378      null,
    379      function (response) {
    380        var apis = JSON.parse(response);
    381        onSuccess(apis);
    382      },
    383      function (response) {
    384        if (!onError) return;
    385        var errorMessage = JSON.parse(response).error;
    386        onError(errorMessage);
    387      }
    388    );
    389  },
    390 
    391  // RESULTS API
    392  createResult: function (token, result, onSuccess, onError) {
    393    sendRequest(
    394      "POST",
    395      "api/results/" + token,
    396      { "Content-Type": "application/json" },
    397      JSON.stringify(result),
    398      function () {
    399        onSuccess();
    400      },
    401      onError
    402    );
    403  },
    404  readResults: function (token, onSuccess, onError) {
    405    sendRequest(
    406      "GET",
    407      "api/results/" + token,
    408      null,
    409      null,
    410      function (response) {
    411        onSuccess(JSON.parse(response));
    412      },
    413      onError
    414    );
    415  },
    416  readResultsCompact: function (token, onSuccess, onError) {
    417    sendRequest(
    418      "GET",
    419      "api/results/" + token + "/compact",
    420      null,
    421      null,
    422      function (response) {
    423        var jsonObject = JSON.parse(response);
    424        onSuccess(jsonObject);
    425      },
    426      onError
    427    );
    428  },
    429  readResultComparison: function (tokens, onSuccess, onError) {
    430    var comparison = {};
    431    var fetchComplete = function (results) {
    432      comparison.total = {};
    433      for (var i = 0; i < results.length; i++) {
    434        var result = results[i];
    435        var token = result.token;
    436        comparison[token] = {};
    437        for (var api in result) {
    438          if (api === "token") continue;
    439          comparison[token][api] = result[api].pass;
    440          if (!comparison.total[api]) {
    441            var total = 0;
    442            for (var status in result[api]) {
    443              total = total + result[api][status];
    444            }
    445            comparison.total[api] = total;
    446          }
    447        }
    448      }
    449      onSuccess(comparison);
    450    };
    451    var requestsLeft = tokens.length;
    452    if (requestsLeft === 0) onSuccess([]);
    453    var results = [];
    454    for (var i = 0; i < tokens.length; i++) {
    455      var token = tokens[i];
    456      (function (token) {
    457        WaveService.readResultsCompact(
    458          token,
    459          function (result) {
    460            requestsLeft--;
    461            result.token = token;
    462            results.push(result);
    463            if (requestsLeft === 0) fetchComplete(results);
    464          },
    465          function (responseStatus) {
    466            if (responseStatus === 404) requestsLeft--;
    467            if (status !== 404 && onError) onError();
    468            if (requestsLeft === 0) fetchComplete(results);
    469          }
    470        );
    471      })(token);
    472    }
    473  },
    474  downloadResults: function (token) {
    475    location.href = "api/results/" + token + "/export";
    476  },
    477  downloadApiResult: function (token, api) {
    478    location.href = "api/results/" + token + "/" + api + "/json";
    479  },
    480  downloadAllApiResults: function (token, api) {
    481    location.href = "api/results/" + token + "/json";
    482  },
    483  downloadReport: function (token, api) {
    484    location.href = "api/results/" + token + "/" + api + "/report";
    485  },
    486  importResults: function (data, onSuccess, onError) {
    487    sendRequest(
    488      "POST",
    489      "api/results/import",
    490      { "Content-Type": "application/octet-stream" },
    491      data,
    492      function (response) {
    493        var token = JSON.parse(response).token;
    494        onSuccess(token);
    495      },
    496      function (status, response) {
    497        var errorMessage;
    498        if (status === 500) {
    499          errorMessage = "Internal server error.";
    500        } else {
    501          errorMessage = JSON.parse(response).error;
    502        }
    503        onError(errorMessage);
    504      }
    505    );
    506  },
    507  readReportUri: function (token, api, onSuccess, onError) {
    508    sendRequest(
    509      "GET",
    510      "api/results/" + token + "/" + api + "/reporturl",
    511      null,
    512      null,
    513      function (response) {
    514        var jsonObject = JSON.parse(response);
    515        onSuccess(jsonObject.uri);
    516      },
    517      onError
    518    );
    519  },
    520  downloadMultiReport: function (tokens, api) {
    521    location.href = "api/results/" + api + "/report?tokens=" + tokens.join(",");
    522  },
    523  readMultiReportUri: function (tokens, api, onSuccess, onError) {
    524    sendRequest(
    525      "GET",
    526      "api/results/" + api + "/reporturl?tokens=" + tokens.join(","),
    527      null,
    528      null,
    529      function (response) {
    530        var jsonObject = JSON.parse(response);
    531        onSuccess(jsonObject.uri);
    532      },
    533      onError
    534    );
    535  },
    536  downloadResultsOverview: function (token) {
    537    location.href = "api/results/" + token + "/overview";
    538  },
    539 
    540  // DEVICES API
    541  _device_token: null,
    542  _deviceEventListeners: {},
    543  _deviceEventNumbers: {},
    544  registerDevice: function (onSuccess, onError) {
    545    sendRequest(
    546      "POST",
    547      "api/devices",
    548      null,
    549      null,
    550      function (response) {
    551        var data = JSON.parse(response);
    552        WaveService._device_token = data.token;
    553        onSuccess(data.token);
    554      },
    555      onError
    556    );
    557  },
    558  readDevice: function (token, onSuccess, onError) {
    559    sendRequest(
    560      "GET",
    561      "api/devices/" + token,
    562      null,
    563      null,
    564      function (response) {
    565        if (!onSuccess) return;
    566        var data = JSON.parse(response);
    567        onSuccess(data);
    568      },
    569      function (error) {
    570        if (!onError) return;
    571        onError(error);
    572      }
    573    );
    574  },
    575  DEVICE_ADDED_EVENT: "device_added",
    576  DEVICE_REMOVED_EVENT: "device_removed",
    577  START_SESSION: "start_session",
    578  addDeviceEventListener: function (token, callback) {
    579    var listeners = WaveService._deviceEventListeners;
    580    if (!listeners[token]) listeners[token] = [];
    581    listeners[token].push(callback);
    582    WaveService._deviceEventListeners = listeners;
    583    WaveService.listenDeviceEvents(token);
    584  },
    585  removeDeviceEventListener: function (callback) {
    586    var listeners = WaveService._deviceEventListeners;
    587    for (var token of Object.keys(listeners)) {
    588      var index = listeners[token].indexOf(callback);
    589      if (index === -1) continue;
    590      listeners[token].splice(index, 1);
    591      break;
    592    }
    593    WaveService._deviceEventListeners = listeners;
    594  },
    595  listenDeviceEvents: function (token) {
    596    var listeners = WaveService._deviceEventListeners;
    597    if (!listeners[token] || listeners.length === 0) return;
    598    var url = "api/devices/" + token + "/events";
    599    var lastEventNumber = WaveService._deviceEventNumbers[token];
    600    if (lastEventNumber) {
    601      url += "?last_active=" + lastEventNumber;
    602    }
    603    WaveService.listenHttpPolling(
    604      url,
    605      function (response) {
    606        if (!response) {
    607          WaveService.listenDeviceEvents(token);
    608          return;
    609        }
    610        for (var listener of listeners[token]) {
    611          listener(response);
    612        }
    613        WaveService._deviceEventNumbers[token] = lastEventNumber;
    614        WaveService.listenDeviceEvents(token);
    615      },
    616      function () {
    617        setTimeout(function () {
    618          WaveService.listenDeviceEvents();
    619        }, 1000);
    620      }
    621    );
    622  },
    623  sendDeviceEvent: function (device_token, event, onSuccess, onError) {
    624    var data = JSON.stringify({
    625      type: event.type,
    626      data: event.data,
    627    });
    628    sendRequest(
    629      "POST",
    630      "api/devices/" + device_token + "/events",
    631      { "Content-Type": "application/json" },
    632      data,
    633      onSuccess,
    634      onError
    635    );
    636  },
    637  addGlobalDeviceEventListener: function (callback) {
    638    WaveService._globalDeviceEventListeners.push(callback);
    639    WaveService.listenGlobalDeviceEvents();
    640  },
    641  removeGlobalDeviceEventListener: function (callback) {
    642    var index = WaveService._globalDeviceEventListeners.indexOf(callback);
    643    WaveService._globalDeviceEventListeners.splice(index, 1);
    644  },
    645  listenGlobalDeviceEvents: function () {
    646    var listeners = WaveService._globalDeviceEventListeners;
    647    if (listeners.length === 0) return;
    648    var query = "";
    649    if (WaveService._device_token) {
    650      query = "?device_token=" + WaveService._device_token;
    651    }
    652    WaveService.listenHttpPolling(
    653      "api/devices/events" + query,
    654      function (response) {
    655        if (!response) {
    656          WaveService.listenGlobalDeviceEvents();
    657          return;
    658        }
    659        for (var listener of listeners) {
    660          listener(response);
    661        }
    662        WaveService.listenGlobalDeviceEvents();
    663      },
    664      function () {
    665        setTimeout(function () {
    666          WaveService.listenGlobalDeviceEvents();
    667        }, 1000);
    668      }
    669    );
    670  },
    671  sendGlobalDeviceEvent: function (event, onSuccess, onError) {
    672    var data = JSON.stringify({
    673      type: event.type,
    674      data: event.data,
    675    });
    676    sendRequest(
    677      "POST",
    678      "api/devices/events",
    679      { "Content-Type": "application/json" },
    680      data,
    681      onSuccess,
    682      onError
    683    );
    684  },
    685 
    686  // GENERAL API
    687  readStatus: function (onSuccess, onError) {
    688    sendRequest(
    689      "GET",
    690      "api/status",
    691      null,
    692      null,
    693      function (response) {
    694        var data = JSON.parse(response);
    695        var configuration = {
    696          readSessionsEnabled: data.read_sessions_enabled,
    697          importResultsEnabled: data.import_results_enabled,
    698          reportsEnabled: data.reports_enabled,
    699          versionString: data.version_string,
    700          testTypeSelectionEnabled: data.test_type_selection_enabled,
    701          testFileSelectionEnabled: data.test_file_selection_enabled
    702        };
    703        onSuccess(configuration);
    704      },
    705      onError
    706    );
    707  },
    708 
    709  // UTILITY
    710  addRecentSession: function (token) {
    711    if (!token) return;
    712    var state = WaveService.getState();
    713    if (!state.recent_sessions) state.recent_sessions = [];
    714    if (state.recent_sessions.indexOf(token) !== -1) return;
    715    state.recent_sessions.unshift(token);
    716    WaveService.setState(state);
    717  },
    718  addRecentSessions: function (tokens) {
    719    for (var i = 0; i < tokens.length; i++) {
    720      var token = tokens[i];
    721      WaveService.addRecentSession(token);
    722    }
    723  },
    724  getPinnedSessions: function () {
    725    var state = WaveService.getState();
    726    if (!state || !state.pinned_sessions) return [];
    727    return state.pinned_sessions;
    728  },
    729  addPinnedSession: function (token) {
    730    if (!token) return;
    731    var state = WaveService.getState();
    732    if (!state.pinned_sessions) state.pinned_sessions = [];
    733    if (state.pinned_sessions.indexOf(token) !== -1) return;
    734    state.pinned_sessions.unshift(token);
    735    WaveService.setState(state);
    736  },
    737  getRecentSessions: function () {
    738    var state = WaveService.getState();
    739    if (!state || !state.recent_sessions) return [];
    740    return state.recent_sessions;
    741  },
    742  setRecentSessions: function (sessionTokens) {
    743    var state = WaveService.getState();
    744    state.recent_sessions = sessionTokens;
    745    WaveService.setState(state);
    746  },
    747  removePinnedSession: function (token) {
    748    if (!token) return;
    749    var state = WaveService.getState();
    750    if (!state.pinned_sessions) return;
    751    var index = state.pinned_sessions.indexOf(token);
    752    if (index === -1) return;
    753    state.pinned_sessions.splice(index, 1);
    754    WaveService.setState(state);
    755  },
    756  removeRecentSession: function (token) {
    757    var state = WaveService.getState();
    758    if (!state.recent_sessions) return;
    759    var index = state.recent_sessions.indexOf(token);
    760    if (index === -1) return;
    761    state.recent_sessions.splice(index, 1);
    762    WaveService.setState(state);
    763  },
    764  getState: function () {
    765    if (!window.localStorage) return null;
    766    var storage = window.localStorage;
    767    var state = JSON.parse(storage.getItem("wave"));
    768    if (!state) return {};
    769    return state;
    770  },
    771  setState: function (state) {
    772    if (!window.localStorage) return null;
    773    var storage = window.localStorage;
    774    storage.setItem("wave", JSON.stringify(state));
    775  },
    776  _globalDeviceEventListeners: [],
    777  _sessionEventListeners: {},
    778  _sessionEventNumbers: {},
    779  listenHttpPolling: function (url, onSuccess, onError) {
    780    var uniqueId = new Date().getTime();
    781    if (url.indexOf("?") === -1) {
    782      url = url + "?id=" + uniqueId;
    783    } else {
    784      url = url + "&id=" + uniqueId;
    785    }
    786    sendRequest(
    787      "GET",
    788      url,
    789      null,
    790      null,
    791      function (response) {
    792        if (!response) {
    793          onSuccess(null);
    794          return;
    795        }
    796        onSuccess(JSON.parse(response));
    797      },
    798      onError
    799    );
    800  },
    801  addSessionEventListener: function (token, callback) {
    802    var listeners = WaveService._sessionEventListeners;
    803    if (!listeners[token]) listeners[token] = [];
    804    if (listeners[token].indexOf(callback) >= 0) return;
    805    listeners[token].push(callback);
    806    WaveService._sessionEventListeners = listeners;
    807    WaveService.listenSessionEvents(token);
    808  },
    809  removeSessionEventListener: function (callback) {
    810    var listeners = WaveService._sessionEventListeners;
    811    for (var token of Object.keys(listeners)) {
    812      var index = listeners[token].indexOf(callback);
    813      if (index === -1) continue;
    814      listeners[token].splice(index, 1);
    815      break;
    816    }
    817    WaveService._sessionEventListeners = listeners;
    818  },
    819  listenSessionEvents: function (token) {
    820    var listeners = WaveService._sessionEventListeners;
    821    if (!listeners[token] || listeners.length === 0) return;
    822    var url = "api/sessions/" + token + "/events";
    823    var lastEventNumber = WaveService._sessionEventNumbers[token];
    824    if (lastEventNumber) {
    825      url += "?last_event=" + lastEventNumber;
    826    }
    827    WaveService.listenHttpPolling(
    828      url,
    829      function (response) {
    830        if (!response) {
    831          WaveService.listenSessionEvents(token);
    832          return;
    833        }
    834        var lastEventNumber = 0;
    835        for (var listener of listeners[token]) {
    836          for (var event of response) {
    837            if (event.number > lastEventNumber) {
    838              lastEventNumber = event.number;
    839            }
    840            listener(event);
    841          }
    842        }
    843        WaveService._sessionEventNumbers[token] = lastEventNumber;
    844        WaveService.listenSessionEvents(token);
    845      },
    846      function () {
    847        setTimeout(function () {
    848          WaveService.listenSessionEvents();
    849        }, 1000);
    850      }
    851    );
    852  },
    853  openSession: function (token) {
    854    location.href = "/results.html?token=" + token;
    855  },
    856 };
    857 
    858 if (!Object.keys)
    859  Object.keys = function (o) {
    860    if (o !== Object(o))
    861      throw new TypeError("Object.keys called on a non-object");
    862    var k = [],
    863      p;
    864    for (p in o) if (Object.prototype.hasOwnProperty.call(o, p)) k.push(p);
    865    return k;
    866  };