Files
zulip/static/js/server_events.js
Anders Kaseorg 02511bff1c js: Automatically convert _.each to for…of.
This commit was automatically generated by the following script,
followed by lint --fix and a few small manual lint-related cleanups.

import * as babelParser from "recast/parsers/babel";
import * as recast from "recast";
import * as tsParser from "recast/parsers/typescript";
import { builders as b, namedTypes as n } from "ast-types";
import { Context } from "ast-types/lib/path-visitor";
import K from "ast-types/gen/kinds";
import { NodePath } from "ast-types/lib/node-path";
import assert from "assert";
import fs from "fs";
import path from "path";
import process from "process";

const checkExpression = (node: n.Node): node is K.ExpressionKind =>
  n.Expression.check(node);
const checkStatement = (node: n.Node): node is K.StatementKind =>
  n.Statement.check(node);

for (const file of process.argv.slice(2)) {
  console.log("Parsing", file);
  const ast = recast.parse(fs.readFileSync(file, { encoding: "utf8" }), {
    parser: path.extname(file) === ".ts" ? tsParser : babelParser,
  });
  let changed = false;
  let inLoop = false;
  let replaceReturn = false;

  const visitLoop = (...args: string[]) =>
    function(this: Context, path: NodePath) {
      for (const arg of args) {
        this.visit(path.get(arg));
      }
      const old = { inLoop };
      inLoop = true;
      this.visit(path.get("body"));
      inLoop = old.inLoop;
      return false;
    };

  recast.visit(ast, {
    visitDoWhileStatement: visitLoop("test"),

    visitExpressionStatement(path) {
      const { expression, comments } = path.node;
      let valueOnly;
      if (
        n.CallExpression.check(expression) &&
        n.MemberExpression.check(expression.callee) &&
        !expression.callee.computed &&
        n.Identifier.check(expression.callee.object) &&
        expression.callee.object.name === "_" &&
        n.Identifier.check(expression.callee.property) &&
        ["each", "forEach"].includes(expression.callee.property.name) &&
        [2, 3].includes(expression.arguments.length) &&
        checkExpression(expression.arguments[0]) &&
        (n.FunctionExpression.check(expression.arguments[1]) ||
          n.ArrowFunctionExpression.check(expression.arguments[1])) &&
        [1, 2].includes(expression.arguments[1].params.length) &&
        n.Identifier.check(expression.arguments[1].params[0]) &&
        ((valueOnly = expression.arguments[1].params[1] === undefined) ||
          n.Identifier.check(expression.arguments[1].params[1])) &&
        (expression.arguments[2] === undefined ||
          n.ThisExpression.check(expression.arguments[2]))
      ) {
        const old = { inLoop, replaceReturn };
        inLoop = false;
        replaceReturn = true;
        this.visit(
          path
            .get("expression")
            .get("arguments")
            .get(1)
            .get("body")
        );
        inLoop = old.inLoop;
        replaceReturn = old.replaceReturn;

        const [right, { body, params }] = expression.arguments;
        const loop = b.forOfStatement(
          b.variableDeclaration("let", [
            b.variableDeclarator(
              valueOnly ? params[0] : b.arrayPattern([params[1], params[0]])
            ),
          ]),
          valueOnly
            ? right
            : b.callExpression(
                b.memberExpression(right, b.identifier("entries")),
                []
              ),
          checkStatement(body) ? body : b.expressionStatement(body)
        );
        loop.comments = comments;
        path.replace(loop);
        changed = true;
      }
      this.traverse(path);
    },

    visitForStatement: visitLoop("init", "test", "update"),

    visitForInStatement: visitLoop("left", "right"),

    visitForOfStatement: visitLoop("left", "right"),

    visitFunction(path) {
      this.visit(path.get("params"));
      const old = { replaceReturn };
      replaceReturn = false;
      this.visit(path.get("body"));
      replaceReturn = old.replaceReturn;
      return false;
    },

    visitReturnStatement(path) {
      if (replaceReturn) {
        assert(!inLoop); // could use labeled continue if this ever fires
        const { argument, comments } = path.node;
        if (argument === null) {
          const s = b.continueStatement();
          s.comments = comments;
          path.replace(s);
        } else {
          const s = b.expressionStatement(argument);
          s.comments = comments;
          path.replace(s, b.continueStatement());
        }
        return false;
      }
      this.traverse(path);
    },

    visitWhileStatement: visitLoop("test"),
  });

  if (changed) {
    console.log("Writing", file);
    fs.writeFileSync(file, recast.print(ast).code, { encoding: "utf8" });
  }
}

Signed-off-by: Anders Kaseorg <anders@zulipchat.com>
2020-02-07 14:09:47 -08:00

324 lines
11 KiB
JavaScript

// Docs: https://zulip.readthedocs.io/en/latest/subsystems/events-system.html
let waiting_on_homeview_load = true;
let events_stored_while_loading = [];
let get_events_xhr;
let get_events_timeout;
let get_events_failures = 0;
const get_events_params = {};
function get_events_success(events) {
let messages = [];
const update_message_events = [];
const post_message_events = [];
let new_pointer;
const clean_event = function clean_event(event) {
// Only log a whitelist of the event to remove private data
return _.pick(event, 'id', 'type', 'op');
};
for (const event of events) {
try {
get_events_params.last_event_id = Math.max(get_events_params.last_event_id,
event.id);
} catch (ex) {
blueslip.error('Failed to update last_event_id',
{event: clean_event(event)},
ex.stack);
}
}
if (waiting_on_homeview_load) {
events_stored_while_loading = events_stored_while_loading.concat(events);
return;
}
if (events_stored_while_loading.length > 0) {
events = events_stored_while_loading.concat(events);
events_stored_while_loading = [];
}
// Most events are dispatched via the code server_events_dispatch,
// called in the default case. The goal of this split is to avoid
// contributors needing to read or understand the complex and
// rarely modified logic for non-normal events.
const dispatch_event = function dispatch_event(event) {
switch (event.type) {
case 'message': {
const msg = event.message;
msg.flags = event.flags;
if (event.local_message_id) {
msg.local_id = event.local_message_id;
sent_messages.report_event_received(event.local_message_id);
}
messages.push(msg);
break;
}
case 'pointer':
new_pointer = event.pointer;
break;
case 'update_message':
update_message_events.push(event);
break;
case 'delete_message':
case 'submessage':
case 'update_message_flags':
post_message_events.push(event);
break;
default:
return server_events_dispatch.dispatch_normal_event(event);
}
};
for (const event of events) {
try {
dispatch_event(event);
} catch (ex1) {
blueslip.error('Failed to process an event\n' +
blueslip.exception_msg(ex1),
{event: clean_event(event)},
ex1.stack);
}
}
if (messages.length !== 0) {
// Sort by ID, so that if we get multiple messages back from
// the server out-of-order, we'll still end up with our
// message lists in order.
messages = _.sortBy(messages, 'id');
try {
messages = echo.process_from_server(messages);
if (messages.length > 0) {
_.each(messages, message_store.set_message_booleans);
let sent_by_this_client = false;
for (const msg of messages) {
const msg_state = sent_messages.messages[msg.local_id];
if (msg_state) {
// Almost every time, this message will be the
// only one in messages, because multiple messages
// being returned by get_events usually only
// happens when a client is offline, but we know
// this client just sent a message in this batch
// of events. But in any case,
// insert_new_messages handles multiple messages,
// only one of which was sent by this client,
// correctly.
sent_by_this_client = true;
}
}
message_events.insert_new_messages(messages, sent_by_this_client);
}
} catch (ex2) {
blueslip.error('Failed to insert new messages\n' +
blueslip.exception_msg(ex2),
undefined,
ex2.stack);
}
}
if (new_pointer !== undefined
&& new_pointer > pointer.furthest_read) {
pointer.set_furthest_read(new_pointer);
pointer.set_server_furthest_read(new_pointer);
home_msg_list.select_id(new_pointer, {then_scroll: true, use_closest: true});
}
if (home_msg_list.selected_id() === -1 && !home_msg_list.empty()) {
home_msg_list.select_id(home_msg_list.first().id, {then_scroll: false});
}
if (update_message_events.length !== 0) {
try {
message_events.update_messages(update_message_events);
} catch (ex3) {
blueslip.error('Failed to update messages\n' +
blueslip.exception_msg(ex3),
undefined,
ex3.stack);
}
}
// We do things like updating message flags and deleting messages last,
// to avoid ordering issues that are caused by batch handling of
// messages above.
for (const event of post_message_events) {
server_events_dispatch.dispatch_normal_event(event);
}
}
function show_ui_connection_error() {
ui_report.show_error($("#connection-error"));
$("#connection-error").addClass('get-events-error');
}
function hide_ui_connection_error() {
ui_report.hide_error($("#connection-error"));
$("#connection-error").removeClass('get-events-error');
}
function get_events(options) {
options = _.extend({dont_block: false}, options);
if (reload_state.is_in_progress()) {
return;
}
get_events_params.dont_block = options.dont_block || get_events_failures > 0;
if (get_events_params.queue_id === undefined) {
get_events_params.queue_id = page_params.queue_id;
get_events_params.last_event_id = page_params.last_event_id;
}
if (get_events_xhr !== undefined) {
get_events_xhr.abort();
}
if (get_events_timeout !== undefined) {
clearTimeout(get_events_timeout);
}
get_events_params.client_gravatar = true;
get_events_params.slim_presence = true;
get_events_timeout = undefined;
get_events_xhr = channel.get({
url: '/json/events',
data: get_events_params,
idempotent: true,
timeout: page_params.poll_timeout,
success: function (data) {
try {
get_events_xhr = undefined;
get_events_failures = 0;
hide_ui_connection_error();
get_events_success(data.events);
} catch (ex) {
blueslip.error('Failed to handle get_events success\n' +
blueslip.exception_msg(ex),
undefined,
ex.stack);
}
get_events_timeout = setTimeout(get_events, 0);
},
error: function (xhr, error_type) {
try {
get_events_xhr = undefined;
// If we're old enough that our message queue has been
// garbage collected, immediately reload.
if (xhr.status === 400 &&
JSON.parse(xhr.responseText).code === 'BAD_EVENT_QUEUE_ID') {
page_params.event_queue_expired = true;
reload.initiate({immediate: true,
save_pointer: false,
save_narrow: true,
save_compose: true});
}
if (error_type === 'abort') {
// Don't restart if we explicitly aborted
return;
} else if (error_type === 'timeout') {
// Retry indefinitely on timeout.
get_events_failures = 0;
hide_ui_connection_error();
} else {
get_events_failures += 1;
}
if (get_events_failures >= 5) {
show_ui_connection_error();
} else {
hide_ui_connection_error();
}
} catch (ex) {
blueslip.error('Failed to handle get_events error\n' +
blueslip.exception_msg(ex),
undefined,
ex.stack);
}
const retry_sec = Math.min(90, Math.exp(get_events_failures / 2));
get_events_timeout = setTimeout(get_events, retry_sec * 1000);
},
});
}
exports.assert_get_events_running = function assert_get_events_running(error_message) {
if (get_events_xhr === undefined && get_events_timeout === undefined) {
exports.restart_get_events({dont_block: true});
blueslip.error(error_message);
}
};
exports.restart_get_events = function restart_get_events(options) {
get_events(options);
};
exports.force_get_events = function force_get_events() {
get_events_timeout = setTimeout(get_events, 0);
};
exports.home_view_loaded = function home_view_loaded() {
waiting_on_homeview_load = false;
get_events_success([]);
$(document).trigger("home_view_loaded.zulip");
};
let watchdog_time = $.now();
exports.check_for_unsuspend = function () {
const new_time = $.now();
if (new_time - watchdog_time > 20000) { // 20 seconds.
// Defensively reset watchdog_time here in case there's an
// exception in one of the event handlers
watchdog_time = new_time;
// Our app's JS wasn't running, which probably means the machine was
// asleep.
$(document).trigger($.Event('unsuspend'));
}
watchdog_time = new_time;
};
setInterval(exports.check_for_unsuspend, 5000);
exports.initialize = function () {
$(document).on('unsuspend', function () {
// Immediately poll for new events on unsuspend
blueslip.log("Restarting get_events due to unsuspend");
get_events_failures = 0;
exports.restart_get_events({dont_block: true});
});
get_events();
};
exports.cleanup_event_queue = function cleanup_event_queue() {
// Submit a request to the server to cleanup our event queue
if (page_params.event_queue_expired === true) {
return;
}
blueslip.log("Cleaning up our event queue");
// Set expired because in a reload we may be called twice.
page_params.event_queue_expired = true;
channel.del({
url: '/json/events',
data: {queue_id: page_params.queue_id},
});
};
window.addEventListener("beforeunload", function () {
exports.cleanup_event_queue();
});
// For unit testing
exports._get_events_success = get_events_success;
window.server_events = exports;