mirror of https://github.com/zulip/zulip.git
138 lines
5.3 KiB
JavaScript
138 lines
5.3 KiB
JavaScript
const FetchStatus = function () {
|
|
const self = {};
|
|
|
|
// The FetchStatus object tracks tracks the state of a
|
|
// message_list_data object, whether rendered in the DOM or not,
|
|
// and is the source of truth for whether the message_list_data
|
|
// object has the complete history of the view or whether more
|
|
// messages should be loaded when scrolling to the top or bottom
|
|
// of the message feed.
|
|
let loading_older = false;
|
|
let loading_newer = false;
|
|
let found_oldest = false;
|
|
let found_newest = false;
|
|
let history_limited = false;
|
|
|
|
// Tracks the highest message ID that we know exist in this view,
|
|
// but are not within the contiguous range of messages we have
|
|
// received from the server. Used to correctly handle a rare race
|
|
// condition where a newly sent message races with fetching a
|
|
// group of messages that would lead to found_newest being set
|
|
// (described in detail below).
|
|
let expected_max_message_id = 0;
|
|
|
|
function max_id_for_messages(messages) {
|
|
let max_id = 0;
|
|
for (const msg of messages) {
|
|
max_id = Math.max(max_id, msg.id);
|
|
}
|
|
return max_id;
|
|
}
|
|
|
|
self.start_older_batch = function (opts) {
|
|
loading_older = true;
|
|
if (opts.update_loading_indicator) {
|
|
message_scroll.show_loading_older();
|
|
}
|
|
};
|
|
|
|
self.finish_older_batch = function (opts) {
|
|
loading_older = false;
|
|
found_oldest = opts.found_oldest;
|
|
history_limited = opts.history_limited;
|
|
if (opts.update_loading_indicator) {
|
|
message_scroll.hide_loading_older();
|
|
}
|
|
};
|
|
|
|
self.can_load_older_messages = function () {
|
|
return !loading_older && !found_oldest;
|
|
};
|
|
|
|
self.has_found_oldest = function () {
|
|
return found_oldest;
|
|
};
|
|
|
|
self.history_limited = function () {
|
|
return history_limited;
|
|
};
|
|
|
|
self.start_newer_batch = function (opts) {
|
|
loading_newer = true;
|
|
if (opts.update_loading_indicator) {
|
|
message_scroll.show_loading_newer();
|
|
}
|
|
};
|
|
|
|
self.finish_newer_batch = function (messages, opts) {
|
|
// Returns true if and only if the caller needs to trigger an
|
|
// additional fetch due to the race described below.
|
|
const found_max_message_id = max_id_for_messages(messages);
|
|
loading_newer = false;
|
|
found_newest = opts.found_newest;
|
|
if (opts.update_loading_indicator) {
|
|
message_scroll.hide_loading_newer();
|
|
}
|
|
if (found_newest && expected_max_message_id > found_max_message_id) {
|
|
// This expected_max_message_id logic is designed to
|
|
// resolve a subtle race condition involving newly sent
|
|
// messages in a view that does not display the currently
|
|
// latest messages.
|
|
//
|
|
// When a new message arrives matching the current view
|
|
// and found_newest is false, we cannot add the message to
|
|
// the view in-order without creating invalid output
|
|
// (where two messages are displaye adjacent but might be
|
|
// weeks and hundreds of messages apart in actuality).
|
|
//
|
|
// So we have to discard those messages. Usually, this is
|
|
// fine; the client will receive those when the user
|
|
// scrolls to the bottom of the page, triggering another
|
|
// fetch. With that solution, a rare race is still possible,
|
|
// with this sequence:
|
|
//
|
|
// 1. Client initiates GET /messages to fetch the last
|
|
// batch of messages in this view. The server
|
|
// completes the database access and and starts sending
|
|
// the response with found_newest=true.
|
|
// 1. A new message is sent matching the view, the event reaches
|
|
// the client. We discard the message because found_newest=false.
|
|
// 1. The client receives the GET /messages response, and
|
|
// marks found_newest=true. As a result, it believes is has
|
|
// the latest messages and won't fetch more, but is missing the
|
|
// recently sent message.
|
|
//
|
|
// To address this problem, we track the highest message
|
|
// ID among messages that were discarded due to
|
|
// fetch_status in expected_max_message_id. If that is
|
|
// higher than the highest ID returned in a GET /messages
|
|
// response with found_newest=true, we know the above race
|
|
// has happened and trigger an additional fetch.
|
|
found_newest = false;
|
|
|
|
// Resetting our tracked last message id is an important
|
|
// circuit-breaker for cases where the message(s) that we
|
|
// "know" exist were deleted or moved to another topic.
|
|
expected_max_message_id = 0;
|
|
return true;
|
|
}
|
|
return false;
|
|
};
|
|
|
|
self.can_load_newer_messages = function () {
|
|
return !loading_newer && !found_newest;
|
|
};
|
|
|
|
self.has_found_newest = function () {
|
|
return found_newest;
|
|
};
|
|
|
|
self.update_expected_max_message_id = function (messages) {
|
|
expected_max_message_id = Math.max(expected_max_message_id, max_id_for_messages(messages));
|
|
};
|
|
|
|
return self;
|
|
};
|
|
module.exports = FetchStatus;
|
|
window.FetchStatus = FetchStatus;
|