docs/subsystems/hashchange-system.md
The Zulip web application has a nice system of hash (#) URLs that can be used to deep-link into the application and allow the browser's "back" functionality to let the user navigate between parts of the UI. Some examples are:
/#settings/your-bots: Bots section of the settings overlay./#channels: Channels overlay, where the user manages channels
(subscription etc.)/#channels/11/announce: Channels overlay with channel ID 11 (called
"announce") selected./#narrow/channel/42-android/topic/fun: Message feed showing channel
"android" and topic "fun". (The 42 represents the id of the
channel.)The main module in the frontend that manages this all is
web/src/hashchange.ts (plus hash_util.js for all the parsing
code), which is unfortunately one of our thorniest modules. Part of
the reason that it's thorny is that it needs to support a lot of
different flows:
/#channels. This makes it easy to have simple links around the app
without custom click handlers for each one./#reload hash prefix.When making changes to the hashchange system, it is essential to test all of these flows, since we don't have great automated tests for all of this (would be a good project to add them to the Puppeteer suite) and there's enough complexity that it's easy to accidentally break something.
The main external API lives in web/src/browser_history.js:
browser_history.update is used to update the browser
history, and it should be called when the app code is taking care
of updating the UI directlybrowser_history.go_to_location is used when you want the hashchange
module to actually dispatch building the next pageInternally you have these functions:
hashchange.hashchanged is the function used to handle the hash,
whether it's changed by the browser (e.g., by clicking on a link to
a hash or using the back button) or triggered internally.hashchange.do_hashchange_normal handles most cases, like loading the main
page (but maybe with a specific URL if you are narrowed to a
channel or topic or direct messages, etc.).hashchange.do_hashchange_overlay handles overlay cases. Overlays have
some minor complexity related to remembering the page from
which the overlay was launched, as well as optimizing in-page
transitions (i.e. don't close/re-open the overlay if you can
easily avoid it).There are a few circumstances when the Zulip browser window needs to reload itself:
If the browser has been offline for more than 10 minutes, the
browser's event queue will have been
garbage-collected by the server, meaning the browser can no longer
get real-time updates altogether. In this case, the browser
auto-reloads immediately in order to reconnect. We have coded an
unsuspend callback (based on some clever time logic) that ensures we
check immediately when a client unsuspends; grep for watchdog to
see the code.
If a new version of the server has been deployed, we want to reload
the browser so that it will start running the latest code. However,
we don't want server deploys to be disruptive. So, the backend
preserves user-side event queues (etc.) and just pushes a special
restart event to all clients. That event causes the browser to
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.ts; but regardless we'll reload
within 30 minutes unconditionally.
An important detail in server-initiated reloads is that we desynchronize when browsers start attempting them randomly, in order to avoid a thundering herd situation bringing down the server.
Here are some key functions in the reload system:
reload.preserve_state is called when a server-initiated browser
reload happens, and encodes a bunch of data like the current scroll
position into the hash.reload_setup.initialize handles restoring the preserved state after a
reload where the hash starts with /#reload.In addition to saving state as described above when reloading the browser, Zulip also does a few bookkeeping things on page reload (like cleaning up its event queue, and saving any text in an open compose box as a draft).