1use super::*;
2
3#[derive(Clone, Debug, PartialEq, Eq, Hash)]
4pub(super) struct KlineCursorKey {
5 pub(super) conn_id: u64,
6 pub(super) connection_generation: u64,
7 pub(super) security_key: String,
8 pub(super) sub_type: i32,
9 pub(super) rehab_type: i32,
10}
11
12#[derive(Clone, Debug, PartialEq, Eq)]
13pub(super) struct KlinePushCursor {
14 time_key: String,
15 fingerprint: u64,
16}
17
18#[derive(Clone, Debug, PartialEq, Eq)]
19pub(super) struct RegisteredKlinePushCursor {
20 intent_epoch: u64,
21 point: KlinePushCursor,
22}
23
24fn cursor_from_body(proto_id: u32, body: &[u8]) -> Option<KlinePushCursor> {
25 use prost::Message;
26
27 if proto_id != futu_core::proto_id::QOT_UPDATE_KL {
28 return None;
29 }
30 let response = futu_proto::qot_update_kl::Response::decode(body).ok()?;
31 let point = response.s2c?.kl_list.into_iter().last()?;
32 Some(KlinePushCursor {
33 time_key: point.time.clone(),
34 fingerprint: push_body_hash(&point.encode_to_vec()),
35 })
36}
37
38fn cursor_allows(previous: Option<&KlinePushCursor>, candidate: &KlinePushCursor) -> bool {
39 !previous.is_some_and(|previous| {
40 previous.time_key > candidate.time_key
41 || (previous.time_key == candidate.time_key
42 && previous.fingerprint == candidate.fingerprint)
43 })
44}
45
46fn registered_cursor_allows(
47 previous: Option<&RegisteredKlinePushCursor>,
48 intent_epoch: u64,
49 candidate: &KlinePushCursor,
50) -> bool {
51 previous.is_none_or(|previous| {
52 if previous.point.time_key > candidate.time_key {
53 return false;
54 }
55 previous.intent_epoch != intent_epoch || cursor_allows(Some(&previous.point), candidate)
56 })
57}
58
59impl PushDispatcher {
60 pub async fn push_qot_to_conn(&self, conn_id: u64, proto_id: u32, body: Vec<u8>) {
62 self.push_qot_to_conn_internal(conn_id, None, None, None, proto_id, body)
63 .await;
64 }
65
66 pub async fn push_qot_to_conn_with_route(
69 &self,
70 conn_id: u64,
71 security_key: &str,
72 sub_type: i32,
73 rehab_type: i32,
74 proto_id: u32,
75 body: Vec<u8>,
76 ) {
77 self.push_qot_to_conn_internal(
78 conn_id,
79 Some((security_key, sub_type, rehab_type)),
80 None,
81 None,
82 proto_id,
83 body,
84 )
85 .await;
86 }
87
88 #[allow(clippy::too_many_arguments)]
91 pub async fn push_qot_to_conn_with_route_lease(
92 &self,
93 conn_id: u64,
94 expected_connection_generation: u64,
95 registration_lease: crate::subscription::QotPushRegistrationLease,
96 security_key: &str,
97 sub_type: i32,
98 rehab_type: i32,
99 proto_id: u32,
100 body: Vec<u8>,
101 ) {
102 self.push_qot_to_conn_internal(
103 conn_id,
104 Some((security_key, sub_type, rehab_type)),
105 Some(expected_connection_generation),
106 Some(®istration_lease),
107 proto_id,
108 body,
109 )
110 .await;
111 }
112
113 async fn push_qot_to_conn_internal(
114 &self,
115 conn_id: u64,
116 route: Option<(&str, i32, i32)>,
117 expected_connection_generation: Option<u64>,
118 registration_lease: Option<&crate::subscription::QotPushRegistrationLease>,
119 proto_id: u32,
120 body: Vec<u8>,
121 ) {
122 if !self.delivery_ready() {
123 return;
124 }
125 let first_ticker_cursor = event_contract_ticker_cursor_from_body(proto_id, &body);
126 let kline_cursor = cursor_from_body(proto_id, &body);
127 let routed_kline = route.is_some() && kline_cursor.is_some();
128 let deliver = |intent_epoch: u64| {
129 let push = self.connections.get(&conn_id).and_then(|conn| {
130 if expected_connection_generation
131 .is_some_and(|expected| conn.session_generation != expected)
132 || !should_push_to(&conn, Scope::QotRead, "quote_first")
133 {
134 return None;
135 }
136 let frame =
137 conn.make_frame(proto_id, self.next_push_serial_no(), Bytes::from(body));
138 Some((conn.session_generation, conn.tx.clone(), frame))
139 });
140 if let Some((connection_generation, tx, frame)) = push {
141 let sent = if let (Some((security_key, sub_type, rehab_type)), Some(candidate)) =
142 (route, kline_cursor)
143 {
144 let key = KlineCursorKey {
145 conn_id,
146 connection_generation,
147 security_key: security_key.to_owned(),
148 sub_type,
149 rehab_type,
150 };
151 let mut cursors = self.kline_cursors.lock();
152 if !registered_cursor_allows(cursors.get(&key), intent_epoch, &candidate) {
153 return;
154 }
155 let sent =
156 self.try_send_qot_client_frame(tx, frame, sub_type, "push_qot_to_conn");
157 if sent {
158 cursors.insert(
159 key,
160 RegisteredKlinePushCursor {
161 intent_epoch,
162 point: candidate,
163 },
164 );
165 }
166 sent
167 } else {
168 self.try_send_qot_client_frame(tx, frame, 0, "push_qot_to_conn")
169 };
170 if sent && let Some((sec_key, sequence)) = first_ticker_cursor {
171 self.event_contract_cursors
172 .lock()
173 .ticker_sequences
174 .entry((conn_id, sec_key))
175 .and_modify(|current| *current = (*current).max(sequence))
176 .or_insert(sequence);
177 }
178 }
179 };
180 if let Some(lease) = registration_lease {
181 let Some((security_key, sub_type, rehab_type)) = route else {
182 return;
183 };
184 if !lease.matches_delivery(
185 conn_id,
186 expected_connection_generation.unwrap_or_default(),
187 security_key,
188 sub_type,
189 rehab_type,
190 ) {
191 return;
192 }
193 let intent_epoch = lease.intent_epoch();
194 let _ = self
195 .subscriptions
196 .with_current_qot_push_registration_lease(lease, || deliver(intent_epoch));
197 } else if routed_kline {
198 let Some((security_key, sub_type, rehab_type)) = route else {
199 return;
200 };
201 let intent_epoch = self
202 .subscriptions
203 .current_qot_push_intent_epoch_by_cache_key(
204 conn_id,
205 security_key,
206 sub_type,
207 rehab_type,
208 );
209 let _ = self
210 .subscriptions
211 .with_current_qot_push_intent_by_cache_key(
212 conn_id,
213 security_key,
214 sub_type,
215 rehab_type,
216 intent_epoch,
217 || deliver(intent_epoch.unwrap_or(0)),
218 );
219 } else {
220 deliver(0);
221 }
222 }
223
224 pub async fn push_qot(
226 &self,
227 security_key: &str,
228 sub_type: i32,
229 rehab_type: i32,
230 proto_id: u32,
231 body: Vec<u8>,
232 ) {
233 self.push_qot_with_kline_section(security_key, sub_type, rehab_type, proto_id, body, None)
234 .await;
235 }
236
237 pub async fn push_qot_with_kline_section(
240 &self,
241 security_key: &str,
242 sub_type: i32,
243 rehab_type: i32,
244 proto_id: u32,
245 body: Vec<u8>,
246 kline_trade_section: Option<futu_domain_qot_klrt::KlineTradeSection>,
247 ) {
248 if !self.delivery_ready() {
249 return;
250 }
251 if matches!(
252 proto_id,
253 futu_core::proto_id::QOT_UPDATE_EVENT_CONTRACT_ORDER_BOOK
254 | futu_core::proto_id::QOT_UPDATE_EVENT_CONTRACT_KLINE
255 | futu_core::proto_id::QOT_UPDATE_EVENT_CONTRACT_TICKER
256 ) {
257 self.push_event_contract_qot(security_key, sub_type, rehab_type, proto_id, &body);
258 return;
259 }
260 let kline_cursor = cursor_from_body(proto_id, &body);
261 if !self.external_sinks.is_empty() {
262 let should_send_external = if let Some(candidate) = kline_cursor.as_ref() {
263 let key = (security_key.to_owned(), sub_type, rehab_type);
264 let mut cursors = self.external_kline_cursors.lock();
265 if !cursor_allows(cursors.get(&key), candidate) {
266 false
267 } else {
268 cursors.insert(key, candidate.clone());
269 true
270 }
271 } else {
272 true
273 };
274 if should_send_external {
275 for sink in &self.external_sinks {
276 sink.on_quote_push(security_key, sub_type, rehab_type, proto_id, &body);
277 }
278 }
279 }
280 let body = Bytes::from(body);
281 let subscribers = self
282 .subscriptions
283 .get_qot_push_subscriber_intents_by_cache_key(security_key, sub_type, rehab_type);
284 let body_sha1 = FutuFrame::body_sha1(&body);
285 for (conn_id, intent_epoch) in subscribers {
286 if kline_trade_section.is_some_and(|section| {
287 !futu_domain_qot_klrt::kline_trade_section_allows_conn(
288 section,
289 self.subscriptions.get_conn_session_by_cache_key(
290 conn_id,
291 security_key,
292 sub_type,
293 ),
294 )
295 }) {
296 continue;
297 }
298 let Some((connection_generation, tx, frame)) =
299 self.connections.get(&conn_id).and_then(|conn| {
300 if !should_push_to(&conn, Scope::QotRead, "quote") {
301 return None;
302 }
303 let serial_no = self.next_push_serial_no();
304 let frame =
305 conn.make_frame_with_sha1(proto_id, serial_no, body.clone(), body_sha1);
306 Some((conn.session_generation, conn.tx.clone(), frame))
307 })
308 else {
309 continue;
310 };
311
312 if let Some(candidate) = kline_cursor.clone() {
313 let key = KlineCursorKey {
314 conn_id,
315 connection_generation,
316 security_key: security_key.to_owned(),
317 sub_type,
318 rehab_type,
319 };
320 let _ = self
321 .subscriptions
322 .with_current_qot_push_intent_by_cache_key(
323 conn_id,
324 security_key,
325 sub_type,
326 rehab_type,
327 Some(intent_epoch),
328 || {
329 let mut cursors = self.kline_cursors.lock();
330 if !registered_cursor_allows(
331 cursors.get(&key),
332 intent_epoch,
333 &candidate,
334 ) {
335 return;
336 }
337 if self.try_send_qot_client_frame(tx, frame, sub_type, "push_qot") {
338 cursors.insert(
339 key,
340 RegisteredKlinePushCursor {
341 intent_epoch,
342 point: candidate,
343 },
344 );
345 }
346 },
347 );
348 } else {
349 let _ = self.try_send_qot_client_frame(tx, frame, sub_type, "push_qot");
350 }
351 }
352 }
353}