mirror of
				https://github.com/zulip/zulip.git
				synced 2025-10-31 12:03:46 +00:00 
			
		
		
		
	
		
			
				
	
	
		
			137 lines
		
	
	
		
			5.2 KiB
		
	
	
	
		
			JavaScript
		
	
	
	
	
	
			
		
		
	
	
			137 lines
		
	
	
		
			5.2 KiB
		
	
	
	
		
			JavaScript
		
	
	
	
	
	
| import * as message_scroll from "./message_scroll";
 | |
| 
 | |
| 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;
 | |
| }
 | |
| 
 | |
| export class FetchStatus {
 | |
|     // 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.
 | |
|     _loading_older = false;
 | |
|     _loading_newer = false;
 | |
|     _found_oldest = false;
 | |
|     _found_newest = false;
 | |
|     _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).
 | |
|     _expected_max_message_id = 0;
 | |
| 
 | |
|     start_older_batch(opts) {
 | |
|         this._loading_older = true;
 | |
|         if (opts.update_loading_indicator) {
 | |
|             message_scroll.show_loading_older();
 | |
|         }
 | |
|     }
 | |
| 
 | |
|     finish_older_batch(opts) {
 | |
|         this._loading_older = false;
 | |
|         this._found_oldest = opts.found_oldest;
 | |
|         this._history_limited = opts.history_limited;
 | |
|         if (opts.update_loading_indicator) {
 | |
|             message_scroll.hide_loading_older();
 | |
|         }
 | |
|     }
 | |
| 
 | |
|     can_load_older_messages() {
 | |
|         return !this._loading_older && !this._found_oldest;
 | |
|     }
 | |
| 
 | |
|     has_found_oldest() {
 | |
|         return this._found_oldest;
 | |
|     }
 | |
| 
 | |
|     history_limited() {
 | |
|         return this._history_limited;
 | |
|     }
 | |
| 
 | |
|     start_newer_batch(opts) {
 | |
|         this._loading_newer = true;
 | |
|         if (opts.update_loading_indicator) {
 | |
|             message_scroll.show_loading_newer();
 | |
|         }
 | |
|     }
 | |
| 
 | |
|     finish_newer_batch(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);
 | |
|         this._loading_newer = false;
 | |
|         this._found_newest = opts.found_newest;
 | |
|         if (opts.update_loading_indicator) {
 | |
|             message_scroll.hide_loading_newer();
 | |
|         }
 | |
|         if (this._found_newest && this._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.
 | |
|             this._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.
 | |
|             this._expected_max_message_id = 0;
 | |
|             return true;
 | |
|         }
 | |
|         return false;
 | |
|     }
 | |
| 
 | |
|     can_load_newer_messages() {
 | |
|         return !this._loading_newer && !this._found_newest;
 | |
|     }
 | |
| 
 | |
|     has_found_newest() {
 | |
|         return this._found_newest;
 | |
|     }
 | |
| 
 | |
|     update_expected_max_message_id(messages) {
 | |
|         this._expected_max_message_id = Math.max(
 | |
|             this._expected_max_message_id,
 | |
|             max_id_for_messages(messages),
 | |
|         );
 | |
|     }
 | |
| }
 |