reload: Convert module to TypeScript.

Signed-off-by: Anders Kaseorg <anders@zulip.com>
This commit is contained in:
Anders Kaseorg 2024-09-18 12:40:01 -07:00 committed by Tim Abbott
parent 0be5cc232c
commit 97ffccb45f
8 changed files with 38 additions and 28 deletions

View File

@ -97,7 +97,7 @@ reload itself:
start looking for a good time to reload, based on when the user is start looking for a good time to reload, based on when the user is
idle (ideally, we'd reload when they're not looking and restore idle (ideally, we'd reload when they're not looking and restore
state so that the user never knew it happened!). The logic for state so that the user never knew it happened!). The logic for
doing this is in `web/src/reload.js`; but regardless we'll reload doing this is in `web/src/reload.ts`; but regardless we'll reload
within 30 minutes unconditionally. within 30 minutes unconditionally.
An important detail in server-initiated reloads is that we An important detail in server-initiated reloads is that we

View File

@ -189,7 +189,7 @@ EXEMPT_FILES = make_set(
"web/src/realm_playground.ts", "web/src/realm_playground.ts",
"web/src/realm_user_settings_defaults.ts", "web/src/realm_user_settings_defaults.ts",
"web/src/recent_view_ui.ts", "web/src/recent_view_ui.ts",
"web/src/reload.js", "web/src/reload.ts",
"web/src/reload_setup.js", "web/src/reload_setup.js",
"web/src/reminder.js", "web/src/reminder.js",
"web/src/resize.ts", "web/src/resize.ts",

View File

@ -71,8 +71,6 @@ async function test_reload_hash(page: Page): Promise<void> {
const initial_hash = await page.evaluate(() => window.location.hash); const initial_hash = await page.evaluate(() => window.location.hash);
await page.evaluate(() => { await page.evaluate(() => {
// We haven't converted reload.js to TypeScript yet.
// eslint-disable-next-line @typescript-eslint/no-unsafe-call
zulip_test.initiate_reload({immediate: true}); zulip_test.initiate_reload({immediate: true});
}); });
await page.waitForNavigation(); await page.waitForNavigation();

View File

@ -52,7 +52,7 @@ export let client_is_active = document.hasFocus();
// new_user_input is a more strict version of client_is_active used // new_user_input is a more strict version of client_is_active used
// primarily for analytics. We initialize this to true, to count new // primarily for analytics. We initialize this to true, to count new
// page loads, but set it to false in the onload function in reload.js // page loads, but set it to false in the onload function in reload.ts
// if this was a server-initiated-reload to avoid counting a // if this was a server-initiated-reload to avoid counting a
// server-initiated reload as user activity. // server-initiated reload as user activity.
export let new_user_input = true; export let new_user_input = true;

View File

@ -1,10 +1,13 @@
import $ from "jquery"; import $ from "jquery";
import assert from "minimalistic-assert";
import {z} from "zod";
import * as blueslip from "./blueslip"; import * as blueslip from "./blueslip";
import * as compose_state from "./compose_state"; import * as compose_state from "./compose_state";
import {csrf_token} from "./csrf"; import {csrf_token} from "./csrf";
import * as drafts from "./drafts"; import * as drafts from "./drafts";
import * as hash_util from "./hash_util"; import * as hash_util from "./hash_util";
import type {LocalStorage} from "./localstorage";
import {localstorage} from "./localstorage"; import {localstorage} from "./localstorage";
import * as message_lists from "./message_lists"; import * as message_lists from "./message_lists";
import {page_params} from "./page_params"; import {page_params} from "./page_params";
@ -14,19 +17,21 @@ import * as util from "./util";
// Read https://zulip.readthedocs.io/en/latest/subsystems/hashchange-system.html // Read https://zulip.readthedocs.io/en/latest/subsystems/hashchange-system.html
const reload_hooks = []; const token_metadata_schema = z.object({url: z.string(), timestamp: z.number()});
export function add_reload_hook(hook) { const reload_hooks: (() => void)[] = [];
export function add_reload_hook(hook: () => void): void {
reload_hooks.push(hook); reload_hooks.push(hook);
} }
function call_reload_hooks() { function call_reload_hooks(): void {
for (const hook of reload_hooks) { for (const hook of reload_hooks) {
hook(); hook();
} }
} }
function preserve_state(send_after_reload, save_compose) { function preserve_state(send_after_reload: boolean, save_compose: boolean): void {
if (!localstorage.supported()) { if (!localstorage.supported()) {
// If local storage is not supported by the browser, we can't // If local storage is not supported by the browser, we can't
// save the browser's position across reloads (since there's // save the browser's position across reloads (since there's
@ -46,13 +51,16 @@ function preserve_state(send_after_reload, save_compose) {
} }
let url = "#reload:send_after_reload=" + Number(send_after_reload); let url = "#reload:send_after_reload=" + Number(send_after_reload);
assert(csrf_token !== undefined);
url += "+csrf_token=" + encodeURIComponent(csrf_token); url += "+csrf_token=" + encodeURIComponent(csrf_token);
if (save_compose) { if (save_compose) {
const msg_type = compose_state.get_message_type(); const msg_type = compose_state.get_message_type();
if (msg_type === "stream") { if (msg_type === "stream") {
const stream_id = compose_state.stream_id();
assert(stream_id !== undefined);
url += "+msg_type=stream"; url += "+msg_type=stream";
url += "+stream_id=" + encodeURIComponent(compose_state.stream_id()); url += "+stream_id=" + encodeURIComponent(stream_id);
url += "+topic=" + encodeURIComponent(compose_state.topic()); url += "+topic=" + encodeURIComponent(compose_state.topic());
} else if (msg_type === "private") { } else if (msg_type === "private") {
url += "+msg_type=private"; url += "+msg_type=private";
@ -94,7 +102,7 @@ function preserve_state(send_after_reload, save_compose) {
// TODO: Remove the now-unnecessary URL-encoding logic above and // TODO: Remove the now-unnecessary URL-encoding logic above and
// just pass the actual data structures through local storage. // just pass the actual data structures through local storage.
const token = util.random_int(0, 1024 * 1024 * 1024 * 1024); const token = util.random_int(0, 1024 * 1024 * 1024 * 1024);
const metadata = { const metadata: z.infer<typeof token_metadata_schema> = {
url, url,
timestamp: Date.now(), timestamp: Date.now(),
}; };
@ -102,33 +110,39 @@ function preserve_state(send_after_reload, save_compose) {
window.location.replace("#reload:" + token); window.location.replace("#reload:" + token);
} }
export function is_stale_refresh_token(token_metadata, now) { export function is_stale_refresh_token(token_metadata: unknown, now: number): boolean {
const parsed = token_metadata_schema.safeParse(token_metadata);
// TODO/compatibility: the metadata was changed from a string // TODO/compatibility: the metadata was changed from a string
// to a map containing the string and a timestamp. For now we'll // to a map containing the string and a timestamp. For now we'll
// delete all tokens that only contain the url. Remove this // delete all tokens that only contain the url. Remove this
// early return once you can no longer directly upgrade from // early return once you can no longer directly upgrade from
// Zulip 5.x to the current version. // Zulip 5.x to the current version.
if (!token_metadata.timestamp) { if (!parsed.success) {
return true; return true;
} }
const {timestamp} = parsed.data;
// The time between reload token generation and use should usually be // The time between reload token generation and use should usually be
// fewer than 30 seconds, but we keep tokens around for a week just in case // fewer than 30 seconds, but we keep tokens around for a week just in case
// (e.g. a tab could fail to load and be refreshed a while later). // (e.g. a tab could fail to load and be refreshed a while later).
const milliseconds_in_a_day = 1000 * 60 * 60 * 24; const milliseconds_in_a_day = 1000 * 60 * 60 * 24;
const timedelta = now - token_metadata.timestamp; const timedelta = now - timestamp;
const days_since_token_creation = timedelta / milliseconds_in_a_day; const days_since_token_creation = timedelta / milliseconds_in_a_day;
return days_since_token_creation > 7; return days_since_token_creation > 7;
} }
function delete_stale_tokens(ls) { function delete_stale_tokens(ls: LocalStorage): void {
const now = Date.now(); const now = Date.now();
ls.removeDataRegexWithCondition("reload:\\d+", (metadata) => ls.removeDataRegexWithCondition("reload:\\d+", (metadata) =>
is_stale_refresh_token(metadata, now), is_stale_refresh_token(metadata, now),
); );
} }
function do_reload_app(send_after_reload, save_compose, message_html) { function do_reload_app(
send_after_reload: boolean,
save_compose: boolean,
message_html: string,
): void {
if (reload_state.is_in_progress()) { if (reload_state.is_in_progress()) {
blueslip.log("do_reload_app: Doing nothing since reload_in_progress"); blueslip.log("do_reload_app: Doing nothing since reload_in_progress");
return; return;
@ -164,7 +178,7 @@ function do_reload_app(send_after_reload, save_compose, message_html) {
}); });
}, 5000); }, 5000);
function retry_reload() { function retry_reload(): void {
blueslip.log("Retrying page reload due to 30s timer"); blueslip.log("Retrying page reload due to 30s timer");
window.location.reload(); window.location.reload();
} }
@ -184,7 +198,7 @@ export function initiate({
save_compose = true, save_compose = true,
send_after_reload = false, send_after_reload = false,
message_html = "Reloading ...", message_html = "Reloading ...",
}) { }): void {
if (immediate) { if (immediate) {
do_reload_app(send_after_reload, save_compose, message_html); do_reload_app(send_after_reload, save_compose, message_html);
} }
@ -215,14 +229,13 @@ export function initiate({
// makes it simple to reason about: We know that reloads will be // makes it simple to reason about: We know that reloads will be
// spread over at least 5 minutes in all cases. // spread over at least 5 minutes in all cases.
let idle_control; let idle_control: ReturnType<JQuery["idle"]>;
const random_variance = util.random_int(0, 1000 * 60 * 5); const random_variance = util.random_int(0, 1000 * 60 * 5);
const unconditional_timeout = 1000 * 60 * 30 + random_variance; const unconditional_timeout = 1000 * 60 * 30 + random_variance;
const composing_idle_timeout = 1000 * 60 * 7 + random_variance; const composing_idle_timeout = 1000 * 60 * 7 + random_variance;
const basic_idle_timeout = 1000 * 60 * 1 + random_variance; const basic_idle_timeout = 1000 * 60 * 1 + random_variance;
let compose_started_handler;
function reload_from_idle() { function reload_from_idle(): void {
do_reload_app(false, save_compose, message_html); do_reload_app(false, save_compose, message_html);
} }
@ -232,22 +245,22 @@ export function initiate({
// particularly disruptive. // particularly disruptive.
setTimeout(reload_from_idle, unconditional_timeout); setTimeout(reload_from_idle, unconditional_timeout);
const compose_done_handler = function () { function compose_done_handler(): void {
// If the user sends their message or otherwise closes // If the user sends their message or otherwise closes
// compose, we return them to the not-composing timeouts. // compose, we return them to the not-composing timeouts.
idle_control.cancel(); idle_control.cancel();
idle_control = $(document).idle({idle: basic_idle_timeout, onIdle: reload_from_idle}); idle_control = $(document).idle({idle: basic_idle_timeout, onIdle: reload_from_idle});
$(document).off("compose_canceled.zulip compose_finished.zulip", compose_done_handler); $(document).off("compose_canceled.zulip compose_finished.zulip", compose_done_handler);
$(document).on("compose_started.zulip", compose_started_handler); $(document).on("compose_started.zulip", compose_started_handler);
}; }
compose_started_handler = function () { function compose_started_handler(): void {
// If the user stops being idle and starts composing a // If the user stops being idle and starts composing a
// message, switch to the compose-open timeouts. // message, switch to the compose-open timeouts.
idle_control.cancel(); idle_control.cancel();
idle_control = $(document).idle({idle: composing_idle_timeout, onIdle: reload_from_idle}); idle_control = $(document).idle({idle: composing_idle_timeout, onIdle: reload_from_idle});
$(document).off("compose_started.zulip", compose_started_handler); $(document).off("compose_started.zulip", compose_started_handler);
$(document).on("compose_canceled.zulip compose_finished.zulip", compose_done_handler); $(document).on("compose_canceled.zulip compose_finished.zulip", compose_done_handler);
}; }
if (compose_state.composing()) { if (compose_state.composing()) {
idle_control = $(document).idle({idle: composing_idle_timeout, onIdle: reload_from_idle}); idle_control = $(document).idle({idle: composing_idle_timeout, onIdle: reload_from_idle});

View File

@ -2,7 +2,7 @@
We want his module to load pretty early in the process We want his module to load pretty early in the process
of starting the app, so that people.js can load early. of starting the app, so that people.js can load early.
All the heavy lifting for reload logic happens in All the heavy lifting for reload logic happens in
reload.js, which has lots of UI dependencies. If we reload.ts, which has lots of UI dependencies. If we
didn't split out this module, our whole dependency tree didn't split out this module, our whole dependency tree
would be kind of upside down. would be kind of upside down.
*/ */

View File

@ -10,7 +10,6 @@ export {get_by_user_id as get_person_by_user_id, get_user_id_from_name} from "./
export {last_visible as last_visible_row, id as row_id} from "./rows"; export {last_visible as last_visible_row, id as row_id} from "./rows";
export {cancel as cancel_compose} from "./compose_actions"; export {cancel as cancel_compose} from "./compose_actions";
export {page_params, page_params_parse_time} from "./base_page_params"; export {page_params, page_params_parse_time} from "./base_page_params";
// @ts-expect-error We haven't converted reload.js yet
export {initiate as initiate_reload} from "./reload"; export {initiate as initiate_reload} from "./reload";
export {page_load_time} from "./setup"; export {page_load_time} from "./setup";
export {current_user, realm} from "./state_data"; export {current_user, realm} from "./state_data";

View File

@ -5,7 +5,7 @@ const {strict: assert} = require("assert");
const {zrequire} = require("./lib/namespace"); const {zrequire} = require("./lib/namespace");
const {run_test, noop} = require("./lib/test"); const {run_test, noop} = require("./lib/test");
// override file-level function call in reload.js // override file-level function call in reload.ts
window.addEventListener = noop; window.addEventListener = noop;
const reload = zrequire("reload"); const reload = zrequire("reload");