From 97ffccb45f85b05c9158ed119c71b6a404afb298 Mon Sep 17 00:00:00 2001 From: Anders Kaseorg Date: Wed, 18 Sep 2024 12:40:01 -0700 Subject: [PATCH] reload: Convert module to TypeScript. Signed-off-by: Anders Kaseorg --- docs/subsystems/hashchange-system.md | 2 +- tools/test-js-with-node | 2 +- web/e2e-tests/navigation.test.ts | 2 -- web/src/activity.ts | 2 +- web/src/{reload.js => reload.ts} | 53 +++++++++++++++++----------- web/src/reload_state.ts | 2 +- web/src/zulip_test.ts | 1 - web/tests/reload.test.js | 2 +- 8 files changed, 38 insertions(+), 28 deletions(-) rename web/src/{reload.js => reload.ts} (88%) diff --git a/docs/subsystems/hashchange-system.md b/docs/subsystems/hashchange-system.md index 6fef63d67c..bbe81e3529 100644 --- a/docs/subsystems/hashchange-system.md +++ b/docs/subsystems/hashchange-system.md @@ -97,7 +97,7 @@ reload itself: 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 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. An important detail in server-initiated reloads is that we diff --git a/tools/test-js-with-node b/tools/test-js-with-node index cb2ff113f3..149c1e3f07 100755 --- a/tools/test-js-with-node +++ b/tools/test-js-with-node @@ -189,7 +189,7 @@ EXEMPT_FILES = make_set( "web/src/realm_playground.ts", "web/src/realm_user_settings_defaults.ts", "web/src/recent_view_ui.ts", - "web/src/reload.js", + "web/src/reload.ts", "web/src/reload_setup.js", "web/src/reminder.js", "web/src/resize.ts", diff --git a/web/e2e-tests/navigation.test.ts b/web/e2e-tests/navigation.test.ts index 9e96a382ac..fd5b2578cb 100644 --- a/web/e2e-tests/navigation.test.ts +++ b/web/e2e-tests/navigation.test.ts @@ -71,8 +71,6 @@ async function test_reload_hash(page: Page): Promise { const initial_hash = await page.evaluate(() => window.location.hash); 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}); }); await page.waitForNavigation(); diff --git a/web/src/activity.ts b/web/src/activity.ts index 9ee7f929ad..9a0b28b527 100644 --- a/web/src/activity.ts +++ b/web/src/activity.ts @@ -52,7 +52,7 @@ export let client_is_active = document.hasFocus(); // new_user_input is a more strict version of client_is_active used // 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 // server-initiated reload as user activity. export let new_user_input = true; diff --git a/web/src/reload.js b/web/src/reload.ts similarity index 88% rename from web/src/reload.js rename to web/src/reload.ts index 69d056cac0..5cbf8b9d71 100644 --- a/web/src/reload.js +++ b/web/src/reload.ts @@ -1,10 +1,13 @@ import $ from "jquery"; +import assert from "minimalistic-assert"; +import {z} from "zod"; import * as blueslip from "./blueslip"; import * as compose_state from "./compose_state"; import {csrf_token} from "./csrf"; import * as drafts from "./drafts"; import * as hash_util from "./hash_util"; +import type {LocalStorage} from "./localstorage"; import {localstorage} from "./localstorage"; import * as message_lists from "./message_lists"; 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 -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); } -function call_reload_hooks() { +function call_reload_hooks(): void { for (const hook of reload_hooks) { hook(); } } -function preserve_state(send_after_reload, save_compose) { +function preserve_state(send_after_reload: boolean, save_compose: boolean): void { if (!localstorage.supported()) { // If local storage is not supported by the browser, we can't // 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); + assert(csrf_token !== undefined); url += "+csrf_token=" + encodeURIComponent(csrf_token); if (save_compose) { const msg_type = compose_state.get_message_type(); if (msg_type === "stream") { + const stream_id = compose_state.stream_id(); + assert(stream_id !== undefined); url += "+msg_type=stream"; - url += "+stream_id=" + encodeURIComponent(compose_state.stream_id()); + url += "+stream_id=" + encodeURIComponent(stream_id); url += "+topic=" + encodeURIComponent(compose_state.topic()); } else if (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 // just pass the actual data structures through local storage. const token = util.random_int(0, 1024 * 1024 * 1024 * 1024); - const metadata = { + const metadata: z.infer = { url, timestamp: Date.now(), }; @@ -102,33 +110,39 @@ function preserve_state(send_after_reload, save_compose) { 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 // to a map containing the string and a timestamp. For now we'll // delete all tokens that only contain the url. Remove this // early return once you can no longer directly upgrade from // Zulip 5.x to the current version. - if (!token_metadata.timestamp) { + if (!parsed.success) { return true; } + const {timestamp} = parsed.data; // 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 // (e.g. a tab could fail to load and be refreshed a while later). 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; return days_since_token_creation > 7; } -function delete_stale_tokens(ls) { +function delete_stale_tokens(ls: LocalStorage): void { const now = Date.now(); ls.removeDataRegexWithCondition("reload:\\d+", (metadata) => 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()) { blueslip.log("do_reload_app: Doing nothing since reload_in_progress"); return; @@ -164,7 +178,7 @@ function do_reload_app(send_after_reload, save_compose, message_html) { }); }, 5000); - function retry_reload() { + function retry_reload(): void { blueslip.log("Retrying page reload due to 30s timer"); window.location.reload(); } @@ -184,7 +198,7 @@ export function initiate({ save_compose = true, send_after_reload = false, message_html = "Reloading ...", -}) { +}): void { if (immediate) { 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 // spread over at least 5 minutes in all cases. - let idle_control; + let idle_control: ReturnType; const random_variance = util.random_int(0, 1000 * 60 * 5); const unconditional_timeout = 1000 * 60 * 30 + random_variance; const composing_idle_timeout = 1000 * 60 * 7 + 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); } @@ -232,22 +245,22 @@ export function initiate({ // particularly disruptive. 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 // compose, we return them to the not-composing timeouts. idle_control.cancel(); idle_control = $(document).idle({idle: basic_idle_timeout, onIdle: reload_from_idle}); $(document).off("compose_canceled.zulip compose_finished.zulip", compose_done_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 // message, switch to the compose-open timeouts. idle_control.cancel(); idle_control = $(document).idle({idle: composing_idle_timeout, onIdle: reload_from_idle}); $(document).off("compose_started.zulip", compose_started_handler); $(document).on("compose_canceled.zulip compose_finished.zulip", compose_done_handler); - }; + } if (compose_state.composing()) { idle_control = $(document).idle({idle: composing_idle_timeout, onIdle: reload_from_idle}); diff --git a/web/src/reload_state.ts b/web/src/reload_state.ts index b0244ef431..b9031b1c14 100644 --- a/web/src/reload_state.ts +++ b/web/src/reload_state.ts @@ -2,7 +2,7 @@ We want his module to load pretty early in the process of starting the app, so that people.js can load early. 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 would be kind of upside down. */ diff --git a/web/src/zulip_test.ts b/web/src/zulip_test.ts index aec53e85ac..e3ea7e75e3 100644 --- a/web/src/zulip_test.ts +++ b/web/src/zulip_test.ts @@ -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 {cancel as cancel_compose} from "./compose_actions"; 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 {page_load_time} from "./setup"; export {current_user, realm} from "./state_data"; diff --git a/web/tests/reload.test.js b/web/tests/reload.test.js index 627000c10f..c3e7a3998a 100644 --- a/web/tests/reload.test.js +++ b/web/tests/reload.test.js @@ -5,7 +5,7 @@ const {strict: assert} = require("assert"); const {zrequire} = require("./lib/namespace"); 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; const reload = zrequire("reload");