1#![doc = include_str!("../README.md")]
2
3use std::{fmt, sync::Arc};
4
5use async_trait::async_trait;
6use bytes::Bytes;
7use futures::stream::BoxStream;
8use thiserror::Error;
9
10pub mod conformance;
11#[cfg(feature = "grpc")]
12pub mod grpc;
13#[cfg(feature = "local")]
14mod local;
15mod memory;
16#[cfg(any(feature = "grpc", feature = "local"))]
17mod wire_codec;
18
19#[allow(missing_docs, clippy::pedantic, clippy::too_many_lines)]
21pub mod wire {
22 include!(concat!(env!("OUT_DIR"), "/acyclic.stream.v2.rs"));
23}
24pub const FILE_DESCRIPTOR_SET: &[u8] = include_bytes!("../proto/stream/v2/stream_descriptor.bin");
26#[cfg(feature = "local")]
27pub use local::{LocalDurability, LocalStream, LocalStreamError, LocalStreamLimits};
28pub use memory::{MemoryLimits, MemoryStream};
29
30pub const MAX_RECORD_BYTES: usize = 64 * 1024;
32pub const MAX_ITEMS: usize = 1_024;
34pub const MAX_COMMAND_BYTES: usize = 1024 * 1024 + 8 * 1024;
36pub const MIN_IDEMPOTENCY_RETENTION_SECS: u64 = 24 * 60 * 60;
38pub const MAX_IDEMPOTENCY_KEY_BYTES: usize = 256;
40pub const MAX_PATH_BYTES: usize = u16::MAX as usize;
42
43#[derive(Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
45pub struct StreamPath(Arc<str>);
46
47impl StreamPath {
48 pub fn new(path: impl AsRef<str>) -> Result<Self, StreamError> {
50 let path = path.as_ref();
51 if path.is_empty()
52 || path.len() > MAX_PATH_BYTES
53 || !path.is_ascii()
54 || path.starts_with('/')
55 || path.ends_with('/')
56 {
57 return Err(StreamError::InvalidPath);
58 }
59 let mut count = 0_usize;
60 for segment in path.split('/') {
61 count = count.checked_add(1).ok_or(StreamError::LimitExceeded)?;
62 if segment.is_empty()
63 || segment == "."
64 || segment == ".."
65 || segment
66 .bytes()
67 .any(|byte| byte.is_ascii_control() || byte.is_ascii_whitespace())
68 || segment.as_bytes().contains(&b'\\')
69 {
70 return Err(StreamError::InvalidPath);
71 }
72 }
73 if count > MAX_ITEMS {
74 return Err(StreamError::LimitExceeded);
75 }
76 Ok(Self(Arc::from(path)))
77 }
78
79 #[must_use]
81 pub fn as_str(&self) -> &str {
82 &self.0
83 }
84
85 #[must_use]
87 #[allow(
88 clippy::string_slice,
89 reason = "`new` rejects any path that is not `is_ascii()`, so every byte offset from \
90 `rfind` is a valid char boundary and this can never panic"
91 )]
92 pub fn parent(&self) -> Option<Self> {
93 self.0
94 .rfind('/')
95 .map(|index| Self(Arc::from(&self.0[..index])))
96 }
97}
98
99impl fmt::Display for StreamPath {
100 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
101 formatter.write_str(self.as_str())
102 }
103}
104
105#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
107pub struct CommitId([u8; 32]);
108
109impl CommitId {
110 #[must_use]
112 pub const fn from_bytes(bytes: [u8; 32]) -> Self {
113 Self(bytes)
114 }
115
116 #[must_use]
118 pub const fn as_bytes(&self) -> &[u8; 32] {
119 &self.0
120 }
121}
122
123#[derive(Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
125pub struct IdempotencyKey(Bytes);
126
127impl IdempotencyKey {
128 pub fn new(value: impl Into<Bytes>) -> Result<Self, StreamError> {
130 let value = value.into();
131 if value.is_empty() || value.len() > MAX_IDEMPOTENCY_KEY_BYTES {
132 return Err(StreamError::InvalidArgument);
133 }
134 Ok(Self(value))
135 }
136
137 #[must_use]
139 pub fn as_bytes(&self) -> &[u8] {
140 &self.0
141 }
142}
143
144#[derive(Clone, Debug, Eq, PartialEq)]
146pub struct Record {
147 pub sequence: u64,
149 pub value: Bytes,
151 pub commit_id: CommitId,
153}
154
155#[derive(Clone, Debug, Eq, PartialEq)]
157pub struct AppendReceipt {
158 pub start: u64,
160 pub end: u64,
162 pub tail: u64,
164 pub commit_id: CommitId,
166}
167
168#[derive(Clone, Debug, Eq, PartialEq)]
170pub enum AppendOutcome {
171 Committed(AppendReceipt),
173 TailConflict {
175 actual_tail: u64,
177 },
178}
179
180#[derive(Clone, Debug, Eq, PartialEq)]
182pub struct AppendRequest {
183 pub path: StreamPath,
185 pub records: Vec<Bytes>,
187 pub if_tail: Option<u64>,
189 pub idempotency_key: Option<IdempotencyKey>,
191}
192
193#[derive(Clone, Debug, Eq, PartialEq)]
195pub struct ForkReceipt {
196 pub source: StreamPath,
198 pub destination: StreamPath,
200 pub forked_at: u64,
202 pub tail: u64,
204 pub commit_id: CommitId,
206}
207
208#[derive(Clone, Debug, Eq, PartialEq)]
210pub struct ForkRequest {
211 pub source: StreamPath,
213 pub destination: StreamPath,
215 pub at_tail: Option<u64>,
217 pub idempotency_key: Option<IdempotencyKey>,
219}
220
221#[derive(Clone, Debug, Eq, PartialEq)]
223pub struct TrimReceipt {
224 pub path: StreamPath,
226 pub trim_point: u64,
228 pub commit_id: CommitId,
230}
231
232#[derive(Clone, Debug, Eq, PartialEq)]
234pub struct DeleteReceipt {
235 pub path: StreamPath,
237 pub commit_id: CommitId,
239}
240
241#[derive(Clone, Debug, Eq, PartialEq)]
243pub struct ReadRequest {
244 pub path: StreamPath,
246 pub from: u64,
248 pub limit: u32,
250}
251
252#[derive(Clone, Debug, Eq, PartialEq)]
254pub struct Child {
255 pub path: StreamPath,
257}
258
259#[derive(Clone, Debug, Eq, PartialEq)]
261pub struct ChildrenRequest {
262 pub parent: Option<StreamPath>,
264 pub limit: u32,
266}
267
268#[derive(Clone, Debug, Eq, PartialEq)]
270pub struct CommittedAppend {
271 pub path: StreamPath,
273 pub start: u64,
275 pub end: u64,
277 pub tail: u64,
279 pub records: Vec<Record>,
281}
282
283#[derive(Clone, Debug, Eq, PartialEq)]
285pub struct CommittedFork {
286 pub source: StreamPath,
288 pub destination: StreamPath,
290 pub forked_at: u64,
292 pub tail: u64,
294}
295
296#[derive(Clone, Debug, Eq, PartialEq)]
298pub struct CommittedTrim {
299 pub path: StreamPath,
301 pub trim_point: u64,
303}
304
305#[derive(Clone, Debug, Eq, PartialEq)]
307pub struct CommittedDelete {
308 pub path: StreamPath,
310}
311
312#[derive(Clone, Debug, Eq, PartialEq)]
314pub enum CommittedMutation {
315 Append(CommittedAppend),
317 Fork(CommittedFork),
319 Trim(CommittedTrim),
321 Delete(CommittedDelete),
323}
324
325#[derive(Clone, Debug, Eq, PartialEq)]
327pub struct CommittedEnvelope {
328 pub commit_id: CommitId,
330 pub mutations: Vec<CommittedMutation>,
332}
333
334#[derive(Clone, Debug, Eq, PartialEq)]
336pub enum CommitCondition {
337 Tail {
339 path: StreamPath,
341 expected: u64,
343 },
344 Absent {
346 path: StreamPath,
348 },
349}
350
351#[derive(Clone, Debug, Eq, PartialEq)]
353pub enum CommitMutation {
354 Append {
356 path: StreamPath,
358 records: Vec<Bytes>,
360 },
361 Fork {
363 source: StreamPath,
365 destination: StreamPath,
367 at_tail: u64,
369 },
370 Trim {
372 path: StreamPath,
374 before: u64,
376 },
377 Delete {
379 path: StreamPath,
381 },
382}
383
384#[derive(Clone, Debug, Eq, PartialEq)]
386pub struct CommitRequest {
387 pub conditions: Vec<CommitCondition>,
389 pub mutations: Vec<CommitMutation>,
391 pub idempotency_key: IdempotencyKey,
393}
394
395pub trait UnixMillisClock: Send + Sync + 'static {
398 fn now_unix_millis(&self) -> u64;
400}
401
402#[derive(Clone, Copy, Debug, Default)]
404pub struct SystemUnixMillisClock;
405
406impl UnixMillisClock for SystemUnixMillisClock {
407 fn now_unix_millis(&self) -> u64 {
408 std::time::SystemTime::now()
409 .duration_since(std::time::UNIX_EPOCH)
410 .map_or(0, |duration| {
411 u64::try_from(duration.as_millis()).unwrap_or(u64::MAX)
412 })
413 }
414}
415
416#[derive(Clone, Debug, Eq, PartialEq)]
418pub enum CommitConflict {
419 Tail {
421 path: StreamPath,
423 expected: u64,
425 actual: Option<u64>,
427 },
428 Exists {
430 path: StreamPath,
432 },
433 Retired {
435 path: StreamPath,
437 },
438}
439
440#[derive(Clone, Debug, Eq, PartialEq)]
442pub enum CommitOutcome {
443 Committed(CommittedEnvelope),
445 Conflict(Vec<CommitConflict>),
447}
448
449#[derive(Clone, Debug, Eq, PartialEq)]
451pub enum IdempotencyOutcome {
452 Append(AppendOutcome),
454 Fork(ForkReceipt),
456 Trim(TrimReceipt),
458 Delete(DeleteReceipt),
460 Commit(CommitOutcome),
462}
463
464#[derive(Clone, Debug, Eq, PartialEq)]
466pub struct IdempotencyObservation {
467 pub idempotency_key: IdempotencyKey,
469 pub request_digest: [u8; 32],
471 pub outcome: IdempotencyOutcome,
473}
474
475pub type RecordStream = BoxStream<'static, Result<Record, StreamError>>;
477pub type ChildStream = BoxStream<'static, Result<Child, StreamError>>;
479
480#[async_trait]
482pub trait StreamProvider: Send + Sync + 'static {
483 async fn inspect_idempotency(
485 &self,
486 idempotency_key: IdempotencyKey,
487 ) -> Result<Option<IdempotencyObservation>, StreamError>;
488 async fn tail(&self, path: StreamPath) -> Result<u64, StreamError>;
490 async fn append(&self, request: AppendRequest) -> Result<AppendOutcome, StreamError>;
492 async fn fork(&self, request: ForkRequest) -> Result<ForkReceipt, StreamError>;
494 async fn trim(
496 &self,
497 path: StreamPath,
498 before: u64,
499 idempotency_key: IdempotencyKey,
500 ) -> Result<TrimReceipt, StreamError>;
501 async fn delete(
503 &self,
504 path: StreamPath,
505 idempotency_key: IdempotencyKey,
506 ) -> Result<DeleteReceipt, StreamError>;
507 async fn read(&self, request: ReadRequest) -> Result<RecordStream, StreamError>;
509 async fn follow(&self, path: StreamPath, from: u64) -> Result<RecordStream, StreamError>;
511 async fn children(&self, request: ChildrenRequest) -> Result<ChildStream, StreamError>;
513 async fn commit(&self, request: CommitRequest) -> Result<CommitOutcome, StreamError>;
515 async fn commit_before(
521 &self,
522 request: CommitRequest,
523 deadline_unix_millis: u64,
524 ) -> Result<CommitOutcome, StreamError> {
525 let _ = (request, deadline_unix_millis);
526 Err(StreamError::Unsupported)
527 }
528 async fn read_commit(&self, commit_id: CommitId) -> Result<CommittedEnvelope, StreamError>;
530}
531
532pub struct StreamClient<P> {
534 provider: Arc<P>,
535}
536
537impl<P> Clone for StreamClient<P> {
538 fn clone(&self) -> Self {
539 Self {
540 provider: Arc::clone(&self.provider),
541 }
542 }
543}
544
545impl<P: StreamProvider> StreamClient<P> {
546 #[must_use]
548 pub fn new(provider: Arc<P>) -> Self {
549 Self { provider }
550 }
551
552 pub fn stream(&self, path: impl AsRef<str>) -> Result<Stream<P>, StreamError> {
554 Ok(Stream {
555 client: Self::new(Arc::clone(&self.provider)),
556 path: StreamPath::new(path)?,
557 })
558 }
559
560 pub async fn inspect_idempotency(
562 &self,
563 idempotency_key: IdempotencyKey,
564 ) -> Result<Option<IdempotencyObservation>, StreamError> {
565 self.provider.inspect_idempotency(idempotency_key).await
566 }
567
568 pub async fn children(
570 &self,
571 parent: Option<&str>,
572 limit: u32,
573 ) -> Result<ChildStream, StreamError> {
574 self.provider
575 .children(ChildrenRequest {
576 parent: parent.map(StreamPath::new).transpose()?,
577 limit,
578 })
579 .await
580 }
581
582 pub async fn commit(&self, request: CommitRequest) -> Result<CommitOutcome, StreamError> {
584 self.provider.commit(request).await
585 }
586
587 pub async fn commit_before(
589 &self,
590 request: CommitRequest,
591 deadline_unix_millis: u64,
592 ) -> Result<CommitOutcome, StreamError> {
593 self.provider
594 .commit_before(request, deadline_unix_millis)
595 .await
596 }
597
598 pub async fn read_commit(&self, commit_id: CommitId) -> Result<CommittedEnvelope, StreamError> {
600 self.provider.read_commit(commit_id).await
601 }
602}
603
604#[cfg(feature = "grpc")]
605impl StreamClient<grpc::Client> {
606 pub async fn connect(
608 endpoint: impl AsRef<str>,
609 bearer_token: impl AsRef<str>,
610 ) -> Result<Self, grpc::ConnectError> {
611 Ok(Self::new(Arc::new(
612 grpc::Client::connect(endpoint, bearer_token).await?,
613 )))
614 }
615
616 pub async fn connect_with_ca_certificate(
618 endpoint: impl AsRef<str>,
619 bearer_token: impl AsRef<str>,
620 certificate_pem: impl AsRef<[u8]>,
621 ) -> Result<Self, grpc::ConnectError> {
622 Ok(Self::new(Arc::new(
623 grpc::Client::connect_with_ca_certificate(endpoint, bearer_token, certificate_pem)
624 .await?,
625 )))
626 }
627}
628
629pub struct Stream<P> {
631 client: StreamClient<P>,
632 path: StreamPath,
633}
634
635impl<P> Clone for Stream<P> {
636 fn clone(&self) -> Self {
637 Self {
638 client: self.client.clone(),
639 path: self.path.clone(),
640 }
641 }
642}
643
644impl<P: StreamProvider> Stream<P> {
645 #[must_use]
647 pub const fn path(&self) -> &StreamPath {
648 &self.path
649 }
650
651 pub async fn tail(&self) -> Result<u64, StreamError> {
653 self.client.provider.tail(self.path.clone()).await
654 }
655
656 pub async fn append(&self, value: impl Into<Bytes>) -> Result<AppendOutcome, StreamError> {
658 self.append_batch(vec![value.into()], None, None).await
659 }
660
661 pub async fn append_at(
663 &self,
664 value: impl Into<Bytes>,
665 if_tail: u64,
666 ) -> Result<AppendOutcome, StreamError> {
667 self.append_batch(vec![value.into()], Some(if_tail), None)
668 .await
669 }
670
671 pub async fn append_batch(
673 &self,
674 records: Vec<Bytes>,
675 if_tail: Option<u64>,
676 idempotency_key: Option<IdempotencyKey>,
677 ) -> Result<AppendOutcome, StreamError> {
678 self.client
679 .provider
680 .append(AppendRequest {
681 path: self.path.clone(),
682 records,
683 if_tail,
684 idempotency_key: Some(idempotency_key.unwrap_or_else(new_idempotency_key)),
685 })
686 .await
687 }
688
689 pub async fn fork(
691 &self,
692 destination: impl AsRef<str>,
693 at_tail: Option<u64>,
694 idempotency_key: Option<IdempotencyKey>,
695 ) -> Result<ForkReceipt, StreamError> {
696 self.client
697 .provider
698 .fork(ForkRequest {
699 source: self.path.clone(),
700 destination: StreamPath::new(destination)?,
701 at_tail,
702 idempotency_key: Some(idempotency_key.unwrap_or_else(new_idempotency_key)),
703 })
704 .await
705 }
706
707 pub async fn trim(
709 &self,
710 before: u64,
711 idempotency_key: Option<IdempotencyKey>,
712 ) -> Result<TrimReceipt, StreamError> {
713 self.client
714 .provider
715 .trim(
716 self.path.clone(),
717 before,
718 idempotency_key.unwrap_or_else(new_idempotency_key),
719 )
720 .await
721 }
722
723 pub async fn delete(
725 &self,
726 idempotency_key: Option<IdempotencyKey>,
727 ) -> Result<DeleteReceipt, StreamError> {
728 self.client
729 .provider
730 .delete(
731 self.path.clone(),
732 idempotency_key.unwrap_or_else(new_idempotency_key),
733 )
734 .await
735 }
736
737 pub async fn read(&self, from: u64, limit: u32) -> Result<RecordStream, StreamError> {
739 self.client
740 .provider
741 .read(ReadRequest {
742 path: self.path.clone(),
743 from,
744 limit,
745 })
746 .await
747 }
748
749 pub async fn follow(&self, from: u64) -> Result<RecordStream, StreamError> {
751 self.client.provider.follow(self.path.clone(), from).await
752 }
753}
754
755fn new_idempotency_key() -> IdempotencyKey {
756 IdempotencyKey(Bytes::copy_from_slice(uuid::Uuid::new_v4().as_bytes()))
757}
758
759#[derive(Clone, Debug, Eq, Error, PartialEq)]
761pub enum StreamError {
762 #[error("invalid stream path")]
764 InvalidPath,
765 #[error("invalid stream argument")]
767 InvalidArgument,
768 #[error("stream limit exceeded")]
770 LimitExceeded,
771 #[error("stream not found")]
773 NotFound,
774 #[error("stream already exists")]
776 AlreadyExists,
777 #[error("stream path retired")]
779 Retired,
780 #[error("stream prefix not retained")]
782 PrefixNotRetained,
783 #[error("stream sequence out of range")]
785 OutOfRange,
786 #[error("idempotency mismatch")]
788 IdempotencyMismatch,
789 #[error("stream capacity exhausted")]
791 Capacity,
792 #[error("stream access denied")]
794 AccessDenied,
795 #[error("stream unavailable")]
797 Unavailable,
798 #[error("stream commit deadline elapsed")]
800 DeadlineElapsed,
801 #[error("stream capability unsupported")]
803 Unsupported,
804}