Skip to main content

acyclic_stream/
grpc.rs

1//! Authenticated gRPC adapter for the canonical Stream provider contract.
2
3use std::{
4    future::Future,
5    pin::Pin,
6    sync::{
7        Arc,
8        atomic::{AtomicUsize, Ordering},
9    },
10};
11
12use async_trait::async_trait;
13use bytes::Bytes;
14use futures::{StreamExt, stream};
15use thiserror::Error;
16use tonic::{
17    Code, Request, Response, Status,
18    metadata::{Ascii, MetadataValue},
19    transport::{Certificate, Channel, ClientTlsConfig, Endpoint},
20};
21
22use crate::wire_codec::{
23    condition_from_wire, condition_wire, mutation_from_wire, mutation_wire, optional_key, path,
24    required_key,
25};
26use crate::{
27    AppendOutcome, AppendReceipt, AppendRequest, Child, ChildStream, ChildrenRequest,
28    CommitConflict, CommitId, CommitOutcome, CommitRequest, CommittedAppend, CommittedDelete,
29    CommittedEnvelope, CommittedFork, CommittedMutation, CommittedTrim, DeleteReceipt, ForkReceipt,
30    ForkRequest, IdempotencyKey, IdempotencyObservation, IdempotencyOutcome, ReadRequest, Record,
31    RecordStream, StreamError, StreamPath, StreamProvider, TrimReceipt, wire,
32};
33
34const OPERATION_DEADLINE: std::time::Duration = std::time::Duration::from_secs(10);
35const OPERATION_ENDPOINT_ATTEMPT_TIMEOUT: std::time::Duration = std::time::Duration::from_secs(1);
36const FOLLOW_ENDPOINT_ATTEMPT_TIMEOUT: std::time::Duration = std::time::Duration::from_millis(500);
37const RETRY_DELAY: std::time::Duration = std::time::Duration::from_millis(10);
38/// Maximum independently reachable endpoints in one operation or follow pool.
39pub const MAX_ENDPOINTS: usize = 16;
40/// Maximum canonical URI bytes accepted for one endpoint.
41pub const MAX_ENDPOINT_URI_BYTES: usize = 2_048;
42/// Maximum caller-supplied private CA bundle bytes.
43pub const MAX_CA_CERTIFICATE_BYTES: usize = 64 * 1024;
44
45/// Connection configuration failure.
46#[derive(Debug, Error)]
47pub enum ConnectError {
48    /// Endpoint URI was invalid or the TLS connection failed.
49    #[error("invalid or unavailable Stream endpoint: {0}")]
50    Endpoint(#[from] tonic::transport::Error),
51    /// Customer endpoints must use authenticated TLS.
52    #[error("Stream endpoints must use https")]
53    InsecureEndpoint,
54    /// Bearer credential cannot be represented as HTTP metadata.
55    #[error("invalid Stream bearer credential")]
56    InvalidCredential,
57    /// Caller-supplied private CA bundle is empty or exceeds its fixed bound.
58    #[error("invalid Stream private CA certificate")]
59    InvalidCaCertificate,
60    /// At least one independently reachable endpoint is required.
61    #[error("at least one Stream endpoint is required")]
62    NoEndpoints,
63    /// Endpoint count or URI bytes exceed the fixed client bound.
64    #[error("Stream endpoint pool exceeds its fixed bound")]
65    EndpointLimit,
66}
67
68/// Authenticated remote provider.
69#[derive(Clone)]
70pub struct Client {
71    channels: Arc<[Channel]>,
72    authorization: MetadataValue<Ascii>,
73    preferred: Arc<AtomicUsize>,
74}
75
76/// Thin server adapter from the canonical wire service to one provider.
77pub struct Service<P> {
78    provider: Arc<P>,
79}
80
81impl<P> Service<P> {
82    /// Binds the generated server to one semantic provider.
83    #[must_use]
84    pub fn new(provider: Arc<P>) -> Self {
85        Self { provider }
86    }
87}
88
89impl<P> Clone for Service<P> {
90    fn clone(&self) -> Self {
91        Self {
92            provider: Arc::clone(&self.provider),
93        }
94    }
95}
96
97impl Client {
98    /// Connects to a TLS endpoint with an account-bound bearer credential.
99    pub async fn connect(
100        endpoint: impl AsRef<str>,
101        bearer_token: impl AsRef<str>,
102    ) -> Result<Self, ConnectError> {
103        Self::connect_endpoints([endpoint], bearer_token).await
104    }
105
106    /// Connects to independently reachable TLS endpoints with transparent failover.
107    pub async fn connect_endpoints<I, S>(
108        endpoints: I,
109        bearer_token: impl AsRef<str>,
110    ) -> Result<Self, ConnectError>
111    where
112        I: IntoIterator<Item = S>,
113        S: AsRef<str>,
114    {
115        Self::connect_with_tls(endpoints, bearer_token, None)
116    }
117
118    /// Connects to a TLS endpoint augmented by one caller-pinned private CA certificate.
119    pub async fn connect_with_ca_certificate(
120        endpoint: impl AsRef<str>,
121        bearer_token: impl AsRef<str>,
122        certificate_pem: impl AsRef<[u8]>,
123    ) -> Result<Self, ConnectError> {
124        Self::connect_endpoints_with_ca_certificate(
125            [endpoint],
126            bearer_token,
127            certificate_pem.as_ref(),
128        )
129        .await
130    }
131
132    /// Connects to independently reachable endpoints using one caller-pinned private CA.
133    pub async fn connect_endpoints_with_ca_certificate<I, S>(
134        endpoints: I,
135        bearer_token: impl AsRef<str>,
136        certificate_pem: impl AsRef<[u8]>,
137    ) -> Result<Self, ConnectError>
138    where
139        I: IntoIterator<Item = S>,
140        S: AsRef<str>,
141    {
142        Self::connect_with_tls(endpoints, bearer_token, Some(certificate_pem.as_ref()))
143    }
144
145    fn connect_with_tls<I, S>(
146        endpoints: I,
147        bearer_token: impl AsRef<str>,
148        certificate_pem: Option<&[u8]>,
149    ) -> Result<Self, ConnectError>
150    where
151        I: IntoIterator<Item = S>,
152        S: AsRef<str>,
153    {
154        let certificate_pem = certificate_pem.map(bounded_ca_certificate).transpose()?;
155        let channels = Self::channels_with_tls(
156            endpoints,
157            certificate_pem,
158            OPERATION_ENDPOINT_ATTEMPT_TIMEOUT,
159        )?;
160        Self::from_channels(channels, bearer_token)
161    }
162
163    fn channels_with_tls<I, S>(
164        endpoints: I,
165        certificate_pem: Option<&[u8]>,
166        connect_timeout: std::time::Duration,
167    ) -> Result<Arc<[Channel]>, ConnectError>
168    where
169        I: IntoIterator<Item = S>,
170        S: AsRef<str>,
171    {
172        let mut channels = Vec::new();
173        for endpoint in endpoints.into_iter().take(MAX_ENDPOINTS + 1) {
174            if channels.len() == MAX_ENDPOINTS {
175                return Err(ConnectError::EndpointLimit);
176            }
177            let endpoint = endpoint.as_ref();
178            if endpoint.len() > MAX_ENDPOINT_URI_BYTES {
179                return Err(ConnectError::EndpointLimit);
180            }
181            // `Endpoint::new` enables the compiled WebPKI roots for HTTPS. The lower-level
182            // `from_shared` constructor leaves TLS disabled and fails only on first use.
183            let mut endpoint = Endpoint::new(endpoint.to_owned())?.connect_timeout(connect_timeout);
184            if endpoint.uri().scheme_str() != Some("https") {
185                return Err(ConnectError::InsecureEndpoint);
186            }
187            if let Some(certificate_pem) = certificate_pem {
188                endpoint = endpoint.tls_config(
189                    ClientTlsConfig::new()
190                        .with_enabled_roots()
191                        .ca_certificate(Certificate::from_pem(certificate_pem)),
192                )?;
193            }
194            channels.push(endpoint.connect_lazy());
195        }
196        if channels.is_empty() {
197            return Err(ConnectError::NoEndpoints);
198        }
199        Ok(channels.into())
200    }
201
202    fn from_channels(
203        channels: Arc<[Channel]>,
204        bearer_token: impl AsRef<str>,
205    ) -> Result<Self, ConnectError> {
206        let authorization = format!("Bearer {}", bearer_token.as_ref())
207            .parse::<MetadataValue<Ascii>>()
208            .map_err(|_| ConnectError::InvalidCredential)?;
209        Ok(Self {
210            channels,
211            authorization,
212            preferred: Arc::new(AtomicUsize::new(0)),
213        })
214    }
215
216    #[allow(
217        clippy::indexing_slicing,
218        reason = "callers derive index via `(start + offset) % channels.len()` against this same slice, so it is always in-bounds"
219    )]
220    fn service(
221        channels: &[Channel],
222        index: usize,
223    ) -> wire::stream_service_client::StreamServiceClient<Channel> {
224        wire::stream_service_client::StreamServiceClient::new(channels[index].clone())
225    }
226
227    fn request<T>(&self, body: T) -> Request<T> {
228        let mut request = Request::new(body);
229        request
230            .metadata_mut()
231            .insert("authorization", self.authorization.clone());
232        request
233    }
234
235    async fn unary<T, U, F>(&self, body: T, mut call: F) -> Result<U, StreamError>
236    where
237        T: Clone,
238        F: FnMut(
239            wire::stream_service_client::StreamServiceClient<Channel>,
240            Request<T>,
241        ) -> Pin<Box<dyn Future<Output = Result<Response<U>, Status>> + Send>>,
242    {
243        self.unary_on(
244            &self.channels,
245            &self.preferred,
246            body,
247            OPERATION_ENDPOINT_ATTEMPT_TIMEOUT,
248            &mut call,
249        )
250        .await
251        .map(|(response, _)| response)
252    }
253
254    async fn follow_unary<T, U, F>(&self, body: T, mut call: F) -> Result<(U, usize), StreamError>
255    where
256        T: Clone,
257        F: FnMut(
258            wire::stream_service_client::StreamServiceClient<Channel>,
259            Request<T>,
260        ) -> Pin<Box<dyn Future<Output = Result<Response<U>, Status>> + Send>>,
261    {
262        self.unary_on(
263            &self.channels,
264            &self.preferred,
265            body,
266            FOLLOW_ENDPOINT_ATTEMPT_TIMEOUT,
267            &mut call,
268        )
269        .await
270    }
271
272    async fn unary_on<T, U, F>(
273        &self,
274        channels: &[Channel],
275        preferred: &AtomicUsize,
276        body: T,
277        attempt_timeout: std::time::Duration,
278        call: &mut F,
279    ) -> Result<(U, usize), StreamError>
280    where
281        T: Clone,
282        F: FnMut(
283            wire::stream_service_client::StreamServiceClient<Channel>,
284            Request<T>,
285        ) -> Pin<Box<dyn Future<Output = Result<Response<U>, Status>> + Send>>,
286    {
287        let deadline = tokio::time::Instant::now() + OPERATION_DEADLINE;
288        let mut last = None;
289        loop {
290            let start = preferred.load(Ordering::Relaxed) % channels.len();
291            for offset in 0..channels.len() {
292                let index = (start + offset) % channels.len();
293                let attempt_deadline = deadline.min(tokio::time::Instant::now() + attempt_timeout);
294                match tokio::time::timeout_at(
295                    attempt_deadline,
296                    call(Self::service(channels, index), self.request(body.clone())),
297                )
298                .await
299                {
300                    Ok(Ok(response)) => {
301                        preferred.store(index, Ordering::Relaxed);
302                        return Ok((response.into_inner(), index));
303                    }
304                    Ok(Err(error)) if retryable(&error) => last = Some(error),
305                    Ok(Err(error)) => return Err(status(&error)),
306                    Err(_) => last = Some(Status::deadline_exceeded("endpoint attempt expired")),
307                }
308            }
309            tokio::time::sleep_until((tokio::time::Instant::now() + RETRY_DELAY).min(deadline))
310                .await;
311            if tokio::time::Instant::now() >= deadline {
312                return Err(last.as_ref().map_or(StreamError::Unavailable, status));
313            }
314        }
315    }
316
317    async fn records(
318        &self,
319        path: StreamPath,
320        from: u64,
321        limit: Option<u32>,
322    ) -> Result<RecordStream, StreamError> {
323        let client = self.clone();
324        Ok(stream::unfold(
325            RecordCursor {
326                client,
327                path,
328                next: from,
329                remaining: limit,
330                active: None,
331            },
332            |mut cursor| async move {
333                loop {
334                    if cursor.remaining == Some(0) {
335                        return None;
336                    }
337                    if cursor.active.is_none() {
338                        match cursor.open().await {
339                            Ok(active) => cursor.active = Some(active),
340                            Err(error) => return Some((Err(error), cursor)),
341                        }
342                    }
343                    let Some(active) = cursor.active.as_mut() else {
344                        return Some((Err(StreamError::Unavailable), cursor));
345                    };
346                    let active_endpoint = active.endpoint;
347                    match active.records.next().await {
348                        Some(Ok(response)) => match read_response(response) {
349                            Ok(record) if record.sequence == cursor.next => {
350                                cursor.next = cursor.next.saturating_add(1);
351                                if let Some(remaining) = &mut cursor.remaining {
352                                    *remaining = remaining.saturating_sub(1);
353                                }
354                                return Some((Ok(record), cursor));
355                            }
356                            Ok(record) if record.sequence < cursor.next => continue,
357                            Ok(_) => return Some((Err(StreamError::Unavailable), cursor)),
358                            Err(error) => return Some((Err(error), cursor)),
359                        },
360                        Some(Err(error)) if retryable(&error) => {
361                            cursor.advance_follow(active_endpoint);
362                            tokio::time::sleep(RETRY_DELAY).await;
363                            cursor.active = None;
364                        }
365                        Some(Err(error)) => return Some((Err(status(&error)), cursor)),
366                        None if cursor.remaining.is_none() => {
367                            cursor.advance_follow(active_endpoint);
368                            tokio::time::sleep(RETRY_DELAY).await;
369                            cursor.active = None;
370                        }
371                        None => return None,
372                    }
373                }
374            },
375        )
376        .boxed())
377    }
378}
379
380fn bounded_ca_certificate(value: &[u8]) -> Result<&[u8], ConnectError> {
381    if value.is_empty() || value.len() > MAX_CA_CERTIFICATE_BYTES {
382        Err(ConnectError::InvalidCaCertificate)
383    } else {
384        Ok(value)
385    }
386}
387
388struct RecordCursor {
389    client: Client,
390    path: StreamPath,
391    next: u64,
392    remaining: Option<u32>,
393    active: Option<ActiveRecords>,
394}
395
396struct ActiveRecords {
397    records: tonic::Streaming<wire::ReadResponse>,
398    endpoint: usize,
399}
400
401impl RecordCursor {
402    fn advance_follow(&self, observed: usize) {
403        if self.remaining.is_some() {
404            return;
405        }
406        let next = (observed + 1) % self.client.channels.len();
407        let _ = self.client.preferred.compare_exchange(
408            observed,
409            next,
410            Ordering::Relaxed,
411            Ordering::Relaxed,
412        );
413    }
414
415    async fn open(&self) -> Result<ActiveRecords, StreamError> {
416        if let Some(limit) = self.remaining {
417            self.client
418                .unary(
419                    wire::ReadRequest {
420                        path: self.path.to_string(),
421                        from: self.next,
422                        limit,
423                    },
424                    |mut service, request| Box::pin(async move { service.read(request).await }),
425                )
426                .await
427                .map(|records| ActiveRecords {
428                    records,
429                    endpoint: 0,
430                })
431        } else {
432            self.client
433                .follow_unary(
434                    wire::FollowRequest {
435                        path: self.path.to_string(),
436                        from: self.next,
437                    },
438                    |mut service, request| Box::pin(async move { service.follow(request).await }),
439                )
440                .await
441                .map(|(records, endpoint)| ActiveRecords { records, endpoint })
442        }
443    }
444}
445
446fn retryable(error: &Status) -> bool {
447    if matches!(error.code(), Code::Unavailable | Code::DeadlineExceeded) {
448        return true;
449    }
450    // Missing final trailers become Unknown without a retained transport cause.
451    // Treat that outcome as ambiguous, never successful: every mutation retains
452    // its idempotency key and every resumed read retains its cursor/deadline.
453    // An explicit peer Unknown is indistinguishable and gets the same bounded retry.
454    if error.code() == Code::Unknown && std::error::Error::source(error).is_none() {
455        return true;
456    }
457    if !matches!(error.code(), Code::Unknown | Code::Cancelled) {
458        return false;
459    }
460    let mut cause = std::error::Error::source(error);
461    while let Some(current) = cause {
462        // Hyper cancels queued dispatch when its connection driver disappears. This is
463        // transport uncertainty, unlike a peer's application-level Cancelled status.
464        if error.code() == Code::Cancelled
465            && current
466                .downcast_ref::<hyper::Error>()
467                .is_some_and(hyper::Error::is_canceled)
468        {
469            return true;
470        }
471        if error.code() == Code::Cancelled
472            && current.downcast_ref::<h2::Error>().is_some_and(|error| {
473                error.is_remote() && error.reason() == Some(h2::Reason::CANCEL)
474            })
475        {
476            return true;
477        }
478        let io = current.downcast_ref::<std::io::Error>().or_else(|| {
479            current
480                .downcast_ref::<h2::Error>()
481                .and_then(h2::Error::get_io)
482        });
483        if error.code() == Code::Unknown
484            && io.is_some_and(|error| {
485                matches!(
486                    error.kind(),
487                    std::io::ErrorKind::UnexpectedEof
488                        | std::io::ErrorKind::ConnectionReset
489                        | std::io::ErrorKind::ConnectionAborted
490                        | std::io::ErrorKind::BrokenPipe
491                        | std::io::ErrorKind::NotConnected
492                        | std::io::ErrorKind::TimedOut
493                )
494            })
495        {
496            return true;
497        }
498        cause = current.source();
499    }
500    false
501}
502
503#[async_trait]
504impl StreamProvider for Client {
505    async fn inspect_idempotency(
506        &self,
507        idempotency_key: IdempotencyKey,
508    ) -> Result<Option<IdempotencyObservation>, StreamError> {
509        let response = self
510            .unary(
511                wire::InspectIdempotencyRequest {
512                    idempotency_key: Bytes::copy_from_slice(idempotency_key.as_bytes()),
513                },
514                |mut service, request| {
515                    Box::pin(async move { service.inspect_idempotency(request).await })
516                },
517            )
518            .await?;
519        bind_observation(&idempotency_key, response.observation)
520    }
521
522    async fn tail(&self, path: StreamPath) -> Result<u64, StreamError> {
523        self.unary(
524            wire::TailRequest {
525                path: path.to_string(),
526            },
527            |mut service, request| Box::pin(async move { service.tail(request).await }),
528        )
529        .await
530        .map(|response| response.tail)
531    }
532
533    async fn append(&self, request: AppendRequest) -> Result<AppendOutcome, StreamError> {
534        let idempotency_key = request.idempotency_key.map_or_else(
535            || Bytes::copy_from_slice(uuid::Uuid::new_v4().as_bytes()),
536            |key| Bytes::copy_from_slice(key.as_bytes()),
537        );
538        let response = self
539            .unary(
540                wire::AppendRequest {
541                    path: request.path.to_string(),
542                    records: request.records,
543                    if_tail: request.if_tail,
544                    idempotency_key: Some(idempotency_key),
545                },
546                |mut service, request| Box::pin(async move { service.append(request).await }),
547            )
548            .await?;
549        append_outcome_from_wire(response)
550    }
551
552    async fn fork(&self, request: ForkRequest) -> Result<ForkReceipt, StreamError> {
553        let idempotency_key = request.idempotency_key.map_or_else(
554            || Bytes::copy_from_slice(uuid::Uuid::new_v4().as_bytes()),
555            |key| Bytes::copy_from_slice(key.as_bytes()),
556        );
557        let receipt = self
558            .unary(
559                wire::ForkRequest {
560                    source: request.source.to_string(),
561                    destination: request.destination.to_string(),
562                    at_tail: request.at_tail,
563                    idempotency_key: Some(idempotency_key),
564                },
565                |mut service, request| Box::pin(async move { service.fork(request).await }),
566            )
567            .await?;
568        Ok(ForkReceipt {
569            source: path(receipt.source)?,
570            destination: path(receipt.destination)?,
571            forked_at: receipt.forked_at,
572            tail: receipt.tail,
573            commit_id: commit_id(&receipt.commit_id)?,
574        })
575    }
576
577    async fn trim(
578        &self,
579        path: StreamPath,
580        before: u64,
581        idempotency_key: IdempotencyKey,
582    ) -> Result<TrimReceipt, StreamError> {
583        let receipt = self
584            .unary(
585                wire::TrimRequest {
586                    path: path.to_string(),
587                    before,
588                    idempotency_key: Some(Bytes::copy_from_slice(idempotency_key.as_bytes())),
589                },
590                |mut service, request| Box::pin(async move { service.trim(request).await }),
591            )
592            .await?;
593        Ok(TrimReceipt {
594            path: crate::grpc::path(receipt.path)?,
595            trim_point: receipt.trim_point,
596            commit_id: commit_id(&receipt.commit_id)?,
597        })
598    }
599
600    async fn delete(
601        &self,
602        path: StreamPath,
603        idempotency_key: IdempotencyKey,
604    ) -> Result<DeleteReceipt, StreamError> {
605        let receipt = self
606            .unary(
607                wire::DeleteRequest {
608                    path: path.to_string(),
609                    idempotency_key: Some(Bytes::copy_from_slice(idempotency_key.as_bytes())),
610                },
611                |mut service, request| Box::pin(async move { service.delete(request).await }),
612            )
613            .await?;
614        Ok(DeleteReceipt {
615            path: crate::grpc::path(receipt.path)?,
616            commit_id: commit_id(&receipt.commit_id)?,
617        })
618    }
619
620    async fn read(&self, request: ReadRequest) -> Result<RecordStream, StreamError> {
621        self.records(request.path, request.from, Some(request.limit))
622            .await
623    }
624
625    async fn follow(&self, path: StreamPath, from: u64) -> Result<RecordStream, StreamError> {
626        self.records(path, from, None).await
627    }
628
629    async fn children(&self, request: ChildrenRequest) -> Result<ChildStream, StreamError> {
630        let body = wire::ChildrenRequest {
631            parent: request.parent.map(|path| path.to_string()),
632            limit: request.limit,
633        };
634        let mut last = None;
635        for _ in 0..self.channels.len() {
636            let response = self
637                .unary(body.clone(), |mut service, request| {
638                    Box::pin(async move { service.children(request).await })
639                })
640                .await?;
641            let collected = response
642                .map(|item| {
643                    let child = item
644                        .map_err(|error| status(&error))?
645                        .child
646                        .ok_or(StreamError::Unavailable)?;
647                    Ok(Child {
648                        path: path(child.path)?,
649                    })
650                })
651                .collect::<Vec<_>>()
652                .await;
653            if collected.iter().all(Result::is_ok) {
654                return Ok(stream::iter(collected).boxed());
655            }
656            let error = collected
657                .into_iter()
658                .find_map(Result::err)
659                .unwrap_or(StreamError::Unavailable);
660            if error != StreamError::Unavailable {
661                return Err(error);
662            }
663            last = Some(error);
664        }
665        Err(last.unwrap_or(StreamError::Unavailable))
666    }
667
668    async fn commit(&self, request: CommitRequest) -> Result<CommitOutcome, StreamError> {
669        let response = self
670            .unary(
671                wire::CommitRequest {
672                    conditions: request.conditions.into_iter().map(condition_wire).collect(),
673                    mutations: request.mutations.into_iter().map(mutation_wire).collect(),
674                    idempotency_key: Bytes::copy_from_slice(request.idempotency_key.as_bytes()),
675                    deadline_unix_millis: None,
676                },
677                |mut service, request| Box::pin(async move { service.commit(request).await }),
678            )
679            .await?;
680        commit_outcome_from_wire(response)
681    }
682
683    async fn commit_before(
684        &self,
685        request: CommitRequest,
686        deadline_unix_millis: u64,
687    ) -> Result<CommitOutcome, StreamError> {
688        let response = self
689            .unary(
690                wire::CommitRequest {
691                    conditions: request.conditions.into_iter().map(condition_wire).collect(),
692                    mutations: request.mutations.into_iter().map(mutation_wire).collect(),
693                    idempotency_key: Bytes::copy_from_slice(request.idempotency_key.as_bytes()),
694                    deadline_unix_millis: Some(deadline_unix_millis),
695                },
696                |mut service, request| Box::pin(async move { service.commit(request).await }),
697            )
698            .await?;
699        commit_outcome_from_wire(response)
700    }
701
702    async fn read_commit(&self, commit_id: CommitId) -> Result<CommittedEnvelope, StreamError> {
703        let envelope = self
704            .unary(
705                wire::ReadCommitRequest {
706                    commit_id: Bytes::copy_from_slice(commit_id.as_bytes()),
707                },
708                |mut service, request| Box::pin(async move { service.read_commit(request).await }),
709            )
710            .await?;
711        envelope_from_wire(envelope)
712    }
713}
714
715#[async_trait]
716impl<P: StreamProvider> wire::stream_service_server::StreamService for Service<P> {
717    type ReadStream = futures::stream::BoxStream<'static, Result<wire::ReadResponse, Status>>;
718    type FollowStream = futures::stream::BoxStream<'static, Result<wire::ReadResponse, Status>>;
719    type ChildrenStream =
720        futures::stream::BoxStream<'static, Result<wire::ChildrenResponse, Status>>;
721
722    async fn inspect_idempotency(
723        &self,
724        request: Request<wire::InspectIdempotencyRequest>,
725    ) -> Result<Response<wire::InspectIdempotencyResponse>, Status> {
726        let key = IdempotencyKey::new(request.into_inner().idempotency_key)
727            .map_err(|error| error_status(&error))?;
728        let observation = self
729            .provider
730            .inspect_idempotency(key)
731            .await
732            .map_err(|error| error_status(&error))?
733            .map(observation_wire);
734        Ok(Response::new(wire::InspectIdempotencyResponse {
735            observation,
736        }))
737    }
738
739    async fn append(
740        &self,
741        request: Request<wire::AppendRequest>,
742    ) -> Result<Response<wire::AppendResponse>, Status> {
743        let request = request.into_inner();
744        let outcome = self
745            .provider
746            .append(AppendRequest {
747                path: path(request.path).map_err(|error| error_status(&error))?,
748                records: request.records,
749                if_tail: request.if_tail,
750                idempotency_key: optional_key(request.idempotency_key)
751                    .map_err(|error| error_status(&error))?,
752            })
753            .await
754            .map_err(|error| error_status(&error))?;
755        Ok(Response::new(append_outcome_wire(outcome)))
756    }
757
758    async fn tail(
759        &self,
760        request: Request<wire::TailRequest>,
761    ) -> Result<Response<wire::TailResponse>, Status> {
762        let path = path(request.into_inner().path).map_err(|error| error_status(&error))?;
763        let tail = self
764            .provider
765            .tail(path)
766            .await
767            .map_err(|error| error_status(&error))?;
768        Ok(Response::new(wire::TailResponse { tail }))
769    }
770
771    async fn fork(
772        &self,
773        request: Request<wire::ForkRequest>,
774    ) -> Result<Response<wire::ForkReceipt>, Status> {
775        let request = request.into_inner();
776        let receipt = self
777            .provider
778            .fork(ForkRequest {
779                source: path(request.source).map_err(|error| error_status(&error))?,
780                destination: path(request.destination).map_err(|error| error_status(&error))?,
781                at_tail: request.at_tail,
782                idempotency_key: optional_key(request.idempotency_key)
783                    .map_err(|error| error_status(&error))?,
784            })
785            .await
786            .map_err(|error| error_status(&error))?;
787        Ok(Response::new(fork_receipt_wire(&receipt)))
788    }
789
790    async fn trim(
791        &self,
792        request: Request<wire::TrimRequest>,
793    ) -> Result<Response<wire::TrimReceipt>, Status> {
794        let request = request.into_inner();
795        let receipt = self
796            .provider
797            .trim(
798                path(request.path).map_err(|error| error_status(&error))?,
799                request.before,
800                required_key(request.idempotency_key).map_err(|error| error_status(&error))?,
801            )
802            .await
803            .map_err(|error| error_status(&error))?;
804        Ok(Response::new(trim_receipt_wire(&receipt)))
805    }
806
807    async fn delete(
808        &self,
809        request: Request<wire::DeleteRequest>,
810    ) -> Result<Response<wire::DeleteReceipt>, Status> {
811        let request = request.into_inner();
812        let receipt = self
813            .provider
814            .delete(
815                path(request.path).map_err(|error| error_status(&error))?,
816                required_key(request.idempotency_key).map_err(|error| error_status(&error))?,
817            )
818            .await
819            .map_err(|error| error_status(&error))?;
820        Ok(Response::new(delete_receipt_wire(&receipt)))
821    }
822
823    async fn read(
824        &self,
825        request: Request<wire::ReadRequest>,
826    ) -> Result<Response<Self::ReadStream>, Status> {
827        let request = request.into_inner();
828        let records = self
829            .provider
830            .read(ReadRequest {
831                path: path(request.path).map_err(|error| error_status(&error))?,
832                from: request.from,
833                limit: request.limit,
834            })
835            .await
836            .map_err(|error| error_status(&error))?;
837        Ok(Response::new(
838            records
839                .map(|record| {
840                    record
841                        .map(record_wire)
842                        .map(|record| wire::ReadResponse {
843                            record: Some(record),
844                        })
845                        .map_err(|error| error_status(&error))
846                })
847                .boxed(),
848        ))
849    }
850
851    async fn follow(
852        &self,
853        request: Request<wire::FollowRequest>,
854    ) -> Result<Response<Self::FollowStream>, Status> {
855        let request = request.into_inner();
856        let records = self
857            .provider
858            .follow(
859                path(request.path).map_err(|error| error_status(&error))?,
860                request.from,
861            )
862            .await
863            .map_err(|error| error_status(&error))?;
864        Ok(Response::new(
865            records
866                .map(|record| {
867                    record
868                        .map(record_wire)
869                        .map(|record| wire::ReadResponse {
870                            record: Some(record),
871                        })
872                        .map_err(|error| error_status(&error))
873                })
874                .boxed(),
875        ))
876    }
877
878    async fn children(
879        &self,
880        request: Request<wire::ChildrenRequest>,
881    ) -> Result<Response<Self::ChildrenStream>, Status> {
882        let request = request.into_inner();
883        let children = self
884            .provider
885            .children(ChildrenRequest {
886                parent: request
887                    .parent
888                    .map(path)
889                    .transpose()
890                    .map_err(|error| error_status(&error))?,
891                limit: request.limit,
892            })
893            .await
894            .map_err(|error| error_status(&error))?;
895        Ok(Response::new(
896            children
897                .map(|child| {
898                    child
899                        .map(|child| wire::ChildrenResponse {
900                            child: Some(wire::Child {
901                                path: child.path.to_string(),
902                            }),
903                        })
904                        .map_err(|error| error_status(&error))
905                })
906                .boxed(),
907        ))
908    }
909
910    async fn commit(
911        &self,
912        request: Request<wire::CommitRequest>,
913    ) -> Result<Response<wire::CommitResponse>, Status> {
914        let request = request.into_inner();
915        let deadline_unix_millis = request.deadline_unix_millis;
916        let request = CommitRequest {
917            conditions: request
918                .conditions
919                .into_iter()
920                .map(condition_from_wire)
921                .collect::<Result<_, _>>()
922                .map_err(|error| error_status(&error))?,
923            mutations: request
924                .mutations
925                .into_iter()
926                .map(mutation_from_wire)
927                .collect::<Result<_, _>>()
928                .map_err(|error| error_status(&error))?,
929            idempotency_key: IdempotencyKey::new(request.idempotency_key)
930                .map_err(|error| error_status(&error))?,
931        };
932        let outcome = if let Some(deadline) = deadline_unix_millis {
933            self.provider.commit_before(request, deadline).await
934        } else {
935            self.provider.commit(request).await
936        }
937        .map_err(|error| error_status(&error))?;
938        Ok(Response::new(commit_outcome_wire(outcome)))
939    }
940
941    async fn read_commit(
942        &self,
943        request: Request<wire::ReadCommitRequest>,
944    ) -> Result<Response<wire::CommittedEnvelope>, Status> {
945        let commit_id =
946            commit_id(&request.into_inner().commit_id).map_err(|error| error_status(&error))?;
947        let envelope = self
948            .provider
949            .read_commit(commit_id)
950            .await
951            .map_err(|error| error_status(&error))?;
952        Ok(Response::new(envelope_wire(envelope)))
953    }
954}
955
956fn observation_from_wire(
957    value: wire::IdempotencyObservation,
958) -> Result<IdempotencyObservation, StreamError> {
959    let request_digest = <[u8; 32]>::try_from(value.request_digest.as_ref())
960        .map_err(|_| StreamError::Unavailable)?;
961    let outcome = match value.outcome.ok_or(StreamError::Unavailable)? {
962        wire::idempotency_observation::Outcome::Append(value) => {
963            IdempotencyOutcome::Append(append_outcome_from_wire(value)?)
964        }
965        wire::idempotency_observation::Outcome::Fork(value) => {
966            IdempotencyOutcome::Fork(ForkReceipt {
967                source: path(value.source)?,
968                destination: path(value.destination)?,
969                forked_at: value.forked_at,
970                tail: value.tail,
971                commit_id: commit_id(&value.commit_id)?,
972            })
973        }
974        wire::idempotency_observation::Outcome::Trim(value) => {
975            IdempotencyOutcome::Trim(TrimReceipt {
976                path: path(value.path)?,
977                trim_point: value.trim_point,
978                commit_id: commit_id(&value.commit_id)?,
979            })
980        }
981        wire::idempotency_observation::Outcome::Delete(value) => {
982            IdempotencyOutcome::Delete(DeleteReceipt {
983                path: path(value.path)?,
984                commit_id: commit_id(&value.commit_id)?,
985            })
986        }
987        wire::idempotency_observation::Outcome::Commit(value) => {
988            IdempotencyOutcome::Commit(commit_outcome_from_wire(value)?)
989        }
990    };
991    Ok(IdempotencyObservation {
992        idempotency_key: IdempotencyKey::new(value.idempotency_key)?,
993        request_digest,
994        outcome,
995    })
996}
997
998fn bind_observation(
999    requested: &IdempotencyKey,
1000    observation: Option<wire::IdempotencyObservation>,
1001) -> Result<Option<IdempotencyObservation>, StreamError> {
1002    let observation = observation.map(observation_from_wire).transpose()?;
1003    if observation
1004        .as_ref()
1005        .is_some_and(|observation| &observation.idempotency_key != requested)
1006    {
1007        return Err(StreamError::Unavailable);
1008    }
1009    Ok(observation)
1010}
1011
1012fn observation_wire(value: IdempotencyObservation) -> wire::IdempotencyObservation {
1013    let outcome = match value.outcome {
1014        IdempotencyOutcome::Append(value) => {
1015            wire::idempotency_observation::Outcome::Append(append_outcome_wire(value))
1016        }
1017        IdempotencyOutcome::Fork(value) => {
1018            wire::idempotency_observation::Outcome::Fork(fork_receipt_wire(&value))
1019        }
1020        IdempotencyOutcome::Trim(value) => {
1021            wire::idempotency_observation::Outcome::Trim(trim_receipt_wire(&value))
1022        }
1023        IdempotencyOutcome::Delete(value) => {
1024            wire::idempotency_observation::Outcome::Delete(delete_receipt_wire(&value))
1025        }
1026        IdempotencyOutcome::Commit(value) => {
1027            wire::idempotency_observation::Outcome::Commit(commit_outcome_wire(value))
1028        }
1029    };
1030    wire::IdempotencyObservation {
1031        idempotency_key: Bytes::copy_from_slice(value.idempotency_key.as_bytes()),
1032        request_digest: Bytes::copy_from_slice(&value.request_digest),
1033        outcome: Some(outcome),
1034    }
1035}
1036
1037fn append_outcome_from_wire(value: wire::AppendResponse) -> Result<AppendOutcome, StreamError> {
1038    match value.outcome.ok_or(StreamError::Unavailable)? {
1039        wire::append_response::Outcome::Committed(receipt) => {
1040            Ok(AppendOutcome::Committed(append_receipt(&receipt)?))
1041        }
1042        wire::append_response::Outcome::Conflict(conflict) => Ok(AppendOutcome::TailConflict {
1043            actual_tail: conflict.actual_tail,
1044        }),
1045    }
1046}
1047
1048fn append_outcome_wire(value: AppendOutcome) -> wire::AppendResponse {
1049    let outcome = match value {
1050        AppendOutcome::Committed(receipt) => {
1051            wire::append_response::Outcome::Committed(append_receipt_wire(&receipt))
1052        }
1053        AppendOutcome::TailConflict { actual_tail } => {
1054            wire::append_response::Outcome::Conflict(wire::TailConflict { actual_tail })
1055        }
1056    };
1057    wire::AppendResponse {
1058        outcome: Some(outcome),
1059    }
1060}
1061
1062fn commit_outcome_from_wire(value: wire::CommitResponse) -> Result<CommitOutcome, StreamError> {
1063    match value.outcome.ok_or(StreamError::Unavailable)? {
1064        wire::commit_response::Outcome::Committed(envelope) => {
1065            Ok(CommitOutcome::Committed(envelope_from_wire(envelope)?))
1066        }
1067        wire::commit_response::Outcome::Conflict(conflicts) => Ok(CommitOutcome::Conflict(
1068            conflicts
1069                .conflicts
1070                .into_iter()
1071                .map(conflict_from_wire)
1072                .collect::<Result<_, _>>()?,
1073        )),
1074    }
1075}
1076
1077fn commit_outcome_wire(value: CommitOutcome) -> wire::CommitResponse {
1078    let outcome = match value {
1079        CommitOutcome::Committed(envelope) => {
1080            wire::commit_response::Outcome::Committed(envelope_wire(envelope))
1081        }
1082        CommitOutcome::Conflict(conflicts) => {
1083            wire::commit_response::Outcome::Conflict(wire::CommitConflicts {
1084                conflicts: conflicts.into_iter().map(conflict_wire).collect(),
1085            })
1086        }
1087    };
1088    wire::CommitResponse {
1089        outcome: Some(outcome),
1090    }
1091}
1092
1093fn error_status(error: &StreamError) -> Status {
1094    match error {
1095        StreamError::InvalidPath => Status::invalid_argument("invalid_path"),
1096        StreamError::InvalidArgument => Status::invalid_argument("invalid_argument"),
1097        StreamError::LimitExceeded => Status::invalid_argument("limit_exceeded"),
1098        StreamError::NotFound => Status::not_found(error.to_string()),
1099        StreamError::AlreadyExists => Status::already_exists(error.to_string()),
1100        StreamError::OutOfRange => Status::out_of_range(error.to_string()),
1101        StreamError::AccessDenied => Status::permission_denied(error.to_string()),
1102        StreamError::Capacity => Status::resource_exhausted(error.to_string()),
1103        StreamError::IdempotencyMismatch => Status::failed_precondition("idempotency_mismatch"),
1104        StreamError::Retired => Status::failed_precondition("retired"),
1105        StreamError::PrefixNotRetained => Status::failed_precondition("prefix_not_retained"),
1106        StreamError::Unavailable => Status::unavailable(error.to_string()),
1107        StreamError::DeadlineElapsed => Status::failed_precondition("deadline_elapsed"),
1108        StreamError::Unsupported => Status::unimplemented("unsupported_capability"),
1109    }
1110}
1111
1112fn status(error: &tonic::Status) -> StreamError {
1113    match error.code() {
1114        Code::InvalidArgument if error.message() == "invalid_path" => StreamError::InvalidPath,
1115        Code::InvalidArgument if error.message() == "limit_exceeded" => StreamError::LimitExceeded,
1116        Code::InvalidArgument => StreamError::InvalidArgument,
1117        Code::NotFound => StreamError::NotFound,
1118        Code::AlreadyExists => StreamError::AlreadyExists,
1119        Code::OutOfRange => StreamError::OutOfRange,
1120        Code::PermissionDenied | Code::Unauthenticated => StreamError::AccessDenied,
1121        Code::ResourceExhausted => StreamError::Capacity,
1122        Code::FailedPrecondition if error.message() == "idempotency_mismatch" => {
1123            StreamError::IdempotencyMismatch
1124        }
1125        Code::FailedPrecondition if error.message() == "retired" => StreamError::Retired,
1126        Code::FailedPrecondition if error.message() == "prefix_not_retained" => {
1127            StreamError::PrefixNotRetained
1128        }
1129        Code::FailedPrecondition if error.message() == "deadline_elapsed" => {
1130            StreamError::DeadlineElapsed
1131        }
1132        Code::Unimplemented if error.message() == "unsupported_capability" => {
1133            StreamError::Unsupported
1134        }
1135        _ => StreamError::Unavailable,
1136    }
1137}
1138
1139fn commit_id(value: &[u8]) -> Result<CommitId, StreamError> {
1140    let bytes = <[u8; 32]>::try_from(value).map_err(|_| StreamError::Unavailable)?;
1141    Ok(CommitId::from_bytes(bytes))
1142}
1143
1144fn record(value: wire::Record) -> Result<Record, StreamError> {
1145    Ok(Record {
1146        sequence: value.sequence,
1147        value: value.value,
1148        commit_id: commit_id(&value.commit_id)?,
1149    })
1150}
1151
1152fn read_response(value: wire::ReadResponse) -> Result<Record, StreamError> {
1153    record(value.record.ok_or(StreamError::Unavailable)?)
1154}
1155
1156fn append_receipt(value: &wire::AppendReceipt) -> Result<AppendReceipt, StreamError> {
1157    Ok(AppendReceipt {
1158        start: value.start,
1159        end: value.end,
1160        tail: value.tail,
1161        commit_id: commit_id(&value.commit_id)?,
1162    })
1163}
1164
1165fn record_wire(value: Record) -> wire::Record {
1166    wire::Record {
1167        sequence: value.sequence,
1168        value: value.value,
1169        commit_id: Bytes::copy_from_slice(value.commit_id.as_bytes()),
1170    }
1171}
1172
1173fn append_receipt_wire(value: &AppendReceipt) -> wire::AppendReceipt {
1174    wire::AppendReceipt {
1175        start: value.start,
1176        end: value.end,
1177        tail: value.tail,
1178        commit_id: Bytes::copy_from_slice(value.commit_id.as_bytes()),
1179    }
1180}
1181
1182fn fork_receipt_wire(value: &ForkReceipt) -> wire::ForkReceipt {
1183    wire::ForkReceipt {
1184        source: value.source.to_string(),
1185        destination: value.destination.to_string(),
1186        forked_at: value.forked_at,
1187        tail: value.tail,
1188        commit_id: Bytes::copy_from_slice(value.commit_id.as_bytes()),
1189    }
1190}
1191
1192fn trim_receipt_wire(value: &TrimReceipt) -> wire::TrimReceipt {
1193    wire::TrimReceipt {
1194        path: value.path.to_string(),
1195        trim_point: value.trim_point,
1196        commit_id: Bytes::copy_from_slice(value.commit_id.as_bytes()),
1197    }
1198}
1199
1200fn delete_receipt_wire(value: &DeleteReceipt) -> wire::DeleteReceipt {
1201    wire::DeleteReceipt {
1202        path: value.path.to_string(),
1203        commit_id: Bytes::copy_from_slice(value.commit_id.as_bytes()),
1204    }
1205}
1206
1207fn envelope_from_wire(value: wire::CommittedEnvelope) -> Result<CommittedEnvelope, StreamError> {
1208    Ok(CommittedEnvelope {
1209        commit_id: commit_id(&value.commit_id)?,
1210        mutations: value
1211            .mutations
1212            .into_iter()
1213            .map(committed_mutation)
1214            .collect::<Result<_, _>>()?,
1215    })
1216}
1217
1218fn envelope_wire(value: CommittedEnvelope) -> wire::CommittedEnvelope {
1219    wire::CommittedEnvelope {
1220        commit_id: Bytes::copy_from_slice(value.commit_id.as_bytes()),
1221        mutations: value
1222            .mutations
1223            .into_iter()
1224            .map(committed_mutation_wire)
1225            .collect(),
1226    }
1227}
1228
1229fn committed_mutation(value: wire::CommittedMutation) -> Result<CommittedMutation, StreamError> {
1230    match value.mutation.ok_or(StreamError::Unavailable)? {
1231        wire::committed_mutation::Mutation::Append(value) => {
1232            Ok(CommittedMutation::Append(CommittedAppend {
1233                path: path(value.path)?,
1234                start: value.start,
1235                end: value.end,
1236                tail: value.tail,
1237                records: value
1238                    .records
1239                    .into_iter()
1240                    .map(record)
1241                    .collect::<Result<_, _>>()?,
1242            }))
1243        }
1244        wire::committed_mutation::Mutation::Fork(value) => {
1245            Ok(CommittedMutation::Fork(CommittedFork {
1246                source: path(value.source)?,
1247                destination: path(value.destination)?,
1248                forked_at: value.forked_at,
1249                tail: value.tail,
1250            }))
1251        }
1252        wire::committed_mutation::Mutation::Trim(value) => {
1253            Ok(CommittedMutation::Trim(CommittedTrim {
1254                path: path(value.path)?,
1255                trim_point: value.trim_point,
1256            }))
1257        }
1258        wire::committed_mutation::Mutation::Delete(value) => {
1259            Ok(CommittedMutation::Delete(CommittedDelete {
1260                path: path(value.path)?,
1261            }))
1262        }
1263    }
1264}
1265
1266fn committed_mutation_wire(value: CommittedMutation) -> wire::CommittedMutation {
1267    let mutation = match value {
1268        CommittedMutation::Append(value) => {
1269            wire::committed_mutation::Mutation::Append(wire::CommittedAppend {
1270                path: value.path.to_string(),
1271                start: value.start,
1272                end: value.end,
1273                tail: value.tail,
1274                records: value.records.into_iter().map(record_wire).collect(),
1275            })
1276        }
1277        CommittedMutation::Fork(value) => {
1278            wire::committed_mutation::Mutation::Fork(wire::CommittedFork {
1279                source: value.source.to_string(),
1280                destination: value.destination.to_string(),
1281                forked_at: value.forked_at,
1282                tail: value.tail,
1283            })
1284        }
1285        CommittedMutation::Trim(value) => {
1286            wire::committed_mutation::Mutation::Trim(wire::CommittedTrim {
1287                path: value.path.to_string(),
1288                trim_point: value.trim_point,
1289            })
1290        }
1291        CommittedMutation::Delete(value) => {
1292            wire::committed_mutation::Mutation::Delete(wire::CommittedDelete {
1293                path: value.path.to_string(),
1294            })
1295        }
1296    };
1297    wire::CommittedMutation {
1298        mutation: Some(mutation),
1299    }
1300}
1301
1302fn conflict_from_wire(value: wire::CommitConflict) -> Result<CommitConflict, StreamError> {
1303    match value.conflict.ok_or(StreamError::Unavailable)? {
1304        wire::commit_conflict::Conflict::Tail(value) => Ok(CommitConflict::Tail {
1305            path: path(value.path)?,
1306            expected: value.expected,
1307            actual: value.actual,
1308        }),
1309        wire::commit_conflict::Conflict::Exists(value) => Ok(CommitConflict::Exists {
1310            path: path(value.path)?,
1311        }),
1312        wire::commit_conflict::Conflict::Retired(value) => Ok(CommitConflict::Retired {
1313            path: path(value.path)?,
1314        }),
1315    }
1316}
1317
1318fn conflict_wire(value: CommitConflict) -> wire::CommitConflict {
1319    let conflict = match value {
1320        CommitConflict::Tail {
1321            path,
1322            expected,
1323            actual,
1324        } => wire::commit_conflict::Conflict::Tail(wire::TailCommitConflict {
1325            path: path.to_string(),
1326            expected,
1327            actual,
1328        }),
1329        CommitConflict::Exists { path } => {
1330            wire::commit_conflict::Conflict::Exists(wire::ExistsCommitConflict {
1331                path: path.to_string(),
1332            })
1333        }
1334        CommitConflict::Retired { path } => {
1335            wire::commit_conflict::Conflict::Retired(wire::RetiredCommitConflict {
1336                path: path.to_string(),
1337            })
1338        }
1339    };
1340    wire::CommitConflict {
1341        conflict: Some(conflict),
1342    }
1343}
1344
1345#[cfg(test)]
1346mod tests {
1347    use super::*;
1348    use crate::MemoryStream;
1349    use rcgen::generate_simple_self_signed;
1350    use tokio::net::TcpListener;
1351    use tokio_stream::wrappers::TcpListenerStream;
1352    use tonic::transport::{Identity, Server, ServerTlsConfig};
1353    use wire::stream_service_server::{StreamService, StreamServiceServer};
1354
1355    struct FiniteFollow {
1356        inner: MemoryStream,
1357        follows: AtomicUsize,
1358        tail_delay: std::time::Duration,
1359    }
1360
1361    #[async_trait]
1362    impl StreamProvider for FiniteFollow {
1363        async fn inspect_idempotency(
1364            &self,
1365            key: IdempotencyKey,
1366        ) -> Result<Option<IdempotencyObservation>, StreamError> {
1367            self.inner.inspect_idempotency(key).await
1368        }
1369
1370        async fn tail(&self, path: StreamPath) -> Result<u64, StreamError> {
1371            tokio::time::sleep(self.tail_delay).await;
1372            self.inner.tail(path).await
1373        }
1374
1375        async fn append(&self, request: AppendRequest) -> Result<AppendOutcome, StreamError> {
1376            self.inner.append(request).await
1377        }
1378
1379        async fn fork(&self, request: ForkRequest) -> Result<ForkReceipt, StreamError> {
1380            self.inner.fork(request).await
1381        }
1382
1383        async fn trim(
1384            &self,
1385            path: StreamPath,
1386            before: u64,
1387            key: IdempotencyKey,
1388        ) -> Result<TrimReceipt, StreamError> {
1389            self.inner.trim(path, before, key).await
1390        }
1391
1392        async fn delete(
1393            &self,
1394            path: StreamPath,
1395            key: IdempotencyKey,
1396        ) -> Result<DeleteReceipt, StreamError> {
1397            self.inner.delete(path, key).await
1398        }
1399
1400        async fn read(&self, request: ReadRequest) -> Result<RecordStream, StreamError> {
1401            self.inner.read(request).await
1402        }
1403
1404        async fn follow(&self, _path: StreamPath, _from: u64) -> Result<RecordStream, StreamError> {
1405            self.follows.fetch_add(1, Ordering::Relaxed);
1406            Ok(stream::empty().boxed())
1407        }
1408
1409        async fn children(&self, request: ChildrenRequest) -> Result<ChildStream, StreamError> {
1410            self.inner.children(request).await
1411        }
1412
1413        async fn commit(&self, request: CommitRequest) -> Result<CommitOutcome, StreamError> {
1414            self.inner.commit(request).await
1415        }
1416
1417        async fn read_commit(&self, commit_id: CommitId) -> Result<CommittedEnvelope, StreamError> {
1418            self.inner.read_commit(commit_id).await
1419        }
1420    }
1421
1422    fn in_memory_channel(provider: Arc<MemoryStream>) -> Channel {
1423        provider_channel(Service::new(provider))
1424    }
1425
1426    fn provider_channel<P: StreamProvider>(service: Service<P>) -> Channel {
1427        Endpoint::from_static("http://fixture.invalid").connect_with_connector_lazy(
1428            tower::service_fn(move |_| {
1429                let service = service.clone();
1430                async move {
1431                    let (client, server) = tokio::io::duplex(64 * 1024);
1432                    tokio::spawn(async move {
1433                        let incoming = stream::once(async { Ok::<_, std::io::Error>(server) });
1434                        let _ = tonic::transport::Server::builder()
1435                            .add_service(StreamServiceServer::new(service))
1436                            .serve_with_incoming(incoming)
1437                            .await;
1438                    });
1439                    Ok::<_, std::io::Error>(hyper_util::rt::TokioIo::new(client))
1440                }
1441            }),
1442        )
1443    }
1444
1445    #[test]
1446    fn private_ca_bundle_is_exactly_bounded() {
1447        assert!(matches!(
1448            bounded_ca_certificate(&[]),
1449            Err(ConnectError::InvalidCaCertificate)
1450        ));
1451        assert!(matches!(
1452            bounded_ca_certificate(&vec![b'x'; MAX_CA_CERTIFICATE_BYTES]),
1453            Ok(certificate) if certificate.len() == MAX_CA_CERTIFICATE_BYTES
1454        ));
1455        assert!(matches!(
1456            bounded_ca_certificate(&vec![b'x'; MAX_CA_CERTIFICATE_BYTES + 1]),
1457            Err(ConnectError::InvalidCaCertificate)
1458        ));
1459    }
1460
1461    fn unavailable_channel() -> Channel {
1462        Endpoint::from_static("http://unavailable.invalid").connect_with_connector_lazy(
1463            tower::service_fn(|_| async {
1464                Err::<hyper_util::rt::TokioIo<tokio::io::DuplexStream>, _>(std::io::Error::new(
1465                    std::io::ErrorKind::ConnectionRefused,
1466                    "unavailable",
1467                ))
1468            }),
1469        )
1470    }
1471
1472    fn stalled_channel() -> Channel {
1473        Endpoint::from_static("http://stalled.invalid").connect_with_connector_lazy(
1474            tower::service_fn(|_| async {
1475                let (client, server) = tokio::io::duplex(64 * 1024);
1476                tokio::spawn(async move {
1477                    let _server = server;
1478                    std::future::pending::<()>().await;
1479                });
1480                Ok::<_, std::io::Error>(hyper_util::rt::TokioIo::new(client))
1481            }),
1482        )
1483    }
1484
1485    #[tokio::test]
1486    async fn endpoint_pool_is_explicit_and_bounded()
1487    -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
1488        let shared = Client::connect_endpoints(
1489            ["https://data-a.invalid", "https://data-b.invalid"],
1490            "fixture",
1491        )
1492        .await?;
1493        assert_eq!(shared.channels.len(), 2);
1494
1495        assert!(matches!(
1496            Client::connect_endpoints(std::iter::empty::<&str>(), "fixture").await,
1497            Err(ConnectError::NoEndpoints)
1498        ));
1499        assert!(matches!(
1500            Client::connect_endpoints(std::iter::repeat("https://data.invalid"), "fixture",).await,
1501            Err(ConnectError::EndpointLimit)
1502        ));
1503        let oversized = format!("https://{}.invalid", "x".repeat(MAX_ENDPOINT_URI_BYTES));
1504        assert!(matches!(
1505            Client::connect_endpoints([oversized], "fixture").await,
1506            Err(ConnectError::EndpointLimit)
1507        ));
1508        Ok(())
1509    }
1510
1511    #[tokio::test]
1512    async fn default_https_pool_installs_tls_before_first_use()
1513    -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
1514        let client = Client::connect("https://127.0.0.1:1", "fixture").await?;
1515        let mut service = Client::service(&client.channels, 0);
1516        let error = tokio::time::timeout(
1517            std::time::Duration::from_secs(2),
1518            service.tail(Request::new(wire::TailRequest {
1519                path: "accounts/events".to_owned(),
1520            })),
1521        )
1522        .await?
1523        .err()
1524        .ok_or("closed local endpoint unexpectedly answered")?;
1525        assert!(
1526            !error.to_string().contains("TLS is not enabled"),
1527            "HTTPS channel was built without TLS: {error}"
1528        );
1529        Ok(())
1530    }
1531
1532    #[tokio::test]
1533    async fn private_ca_https_connection_preserves_ambient_roots_and_exact_bearer()
1534    -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
1535        let certified = generate_simple_self_signed(["localhost".to_owned()])?;
1536        let certificate_pem = certified.cert.pem();
1537        let private_key_pem = certified.signing_key.serialize_pem();
1538        let listener = TcpListener::bind("127.0.0.1:0").await?;
1539        let address = listener.local_addr()?;
1540        let (shutdown_tx, shutdown_rx) = tokio::sync::oneshot::channel();
1541        let server_certificate_pem = certificate_pem.clone();
1542        let service = StreamServiceServer::with_interceptor(
1543            Service::new(Arc::new(MemoryStream::default())),
1544            |request: Request<()>| {
1545                if request
1546                    .metadata()
1547                    .get("authorization")
1548                    .and_then(|value| value.to_str().ok())
1549                    != Some("Bearer exact-token")
1550                {
1551                    return Err(Status::unauthenticated("missing exact bearer credential"));
1552                }
1553                Ok(request)
1554            },
1555        );
1556        let server = tokio::spawn(async move {
1557            Server::builder()
1558                .tls_config(
1559                    ServerTlsConfig::new()
1560                        .identity(Identity::from_pem(server_certificate_pem, private_key_pem)),
1561                )?
1562                .add_service(service)
1563                .serve_with_incoming_shutdown(TcpListenerStream::new(listener), async {
1564                    let _ = shutdown_rx.await;
1565                })
1566                .await
1567        });
1568
1569        let endpoint = format!("https://localhost:{}", address.port());
1570        let path = StreamPath::new("accounts/events")?;
1571        let ambient = Client::connect(&endpoint, "exact-token").await?;
1572        let mut ambient_service = Client::service(&ambient.channels, 0);
1573        assert!(
1574            ambient_service
1575                .tail(Request::new(wire::TailRequest {
1576                    path: path.to_string(),
1577                }))
1578                .await
1579                .is_err()
1580        );
1581        let explicit = crate::StreamClient::connect_with_ca_certificate(
1582            endpoint,
1583            "exact-token",
1584            certificate_pem,
1585        )
1586        .await?;
1587        assert_eq!(
1588            explicit.stream(path.as_str())?.tail().await,
1589            Err(StreamError::NotFound)
1590        );
1591
1592        let _ = shutdown_tx.send(());
1593        server.await??;
1594        Ok(())
1595    }
1596
1597    #[tokio::test]
1598    async fn hung_operation_fails_over_without_truncating_a_healthy_slow_response()
1599    -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
1600        let provider = Arc::new(FiniteFollow {
1601            inner: MemoryStream::default(),
1602            follows: AtomicUsize::new(0),
1603            tail_delay: std::time::Duration::from_millis(600),
1604        });
1605        provider
1606            .inner
1607            .append(AppendRequest {
1608                path: StreamPath::new("accounts/events")?,
1609                records: vec![Bytes::from_static(b"seed")],
1610                if_tail: Some(0),
1611                idempotency_key: Some(IdempotencyKey::new(Bytes::from_static(b"slow-tail"))?),
1612            })
1613            .await?;
1614        let transport = Client::from_channels(
1615            Arc::from([stalled_channel(), provider_channel(Service::new(provider))]),
1616            "fixture",
1617        )?;
1618        let tail = tokio::time::timeout(
1619            std::time::Duration::from_secs(2),
1620            transport.tail(StreamPath::new("accounts/events")?),
1621        )
1622        .await??;
1623        assert_eq!(tail, 1);
1624        Ok(())
1625    }
1626
1627    #[tokio::test]
1628    async fn clean_follow_eof_advances_once_to_the_next_endpoint()
1629    -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
1630        let ended = Arc::new(FiniteFollow {
1631            inner: MemoryStream::default(),
1632            follows: AtomicUsize::new(0),
1633            tail_delay: std::time::Duration::ZERO,
1634        });
1635        let durable = Arc::new(MemoryStream::default());
1636        durable
1637            .append(AppendRequest {
1638                path: StreamPath::new("accounts/events")?,
1639                records: vec![Bytes::from_static(b"resumed")],
1640                if_tail: Some(0),
1641                idempotency_key: Some(IdempotencyKey::new(Bytes::from_static(b"resume"))?),
1642            })
1643            .await?;
1644        let transport = Client::from_channels(
1645            Arc::from([
1646                provider_channel(Service::new(Arc::clone(&ended))),
1647                in_memory_channel(durable),
1648            ]),
1649            "fixture",
1650        )?;
1651        let preferred = Arc::clone(&transport.preferred);
1652        let stream = crate::StreamClient::new(Arc::new(transport)).stream("accounts/events")?;
1653
1654        let record = tokio::time::timeout(std::time::Duration::from_secs(1), async {
1655            stream
1656                .follow(0)
1657                .await?
1658                .next()
1659                .await
1660                .ok_or(StreamError::Unavailable)?
1661        })
1662        .await??;
1663        assert_eq!(record.value, Bytes::from_static(b"resumed"));
1664        assert_eq!(ended.follows.load(Ordering::Relaxed), 1);
1665        assert_eq!(preferred.load(Ordering::Relaxed), 1);
1666
1667        // A stale cursor cannot overwrite a newer successful endpoint choice.
1668        let stale = RecordCursor {
1669            client: Client::from_channels(
1670                Arc::from([unavailable_channel(), unavailable_channel()]),
1671                "fixture",
1672            )?,
1673            path: StreamPath::new("accounts/events")?,
1674            next: 0,
1675            remaining: None,
1676            active: None,
1677        };
1678        stale.client.preferred.store(1, Ordering::Relaxed);
1679        stale.advance_follow(0);
1680        assert_eq!(stale.client.preferred.load(Ordering::Relaxed), 1);
1681        Ok(())
1682    }
1683
1684    #[tokio::test]
1685    async fn lost_connection_dispatch_retries_but_peer_cancellation_does_not()
1686    -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
1687        let (client_io, _peer) = tokio::io::duplex(4096);
1688        let (mut sender, connection) = hyper::client::conn::http2::handshake(
1689            hyper_util::rt::TokioExecutor::new(),
1690            hyper_util::rt::TokioIo::new(client_io),
1691        )
1692        .await?;
1693        // Losing the driver closes Hyper's dispatch queue, independently of a peer status.
1694        // No background task or network listener is needed to reproduce this transport error.
1695        drop(connection);
1696        let request = tonic::codegen::http::Request::new(tonic::body::Body::empty());
1697        let error = tokio::time::timeout(
1698            std::time::Duration::from_secs(1),
1699            sender.send_request(request),
1700        )
1701        .await?
1702        .err()
1703        .ok_or("closed connection accepted a request")?;
1704        assert!(error.is_canceled());
1705        let error = Status::from_error(Box::new(error));
1706        assert_eq!(error.code(), Code::Cancelled);
1707        assert!(retryable(&error), "{error:?}");
1708        assert_unary_retry(error, true).await?;
1709        assert_unary_retry(Status::cancelled("application cancellation"), false).await?;
1710        Ok(())
1711    }
1712
1713    #[tokio::test]
1714    async fn incomplete_response_keeps_the_same_request_retryable()
1715    -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
1716        use tonic::codec::Codec;
1717        let mut codec =
1718            tonic_prost::ProstCodec::<wire::AppendResponse, wire::AppendResponse>::default();
1719        let mut response = tonic::Streaming::new_response(
1720            codec.decoder(),
1721            tonic::body::Body::empty(),
1722            tonic::codegen::http::StatusCode::OK,
1723            None,
1724            Some(1024),
1725        );
1726        let error = tokio::time::timeout(std::time::Duration::from_secs(1), response.message())
1727            .await?
1728            .err()
1729            .ok_or("missing terminal status must not succeed")?;
1730        assert_eq!(error.code(), Code::Unknown);
1731        assert!(std::error::Error::source(&error).is_none());
1732        assert!(
1733            retryable(&error),
1734            "incomplete response must preserve the retry identity: {error}"
1735        );
1736        assert_unary_retry(error, true).await?;
1737        assert_unary_retry(Status::unknown("peer outcome unknown"), true).await?;
1738        Ok(())
1739    }
1740
1741    async fn assert_unary_retry(
1742        error: Status,
1743        retry: bool,
1744    ) -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
1745        let client = Client::connect("https://fixture.invalid", "fixture").await?;
1746        let body = wire::AppendRequest {
1747            path: "/fixture".to_owned(),
1748            records: vec![Bytes::from_static(b"mutation")],
1749            if_tail: Some(3),
1750            idempotency_key: Some(Bytes::from_static(b"stable-key")),
1751        };
1752        let mut attempts = Vec::new();
1753        let mut first = Some(error);
1754        let result = tokio::time::timeout(
1755            std::time::Duration::from_secs(1),
1756            client.unary(body.clone(), |_, request| {
1757                attempts.push(request.into_inner());
1758                let result = first.take().map_or_else(|| Ok(Response::new(())), Err);
1759                Box::pin(async move { result })
1760            }),
1761        )
1762        .await?;
1763        if retry {
1764            assert_eq!(result, Ok(()));
1765            assert_eq!(attempts, vec![body.clone(), body]);
1766        } else {
1767            assert!(result.is_err());
1768            assert_eq!(attempts, vec![body]);
1769        }
1770        Ok(())
1771    }
1772
1773    #[tokio::test(start_paused = true)]
1774    async fn persistent_unknown_expires_without_changing_request_identity()
1775    -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
1776        let client = Client::connect("https://fixture.invalid", "fixture").await?;
1777        let started = tokio::time::Instant::now();
1778        let mut attempts = 0;
1779        let result: Result<(), StreamError> = client
1780            .unary(Bytes::from_static(b"stable-key-and-body"), |_, request| {
1781                assert_eq!(
1782                    request.into_inner(),
1783                    Bytes::from_static(b"stable-key-and-body")
1784                );
1785                attempts += 1;
1786                Box::pin(async {
1787                    tokio::time::sleep(std::time::Duration::from_millis(100)).await;
1788                    Err(Status::unknown("outcome unavailable"))
1789                })
1790            })
1791            .await;
1792        assert_eq!(result, Err(StreamError::Unavailable));
1793        assert_eq!(tokio::time::Instant::now() - started, OPERATION_DEADLINE);
1794        assert!((2..=100).contains(&attempts));
1795        Ok(())
1796    }
1797
1798    #[tokio::test]
1799    async fn remote_request_reset_is_retryable_without_retrying_protocol_errors()
1800    -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
1801        for reason in [h2::Reason::CANCEL, h2::Reason::PROTOCOL_ERROR] {
1802            let mut tasks = tokio::task::JoinSet::new();
1803            let outcome = tokio::time::timeout(std::time::Duration::from_secs(1), async {
1804                let (client_io, server_io) = tokio::io::duplex(4096);
1805                tasks.spawn(async move {
1806                    let mut connection = h2::server::handshake(server_io).await?;
1807                    let (_, mut response) = connection.accept().await.ok_or("request absent")??;
1808                    response.send_reset(reason);
1809                    connection.graceful_shutdown();
1810                    while connection.accept().await.is_some() {}
1811                    Ok::<(), Box<dyn std::error::Error + Send + Sync>>(())
1812                });
1813                let (mut sender, connection) = h2::client::handshake(client_io).await?;
1814                tasks.spawn(async move {
1815                    connection.await?;
1816                    Ok::<(), Box<dyn std::error::Error + Send + Sync>>(())
1817                });
1818                let request = tonic::codegen::http::Request::builder()
1819                    .uri("http://fixture.invalid/stream")
1820                    .body(())?;
1821                let (response, _) = sender.send_request(request, true)?;
1822                response.await.err().ok_or_else(|| {
1823                    Box::<dyn std::error::Error + Send + Sync>::from("reset response succeeded")
1824                })
1825            })
1826            .await;
1827            tasks.shutdown().await;
1828            let error = outcome??;
1829            assert!(error.is_remote());
1830            let error = Status::from_error(Box::new(error));
1831            assert_eq!(retryable(&error), reason == h2::Reason::CANCEL, "{error:?}");
1832            assert_unary_retry(error, reason == h2::Reason::CANCEL).await?;
1833        }
1834        Ok(())
1835    }
1836
1837    #[test]
1838    fn retries_transport_loss_but_not_peer_errors_or_corrupt_data() {
1839        for kind in [
1840            std::io::ErrorKind::UnexpectedEof,
1841            std::io::ErrorKind::ConnectionReset,
1842            std::io::ErrorKind::ConnectionAborted,
1843            std::io::ErrorKind::BrokenPipe,
1844            std::io::ErrorKind::NotConnected,
1845            std::io::ErrorKind::TimedOut,
1846        ] {
1847            let mut error = Status::unknown("response body interrupted");
1848            error.set_source(Arc::new(std::io::Error::from(kind)));
1849            assert!(retryable(&error), "{kind:?}");
1850        }
1851        for error in [
1852            Status::internal("protocol error"),
1853            Status::data_loss("corrupt record"),
1854            Status::permission_denied("denied"),
1855            Status::cancelled("caller cancelled"),
1856            Status::from_error(Box::new(std::io::Error::from(
1857                std::io::ErrorKind::InvalidData,
1858            ))),
1859        ] {
1860            assert!(!retryable(&error), "{error:?}");
1861        }
1862    }
1863
1864    #[test]
1865    fn inspection_rejects_an_observation_for_another_retry_identity() -> Result<(), StreamError> {
1866        let requested = IdempotencyKey::new(Bytes::from_static(b"requested"))?;
1867        let forged = wire::IdempotencyObservation {
1868            idempotency_key: Bytes::from_static(b"other"),
1869            request_digest: Bytes::from_static(&[7; 32]),
1870            outcome: Some(wire::idempotency_observation::Outcome::Append(
1871                append_outcome_wire(AppendOutcome::TailConflict { actual_tail: 3 }),
1872            )),
1873        };
1874        assert_eq!(
1875            bind_observation(&requested, Some(forged)),
1876            Err(StreamError::Unavailable)
1877        );
1878        Ok(())
1879    }
1880
1881    #[tokio::test]
1882    async fn service_maps_the_canonical_contract_without_an_alternate_state_machine()
1883    -> Result<(), Status> {
1884        let service = Service::new(Arc::new(MemoryStream::default()));
1885        let append = service
1886            .append(Request::new(wire::AppendRequest {
1887                path: "accounts/events".to_owned(),
1888                records: vec![Bytes::from_static(b"one")],
1889                if_tail: Some(0),
1890                idempotency_key: Some(Bytes::from_static(b"wire-append")),
1891            }))
1892            .await?
1893            .into_inner();
1894        let Some(wire::append_response::Outcome::Committed(receipt)) = append.outcome else {
1895            return Err(Status::internal("append outcome missing"));
1896        };
1897        if receipt.start != 0 || receipt.end != 1 || receipt.commit_id.len() != 32 {
1898            return Err(Status::internal("append receipt changed"));
1899        }
1900        let observation = service
1901            .inspect_idempotency(Request::new(wire::InspectIdempotencyRequest {
1902                idempotency_key: Bytes::from_static(b"wire-append"),
1903            }))
1904            .await?
1905            .into_inner()
1906            .observation
1907            .ok_or_else(|| Status::internal("idempotency observation missing"))?;
1908        if observation.request_digest.len() != 32
1909            || !matches!(
1910                observation.outcome,
1911                Some(wire::idempotency_observation::Outcome::Append(_))
1912            )
1913        {
1914            return Err(Status::internal("idempotency observation changed"));
1915        }
1916        let mut read = service
1917            .read(Request::new(wire::ReadRequest {
1918                path: "accounts/events".to_owned(),
1919                from: 0,
1920                limit: 1,
1921            }))
1922            .await?
1923            .into_inner();
1924        let record = read
1925            .next()
1926            .await
1927            .ok_or_else(|| Status::internal("read ended"))??
1928            .record
1929            .ok_or_else(|| Status::internal("record missing"))?;
1930        if record.sequence != 0 || record.value != Bytes::from_static(b"one") {
1931            return Err(Status::internal("record changed"));
1932        }
1933        Ok(())
1934    }
1935}