From c9152b0b3a2ec974f925eaff6bb1dd084e9593e6 Mon Sep 17 00:00:00 2001
From: John Preston <johnprestonmail@gmail.com>
Date: Tue, 31 Oct 2017 11:03:18 +0400
Subject: [PATCH] Cache media search results until empty query.

---
 Telegram/SourceFiles/base/value_ordering.h    | 155 ++++++++++++++++++
 .../history/history_search_controller.cpp     | 100 ++++++++---
 .../history/history_search_controller.h       |  84 +++++-----
 Telegram/SourceFiles/rpl/event_stream.h       |   8 +
 Telegram/gyp/telegram_sources.txt             |   1 +
 5 files changed, 281 insertions(+), 67 deletions(-)
 create mode 100644 Telegram/SourceFiles/base/value_ordering.h

diff --git a/Telegram/SourceFiles/base/value_ordering.h b/Telegram/SourceFiles/base/value_ordering.h
new file mode 100644
index 000000000..0841e7b27
--- /dev/null
+++ b/Telegram/SourceFiles/base/value_ordering.h
@@ -0,0 +1,155 @@
+/*
+This file is part of Telegram Desktop,
+the official desktop version of Telegram messaging app, see https://telegram.org
+
+Telegram Desktop is free software: you can redistribute it and/or modify
+it under the terms of the GNU General Public License as published by
+the Free Software Foundation, either version 3 of the License, or
+(at your option) any later version.
+
+It is distributed in the hope that it will be useful,
+but WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+GNU General Public License for more details.
+
+In addition, as a special exception, the copyright holders give permission
+to link the code of portions of this program with the OpenSSL library.
+
+Full license: https://github.com/telegramdesktop/tdesktop/blob/master/LICENSE
+Copyright (c) 2014-2017 John Preston, https://desktop.telegram.org
+*/
+#pragma once
+
+namespace base {
+namespace details {
+
+template <
+	typename Type,
+	typename Operator,
+	typename = decltype(Operator{}(
+		std::declval<Type>(),
+		std::declval<Type>()))>
+char test_operator(const Type &, const Operator &);
+int test_operator(...);
+
+template <typename Type, template <typename> typename Operator>
+struct has_operator
+	: std::bool_constant<
+		sizeof(test_operator(
+			std::declval<Type>(),
+			std::declval<Operator<Type>>()
+		)) == sizeof(char)> {
+};
+
+template <
+	typename Type,
+	template <typename> typename Operator>
+constexpr bool has_operator_v
+	= has_operator<Type, Operator>::value;
+
+template <typename Type>
+constexpr bool has_less_v = has_operator_v<Type, std::less>;
+
+template <typename Type>
+constexpr bool has_greater_v = has_operator_v<Type, std::greater>;
+
+template <typename Type>
+constexpr bool has_less_equal_v = has_operator_v<Type, std::less_equal>;
+
+template <typename Type>
+constexpr bool has_greater_equal_v = has_operator_v<Type, std::greater_equal>;
+
+template <typename Type>
+constexpr bool has_equal_to_v = has_operator_v<Type, std::equal_to>;
+
+template <typename Type>
+constexpr bool has_not_equal_to_v = has_operator_v<Type, std::not_equal_to>;
+
+} // namespace details
+} // namespace base
+
+template <
+	typename ValueType,
+	typename Helper = decltype(
+		value_ordering_helper(std::declval<ValueType>()))>
+inline auto operator<(const ValueType &a, const ValueType &b)
+-> std::enable_if_t<base::details::has_less_v<Helper>, bool> {
+	return value_ordering_helper(a) < value_ordering_helper(b);
+}
+
+template <
+	typename ValueType,
+	typename Helper = decltype(
+		value_ordering_helper(std::declval<ValueType>()))>
+inline auto operator>(const ValueType &a, const ValueType &b)
+-> std::enable_if_t<
+	base::details::has_greater_v<Helper>
+		|| base::details::has_less_v<Helper>,
+	bool
+> {
+	if constexpr (base::details::has_greater_v<Helper>) {
+		return value_ordering_helper(a) > value_ordering_helper(b);
+	} else {
+		return value_ordering_helper(b) < value_ordering_helper(a);
+	}
+}
+
+template <
+	typename ValueType,
+	typename Helper = decltype(
+		value_ordering_helper(std::declval<ValueType>()))>
+inline auto operator<=(const ValueType &a, const ValueType &b)
+-> std::enable_if_t<
+	base::details::has_less_equal_v<Helper>
+		|| base::details::has_less_v<Helper>,
+	bool
+> {
+	if constexpr (base::details::has_less_equal_v<Helper>) {
+		return value_ordering_helper(a) <= value_ordering_helper(b);
+	} else {
+		return !(value_ordering_helper(b) < value_ordering_helper(a));
+	}
+}
+
+template <
+	typename ValueType,
+	typename Helper = decltype(
+		value_ordering_helper(std::declval<ValueType>()))>
+inline auto operator>=(const ValueType &a, const ValueType &b)
+-> std::enable_if_t<
+	base::details::has_greater_equal_v<Helper>
+		|| base::details::has_less_v<Helper>,
+	bool
+> {
+	if constexpr (base::details::has_greater_equal_v<Helper>) {
+		return value_ordering_helper(a) >= value_ordering_helper(b);
+	} else {
+		return !(value_ordering_helper(a) < value_ordering_helper(b));
+	}
+}
+
+template <
+	typename ValueType,
+	typename Helper = decltype(
+		value_ordering_helper(std::declval<ValueType>()))>
+inline auto operator==(const ValueType &a, const ValueType &b)
+-> std::enable_if_t<base::details::has_equal_to_v<Helper>, bool> {
+	return value_ordering_helper(a) == value_ordering_helper(b);
+}
+
+template <
+	typename ValueType,
+	typename Helper = decltype(
+		value_ordering_helper(std::declval<ValueType>()))>
+inline auto operator!=(const ValueType &a, const ValueType &b)
+-> std::enable_if_t<
+	base::details::has_not_equal_to_v<Helper>
+		|| base::details::has_equal_to_v<Helper>,
+	bool
+> {
+	if constexpr (base::details::has_not_equal_to_v<Helper>) {
+		return value_ordering_helper(a) != value_ordering_helper(b);
+	} else {
+		return !(value_ordering_helper(a) == value_ordering_helper(b));
+	}
+}
diff --git a/Telegram/SourceFiles/history/history_search_controller.cpp b/Telegram/SourceFiles/history/history_search_controller.cpp
index b693d06ef..9f52087fb 100644
--- a/Telegram/SourceFiles/history/history_search_controller.cpp
+++ b/Telegram/SourceFiles/history/history_search_controller.cpp
@@ -169,19 +169,39 @@ SearchResult ParseSearchResult(
 	return result;
 }
 
-SingleSearchController::SingleSearchController(const Query &query)
-: _query(query)
-, _peerData(App::peer(query.peerId))
-, _migratedData(query.migratedPeerId
+SearchController::CacheEntry::CacheEntry(const Query &query)
+: peerData(App::peer(query.peerId))
+, migratedData(query.migratedPeerId
 	? base::make_optional(Data(App::peer(query.migratedPeerId)))
 	: base::none) {
 }
 
-rpl::producer<SparseIdsMergedSlice> SingleSearchController::idsSlice(
+bool SearchController::hasInCache(const Query &query) const {
+	return query.query.isEmpty() || _cache.contains(query);
+}
+
+void SearchController::setQuery(const Query &query) {
+	if (query.query.isEmpty()) {
+		_cache.clear();
+		_current = _cache.end();
+	} else {
+		_current = _cache.find(query);
+	}
+	if (_current == _cache.end()) {
+		_current = _cache.emplace(
+			query,
+			std::make_unique<CacheEntry>(query)).first;
+	}
+}
+
+rpl::producer<SparseIdsMergedSlice> SearchController::idsSlice(
 		SparseIdsMergedSlice::UniversalMsgId aroundId,
 		int limitBefore,
 		int limitAfter) {
-	auto createSimpleViewer = [this](
+	Expects(_current != _cache.cend());
+
+	auto query = (const Query&)_current->first;
+	auto createSimpleViewer = [=](
 			PeerId peerId,
 			SparseIdsSlice::Key simpleKey,
 			int limitBefore,
@@ -189,34 +209,41 @@ rpl::producer<SparseIdsMergedSlice> SingleSearchController::idsSlice(
 		return simpleIdsSlice(
 			peerId,
 			simpleKey,
+			query,
 			limitBefore,
 			limitAfter);
 	};
 	return SparseIdsMergedSlice::CreateViewer(
 		SparseIdsMergedSlice::Key(
-			_query.peerId,
-			_query.migratedPeerId,
+			query.peerId,
+			query.migratedPeerId,
 			aroundId),
 		limitBefore,
 		limitAfter,
 		std::move(createSimpleViewer));
 }
 
-rpl::producer<SparseIdsSlice> SingleSearchController::simpleIdsSlice(
+rpl::producer<SparseIdsSlice> SearchController::simpleIdsSlice(
 		PeerId peerId,
 		MsgId aroundId,
+		const Query &query,
 		int limitBefore,
 		int limitAfter) {
 	Expects(peerId != 0);
 	Expects(IsServerMsgId(aroundId) || (aroundId == 0));
 	Expects((aroundId != 0)
 		|| (limitBefore == 0 && limitAfter == 0));
-	Expects((_query.peerId == peerId)
-		|| (_query.migratedPeerId == peerId));
+	Expects((query.peerId == peerId)
+		|| (query.migratedPeerId == peerId));
 
-	auto listData = (peerId == _query.peerId)
-		? &_peerData
-		: &*_migratedData;
+	auto it = _cache.find(query);
+	if (it == _cache.end()) {
+		return [=](auto) { return rpl::lifetime(); };
+	}
+
+	auto listData = (peerId == query.peerId)
+		? &it->second->peerData
+		: &*it->second->migratedData;
 	return [=](auto consumer) {
 		auto lifetime = rpl::lifetime();
 		auto builder = lifetime.make_state<SparseIdsSliceBuilder>(
@@ -226,7 +253,7 @@ rpl::producer<SparseIdsSlice> SingleSearchController::simpleIdsSlice(
 		builder->insufficientAround()
 			| rpl::start_with_next([=](
 					const SparseIdsSliceBuilder::AroundData &data) {
-				requestMore(data, listData);
+				requestMore(data, query, listData);
 			}, lifetime);
 
 		auto pushNextSnapshot = [=] {
@@ -255,28 +282,41 @@ rpl::producer<SparseIdsSlice> SingleSearchController::simpleIdsSlice(
 			| rpl::filter([=] { return builder->removeAll(); })
 			| rpl::start_with_next(pushNextSnapshot, lifetime);
 
-		builder->checkInsufficient();
+		using Result = Storage::SparseIdsListResult;
+		listData->list.query(Storage::SparseIdsListQuery(
+			aroundId,
+			limitBefore,
+			limitAfter))
+			| rpl::filter([=](const Result &result) {
+				return builder->applyInitial(result);
+			})
+			| rpl::start_with_next_done(
+				pushNextSnapshot,
+				[=] { builder->checkInsufficient(); },
+				lifetime);
 
 		return lifetime;
 	};
 }
 
-void SingleSearchController::requestMore(
+void SearchController::requestMore(
 		const SparseIdsSliceBuilder::AroundData &key,
+		const Query &query,
 		Data *listData) {
 	if (listData->requests.contains(key)) {
 		return;
 	}
 	auto requestId = request(PrepareSearchRequest(
 		listData->peer,
-		_query.type,
-		_query.query,
+		query.type,
+		query.query,
 		key.aroundId,
 		key.direction)
 	).done([=](const MTPmessages_Messages &result) {
+		listData->requests.remove(key);
 		auto parsed = ParseSearchResult(
 			listData->peer,
-			_query.type,
+			query.type,
 			key.aroundId,
 			key.direction,
 			result);
@@ -285,8 +325,9 @@ void SingleSearchController::requestMore(
 			parsed.noSkipRange,
 			parsed.fullCount);
 	}).send();
-
-	listData->requests.emplace(key, requestId);
+	listData->requests.emplace(key, [=] {
+		request(requestId).cancel();
+	});
 }
 
 DelayedSearchController::DelayedSearchController() {
@@ -294,9 +335,18 @@ DelayedSearchController::DelayedSearchController() {
 }
 
 void DelayedSearchController::setQuery(const Query &query) {
-	setQuery(
-		query,
-		query.query.isEmpty() ? 0 : kDefaultSearchTimeoutMs);
+	setQuery(query, kDefaultSearchTimeoutMs);
+}
+
+void DelayedSearchController::setQuery(
+		const Query &query,
+		TimeMs delay) {
+	if (_controller.hasInCache(query)) {
+		setQueryFast(query);
+	} else {
+		_nextQuery = query;
+		_timer.callOnce(delay);
+	}
 }
 
 void DelayedSearchController::setQueryFast(const Query &query) {
diff --git a/Telegram/SourceFiles/history/history_search_controller.h b/Telegram/SourceFiles/history/history_search_controller.h
index 1928aaecd..f0a7fe251 100644
--- a/Telegram/SourceFiles/history/history_search_controller.h
+++ b/Telegram/SourceFiles/history/history_search_controller.h
@@ -24,6 +24,7 @@ Copyright (c) 2014-2017 John Preston, https://desktop.telegram.org
 #include "history/history_sparse_ids.h"
 #include "storage/storage_sparse_ids_list.h"
 #include "storage/storage_shared_media.h"
+#include "base/value_ordering.h"
 
 namespace Api {
 
@@ -47,7 +48,7 @@ SearchResult ParseSearchResult(
 	SparseIdsLoadDirection direction,
 	const MTPmessages_Messages &data);
 
-class SingleSearchController : private MTP::Sender {
+class SearchController : private MTP::Sender {
 public:
 	struct Query {
 		using MediaType = Storage::SharedMediaType;
@@ -57,19 +58,30 @@ public:
 		MediaType type = MediaType::kCount;
 		QString query;
 		// from_id, min_date, max_date
+
+		friend inline auto value_ordering_helper(const Query &value) {
+			return std::tie(
+				value.peerId,
+				value.migratedPeerId,
+				value.type,
+				value.query);
+		}
+
 	};
 
-	SingleSearchController(const Query &query);
+	void setQuery(const Query &query);
+	bool hasInCache(const Query &query) const;
+
+	Query query() const {
+		Expects(_current != _cache.cend());
+		return _current->first;
+	}
 
 	rpl::producer<SparseIdsMergedSlice> idsSlice(
 		SparseIdsMergedSlice::UniversalMsgId aroundId,
 		int limitBefore,
 		int limitAfter);
 
-	Query query() const {
-		return _query;
-	}
-
 private:
 	struct Data {
 		explicit Data(not_null<PeerData*> peer) : peer(peer) {
@@ -79,49 +91,40 @@ private:
 		Storage::SparseIdsList list;
 		base::flat_map<
 			SparseIdsSliceBuilder::AroundData,
-			mtpRequestId> requests;
+			rpl::lifetime> requests;
 	};
 	using SliceUpdate = Storage::SparseIdsSliceUpdate;
 
+	struct CacheEntry {
+		CacheEntry(const Query &query);
+
+		Data peerData;
+		base::optional<Data> migratedData;
+	};
+
+	struct CacheLess {
+		inline bool operator()(const Query &a, const Query &b) const {
+			return (a < b);
+		}
+	};
+	using Cache = base::flat_map<
+		Query,
+		std::unique_ptr<CacheEntry>,
+		CacheLess>;
+
 	rpl::producer<SparseIdsSlice> simpleIdsSlice(
 		PeerId peerId,
 		MsgId aroundId,
+		const Query &query,
 		int limitBefore,
 		int limitAfter);
 	void requestMore(
 		const SparseIdsSliceBuilder::AroundData &key,
+		const Query &query,
 		Data *listData);
 
-	Query _query;
-	Data _peerData;
-	base::optional<Data> _migratedData;
-
-};
-
-class SearchController {
-public:
-	using Query = SingleSearchController::Query;
-	void setQuery(const Query &query) {
-		_controller = SingleSearchController(query);
-	}
-
-	Query query() const {
-		return _controller ? _controller->query() : Query();
-	}
-
-	rpl::producer<SparseIdsMergedSlice> idsSlice(
-			SparseIdsMergedSlice::UniversalMsgId aroundId,
-			int limitBefore,
-			int limitAfter) {
-		Expects(_controller.has_value());
-		return _controller->idsSlice(
-			aroundId,
-			limitBefore,
-			limitAfter);
-	}
-
-private:
-	base::optional<SingleSearchController> _controller;
+	Cache _cache;
+	Cache::iterator _current = _cache.end();
 
 };
 
@@ -129,12 +132,9 @@ class DelayedSearchController {
 public:
 	DelayedSearchController();
 
-	using Query = SingleSearchController::Query;
+	using Query = SearchController::Query;
 	void setQuery(const Query &query);
-	void setQuery(const Query &query, TimeMs delay) {
-		_nextQuery = query;
-		_timer.callOnce(delay);
-	}
+	void setQuery(const Query &query, TimeMs delay);
 	void setQueryFast(const Query &query);
 
 	Query currentQuery() const {
diff --git a/Telegram/SourceFiles/rpl/event_stream.h b/Telegram/SourceFiles/rpl/event_stream.h
index 0c40cbd9f..c1b517a44 100644
--- a/Telegram/SourceFiles/rpl/event_stream.h
+++ b/Telegram/SourceFiles/rpl/event_stream.h
@@ -38,6 +38,7 @@ class event_stream {
 public:
 	event_stream();
 	event_stream(event_stream &&other);
+	event_stream &operator=(event_stream &&other);
 
 	template <typename OtherValue>
 	void fire_forward(OtherValue &&value) const;
@@ -90,6 +91,13 @@ inline event_stream<Value>::event_stream(event_stream &&other)
 : _consumers(base::take(other._consumers)) {
 }
 
+template <typename Value>
+inline event_stream<Value> &event_stream<Value>::operator=(
+		event_stream &&other) {
+	_consumers = base::take(other._consumers);
+	return *this;
+}
+
 template <typename Value>
 template <typename OtherValue>
 inline void event_stream<Value>::fire_forward(
diff --git a/Telegram/gyp/telegram_sources.txt b/Telegram/gyp/telegram_sources.txt
index de2744bfb..0664c3e5e 100644
--- a/Telegram/gyp/telegram_sources.txt
+++ b/Telegram/gyp/telegram_sources.txt
@@ -29,6 +29,7 @@
 <(src_loc)/base/unique_any.h
 <(src_loc)/base/unique_function.h
 <(src_loc)/base/unique_qptr.h
+<(src_loc)/base/value_ordering.h
 <(src_loc)/base/variant.h
 <(src_loc)/base/virtual_method.h
 <(src_loc)/base/weak_unique_ptr.h