1use std::collections::HashMap;
84use std::sync::Arc;
85
86use parking_lot::Mutex;
87
88#[derive(Debug, Clone, PartialEq, Eq, Hash)]
91pub enum StreamScope {
92 Tenant(String),
94 Global,
96}
97
98impl StreamScope {
99 pub fn from_principal_tenant(tenant: Option<&str>) -> Self {
106 match tenant {
107 Some(t) => StreamScope::Tenant(t.to_string()),
108 None => StreamScope::Global,
109 }
110 }
111
112 pub fn label(&self) -> String {
114 match self {
115 StreamScope::Tenant(t) => format!("tenant:{t}"),
116 StreamScope::Global => "global".to_string(),
117 }
118 }
119}
120
121#[derive(Debug, Clone, Default, PartialEq, Eq)]
125pub struct StreamRetention {
126 pub max_events: Option<usize>,
129 pub max_age_ms: Option<u64>,
132}
133
134#[derive(Debug, Clone, PartialEq, Eq)]
139pub struct StreamEvent {
140 pub scope: StreamScope,
141 pub stream: String,
142 pub key: Option<String>,
147 pub payload: String,
150 pub offset: u64,
156 pub appended_at_ms: u128,
157}
158
159#[derive(Debug, Clone, PartialEq, Eq)]
163pub struct StreamDescriptor {
164 pub scope: StreamScope,
165 pub name: String,
166 pub retention: StreamRetention,
167 pub head_offset: u64,
170 pub tail_offset: u64,
173 pub event_count: usize,
174}
175
176#[derive(Debug, PartialEq, Eq)]
178pub enum StreamError {
179 AlreadyExists { scope: StreamScope, name: String },
182 NotFound { scope: StreamScope, name: String },
184 CrossTenantDenied {
187 principal_tenant: Option<String>,
188 target: StreamScope,
189 stream: String,
190 },
191}
192
193impl std::fmt::Display for StreamError {
194 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
195 match self {
196 StreamError::AlreadyExists { scope, name } => {
197 write!(f, "stream: `{}/{}` already exists", scope.label(), name)
198 }
199 StreamError::NotFound { scope, name } => {
200 write!(f, "stream: `{}/{}` not found", scope.label(), name)
201 }
202 StreamError::CrossTenantDenied {
203 principal_tenant,
204 target,
205 stream,
206 } => {
207 let from = principal_tenant.as_deref().unwrap_or("<platform>");
208 write!(
209 f,
210 "stream: principal in tenant `{}` is not allowed to address `{}` stream `{}` without the `stream:cross-tenant` capability",
211 from,
212 target.label(),
213 stream
214 )
215 }
216 }
217 }
218}
219
220impl std::error::Error for StreamError {}
221
222#[derive(Debug, Clone, PartialEq, Eq, Hash)]
223struct StreamKey {
224 scope: StreamScope,
225 name: String,
226}
227
228#[derive(Debug)]
229struct DurableStream {
230 retention: StreamRetention,
231 events: Vec<StreamEvent>,
235 next_offset: u64,
237 consumer_offsets: HashMap<String, u64>,
240}
241
242impl DurableStream {
243 fn new(retention: StreamRetention) -> Self {
244 Self {
245 retention,
246 events: Vec::new(),
247 next_offset: 1,
248 consumer_offsets: HashMap::new(),
249 }
250 }
251
252 fn descriptor(&self, scope: StreamScope, name: String) -> StreamDescriptor {
253 let head_offset = self.events.first().map(|e| e.offset).unwrap_or(0);
254 let tail_offset = self.events.last().map(|e| e.offset).unwrap_or(0);
255 StreamDescriptor {
256 scope,
257 name,
258 retention: self.retention.clone(),
259 head_offset,
260 tail_offset,
261 event_count: self.events.len(),
262 }
263 }
264
265 fn apply_retention(&mut self, now_ms: u128) {
266 if let Some(max_events) = self.retention.max_events {
269 let overflow = self.events.len().saturating_sub(max_events);
270 if overflow > 0 {
271 self.events.drain(0..overflow);
272 }
273 }
274 if let Some(max_age_ms) = self.retention.max_age_ms {
275 let cutoff = now_ms.saturating_sub(max_age_ms as u128);
276 let expired = self
279 .events
280 .iter()
281 .take_while(|e| e.appended_at_ms < cutoff)
282 .count();
283 if expired > 0 {
284 self.events.drain(0..expired);
285 }
286 }
287 }
288}
289
290#[derive(Default, Clone)]
292pub struct StreamRegistry {
293 inner: Arc<Mutex<HashMap<StreamKey, DurableStream>>>,
294}
295
296impl StreamRegistry {
297 pub fn new() -> Self {
298 Self::default()
299 }
300
301 pub fn create_stream(
306 &self,
307 scope: StreamScope,
308 name: impl Into<String>,
309 retention: StreamRetention,
310 ) -> Result<(), StreamError> {
311 let name = name.into();
312 let key = StreamKey {
313 scope: scope.clone(),
314 name: name.clone(),
315 };
316 let mut guard = self.inner.lock();
317 if guard.contains_key(&key) {
318 return Err(StreamError::AlreadyExists { scope, name });
319 }
320 guard.insert(key, DurableStream::new(retention));
321 Ok(())
322 }
323
324 pub fn exists(&self, scope: &StreamScope, name: &str) -> bool {
326 let key = StreamKey {
327 scope: scope.clone(),
328 name: name.to_string(),
329 };
330 self.inner.lock().contains_key(&key)
331 }
332
333 pub fn list_streams(&self, scope: &StreamScope) -> Vec<StreamDescriptor> {
338 let guard = self.inner.lock();
339 guard
340 .iter()
341 .filter(|(k, _)| &k.scope == scope)
342 .map(|(k, s)| s.descriptor(k.scope.clone(), k.name.clone()))
343 .collect()
344 }
345
346 pub fn describe(&self, scope: &StreamScope, name: &str) -> Option<StreamDescriptor> {
348 let key = StreamKey {
349 scope: scope.clone(),
350 name: name.to_string(),
351 };
352 let guard = self.inner.lock();
353 guard.get(&key).map(|s| s.descriptor(key.scope, key.name))
354 }
355
356 pub fn append(
361 &self,
362 scope: StreamScope,
363 name: impl Into<String>,
364 key: Option<String>,
365 payload: impl Into<String>,
366 now_ms: u128,
367 ) -> Result<u64, StreamError> {
368 let name = name.into();
369 let lookup_key = StreamKey {
370 scope: scope.clone(),
371 name: name.clone(),
372 };
373 let mut guard = self.inner.lock();
374 let stream = guard
375 .get_mut(&lookup_key)
376 .ok_or_else(|| StreamError::NotFound {
377 scope: scope.clone(),
378 name: name.clone(),
379 })?;
380 let offset = stream.next_offset;
381 stream.next_offset += 1;
382 stream.events.push(StreamEvent {
383 scope,
384 stream: name,
385 key,
386 payload: payload.into(),
387 offset,
388 appended_at_ms: now_ms,
389 });
390 stream.apply_retention(now_ms);
391 Ok(offset)
392 }
393
394 pub fn append_authorized(
396 &self,
397 principal_tenant: Option<&str>,
398 target: StreamScope,
399 name: impl Into<String>,
400 key: Option<String>,
401 payload: impl Into<String>,
402 has_cross_tenant_cap: bool,
403 now_ms: u128,
404 ) -> Result<u64, StreamError> {
405 let name = name.into();
406 Self::authorize(principal_tenant, &target, &name, has_cross_tenant_cap)?;
407 self.append(target, name, key, payload, now_ms)
408 }
409
410 pub fn read_since(
417 &self,
418 scope: &StreamScope,
419 name: &str,
420 from: u64,
421 limit: usize,
422 ) -> Result<Vec<StreamEvent>, StreamError> {
423 let key = StreamKey {
424 scope: scope.clone(),
425 name: name.to_string(),
426 };
427 let guard = self.inner.lock();
428 let stream = guard.get(&key).ok_or_else(|| StreamError::NotFound {
429 scope: scope.clone(),
430 name: name.to_string(),
431 })?;
432 Ok(stream
433 .events
434 .iter()
435 .filter(|e| e.offset >= from)
436 .take(limit)
437 .cloned()
438 .collect())
439 }
440
441 pub fn read_since_authorized(
443 &self,
444 principal_tenant: Option<&str>,
445 target: StreamScope,
446 name: impl Into<String>,
447 from: u64,
448 limit: usize,
449 has_cross_tenant_cap: bool,
450 ) -> Result<Vec<StreamEvent>, StreamError> {
451 let name = name.into();
452 Self::authorize(principal_tenant, &target, &name, has_cross_tenant_cap)?;
453 self.read_since(&target, &name, from, limit)
454 }
455
456 pub fn save_offset(
464 &self,
465 scope: &StreamScope,
466 name: &str,
467 consumer: &str,
468 offset: u64,
469 ) -> Result<u64, StreamError> {
470 let key = StreamKey {
471 scope: scope.clone(),
472 name: name.to_string(),
473 };
474 let mut guard = self.inner.lock();
475 let stream = guard.get_mut(&key).ok_or_else(|| StreamError::NotFound {
476 scope: scope.clone(),
477 name: name.to_string(),
478 })?;
479 let entry = stream
480 .consumer_offsets
481 .entry(consumer.to_string())
482 .or_insert(0);
483 if offset > *entry {
484 *entry = offset;
485 }
486 Ok(*entry)
487 }
488
489 pub fn get_offset(
494 &self,
495 scope: &StreamScope,
496 name: &str,
497 consumer: &str,
498 ) -> Result<u64, StreamError> {
499 let key = StreamKey {
500 scope: scope.clone(),
501 name: name.to_string(),
502 };
503 let guard = self.inner.lock();
504 let stream = guard.get(&key).ok_or_else(|| StreamError::NotFound {
505 scope: scope.clone(),
506 name: name.to_string(),
507 })?;
508 Ok(stream.consumer_offsets.get(consumer).copied().unwrap_or(0))
509 }
510
511 fn authorize(
512 principal_tenant: Option<&str>,
513 target: &StreamScope,
514 stream: &str,
515 has_cross_tenant_cap: bool,
516 ) -> Result<(), StreamError> {
517 let same_scope = match (principal_tenant, target) {
518 (Some(pt), StreamScope::Tenant(tt)) => pt == tt,
519 (None, StreamScope::Global) => true,
523 _ => false,
524 };
525 if same_scope || has_cross_tenant_cap {
526 return Ok(());
527 }
528 Err(StreamError::CrossTenantDenied {
529 principal_tenant: principal_tenant.map(str::to_string),
530 target: target.clone(),
531 stream: stream.to_string(),
532 })
533 }
534}
535
536#[cfg(test)]
537mod tests {
538 use super::*;
539
540 fn t(name: &str) -> StreamScope {
541 StreamScope::Tenant(name.into())
542 }
543
544 #[test]
545 fn create_then_discover_via_list() {
546 let reg = StreamRegistry::new();
547 reg.create_stream(t("acme"), "orders", StreamRetention::default())
548 .unwrap();
549 let listed = reg.list_streams(&t("acme"));
550 assert_eq!(listed.len(), 1);
551 assert_eq!(listed[0].name, "orders");
552 assert_eq!(listed[0].event_count, 0);
553 assert_eq!(listed[0].head_offset, 0);
554 assert_eq!(listed[0].tail_offset, 0);
555 assert!(reg.exists(&t("acme"), "orders"));
556 assert!(reg.describe(&t("acme"), "orders").is_some());
557 }
558
559 #[test]
560 fn duplicate_create_rejected() {
561 let reg = StreamRegistry::new();
562 reg.create_stream(t("acme"), "orders", StreamRetention::default())
563 .unwrap();
564 let err = reg
565 .create_stream(t("acme"), "orders", StreamRetention::default())
566 .expect_err("dup create must fail");
567 assert!(matches!(err, StreamError::AlreadyExists { .. }));
568 }
569
570 #[test]
571 fn append_assigns_monotonic_offsets() {
572 let reg = StreamRegistry::new();
573 reg.create_stream(t("acme"), "orders", StreamRetention::default())
574 .unwrap();
575 let o1 = reg.append(t("acme"), "orders", None, "a", 100).unwrap();
576 let o2 = reg
577 .append(t("acme"), "orders", Some("k".into()), "b", 101)
578 .unwrap();
579 let o3 = reg.append(t("acme"), "orders", None, "c", 102).unwrap();
580 assert_eq!((o1, o2, o3), (1, 2, 3));
581 let desc = reg.describe(&t("acme"), "orders").unwrap();
582 assert_eq!(desc.head_offset, 1);
583 assert_eq!(desc.tail_offset, 3);
584 assert_eq!(desc.event_count, 3);
585 }
586
587 #[test]
588 fn append_on_unknown_stream_errors() {
589 let reg = StreamRegistry::new();
590 let err = reg
591 .append(t("acme"), "missing", None, "x", 0)
592 .expect_err("append on unknown stream must error");
593 assert!(matches!(err, StreamError::NotFound { .. }));
594 }
595
596 #[test]
597 fn read_since_returns_events_from_offset() {
598 let reg = StreamRegistry::new();
599 reg.create_stream(t("acme"), "orders", StreamRetention::default())
600 .unwrap();
601 for (i, payload) in ["a", "b", "c", "d"].iter().enumerate() {
602 reg.append(t("acme"), "orders", None, *payload, 100 + i as u128)
603 .unwrap();
604 }
605 let from_start = reg.read_since(&t("acme"), "orders", 0, 100).unwrap();
606 assert_eq!(from_start.len(), 4);
607 assert_eq!(from_start[0].offset, 1);
608 assert_eq!(from_start[3].payload, "d");
609
610 let from_middle = reg.read_since(&t("acme"), "orders", 3, 100).unwrap();
611 assert_eq!(from_middle.len(), 2);
612 assert_eq!(from_middle[0].offset, 3);
613 assert_eq!(from_middle[1].offset, 4);
614
615 let bounded = reg.read_since(&t("acme"), "orders", 0, 2).unwrap();
616 assert_eq!(bounded.len(), 2);
617 assert_eq!(bounded[1].offset, 2);
618 }
619
620 #[test]
621 fn read_does_not_advance_consumer_offset_no_pending_state() {
622 let reg = StreamRegistry::new();
623 reg.create_stream(t("acme"), "orders", StreamRetention::default())
624 .unwrap();
625 for i in 0..3 {
626 reg.append(t("acme"), "orders", None, "x", i).unwrap();
627 }
628 for _ in 0..3 {
632 let events = reg.read_since(&t("acme"), "orders", 0, 100).unwrap();
633 assert_eq!(events.len(), 3);
634 }
635 assert_eq!(reg.get_offset(&t("acme"), "orders", "c1").unwrap(), 0);
636 }
637
638 #[test]
639 fn save_offset_is_monotonic() {
640 let reg = StreamRegistry::new();
641 reg.create_stream(t("acme"), "orders", StreamRetention::default())
642 .unwrap();
643 for i in 0..5 {
644 reg.append(t("acme"), "orders", None, "x", i).unwrap();
645 }
646 assert_eq!(reg.save_offset(&t("acme"), "orders", "c1", 3).unwrap(), 3);
647 assert_eq!(
649 reg.save_offset(&t("acme"), "orders", "c1", 1).unwrap(),
650 3,
651 "stale save must not rewind",
652 );
653 assert_eq!(reg.save_offset(&t("acme"), "orders", "c1", 3).unwrap(), 3,);
655 assert_eq!(reg.save_offset(&t("acme"), "orders", "c1", 5).unwrap(), 5,);
657 assert_eq!(reg.get_offset(&t("acme"), "orders", "c1").unwrap(), 5);
658 }
659
660 #[test]
661 fn get_offset_defaults_to_zero_for_new_consumer() {
662 let reg = StreamRegistry::new();
663 reg.create_stream(t("acme"), "orders", StreamRetention::default())
664 .unwrap();
665 assert_eq!(reg.get_offset(&t("acme"), "orders", "fresh").unwrap(), 0);
666 }
667
668 #[test]
669 fn consumer_offsets_are_isolated_per_consumer() {
670 let reg = StreamRegistry::new();
671 reg.create_stream(t("acme"), "orders", StreamRetention::default())
672 .unwrap();
673 reg.append(t("acme"), "orders", None, "x", 0).unwrap();
674 reg.save_offset(&t("acme"), "orders", "c1", 1).unwrap();
675 assert_eq!(reg.get_offset(&t("acme"), "orders", "c1").unwrap(), 1);
676 assert_eq!(reg.get_offset(&t("acme"), "orders", "c2").unwrap(), 0);
677 }
678
679 #[test]
680 fn streams_are_tenant_isolated() {
681 let reg = StreamRegistry::new();
682 reg.create_stream(t("acme"), "orders", StreamRetention::default())
683 .unwrap();
684 reg.create_stream(t("globex"), "orders", StreamRetention::default())
685 .unwrap();
686 reg.append(t("acme"), "orders", None, "acme-only", 0)
687 .unwrap();
688 let globex_events = reg.read_since(&t("globex"), "orders", 0, 100).unwrap();
689 assert!(
690 globex_events.is_empty(),
691 "globex must not see acme's events"
692 );
693 assert_eq!(reg.list_streams(&t("acme")).len(), 1);
696 assert_eq!(reg.list_streams(&t("globex")).len(), 1);
697 }
698
699 #[test]
700 fn retention_max_events_drops_oldest() {
701 let reg = StreamRegistry::new();
702 reg.create_stream(
703 t("acme"),
704 "orders",
705 StreamRetention {
706 max_events: Some(3),
707 max_age_ms: None,
708 },
709 )
710 .unwrap();
711 for i in 0..5 {
712 reg.append(t("acme"), "orders", None, "x", 100 + i as u128)
713 .unwrap();
714 }
715 let desc = reg.describe(&t("acme"), "orders").unwrap();
716 assert_eq!(desc.event_count, 3);
718 assert_eq!(desc.head_offset, 3);
719 assert_eq!(desc.tail_offset, 5);
720 let events = reg.read_since(&t("acme"), "orders", 0, 100).unwrap();
721 assert_eq!(
722 events.iter().map(|e| e.offset).collect::<Vec<_>>(),
723 vec![3, 4, 5],
724 );
725 }
726
727 #[test]
728 fn retention_max_age_drops_old_events() {
729 let reg = StreamRegistry::new();
730 reg.create_stream(
731 t("acme"),
732 "orders",
733 StreamRetention {
734 max_events: None,
735 max_age_ms: Some(1_000),
736 },
737 )
738 .unwrap();
739 reg.append(t("acme"), "orders", None, "old", 0).unwrap();
740 reg.append(t("acme"), "orders", None, "old2", 500).unwrap();
741 reg.append(t("acme"), "orders", None, "fresh", 10_000)
744 .unwrap();
745 let events = reg.read_since(&t("acme"), "orders", 0, 100).unwrap();
746 assert_eq!(events.len(), 1);
747 assert_eq!(events[0].payload, "fresh");
748 assert_eq!(events[0].offset, 3, "retention must not rewrite offsets");
749 }
750
751 #[test]
752 fn consumer_lagged_past_retention_does_not_error() {
753 let reg = StreamRegistry::new();
754 reg.create_stream(
755 t("acme"),
756 "orders",
757 StreamRetention {
758 max_events: Some(2),
759 max_age_ms: None,
760 },
761 )
762 .unwrap();
763 for i in 0..5 {
764 reg.append(t("acme"), "orders", None, "x", i).unwrap();
765 }
766 let events = reg.read_since(&t("acme"), "orders", 2, 100).unwrap();
771 assert_eq!(
772 events.iter().map(|e| e.offset).collect::<Vec<_>>(),
773 vec![4, 5],
774 );
775 }
776
777 #[test]
778 fn same_tenant_append_does_not_require_cross_tenant_cap() {
779 let reg = StreamRegistry::new();
780 reg.create_stream(t("acme"), "orders", StreamRetention::default())
781 .unwrap();
782 let offset = reg
783 .append_authorized(Some("acme"), t("acme"), "orders", None, "x", false, 0)
784 .expect("same-tenant append must succeed without cross-tenant cap");
785 assert_eq!(offset, 1);
786
787 reg.read_since_authorized(Some("acme"), t("acme"), "orders", 0, 100, false)
788 .expect("same-tenant read must succeed without cross-tenant cap");
789 }
790
791 #[test]
792 fn cross_tenant_append_denied_without_cap() {
793 let reg = StreamRegistry::new();
794 reg.create_stream(t("globex"), "orders", StreamRetention::default())
795 .unwrap();
796 let err = reg
797 .append_authorized(Some("acme"), t("globex"), "orders", None, "leak", false, 0)
798 .expect_err("cross-tenant append must be denied without cap");
799 match err {
800 StreamError::CrossTenantDenied {
801 principal_tenant,
802 target,
803 stream,
804 } => {
805 assert_eq!(principal_tenant.as_deref(), Some("acme"));
806 assert_eq!(target, t("globex"));
807 assert_eq!(stream, "orders");
808 }
809 other => panic!("unexpected error: {other:?}"),
810 }
811 }
812
813 #[test]
814 fn cross_tenant_read_denied_without_cap() {
815 let reg = StreamRegistry::new();
816 reg.create_stream(t("globex"), "orders", StreamRetention::default())
817 .unwrap();
818 let err = reg
819 .read_since_authorized(Some("acme"), t("globex"), "orders", 0, 100, false)
820 .expect_err("cross-tenant read must be denied without cap");
821 assert!(matches!(err, StreamError::CrossTenantDenied { .. }));
822 }
823
824 #[test]
825 fn cross_tenant_append_allowed_with_cap() {
826 let reg = StreamRegistry::new();
827 reg.create_stream(t("globex"), "orders", StreamRetention::default())
828 .unwrap();
829 let offset = reg
830 .append_authorized(
831 Some("acme"),
832 t("globex"),
833 "orders",
834 None,
835 "allowed",
836 true,
837 0,
838 )
839 .expect("append with cross-tenant cap must succeed");
840 assert_eq!(offset, 1);
841 }
842
843 #[test]
844 fn global_scope_requires_cross_tenant_cap_for_tenant_principal() {
845 let reg = StreamRegistry::new();
846 reg.create_stream(StreamScope::Global, "platform", StreamRetention::default())
847 .unwrap();
848 let err = reg
849 .append_authorized(
850 Some("acme"),
851 StreamScope::Global,
852 "platform",
853 None,
854 "leak",
855 false,
856 0,
857 )
858 .expect_err("tenant principal targeting Global must require cap");
859 assert!(matches!(err, StreamError::CrossTenantDenied { .. }));
860
861 let offset = reg
864 .append_authorized(None, StreamScope::Global, "platform", None, "ok", false, 0)
865 .expect("platform principal targeting global is same-scope");
866 assert_eq!(offset, 1);
867 }
868
869 #[test]
870 fn from_principal_tenant_maps_correctly() {
871 assert_eq!(
872 StreamScope::from_principal_tenant(Some("acme")),
873 StreamScope::Tenant("acme".into())
874 );
875 assert_eq!(
876 StreamScope::from_principal_tenant(None),
877 StreamScope::Global
878 );
879 }
880
881 #[test]
882 fn event_carries_optional_key_for_future_cdc() {
883 let reg = StreamRegistry::new();
884 reg.create_stream(t("acme"), "rows", StreamRetention::default())
885 .unwrap();
886 reg.append(t("acme"), "rows", Some("user:42".into()), "{}", 0)
887 .unwrap();
888 let events = reg.read_since(&t("acme"), "rows", 0, 100).unwrap();
889 assert_eq!(events[0].key.as_deref(), Some("user:42"));
890 }
891}