Skip to main content

umadb_client/
lib.rs

1use async_trait::async_trait;
2use futures::Stream;
3use futures::ready;
4use std::collections::{HashMap, VecDeque};
5use std::fs;
6use std::path::PathBuf;
7use std::pin::Pin;
8use std::str::FromStr;
9use std::sync::atomic::{AtomicU64, Ordering};
10use std::task::{Context, Poll};
11use tonic::Request;
12use tonic::metadata::MetadataValue;
13use tonic::transport::{Certificate, Channel, ClientTlsConfig, Endpoint};
14
15use tokio::runtime::{Handle, Runtime};
16use umadb_dcb::{
17    DcbAppendCondition, DcbError, DcbEvent, DcbEventStoreAsync, DcbEventStoreSync, DcbQuery,
18    DcbReadResponseAsync, DcbReadResponseSync, DcbResult, DcbSequencedEvent, DcbSubscriptionAsync,
19    DcbSubscriptionSync, ShutdownStatus, StreamCancelHandle, TrackingInfo,
20};
21
22use futures::FutureExt;
23use futures::future::BoxFuture;
24use std::sync::{Arc, Mutex, Once, OnceLock};
25use std::time::Duration;
26use tokio::sync::watch;
27
28/// A global watch channel for shutdown/cancel signals.
29static CANCEL_SENDER: OnceLock<watch::Sender<()>> = OnceLock::new();
30
31/// Returns a receiver subscribed to the global cancel signal.
32fn cancel_receiver() -> watch::Receiver<()> {
33    let sender = CANCEL_SENDER.get_or_init(|| {
34        let (tx, _rx) = watch::channel::<()>(());
35        tx
36    });
37    sender.subscribe()
38}
39
40/// Sends the cancel signal to all receivers (e.g., on Ctrl-C).
41pub fn cancel_all_stream_responses() {
42    if let Some(sender) = CANCEL_SENDER.get() {
43        let _ = sender.send(()); // ignore error if already closed
44    }
45}
46
47static REGISTER_SIGINT: Once = Once::new();
48
49pub fn register_cancel_sigint_handler() {
50    REGISTER_SIGINT.call_once(|| {
51        // Capture the current runtime handle; panic if none exists
52        let handle = Handle::current();
53
54        // Spawn a detached task on that runtime
55        handle.spawn(async {
56            if tokio::signal::ctrl_c().await.is_ok() {
57                cancel_all_stream_responses();
58            }
59        });
60    });
61}
62
63pub trait ShutdownEvaluator {
64    // These methods act as "getters" for the underlying watch channels
65    fn cancel_channel(&self) -> &watch::Receiver<()>;
66    fn local_cancel_channel(&self) -> &watch::Receiver<()>;
67    fn is_ended(&self) -> bool;
68
69    fn evaluate_shutdown(&self) -> ShutdownStatus {
70        // 1. Prioritize user cancellations (Ctrl-C / interrupt triggers)
71        if self.cancel_channel().has_changed().unwrap_or(false)
72            || self.local_cancel_channel().has_changed().unwrap_or(false)
73        {
74            return ShutdownStatus::CancelledByUser;
75        }
76
77        // 2. Fall back to graceful stop paths
78        if self.is_ended() {
79            return ShutdownStatus::StoppedGracefully;
80        }
81
82        ShutdownStatus::NotStopped
83    }
84}
85
86pub trait IteratorWithTimeout {
87    type Item;
88
89    /// Pulls the next item, returning an explicit Timeout error if the window expires.
90    fn next_timeout(&mut self, timeout: Duration) -> Option<Self::Item>;
91}
92
93/// Async read response wrapper that provides batched access and head metadata
94pub struct AsyncClientReadResponse {
95    stream: tonic::Streaming<umadb_proto::v1::ReadResponse>,
96    buffer: VecDeque<DcbSequencedEvent>,
97    last_head: Option<Option<u64>>, // None = unknown yet; Some(x) = known
98    ended: bool,
99    cancel: watch::Receiver<()>,
100    // Per-stream stop signal, affecting only this individual response.
101    local_cancel_tx: Arc<watch::Sender<()>>,
102    local_cancel: watch::Receiver<()>,
103}
104
105impl AsyncClientReadResponse {
106    pub fn new(stream: tonic::Streaming<umadb_proto::v1::ReadResponse>) -> Self {
107        let (local_cancel_tx, local_cancel) = watch::channel(());
108        let local_cancel_tx = Arc::new(local_cancel_tx);
109        Self {
110            stream,
111            buffer: VecDeque::new(),
112            last_head: None,
113            ended: false,
114            cancel: cancel_receiver(),
115            local_cancel_tx,
116            local_cancel,
117        }
118    }
119
120    /// Fetches the next batch if needed, filling the buffer
121    async fn fetch_next_if_needed(&mut self) -> DcbResult<()> {
122        if !self.buffer.is_empty() || self.ended {
123            return Ok(());
124        }
125
126        tokio::select! {
127            biased;
128            _ = self.cancel.changed() => {
129                self.ended = true;
130                return Err(DcbError::CancelledByUser());
131            }
132            _ = self.local_cancel.changed() => {
133                self.ended = true;
134                return Err(DcbError::CancelledByUser());
135            }
136            msg = self.stream.message() => {
137                match msg {
138                    Ok(Some(resp)) => {
139                        self.last_head = Some(resp.head);
140                        let mut buffer = VecDeque::with_capacity(resp.events.len());
141                        for e in resp.events {
142                            if let Some(ev) = e.event {
143                                let event = DcbEvent::try_from(ev)?;
144                                buffer.push_back(DcbSequencedEvent { position: e.position, event });
145                            }
146                        }
147                        self.buffer = buffer;
148                    }
149                    Ok(None) => self.ended = true,
150                    Err(status) => return Err(umadb_proto::dcb_error_from_status(status)),
151                }
152            }
153        }
154
155        Ok(())
156    }
157
158    async fn fetch_next_if_needed_timeout(&mut self, timeout: Duration) -> DcbResult<()> {
159        if !self.buffer.is_empty() || self.ended {
160            return Ok(());
161        }
162
163        tokio::select! {
164            biased;
165            _ = self.cancel.changed() => {
166                self.ended = true;
167                return Err(DcbError::CancelledByUser());
168            }
169            _ = self.local_cancel.changed() => {
170                self.ended = true;
171                return Err(DcbError::CancelledByUser());
172            }
173            _ = tokio::time::sleep(timeout) => {
174                return Err(DcbError::Timeout());
175            }
176            msg = self.stream.message() => {
177                match msg {
178                    Ok(Some(resp)) => {
179                        self.last_head = Some(resp.head);
180                        let mut buffer = VecDeque::with_capacity(resp.events.len());
181                        for e in resp.events {
182                            if let Some(ev) = e.event {
183                                let event = DcbEvent::try_from(ev)?;
184                                buffer.push_back(DcbSequencedEvent { position: e.position, event });
185                            }
186                        }
187                        self.buffer = buffer;
188                    }
189                    Ok(None) => self.ended = true,
190                    Err(status) => return Err(umadb_proto::dcb_error_from_status(status)),
191                }
192            }
193        }
194
195        Ok(())
196    }
197}
198
199impl ShutdownEvaluator for AsyncClientReadResponse {
200    fn cancel_channel(&self) -> &watch::Receiver<()> {
201        &self.cancel
202    }
203    fn local_cancel_channel(&self) -> &watch::Receiver<()> {
204        &self.local_cancel
205    }
206    fn is_ended(&self) -> bool {
207        self.ended
208    }
209}
210
211#[async_trait]
212impl DcbReadResponseAsync for AsyncClientReadResponse {
213    async fn head(&mut self) -> DcbResult<Option<u64>> {
214        if let Some(h) = self.last_head {
215            return Ok(h);
216        }
217        // Need to read at least one message to learn head
218        self.fetch_next_if_needed().await?;
219        Ok(self.last_head.unwrap_or(None))
220    }
221
222    async fn next_batch(&mut self) -> DcbResult<Vec<DcbSequencedEvent>> {
223        if !self.buffer.is_empty() {
224            return Ok(self.buffer.drain(..).collect());
225        }
226
227        self.fetch_next_if_needed().await?;
228
229        if !self.buffer.is_empty() {
230            return Ok(self.buffer.drain(..).collect());
231        }
232
233        Ok(Vec::new())
234    }
235
236    async fn next_batch_timeout(&mut self, timeout: Duration) -> DcbResult<Vec<DcbSequencedEvent>> {
237        if !self.buffer.is_empty() {
238            return Ok(self.buffer.drain(..).collect());
239        }
240
241        // Invoke the updated timeout select loop
242        self.fetch_next_if_needed_timeout(timeout).await?;
243
244        if !self.buffer.is_empty() {
245            return Ok(self.buffer.drain(..).collect());
246        }
247
248        Ok(Vec::new())
249    }
250
251    fn cancel(&mut self) {
252        let _ = self.local_cancel_tx.send(());
253    }
254
255    fn cancel_handle(&self) -> Option<StreamCancelHandle> {
256        let tx = self.local_cancel_tx.clone();
257        Some(StreamCancelHandle::new(move || {
258            let _ = tx.send(());
259        }))
260    }
261
262    fn check_shutdown_status(&self) -> ShutdownStatus {
263        <Self as ShutdownEvaluator>::evaluate_shutdown(self)
264    }
265}
266
267impl Stream for AsyncClientReadResponse {
268    type Item = DcbResult<DcbSequencedEvent>;
269
270    fn poll_next(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Option<Self::Item>> {
271        let this = self.get_mut();
272
273        loop {
274            match this.evaluate_shutdown() {
275                ShutdownStatus::CancelledByUser => {
276                    this.buffer.clear();
277                    // Explicitly bubble user cancellation up to the async stream collector
278                    return Poll::Ready(Some(Err(DcbError::CancelledByUser())));
279                }
280                ShutdownStatus::StoppedGracefully => {
281                    this.buffer.clear();
282                    // Clean, silent stream end
283                    return Poll::Ready(None);
284                }
285                ShutdownStatus::NotStopped => {}
286            }
287
288            // Return buffered event if available
289            if let Some(ev) = this.buffer.pop_front() {
290                return Poll::Ready(Some(Ok(ev)));
291            }
292
293            // Poll the underlying tonic::Streaming
294            return match ready!(Pin::new(&mut this.stream).poll_next(cx)) {
295                Some(Ok(resp)) => {
296                    this.last_head = Some(resp.head);
297
298                    let mut buffer = VecDeque::with_capacity(resp.events.len());
299                    for e in resp.events {
300                        if let Some(ev) = e.event {
301                            let event = match DcbEvent::try_from(ev) {
302                                Ok(event) => event,
303                                Err(err) => return Poll::Ready(Some(Err(err))),
304                            };
305                            buffer.push_back(DcbSequencedEvent {
306                                position: e.position,
307                                event,
308                            });
309                        }
310                    }
311
312                    this.buffer = buffer;
313
314                    // If the batch is empty, loop again to poll the next message
315                    if this.buffer.is_empty() {
316                        continue;
317                    }
318
319                    // Otherwise, return the first event
320                    let ev = this.buffer.pop_front().unwrap();
321                    Poll::Ready(Some(Ok(ev)))
322                }
323                Some(Err(status)) => {
324                    this.ended = true;
325                    Poll::Ready(Some(Err(umadb_proto::dcb_error_from_status(status))))
326                }
327                None => {
328                    this.ended = true;
329                    Poll::Ready(None)
330                }
331            };
332        }
333    }
334}
335
336// Async subscribe response wrapper: similar to AsyncClientReadResponse but without head
337pub struct AsyncClientSubscription {
338    stream: tonic::Streaming<umadb_proto::v1::SubscribeResponse>,
339    buffer: VecDeque<DcbSequencedEvent>,
340    ended: bool,
341    cancel: watch::Receiver<()>,
342    // Per-stream cancel signal, affecting only this individual subscription.
343    local_cancel_tx: Arc<watch::Sender<()>>,
344    local_cancel: watch::Receiver<()>,
345}
346
347impl AsyncClientSubscription {
348    pub fn new(stream: tonic::Streaming<umadb_proto::v1::SubscribeResponse>) -> Self {
349        let (local_stop_tx, local_cancel) = watch::channel(());
350        let local_cancel_tx = Arc::new(local_stop_tx);
351        Self {
352            stream,
353            buffer: VecDeque::new(),
354            ended: false,
355            cancel: cancel_receiver(),
356            local_cancel_tx,
357            local_cancel,
358        }
359    }
360
361    async fn fetch_next_if_needed(&mut self, timeout: Duration) -> DcbResult<()> {
362        if !self.buffer.is_empty() || self.ended {
363            return Ok(());
364        }
365
366        tokio::select! {
367            biased;
368            _ = self.cancel.changed() => {
369                self.ended = true;
370                return Err(DcbError::CancelledByUser());
371            }
372            _ = self.local_cancel.changed() => {
373                self.ended = true;
374                return Err(DcbError::CancelledByUser());
375            }
376            _ = tokio::time::sleep(timeout) => {
377                return Err(DcbError::Timeout());
378            }
379            msg = self.stream.message() => {
380                match msg {
381                    Ok(Some(resp)) => {
382                        let mut buffer = VecDeque::with_capacity(resp.events.len());
383                        for e in resp.events {
384                            if let Some(ev) = e.event {
385                                let event = DcbEvent::try_from(ev)?;
386                                buffer.push_back(DcbSequencedEvent { position: e.position, event });
387                            }
388                        }
389                        self.buffer = buffer;
390                    }
391                    Ok(None) => self.ended = true,
392                    Err(status) => return Err(umadb_proto::dcb_error_from_status(status)),
393                }
394            }
395        }
396        Ok(())
397    }
398}
399
400impl ShutdownEvaluator for AsyncClientSubscription {
401    fn cancel_channel(&self) -> &watch::Receiver<()> {
402        &self.cancel
403    }
404    fn local_cancel_channel(&self) -> &watch::Receiver<()> {
405        &self.local_cancel
406    }
407    fn is_ended(&self) -> bool {
408        self.ended
409    }
410}
411
412#[async_trait]
413impl DcbSubscriptionAsync for AsyncClientSubscription {
414    async fn next_batch(&mut self) -> DcbResult<Vec<DcbSequencedEvent>> {
415        if !self.buffer.is_empty() {
416            return Ok(self.buffer.drain(..).collect());
417        }
418        self.fetch_next_if_needed(Duration::from_secs(u64::MAX))
419            .await?;
420        if !self.buffer.is_empty() {
421            return Ok(self.buffer.drain(..).collect());
422        }
423        Ok(Vec::new())
424    }
425
426    async fn next_batch_timeout(&mut self, timeout: Duration) -> DcbResult<Vec<DcbSequencedEvent>> {
427        if !self.buffer.is_empty() {
428            return Ok(self.buffer.drain(..).collect());
429        }
430        self.fetch_next_if_needed(timeout).await?;
431        if !self.buffer.is_empty() {
432            return Ok(self.buffer.drain(..).collect());
433        }
434        Ok(Vec::new())
435    }
436
437    fn cancel(&mut self) {
438        let _ = self.local_cancel_tx.send(());
439    }
440
441    fn cancel_handle(&self) -> Option<StreamCancelHandle> {
442        let tx = self.local_cancel_tx.clone();
443        Some(StreamCancelHandle::new(move || {
444            let _ = tx.send(());
445        }))
446    }
447
448    fn check_shutdown_status(&self) -> ShutdownStatus {
449        <Self as ShutdownEvaluator>::evaluate_shutdown(self)
450    }
451}
452
453impl Stream for AsyncClientSubscription {
454    type Item = DcbResult<DcbSequencedEvent>;
455
456    fn poll_next(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Option<Self::Item>> {
457        let this = self.get_mut();
458
459        loop {
460            match this.evaluate_shutdown() {
461                ShutdownStatus::CancelledByUser => {
462                    this.buffer.clear();
463                    // Explicitly bubble user cancellation up to the async stream collector
464                    return Poll::Ready(Some(Err(DcbError::CancelledByUser())));
465                }
466                ShutdownStatus::StoppedGracefully => {
467                    this.buffer.clear();
468                    // Clean, silent stream end
469                    return Poll::Ready(None);
470                }
471                ShutdownStatus::NotStopped => {}
472            }
473
474            if let Some(ev) = this.buffer.pop_front() {
475                return Poll::Ready(Some(Ok(ev)));
476            }
477
478            return match ready!(Pin::new(&mut this.stream).poll_next(cx)) {
479                Some(Ok(resp)) => {
480                    let mut buffer = VecDeque::with_capacity(resp.events.len());
481                    for e in resp.events {
482                        if let Some(ev) = e.event {
483                            let event = match DcbEvent::try_from(ev) {
484                                Ok(event) => event,
485                                Err(err) => return Poll::Ready(Some(Err(err))),
486                            };
487                            buffer.push_back(DcbSequencedEvent {
488                                position: e.position,
489                                event,
490                            });
491                        }
492                    }
493                    this.buffer = buffer;
494                    if this.buffer.is_empty() {
495                        continue;
496                    }
497                    let ev = this.buffer.pop_front().unwrap();
498                    Poll::Ready(Some(Ok(ev)))
499                }
500                Some(Err(status)) => {
501                    this.ended = true;
502                    Poll::Ready(Some(Err(umadb_proto::dcb_error_from_status(status))))
503                }
504                None => {
505                    this.ended = true;
506                    Poll::Ready(None)
507                }
508            };
509        }
510    }
511}
512
513// Avoid duplication in sync "read response" and "subscription" code.
514pub trait AsyncStreamBackend: Send + Unpin {
515    fn next_batch_box(&mut self) -> BoxFuture<'_, DcbResult<Vec<DcbSequencedEvent>>>;
516    fn next_batch_timeout_box(
517        &mut self,
518        timeout: Duration,
519    ) -> BoxFuture<'_, DcbResult<Vec<DcbSequencedEvent>>>;
520    fn cancel(&mut self);
521    fn check_shutdown_status(&self) -> ShutdownStatus;
522}
523
524// Bridge to DcbReadResponseAsync.
525impl AsyncStreamBackend for dyn DcbReadResponseAsync + Send + 'static {
526    fn next_batch_box(&mut self) -> BoxFuture<'_, DcbResult<Vec<DcbSequencedEvent>>> {
527        self.next_batch().boxed()
528    }
529    fn next_batch_timeout_box(
530        &mut self,
531        timeout: Duration,
532    ) -> BoxFuture<'_, DcbResult<Vec<DcbSequencedEvent>>> {
533        self.next_batch_timeout(timeout).boxed()
534    }
535    fn cancel(&mut self) {
536        self.cancel();
537    }
538
539    fn check_shutdown_status(&self) -> ShutdownStatus {
540        self.check_shutdown_status()
541    }
542}
543
544// Bridge to DcbSubscriptionAsync.
545impl AsyncStreamBackend for dyn DcbSubscriptionAsync + Send + 'static {
546    fn next_batch_box(&mut self) -> BoxFuture<'_, DcbResult<Vec<DcbSequencedEvent>>> {
547        self.next_batch().boxed()
548    }
549    fn next_batch_timeout_box(
550        &mut self,
551        timeout: Duration,
552    ) -> BoxFuture<'_, DcbResult<Vec<DcbSequencedEvent>>> {
553        self.next_batch_timeout(timeout).boxed()
554    }
555    fn cancel(&mut self) {
556        self.cancel();
557    }
558    fn check_shutdown_status(&self) -> ShutdownStatus {
559        self.check_shutdown_status()
560    }
561}
562
563pub struct SyncStreamEngine<A: AsyncStreamBackend + ?Sized> {
564    pub rt: Handle,
565    pub finished: bool,
566    pub buffer: VecDeque<DcbSequencedEvent>,
567    pub async_resp: Box<A>,
568}
569
570impl<A: AsyncStreamBackend + ?Sized> SyncStreamEngine<A> {
571    pub fn new(rt: Handle, async_resp: Box<A>) -> Self {
572        Self {
573            rt,
574            finished: false,
575            buffer: VecDeque::new(),
576            async_resp,
577        }
578    }
579
580    /// Reusable implementation for standard Iterator::next()
581    pub fn next_item(&mut self) -> Option<DcbResult<DcbSequencedEvent>> {
582        // Process shutdown status inline to avoid mismatched type boundaries
583        if !self.finished {
584            match self.async_resp.check_shutdown_status() {
585                ShutdownStatus::CancelledByUser => {
586                    self.finished = true;
587                    self.buffer.clear();
588                    return Some(Err(DcbError::CancelledByUser()));
589                }
590                ShutdownStatus::StoppedGracefully => {
591                    self.finished = true;
592                    self.buffer.clear();
593                    return None; // Clean iterator end!
594                }
595                ShutdownStatus::NotStopped => {}
596            }
597        }
598
599        while self.buffer.is_empty() && !self.finished {
600            let res = self.rt.block_on(self.async_resp.next_batch_box());
601            match res {
602                Ok(batch) => {
603                    if batch.is_empty() {
604                        self.finished = true;
605                    } else {
606                        self.buffer = batch.into();
607                    }
608                }
609                Err(e) => return Some(Err(e)),
610            }
611        }
612
613        self.buffer.pop_front().map(Ok)
614    }
615
616    /// The single, reusable implementation for IteratorWithTimeout::next_timeout()
617    pub fn next_item_timeout(&mut self, timeout: Duration) -> Option<DcbResult<DcbSequencedEvent>> {
618        if !self.finished {
619            match self.async_resp.check_shutdown_status() {
620                ShutdownStatus::CancelledByUser => {
621                    self.finished = true;
622                    self.buffer.clear();
623                    return Some(Err(DcbError::CancelledByUser()));
624                }
625                ShutdownStatus::StoppedGracefully => {
626                    self.finished = true;
627                    self.buffer.clear();
628                    return None; // Clean timeout end
629                }
630                ShutdownStatus::NotStopped => {}
631            }
632        }
633
634        if let Some(ev) = self.buffer.pop_front() {
635            return Some(Ok(ev));
636        }
637
638        if self.finished {
639            return None;
640        }
641
642        let res = self
643            .rt
644            .block_on(self.async_resp.next_batch_timeout_box(timeout));
645        match res {
646            Ok(batch) => {
647                if batch.is_empty() {
648                    self.finished = true;
649                } else {
650                    self.buffer = batch.into();
651                }
652            }
653            Err(DcbError::Timeout()) => return Some(Err(DcbError::Timeout())),
654            Err(e) => return Some(Err(e)),
655        }
656
657        self.buffer.pop_front().map(Ok)
658    }
659
660    /// Centralized next_batch logic shared by all synchronous traits
661    pub fn engine_next_batch(&mut self) -> DcbResult<Vec<DcbSequencedEvent>> {
662        if !self.buffer.is_empty() {
663            return Ok(self.buffer.drain(..).collect());
664        }
665
666        // Use the internal trait object bridge to load fresh logs
667        let batch = self.rt.block_on(self.async_resp.next_batch_box())?;
668        if batch.is_empty() {
669            self.finished = true;
670        } else {
671            self.buffer = batch.into();
672        }
673        Ok(self.buffer.drain(..).collect())
674    }
675
676    /// Centralized next_batch_timeout logic
677    pub fn engine_next_batch_timeout(
678        &mut self,
679        timeout: Duration,
680    ) -> DcbResult<Vec<DcbSequencedEvent>> {
681        if !self.buffer.is_empty() {
682            return Ok(self.buffer.drain(..).collect());
683        }
684
685        let batch = self
686            .rt
687            .block_on(self.async_resp.next_batch_timeout_box(timeout))?;
688        if batch.is_empty() {
689            self.finished = true;
690        } else {
691            self.buffer = batch.into();
692        }
693        Ok(self.buffer.drain(..).collect())
694    }
695
696    /// Centralized stop routine
697    pub fn engine_stop(&mut self) {
698        self.finished = true;
699        self.async_resp.cancel();
700    }
701}
702
703pub struct SyncClientReadResponse {
704    engine: SyncStreamEngine<dyn DcbReadResponseAsync + Send + 'static>,
705}
706
707impl SyncClientReadResponse {
708    pub fn new(rt: Handle, async_resp: Box<dyn DcbReadResponseAsync + Send + 'static>) -> Self {
709        Self {
710            engine: SyncStreamEngine::new(rt, async_resp),
711        }
712    }
713}
714
715impl Iterator for SyncClientReadResponse {
716    type Item = DcbResult<DcbSequencedEvent>;
717
718    fn next(&mut self) -> Option<Self::Item> {
719        self.engine.next_item()
720    }
721}
722
723impl IteratorWithTimeout for SyncClientReadResponse {
724    type Item = DcbResult<DcbSequencedEvent>;
725
726    fn next_timeout(&mut self, timeout: Duration) -> Option<Self::Item> {
727        // Delegates directly to the generic engine, identical to the subscription!
728        self.engine.next_item_timeout(timeout)
729    }
730}
731
732impl DcbReadResponseSync for SyncClientReadResponse {
733    fn head(&mut self) -> DcbResult<Option<u64>> {
734        self.engine.rt.block_on(self.engine.async_resp.head())
735    }
736
737    fn collect_with_head(&mut self) -> DcbResult<(Vec<DcbSequencedEvent>, Option<u64>)> {
738        let mut out = Vec::new();
739        for result in self.by_ref() {
740            out.push(result?);
741        }
742        Ok((out, self.head()?))
743    }
744
745    fn next_batch(&mut self) -> DcbResult<Vec<DcbSequencedEvent>> {
746        self.engine.engine_next_batch()
747    }
748
749    fn cancel(&mut self) {
750        self.engine.engine_stop();
751    }
752
753    fn next_timeout(&mut self, timeout: Duration) -> Option<DcbResult<DcbSequencedEvent>> {
754        <Self as IteratorWithTimeout>::next_timeout(self, timeout)
755    }
756}
757
758pub struct SyncClientSubscription {
759    engine: SyncStreamEngine<dyn DcbSubscriptionAsync + Send + 'static>,
760}
761
762impl SyncClientSubscription {
763    pub fn new(rt: Handle, async_resp: Box<dyn DcbSubscriptionAsync + Send + 'static>) -> Self {
764        Self {
765            engine: SyncStreamEngine::new(rt, async_resp),
766        }
767    }
768}
769
770impl Iterator for SyncClientSubscription {
771    type Item = DcbResult<DcbSequencedEvent>;
772
773    fn next(&mut self) -> Option<Self::Item> {
774        self.engine.next_item()
775    }
776}
777
778impl IteratorWithTimeout for SyncClientSubscription {
779    type Item = DcbResult<DcbSequencedEvent>;
780
781    fn next_timeout(&mut self, timeout: Duration) -> Option<Self::Item> {
782        self.engine.next_item_timeout(timeout)
783    }
784}
785
786impl DcbSubscriptionSync for SyncClientSubscription {
787    fn next_batch(&mut self) -> DcbResult<Vec<DcbSequencedEvent>> {
788        self.engine.engine_next_batch()
789    }
790
791    fn next_batch_timeout(&mut self, timeout: Duration) -> DcbResult<Vec<DcbSequencedEvent>> {
792        self.engine.engine_next_batch_timeout(timeout)
793    }
794
795    fn cancel(&mut self) {
796        self.engine.async_resp.cancel();
797    }
798
799    fn next_timeout(&mut self, timeout: Duration) -> Option<DcbResult<DcbSequencedEvent>> {
800        <Self as IteratorWithTimeout>::next_timeout(self, timeout)
801    }
802}
803
804/// Tracks the streaming responses opened by a single client so they can all be
805/// stopped together (e.g. when the client is closed or dropped).
806///
807/// Cloneable and shared: the client holds one, and every stream it opens holds a
808/// [`StreamGuard`] that deregisters the stream when the response is dropped.
809#[derive(Clone, Default)]
810struct StreamRegistry {
811    next_id: Arc<AtomicU64>,
812    active: Arc<Mutex<HashMap<u64, StreamCancelHandle>>>,
813}
814
815impl StreamRegistry {
816    /// Registers a stream's stop handle (if any) and returns a guard that
817    /// deregisters it on drop.
818    fn register(&self, handle: Option<StreamCancelHandle>) -> StreamGuard {
819        let id = self.next_id.fetch_add(1, Ordering::Relaxed);
820        if let Some(handle) = handle {
821            self.active.lock().unwrap().insert(id, handle);
822        }
823        StreamGuard {
824            active: self.active.clone(),
825            id,
826        }
827    }
828
829    /// Cancels every currently-active stream and clears the registry.
830    fn cancel_all(&self) {
831        let mut lock = self.active.lock().unwrap();
832        for (_id, handle) in lock.drain() {
833            handle.cancel();
834        }
835    }
836}
837
838/// Deregisters a single stream from its [`StreamRegistry`] when dropped.
839struct StreamGuard {
840    active: Arc<Mutex<HashMap<u64, StreamCancelHandle>>>,
841    id: u64,
842}
843
844impl Drop for StreamGuard {
845    fn drop(&mut self) {
846        if let Ok(mut lock) = self.active.lock() {
847            lock.remove(&self.id);
848        }
849    }
850}
851
852/// Wraps a read response so it deregisters from the client's [`StreamRegistry`]
853/// when dropped. All behaviour is delegated to the inner response.
854struct RegisteredReadResponse {
855    inner: Box<dyn DcbReadResponseAsync + Send + 'static>,
856    _guard: StreamGuard,
857}
858
859impl Stream for RegisteredReadResponse {
860    type Item = DcbResult<DcbSequencedEvent>;
861
862    fn poll_next(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Option<Self::Item>> {
863        // `Box<dyn ... + Unpin>` is `Unpin`, so it is safe to reborrow the inner box.
864        Pin::new(&mut self.get_mut().inner).poll_next(cx)
865    }
866}
867
868#[async_trait]
869impl DcbReadResponseAsync for RegisteredReadResponse {
870    async fn head(&mut self) -> DcbResult<Option<u64>> {
871        self.inner.head().await
872    }
873
874    async fn next_batch(&mut self) -> DcbResult<Vec<DcbSequencedEvent>> {
875        self.inner.next_batch().await
876    }
877
878    async fn next_batch_timeout(&mut self, timeout: Duration) -> DcbResult<Vec<DcbSequencedEvent>> {
879        self.inner.next_batch_timeout(timeout).await
880    }
881
882    fn cancel(&mut self) {
883        self.inner.cancel();
884    }
885
886    fn cancel_handle(&self) -> Option<StreamCancelHandle> {
887        self.inner.cancel_handle()
888    }
889
890    fn check_shutdown_status(&self) -> ShutdownStatus {
891        self.inner.check_shutdown_status()
892    }
893}
894
895/// Wraps a subscription so it deregisters from the client's [`StreamRegistry`]
896/// when dropped. All behaviour is delegated to the inner subscription.
897struct RegisteredSubscription {
898    inner: Box<dyn DcbSubscriptionAsync + Send + 'static>,
899    _guard: StreamGuard,
900}
901
902impl Stream for RegisteredSubscription {
903    type Item = DcbResult<DcbSequencedEvent>;
904
905    fn poll_next(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Option<Self::Item>> {
906        Pin::new(&mut self.get_mut().inner).poll_next(cx)
907    }
908}
909
910#[async_trait]
911impl DcbSubscriptionAsync for RegisteredSubscription {
912    async fn next_batch(&mut self) -> DcbResult<Vec<DcbSequencedEvent>> {
913        self.inner.next_batch().await
914    }
915
916    async fn next_batch_timeout(&mut self, timeout: Duration) -> DcbResult<Vec<DcbSequencedEvent>> {
917        self.inner.next_batch_timeout(timeout).await
918    }
919
920    fn cancel(&mut self) {
921        self.inner.cancel();
922    }
923
924    fn cancel_handle(&self) -> Option<StreamCancelHandle> {
925        self.inner.cancel_handle()
926    }
927
928    fn check_shutdown_status(&self) -> ShutdownStatus {
929        self.inner.check_shutdown_status()
930    }
931}
932
933#[derive(Clone, Debug, Default)]
934pub struct ClientTlsOptions {
935    pub domain: Option<String>,
936    pub ca_pem: Option<Vec<u8>>, // trusted CA cert in PEM for self-signed setups
937}
938
939fn new_endpoint(
940    url: String,
941    tls: Option<ClientTlsOptions>,
942) -> Result<Endpoint, tonic::transport::Error> {
943    // Accept grpcs:// as an alias for https://
944    let mut url_owned = url.to_string();
945    if url_owned.starts_with("grpcs://") {
946        url_owned = url_owned.replacen("grpcs://", "https://", 1);
947    }
948
949    let mut endpoint = Endpoint::from_shared(url_owned)?
950        .tcp_nodelay(true)
951        .http2_keep_alive_interval(Duration::from_secs(5))
952        .keep_alive_timeout(Duration::from_secs(10))
953        .initial_stream_window_size(Some(4 * 1024 * 1024))
954        .initial_connection_window_size(Some(8 * 1024 * 1024));
955
956    if let Some(opts) = tls {
957        let mut cfg = ClientTlsConfig::new();
958        if let Some(domain) = &opts.domain {
959            cfg = cfg.domain_name(domain.clone());
960        }
961        if let Some(ca) = opts.ca_pem {
962            cfg = cfg.ca_certificate(Certificate::from_pem(ca));
963        }
964        endpoint = endpoint.tls_config(cfg)?;
965    } else if url.starts_with("https://") {
966        // When using https without explicit options, still enable default TLS.
967        endpoint = endpoint.tls_config(ClientTlsConfig::new())?;
968    }
969
970    Ok(endpoint)
971}
972
973async fn new_channel(
974    url: String,
975    tls: Option<ClientTlsOptions>,
976) -> Result<Channel, tonic::transport::Error> {
977    new_endpoint(url, tls)?.connect().await
978}
979
980// Async client implementation
981pub struct AsyncUmaDbClient {
982    client: umadb_proto::v1::dcb_client::DcbClient<Channel>,
983    batch_size: Option<u32>,
984    tls_enabled: bool,
985    api_key: Option<String>,
986    // Tracks streams opened by this client so they can all be stopped on close/drop.
987    registry: StreamRegistry,
988}
989
990impl AsyncUmaDbClient {
991    pub async fn connect(
992        url: String,
993        ca_path: Option<String>,
994        batch_size: Option<u32>,
995        api_key: Option<String>,
996    ) -> DcbResult<Self> {
997        // Try to read the CA certificate.
998        let ca_pem = {
999            if let Some(ca_path) = ca_path {
1000                let ca_path = PathBuf::from(ca_path);
1001                Some(
1002                    fs::read(&ca_path)
1003                        .unwrap_or_else(|_| panic!("couldn't read cert_path: {:?}", ca_path)),
1004                )
1005            } else {
1006                None
1007            }
1008        };
1009
1010        let client_tls_options = Some(ClientTlsOptions {
1011            domain: None,
1012            ca_pem,
1013        });
1014
1015        Self::connect_with_tls_options(url, client_tls_options, batch_size, api_key).await
1016    }
1017
1018    pub async fn subscribe(
1019        &self,
1020        query: Option<DcbQuery>,
1021        after: Option<u64>,
1022    ) -> DcbResult<Box<dyn DcbSubscriptionAsync + Send + 'static>> {
1023        let query_proto = query.map(|q| q.into());
1024        let mut client = self.client.clone();
1025        let req_body = umadb_proto::v1::SubscribeRequest {
1026            query: query_proto,
1027            after,
1028            batch_size: self.batch_size,
1029        };
1030        let req = self.add_auth(Request::new(req_body))?;
1031        let response = client
1032            .subscribe(req)
1033            .await
1034            .map_err(umadb_proto::dcb_error_from_status)?;
1035        let stream = response.into_inner();
1036        let inner: Box<dyn DcbSubscriptionAsync + Send + 'static> =
1037            Box::new(AsyncClientSubscription::new(stream));
1038        let guard = self.registry.register(inner.cancel_handle());
1039        Ok(Box::new(RegisteredSubscription {
1040            inner,
1041            _guard: guard,
1042        }))
1043    }
1044
1045    /// Stops all streaming responses opened by this client.
1046    ///
1047    /// This affects only streams opened via this client instance's `read()` and
1048    /// `subscribe()` methods; it does not stop or otherwise affect the server, nor
1049    /// streams opened by other clients.
1050    pub fn close(&self) {
1051        self.registry.cancel_all();
1052    }
1053
1054    pub async fn connect_with_tls_options(
1055        url: String,
1056        tls_options: Option<ClientTlsOptions>,
1057        batch_size: Option<u32>,
1058        api_key: Option<String>,
1059    ) -> DcbResult<Self> {
1060        let tls_enabled = url.starts_with("https://") || url.starts_with("grpcs://");
1061        match new_channel(url, tls_options).await {
1062            Ok(channel) => Ok(Self {
1063                client: umadb_proto::v1::dcb_client::DcbClient::new(channel),
1064                batch_size,
1065                tls_enabled,
1066                api_key,
1067                registry: StreamRegistry::default(),
1068            }),
1069            Err(err) => Err(DcbError::TransportError(format!("{err}"))),
1070        }
1071    }
1072
1073    fn add_auth<T>(&self, mut req: Request<T>) -> DcbResult<Request<T>> {
1074        if let Some(key) = &self.api_key {
1075            if !self.tls_enabled {
1076                return Err(DcbError::TransportError(
1077                    "API key configured but TLS is not enabled; refusing to send credentials over insecure channel".to_string(),
1078                ));
1079            }
1080            let token = MetadataValue::from_str(&format!("Bearer {}", key))
1081                .map_err(|e| DcbError::TransportError(format!("invalid API key: {}", e)))?;
1082            req.metadata_mut().insert("authorization", token);
1083        }
1084        Ok(req)
1085    }
1086}
1087
1088#[async_trait]
1089impl DcbEventStoreAsync for AsyncUmaDbClient {
1090    // Async inherent methods: use the gRPC client directly (no trait required)
1091    async fn read<'a>(
1092        &'a self,
1093        query: Option<DcbQuery>,
1094        start: Option<u64>,
1095        backwards: bool,
1096        limit: Option<u32>,
1097    ) -> DcbResult<Box<dyn DcbReadResponseAsync + Send + 'static>> {
1098        let query_proto = query.map(|q| q.into());
1099        #[allow(deprecated)]
1100        let req_body = umadb_proto::v1::ReadRequest {
1101            query: query_proto,
1102            start,
1103            backwards: Some(backwards),
1104            limit,
1105            subscribe: None,
1106            batch_size: self.batch_size,
1107        };
1108        let mut client = self.client.clone();
1109        let req = self.add_auth(Request::new(req_body))?;
1110        let response = client
1111            .read(req)
1112            .await
1113            .map_err(umadb_proto::dcb_error_from_status)?;
1114        let stream = response.into_inner();
1115        let inner: Box<dyn DcbReadResponseAsync + Send + 'static> =
1116            Box::new(AsyncClientReadResponse::new(stream));
1117        let guard = self.registry.register(inner.cancel_handle());
1118        Ok(Box::new(RegisteredReadResponse {
1119            inner,
1120            _guard: guard,
1121        }))
1122    }
1123
1124    async fn head(&self) -> DcbResult<Option<u64>> {
1125        let mut client = self.client.clone();
1126        let req = self.add_auth(Request::new(umadb_proto::v1::HeadRequest {}))?;
1127        match client.head(req).await {
1128            Ok(response) => Ok(response.into_inner().position),
1129            Err(status) => Err(umadb_proto::dcb_error_from_status(status)),
1130        }
1131    }
1132
1133    async fn get_tracking_info(&self, source: &str) -> DcbResult<Option<u64>> {
1134        let mut client = self.client.clone();
1135        let req = self.add_auth(Request::new(umadb_proto::v1::TrackingRequest {
1136            source: source.to_string(),
1137        }))?;
1138        match client.get_tracking_info(req).await {
1139            Ok(response) => Ok(response.into_inner().position),
1140            Err(status) => Err(umadb_proto::dcb_error_from_status(status)),
1141        }
1142    }
1143
1144    async fn append(
1145        &self,
1146        events: Vec<DcbEvent>,
1147        condition: Option<DcbAppendCondition>,
1148        tracking_info: Option<TrackingInfo>,
1149    ) -> DcbResult<u64> {
1150        let events_proto: Vec<umadb_proto::v1::Event> = events
1151            .into_iter()
1152            .map(umadb_proto::v1::Event::from)
1153            .collect();
1154        let condition_proto = condition.map(|c| umadb_proto::v1::AppendCondition {
1155            fail_if_events_match: Some(c.fail_if_events_match.into()),
1156            after: c.after,
1157        });
1158        let tracking_info_proto = tracking_info.map(|t| umadb_proto::v1::TrackingInfo {
1159            source: t.source,
1160            position: t.position,
1161        });
1162        let body = umadb_proto::v1::AppendRequest {
1163            events: events_proto,
1164            condition: condition_proto,
1165            tracking_info: tracking_info_proto,
1166        };
1167        let mut client = self.client.clone();
1168        let req = self.add_auth(Request::new(body))?;
1169        match client.append(req).await {
1170            Ok(response) => Ok(response.into_inner().position),
1171            Err(status) => Err(umadb_proto::dcb_error_from_status(status)),
1172        }
1173    }
1174}
1175
1176impl Drop for AsyncUmaDbClient {
1177    fn drop(&mut self) {
1178        // Signal all still-active streams to stop before the underlying channel
1179        // is torn down. This mirrors closing the client as a context manager.
1180        self.registry.cancel_all();
1181    }
1182}
1183
1184// --- Sync wrapper around the async client ---
1185pub struct SyncUmaDbClient {
1186    async_client: AsyncUmaDbClient,
1187    handle: Handle,
1188    _runtime: Option<Runtime>, // Keeps runtime alive if we created it
1189}
1190
1191impl SyncUmaDbClient {
1192    pub fn connect(
1193        url: String,
1194        ca_path: Option<String>,
1195        batch_size: Option<u32>,
1196        api_key: Option<String>,
1197    ) -> DcbResult<Self> {
1198        let (rt, handle) = Self::get_rt_handle();
1199        let async_client =
1200            handle.block_on(AsyncUmaDbClient::connect(url, ca_path, batch_size, api_key))?;
1201        Ok(Self {
1202            async_client,
1203            _runtime: rt, // Keep runtime alive for the client lifetime
1204            handle,
1205        })
1206    }
1207
1208    /// Subscribe to events starting from an optional position.
1209    /// This is a convenience wrapper around the async client's Subscribe RPC.
1210    /// The returned iterator yields events indefinitely until cancelled or the stream ends.
1211    pub fn subscribe(
1212        &self,
1213        query: Option<DcbQuery>,
1214        after: Option<u64>,
1215    ) -> DcbResult<Box<dyn DcbSubscriptionSync + Send + 'static>> {
1216        let async_subscription = self
1217            .handle
1218            .block_on(self.async_client.subscribe(query, after))?;
1219
1220        // Use the updated engine-backed constructor
1221        Ok(Box::new(SyncClientSubscription::new(
1222            self.handle.clone(),
1223            async_subscription,
1224        )))
1225    }
1226
1227    /// Stops all streaming responses opened by this client.
1228    ///
1229    /// Delegates to the underlying [`AsyncUmaDbClient`]; see its `close()` for
1230    /// details. Affects only streams opened by this client instance.
1231    pub fn close(&self) {
1232        self.async_client.close();
1233    }
1234
1235    // pub fn connect_with_tls_options(
1236    //     url: String,
1237    //     tls_options: Option<ClientTlsOptions>,
1238    //     batch_size: Option<u32>,
1239    // ) -> DcbResult<Self> {
1240    //     let (rt, handle) = Self::get_rt_handle();
1241    //     let async_client = handle.block_on(AsyncUmaDbClient::connect_with_tls_options(
1242    //         url,
1243    //         tls_options,
1244    //         batch_size,
1245    //         None,
1246    //     ))?;
1247    //     Ok(Self {
1248    //         async_client,
1249    //         _runtime: rt, // Keep runtime alive for the client lifetime
1250    //         handle,
1251    //     })
1252    // }
1253
1254    fn get_rt_handle() -> (Option<Runtime>, Handle) {
1255        let (rt, handle) = {
1256            // Try to use an existing runtime first
1257            if let Ok(handle) = Handle::try_current() {
1258                (None, handle)
1259            } else {
1260                // No runtime → create and own one
1261                let rt = Runtime::new().expect("failed to create Tokio runtime");
1262                let handle = rt.handle().clone();
1263                (Some(rt), handle)
1264            }
1265        };
1266        (rt, handle)
1267    }
1268}
1269
1270impl DcbEventStoreSync for SyncUmaDbClient {
1271    fn read(
1272        &self,
1273        query: Option<DcbQuery>,
1274        start: Option<u64>,
1275        backwards: bool,
1276        limit: Option<u32>,
1277    ) -> DcbResult<Box<dyn DcbReadResponseSync + Send + 'static>> {
1278        let async_read_response = self
1279            .handle
1280            .block_on(self.async_client.read(query, start, backwards, limit))?;
1281
1282        // Use the updated engine-backed constructor
1283        Ok(Box::new(SyncClientReadResponse::new(
1284            self.handle.clone(),
1285            async_read_response,
1286        )))
1287    }
1288
1289    fn head(&self) -> DcbResult<Option<u64>> {
1290        self.handle.block_on(self.async_client.head())
1291    }
1292
1293    fn get_tracking_info(&self, source: &str) -> DcbResult<Option<u64>> {
1294        self.handle
1295            .block_on(self.async_client.get_tracking_info(source))
1296    }
1297
1298    fn append(
1299        &self,
1300        events: Vec<DcbEvent>,
1301        condition: Option<DcbAppendCondition>,
1302        tracking_info: Option<TrackingInfo>,
1303    ) -> DcbResult<u64> {
1304        self.handle
1305            .block_on(self.async_client.append(events, condition, tracking_info))
1306    }
1307}
1308
1309pub struct UmaDbClient {
1310    url: String,
1311    ca_path: Option<String>,
1312    batch_size: Option<u32>,
1313    api_key: Option<String>,
1314}
1315
1316impl UmaDbClient {
1317    pub fn new(url: String) -> Self {
1318        Self {
1319            url,
1320            ca_path: None,
1321            batch_size: None,
1322            api_key: None,
1323        }
1324    }
1325
1326    pub fn ca_path(self, ca_path: String) -> Self {
1327        Self {
1328            ca_path: Some(ca_path),
1329            ..self
1330        }
1331    }
1332
1333    pub fn api_key(self, api_key: String) -> Self {
1334        Self {
1335            api_key: Some(api_key),
1336            ..self
1337        }
1338    }
1339
1340    pub fn batch_size(self, batch_size: u32) -> Self {
1341        Self {
1342            batch_size: Some(batch_size),
1343            ..self
1344        }
1345    }
1346
1347    pub fn connect(&self) -> DcbResult<SyncUmaDbClient> {
1348        let client = SyncUmaDbClient::connect(
1349            self.url.clone(),
1350            self.ca_path.clone(),
1351            self.batch_size,
1352            self.api_key.clone(),
1353        );
1354        client
1355    }
1356    pub async fn connect_async(&self) -> DcbResult<AsyncUmaDbClient> {
1357        let client = AsyncUmaDbClient::connect(
1358            self.url.clone(),
1359            self.ca_path.clone(),
1360            self.batch_size,
1361            self.api_key.clone(),
1362        )
1363        .await;
1364        client
1365    }
1366}