mirror of
				https://github.com/zulip/zulip.git
				synced 2025-10-31 03:53:50 +00:00 
			
		
		
		
	This reduces the complexity of our dependency graph. It also makes sub_store.get parallel to message_store.get. For both you pass in the relevant id to get the full validated object.
		
			
				
	
	
		
			314 lines
		
	
	
		
			9.4 KiB
		
	
	
	
		
			JavaScript
		
	
	
	
	
	
			
		
		
	
	
			314 lines
		
	
	
		
			9.4 KiB
		
	
	
	
		
			JavaScript
		
	
	
	
	
	
| import {all_messages_data} from "./all_messages_data";
 | |
| import {FoldDict} from "./fold_dict";
 | |
| import * as message_util from "./message_util";
 | |
| import * as sub_store from "./sub_store";
 | |
| import * as unread from "./unread";
 | |
| 
 | |
| const stream_dict = new Map(); // stream_id -> PerStreamHistory object
 | |
| const fetched_stream_ids = new Set();
 | |
| 
 | |
| export function all_topics_in_cache(sub) {
 | |
|     // Checks whether this browser's cache of contiguous messages
 | |
|     // (used to locally render narrows) in all_messages_data has all
 | |
|     // messages from a given stream, and thus all historical topics
 | |
|     // for it.  Because all_messages_data is a range, we just need to
 | |
|     // compare it to the range of history on the stream.
 | |
| 
 | |
|     // If the cache isn't initialized, it's a clear false.
 | |
|     if (all_messages_data === undefined || all_messages_data.empty()) {
 | |
|         return false;
 | |
|     }
 | |
| 
 | |
|     // If the cache doesn't have the latest messages, we can't be sure
 | |
|     // we have all topics.
 | |
|     if (!all_messages_data.fetch_status.has_found_newest()) {
 | |
|         return false;
 | |
|     }
 | |
| 
 | |
|     if (sub.first_message_id === null) {
 | |
|         // If the stream has no message history, we have it all
 | |
|         // vacuously.  This should be a very rare condition, since
 | |
|         // stream creation sends a message.
 | |
|         return true;
 | |
|     }
 | |
| 
 | |
|     // Now, we can just compare the first cached message to the first
 | |
|     // message ID in the stream; if it's older, we're good, otherwise,
 | |
|     // we might be missing the oldest topics in this stream in our
 | |
|     // cache.
 | |
|     const first_cached_message = all_messages_data.first();
 | |
|     return first_cached_message.id <= sub.first_message_id;
 | |
| }
 | |
| 
 | |
| export function is_complete_for_stream_id(stream_id) {
 | |
|     if (fetched_stream_ids.has(stream_id)) {
 | |
|         return true;
 | |
|     }
 | |
| 
 | |
|     const sub = sub_store.get(stream_id);
 | |
|     const in_cache = all_topics_in_cache(sub);
 | |
| 
 | |
|     if (in_cache) {
 | |
|         /*
 | |
|             If the stream is cached, we can add it to
 | |
|             fetched_stream_ids.  Note that for the opposite
 | |
|             scenario, we don't delete from
 | |
|             fetched_stream_ids, because we may just be
 | |
|             waiting for the initial message fetch.
 | |
|         */
 | |
|         fetched_stream_ids.add(stream_id);
 | |
|     }
 | |
| 
 | |
|     return in_cache;
 | |
| }
 | |
| 
 | |
| export function stream_has_topics(stream_id) {
 | |
|     if (!stream_dict.has(stream_id)) {
 | |
|         return false;
 | |
|     }
 | |
| 
 | |
|     const history = stream_dict.get(stream_id);
 | |
| 
 | |
|     return history.has_topics();
 | |
| }
 | |
| 
 | |
| export class PerStreamHistory {
 | |
|     /*
 | |
|         For a given stream, this structure has a dictionary of topics.
 | |
|         The main getter of this object is get_recent_topic_names, and
 | |
|         we just sort on the fly every time we are called.
 | |
| 
 | |
|         Attributes for a topic are:
 | |
|         * message_id: The latest message_id in the topic.  Only usable
 | |
|           for imprecise applications like sorting.  The message_id
 | |
|           cannot be fully accurate given message editing and deleting
 | |
|           (as we don't have a way to handle the latest message in a
 | |
|           stream having its stream edited or deleted).
 | |
| 
 | |
|           TODO: We can probably fix this limitation by doing a
 | |
|           single-message `GET /messages` query with anchor="latest",
 | |
|           num_before=0, num_after=0, to update this field when its
 | |
|           value becomes ambiguous.  Or probably better to avoid a
 | |
|           thundering herd (of a fast query), having the server send
 | |
|           the data needed to do this update in stream/topic-edit and
 | |
|           delete events (just the new max_message_id for the relevant
 | |
|           topic would likely suffice, though we need to think about
 | |
|           private stream corner cases).
 | |
|         * pretty_name: The topic_name, with original case.
 | |
|         * historical: Whether the user actually received any messages in
 | |
|           the topic (has UserMessage rows) or is just viewing the stream.
 | |
|         * count: Number of known messages in the topic.  Used to detect
 | |
|           when the last messages in a topic were moved to other topics or
 | |
|           deleted.
 | |
|     */
 | |
| 
 | |
|     topics = new FoldDict();
 | |
|     // Most recent message ID for the stream.
 | |
|     max_message_id = 0;
 | |
| 
 | |
|     constructor(stream_id) {
 | |
|         this.stream_id = stream_id;
 | |
|     }
 | |
| 
 | |
|     has_topics() {
 | |
|         return this.topics.size !== 0;
 | |
|     }
 | |
| 
 | |
|     update_stream_max_message_id(message_id) {
 | |
|         if (message_id > this.max_message_id) {
 | |
|             this.max_message_id = message_id;
 | |
|         }
 | |
|     }
 | |
| 
 | |
|     add_or_update({topic_name, message_id = 0}) {
 | |
|         message_id = Number.parseInt(message_id, 10);
 | |
|         this.update_stream_max_message_id(message_id);
 | |
| 
 | |
|         const existing = this.topics.get(topic_name);
 | |
| 
 | |
|         if (!existing) {
 | |
|             this.topics.set(topic_name, {
 | |
|                 message_id,
 | |
|                 pretty_name: topic_name,
 | |
|                 historical: false,
 | |
|                 count: 1,
 | |
|             });
 | |
|             return;
 | |
|         }
 | |
| 
 | |
|         if (!existing.historical) {
 | |
|             existing.count += 1;
 | |
|         }
 | |
| 
 | |
|         if (message_id > existing.message_id) {
 | |
|             existing.message_id = message_id;
 | |
|             existing.pretty_name = topic_name;
 | |
|         }
 | |
|     }
 | |
| 
 | |
|     maybe_remove(topic_name, num_messages) {
 | |
|         const existing = this.topics.get(topic_name);
 | |
| 
 | |
|         if (!existing) {
 | |
|             return;
 | |
|         }
 | |
| 
 | |
|         if (existing.historical) {
 | |
|             // We can't trust that a topic rename applied to
 | |
|             // the entire history of historical topic, so we
 | |
|             // will always leave it in the sidebar.
 | |
|             return;
 | |
|         }
 | |
| 
 | |
|         if (existing.count <= num_messages) {
 | |
|             this.topics.delete(topic_name);
 | |
|             return;
 | |
|         }
 | |
| 
 | |
|         existing.count -= num_messages;
 | |
|     }
 | |
| 
 | |
|     add_history(server_history) {
 | |
|         // This method populates historical topics from the
 | |
|         // server.  We have less data about these than the
 | |
|         // client can maintain for newer topics.
 | |
| 
 | |
|         for (const obj of server_history) {
 | |
|             const topic_name = obj.name;
 | |
|             const message_id = obj.max_id;
 | |
| 
 | |
|             const existing = this.topics.get(topic_name);
 | |
| 
 | |
|             if (existing && !existing.historical) {
 | |
|                 // Trust out local data more, since it
 | |
|                 // maintains counts.
 | |
|                 continue;
 | |
|             }
 | |
| 
 | |
|             // If we get here, we are either finding out about
 | |
|             // the topic for the first time, or we are getting
 | |
|             // more current data for it.
 | |
| 
 | |
|             this.topics.set(topic_name, {
 | |
|                 message_id,
 | |
|                 pretty_name: topic_name,
 | |
|                 historical: true,
 | |
|             });
 | |
|             this.update_stream_max_message_id(message_id);
 | |
|         }
 | |
|     }
 | |
| 
 | |
|     get_recent_topic_names() {
 | |
|         const my_recents = Array.from(this.topics.values());
 | |
| 
 | |
|         const missing_topics = unread.get_missing_topics({
 | |
|             stream_id: this.stream_id,
 | |
|             topic_dict: this.topics,
 | |
|         });
 | |
| 
 | |
|         const recents = my_recents.concat(missing_topics);
 | |
| 
 | |
|         recents.sort((a, b) => b.message_id - a.message_id);
 | |
| 
 | |
|         const names = recents.map((obj) => obj.pretty_name);
 | |
| 
 | |
|         return names;
 | |
|     }
 | |
| 
 | |
|     get_max_message_id() {
 | |
|         return this.max_message_id;
 | |
|     }
 | |
| }
 | |
| 
 | |
| export function remove_messages(opts) {
 | |
|     const stream_id = opts.stream_id;
 | |
|     const topic_name = opts.topic_name;
 | |
|     const num_messages = opts.num_messages;
 | |
|     const max_removed_msg_id = opts.max_removed_msg_id;
 | |
|     const history = stream_dict.get(stream_id);
 | |
| 
 | |
|     // This is the special case of "removing" a message from
 | |
|     // a topic, which happens when we edit topics.
 | |
| 
 | |
|     if (!history) {
 | |
|         return;
 | |
|     }
 | |
| 
 | |
|     // This is the normal case of an incoming message.
 | |
|     history.maybe_remove(topic_name, num_messages);
 | |
| 
 | |
|     const existing_topic = history.topics.get(topic_name);
 | |
|     if (!existing_topic) {
 | |
|         return;
 | |
|     }
 | |
| 
 | |
|     // Update max_message_id in topic
 | |
|     if (existing_topic.message_id <= max_removed_msg_id) {
 | |
|         const msgs_in_topic = message_util.get_messages_in_topic(stream_id, topic_name);
 | |
|         let max_message_id = 0;
 | |
|         for (const msg of msgs_in_topic) {
 | |
|             if (msg.id > max_message_id) {
 | |
|                 max_message_id = msg.id;
 | |
|             }
 | |
|         }
 | |
|         existing_topic.message_id = max_message_id;
 | |
|     }
 | |
| 
 | |
|     // Update max_message_id in stream
 | |
|     if (history.max_message_id <= max_removed_msg_id) {
 | |
|         history.max_message_id = message_util.get_max_message_id_in_stream(stream_id);
 | |
|     }
 | |
| }
 | |
| 
 | |
| export function find_or_create(stream_id) {
 | |
|     let history = stream_dict.get(stream_id);
 | |
| 
 | |
|     if (!history) {
 | |
|         history = new PerStreamHistory(stream_id);
 | |
|         stream_dict.set(stream_id, history);
 | |
|     }
 | |
| 
 | |
|     return history;
 | |
| }
 | |
| 
 | |
| export function add_message(opts) {
 | |
|     const stream_id = opts.stream_id;
 | |
|     const message_id = opts.message_id;
 | |
|     const topic_name = opts.topic_name;
 | |
| 
 | |
|     const history = find_or_create(stream_id);
 | |
| 
 | |
|     history.add_or_update({
 | |
|         topic_name,
 | |
|         message_id,
 | |
|     });
 | |
| }
 | |
| 
 | |
| export function add_history(stream_id, server_history) {
 | |
|     const history = find_or_create(stream_id);
 | |
|     history.add_history(server_history);
 | |
|     fetched_stream_ids.add(stream_id);
 | |
| }
 | |
| 
 | |
| export function has_history_for(stream_id) {
 | |
|     return fetched_stream_ids.has(stream_id);
 | |
| }
 | |
| 
 | |
| export function get_recent_topic_names(stream_id) {
 | |
|     const history = find_or_create(stream_id);
 | |
| 
 | |
|     return history.get_recent_topic_names();
 | |
| }
 | |
| 
 | |
| export function get_max_message_id(stream_id) {
 | |
|     const history = find_or_create(stream_id);
 | |
| 
 | |
|     return history.get_max_message_id();
 | |
| }
 | |
| 
 | |
| export function reset() {
 | |
|     // This is only used by tests.
 | |
|     stream_dict.clear();
 | |
|     fetched_stream_ids.clear();
 | |
| }
 |