use std::collections::{HashMap, VecDeque};
use std::hash::Hash;
use std::rc::Rc;
use crate::Context;
use crate::cell::Computed;
use crate::cell::Source;
use crate::context::ComputeOps;
use crate::topic_core::TopicCore;
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum QueuePushError {
Full,
Closed,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum QueuePopError {
Empty,
Closed,
}
#[allow(clippy::len_without_is_empty)]
pub trait QueueStorage<T> {
fn try_push(&mut self, value: T) -> Result<(), QueuePushError>;
fn try_pop(&mut self) -> Result<T, QueuePopError>;
fn peek(&self) -> Option<&T> {
None
}
fn len(&self) -> usize;
fn capacity(&self) -> Option<usize> {
None
}
fn is_closed(&self) -> bool;
fn close(&mut self);
}
#[derive(Debug, Clone, Default)]
pub struct VecDequeStorage<T> {
elements: VecDeque<T>,
capacity: Option<usize>,
closed: bool,
}
impl<T> VecDequeStorage<T> {
pub fn unbounded() -> Self {
Self {
elements: VecDeque::new(),
capacity: None,
closed: false,
}
}
pub fn with_capacity(capacity: usize) -> Self {
assert!(capacity > 0, "VecDequeStorage capacity must be > 0");
Self {
elements: VecDeque::with_capacity(capacity),
capacity: Some(capacity),
closed: false,
}
}
pub fn elements(&self) -> Vec<T>
where
T: Clone,
{
self.elements.iter().cloned().collect()
}
}
impl<T> QueueStorage<T> for VecDequeStorage<T> {
fn try_push(&mut self, value: T) -> Result<(), QueuePushError> {
if self.closed {
return Err(QueuePushError::Closed);
}
if let Some(cap) = self.capacity
&& self.elements.len() >= cap
{
return Err(QueuePushError::Full);
}
self.elements.push_back(value);
Ok(())
}
fn try_pop(&mut self) -> Result<T, QueuePopError> {
match self.elements.pop_front() {
Some(v) => Ok(v),
None => {
if self.closed {
Err(QueuePopError::Closed)
} else {
Err(QueuePopError::Empty)
}
}
}
}
fn peek(&self) -> Option<&T> {
self.elements.front()
}
fn len(&self) -> usize {
self.elements.len()
}
fn capacity(&self) -> Option<usize> {
self.capacity
}
fn is_closed(&self) -> bool {
self.closed
}
fn close(&mut self) {
self.closed = true;
}
}
#[cfg(feature = "serde")]
impl<T> serde::Serialize for VecDequeStorage<T>
where
T: serde::Serialize,
{
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: serde::Serializer,
{
use serde::ser::SerializeSeq;
let mut seq = serializer.serialize_seq(None)?;
for e in &self.elements {
seq.serialize_element(e)?;
}
seq.end()
}
}
struct QueueCellInner<T, S> {
storage: Rc<std::cell::RefCell<S>>,
capacity: Option<usize>,
head: Computed<Option<T>>,
len: Computed<usize>,
is_empty: Computed<bool>,
is_full: Computed<bool>,
closed: Source<bool>,
}
pub struct QueueCell<T, S = VecDequeStorage<T>> {
inner: Rc<QueueCellInner<T, S>>,
}
impl<T, S> Clone for QueueCell<T, S> {
fn clone(&self) -> Self {
Self {
inner: Rc::clone(&self.inner),
}
}
}
impl<T> QueueCell<T, VecDequeStorage<T>>
where
T: PartialEq + Clone + 'static,
{
pub fn new(ctx: &Context) -> Self {
Self::with_storage(ctx, VecDequeStorage::unbounded())
}
pub fn with_capacity(ctx: &Context, capacity: usize) -> Self {
Self::with_storage(ctx, VecDequeStorage::with_capacity(capacity))
}
}
impl<T, S> QueueCell<T, S>
where
T: PartialEq + Clone + 'static,
S: QueueStorage<T> + 'static,
{
pub fn with_storage(ctx: &Context, storage: S) -> Self {
let closed_val = storage.is_closed();
let capacity = storage.capacity();
let storage = Rc::new(std::cell::RefCell::new(storage));
let head = {
let storage = Rc::clone(&storage);
ctx.computed(move |_ctx| storage.borrow().peek().cloned())
};
let len = {
let storage = Rc::clone(&storage);
ctx.computed(move |_ctx| storage.borrow().len())
};
let is_empty = {
let storage = Rc::clone(&storage);
ctx.computed(move |_ctx| storage.borrow().len() == 0)
};
let is_full = {
let storage = Rc::clone(&storage);
ctx.computed(move |_ctx| {
let s = storage.borrow();
match s.capacity() {
Some(cap) => s.len() >= cap,
None => false,
}
})
};
Self {
inner: Rc::new(QueueCellInner {
storage,
capacity,
head,
len,
is_empty,
is_full,
closed: ctx.source(closed_val),
}),
}
}
fn invalidate_readers(
&self,
ctx: &Context,
len_before: usize,
len_after: usize,
head_changed: bool,
) {
let is_empty_changed = (len_before == 0) != (len_after == 0);
let is_full_changed = self
.inner
.capacity
.map(|c| (len_before >= c) != (len_after >= c))
.unwrap_or(false);
let mut roots = [self.inner.len.id; 4];
let mut n = 1;
if is_empty_changed {
roots[n] = self.inner.is_empty.id;
n += 1;
}
if is_full_changed {
roots[n] = self.inner.is_full.id;
n += 1;
}
if head_changed {
roots[n] = self.inner.head.id;
n += 1;
}
ctx.clear_slots(&roots[..n]);
}
pub fn try_push(&self, ctx: &Context, value: T) -> Result<(), QueuePushError> {
let (result, len_before) = {
let mut s = self.inner.storage.borrow_mut();
let len_before = s.len();
(s.try_push(value), len_before)
};
if result.is_ok() {
self.invalidate_readers(ctx, len_before, len_before + 1, len_before == 0);
}
result
}
pub fn try_pop(&self, ctx: &Context) -> Result<T, QueuePopError> {
let (result, len_before) = {
let mut s = self.inner.storage.borrow_mut();
let len_before = s.len();
(s.try_pop(), len_before)
};
if result.is_ok() {
self.invalidate_readers(ctx, len_before, len_before - 1, true);
}
result
}
pub fn close(&self, ctx: &Context) {
let was_closed = self.inner.storage.borrow().is_closed();
if was_closed {
return;
}
self.inner.storage.borrow_mut().close();
ctx.set(&self.inner.closed, true);
}
pub fn head<C: ComputeOps>(&self, ctx: &C) -> Option<T> {
self.inner.head.get(ctx)
}
pub fn head_handle(&self) -> Computed<Option<T>> {
self.inner.head
}
pub fn len<C: ComputeOps>(&self, ctx: &C) -> usize {
self.inner.len.get(ctx)
}
pub fn is_empty<C: ComputeOps>(&self, ctx: &C) -> bool {
self.inner.is_empty.get(ctx)
}
pub fn is_full<C: ComputeOps>(&self, ctx: &C) -> bool {
self.inner.is_full.get(ctx)
}
pub fn is_closed<C: ComputeOps>(&self, ctx: &C) -> bool {
self.inner.closed.get(ctx)
}
pub fn reader_handles(&self) -> QueueReaderHandles<T> {
QueueReaderHandles {
head: self.inner.head,
len: self.inner.len,
is_empty: self.inner.is_empty,
is_full: self.inner.is_full,
closed: self.inner.closed,
}
}
pub fn capacity(&self) -> Option<usize> {
self.inner.capacity
}
}
impl<T> QueueCell<T, VecDequeStorage<T>>
where
T: PartialEq + Clone + 'static,
{
pub fn elements(&self) -> Vec<T> {
self.inner.storage.borrow().elements()
}
}
#[derive(Debug, Clone, Copy)]
pub struct QueueReaderHandles<T> {
pub head: Computed<Option<T>>,
pub len: Computed<usize>,
pub is_empty: Computed<bool>,
pub is_full: Computed<bool>,
pub closed: Source<bool>,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum TopicDurability {
Durable,
Ephemeral,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct TopicSubscriptionSnapshot {
pub cursor: u64,
pub durability: TopicDurability,
pub connected: bool,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct TopicSnapshot<T, I: Eq + Hash = String> {
pub base_offset: u64,
pub elements: Vec<T>,
pub subscriptions: HashMap<I, TopicSubscriptionSnapshot>,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum TopicSubscribeOutcome {
Created,
Reconnected,
AlreadyConnected,
}
struct TopicCellInner<T, I> {
core: Rc<std::cell::RefCell<TopicCore<T, I>>>,
readers: std::cell::RefCell<HashMap<I, Computed<Vec<T>>>>,
}
pub struct TopicCell<T, I = String> {
inner: Rc<TopicCellInner<T, I>>,
}
impl<T, I> Clone for TopicCell<T, I> {
fn clone(&self) -> Self {
Self {
inner: Rc::clone(&self.inner),
}
}
}
impl<T, I> TopicCell<T, I>
where
T: PartialEq + Clone + 'static,
I: Eq + Hash + Clone + 'static,
{
pub fn new(_ctx: &Context) -> Self {
Self {
inner: Rc::new(TopicCellInner {
core: Rc::new(std::cell::RefCell::new(TopicCore::new())),
readers: std::cell::RefCell::new(HashMap::new()),
}),
}
}
pub fn from_snapshot(ctx: &Context, snapshot: TopicSnapshot<T, I>) -> Self {
let topic = Self {
inner: Rc::new(TopicCellInner {
core: Rc::new(std::cell::RefCell::new(TopicCore::from_snapshot(snapshot))),
readers: std::cell::RefCell::new(HashMap::new()),
}),
};
let ids = topic.inner.core.borrow().subscription_ids();
for id in ids {
topic.ensure_reader(ctx, id);
}
topic
}
fn ensure_reader(&self, ctx: &Context, id: I) -> Computed<Vec<T>> {
if let Some(handle) = self.inner.readers.borrow().get(&id) {
return *handle;
}
let core = Rc::clone(&self.inner.core);
let reader_id = id.clone();
let handle = ctx.computed(move |_ctx| core.borrow().read_suffix(&reader_id));
self.inner.readers.borrow_mut().insert(id, handle);
handle
}
pub fn subscribe(
&self,
ctx: &Context,
id: I,
durability: TopicDurability,
) -> TopicSubscribeOutcome {
let (outcome, invalidate) = self
.inner
.core
.borrow_mut()
.subscribe(id.clone(), durability);
let reader = self.ensure_reader(ctx, id);
if invalidate {
ctx.clear_slots(&[reader.id]);
}
outcome
}
pub fn reconnect(&self, ctx: &Context, id: I) -> TopicSubscribeOutcome {
self.subscribe(ctx, id, TopicDurability::Durable)
}
pub fn disconnect(&self, ctx: &Context, id: &I) -> bool {
let Some(remove_reader) = self.inner.core.borrow_mut().disconnect(id) else {
return false;
};
let reader = if remove_reader {
self.inner.readers.borrow_mut().remove(id)
} else {
self.inner.readers.borrow().get(id).copied()
};
if let Some(reader) = reader {
ctx.clear_slots(&[reader.id]);
}
true
}
pub fn publish(&self, ctx: &Context, value: T) -> u64 {
let (offset, connected) = self.inner.core.borrow_mut().publish(value);
let readers = self.inner.readers.borrow();
let roots: Vec<_> = connected
.iter()
.filter_map(|id| readers.get(id).map(|reader| reader.id))
.collect();
ctx.clear_slots(&roots);
offset
}
pub fn read_stream(&self, ctx: &Context, id: &I) -> Vec<T> {
self.inner
.readers
.borrow()
.get(id)
.copied()
.map(|reader| ctx.get(&reader))
.unwrap_or_default()
}
pub fn read(&self, ctx: &Context, id: &I) -> Option<T> {
self.read_stream(ctx, id).into_iter().next()
}
pub fn advance(&self, ctx: &Context, id: &I) -> Option<T> {
let value = self.inner.core.borrow_mut().advance(id)?;
if let Some(reader) = self.inner.readers.borrow().get(id) {
ctx.clear_slots(&[reader.id]);
}
Some(value)
}
pub fn gc(&self) -> usize {
self.inner.core.borrow_mut().gc()
}
pub fn base_offset(&self) -> u64 {
self.inner.core.borrow().base_offset()
}
pub fn end_offset(&self) -> u64 {
self.inner.core.borrow().end_offset()
}
pub fn elements(&self) -> Vec<T> {
self.inner.core.borrow().elements()
}
pub fn subscription(&self, id: &I) -> Option<TopicSubscriptionSnapshot> {
self.inner.core.borrow().subscription(id)
}
pub fn reader_handle(&self, id: &I) -> Option<Computed<Vec<T>>> {
self.inner.readers.borrow().get(id).copied()
}
pub fn snapshot(&self) -> TopicSnapshot<T, I> {
self.inner.core.borrow().snapshot()
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn spsc_fifo_basic() {
let ctx = Context::new();
let q: QueueCell<i32> = QueueCell::new(&ctx);
assert!(q.is_empty(&ctx));
assert_eq!(q.head(&ctx), None);
q.try_push(&ctx, 1).unwrap();
q.try_push(&ctx, 2).unwrap();
q.try_push(&ctx, 3).unwrap();
assert_eq!(q.len(&ctx), 3);
assert_eq!(q.head(&ctx), Some(1));
assert_eq!(q.elements(), vec![1, 2, 3]);
assert_eq!(q.try_pop(&ctx).unwrap(), 1);
assert_eq!(q.try_pop(&ctx).unwrap(), 2);
assert_eq!(q.try_pop(&ctx).unwrap(), 3);
assert_eq!(q.try_pop(&ctx), Err(QueuePopError::Empty));
}
#[test]
fn bounded_rejects_at_capacity() {
let ctx = Context::new();
let q = QueueCell::<i32>::with_capacity(&ctx, 2);
assert_eq!(q.capacity(), Some(2));
assert!(!q.is_full(&ctx));
q.try_push(&ctx, 1).unwrap();
q.try_push(&ctx, 2).unwrap();
assert!(q.is_full(&ctx));
assert_eq!(q.try_push(&ctx, 3), Err(QueuePushError::Full));
assert_eq!(q.try_pop(&ctx).unwrap(), 1);
assert!(!q.is_full(&ctx));
q.try_push(&ctx, 3).unwrap();
assert!(q.is_full(&ctx));
}
#[test]
fn closure_lifecycle() {
let ctx = Context::new();
let q: QueueCell<&str> = QueueCell::new(&ctx);
q.try_push(&ctx, "a").unwrap();
q.try_push(&ctx, "b").unwrap();
q.close(&ctx);
assert!(q.is_closed(&ctx));
assert_eq!(q.try_push(&ctx, "c"), Err(QueuePushError::Closed));
assert_eq!(q.try_pop(&ctx).unwrap(), "a");
assert_eq!(q.try_pop(&ctx).unwrap(), "b");
assert_eq!(q.try_pop(&ctx), Err(QueuePopError::Closed));
q.close(&ctx);
assert!(q.is_closed(&ctx));
}
#[test]
fn reader_kind_independence_head_not_invalidated_on_push_to_nonempty() {
let ctx = Context::new();
let q: QueueCell<i32> = QueueCell::new(&ctx);
let head_reader = ctx.computed({
let q = q.clone();
move |ctx| q.head(ctx)
});
assert_eq!(ctx.get(&head_reader), None);
q.try_push(&ctx, 1).unwrap();
assert_eq!(ctx.get(&head_reader), Some(1));
q.try_push(&ctx, 2).unwrap();
q.try_push(&ctx, 3).unwrap();
assert!(
ctx.is_set(&head_reader),
"push to non-empty must not invalidate head reader"
);
q.try_pop(&ctx).unwrap();
assert_eq!(ctx.get(&head_reader), Some(2));
}
#[test]
fn mpsc_via_batch_is_one_invalidation_pass() {
let ctx = Context::new();
let q: QueueCell<i32> = QueueCell::new(&ctx);
let len_reader = ctx.computed({
let q = q.clone();
move |ctx| q.len(ctx)
});
assert_eq!(ctx.get(&len_reader), 0);
ctx.batch(|ctx| {
q.try_push(ctx, 10).unwrap();
q.try_push(ctx, 20).unwrap();
q.try_push(ctx, 30).unwrap();
});
assert!(
!ctx.is_set(&len_reader),
"batch should have invalidated the len reader once"
);
assert_eq!(ctx.get(&len_reader), 3);
assert_eq!(q.elements(), vec![10, 20, 30]);
}
#[test]
fn clone_shares_state() {
let ctx = Context::new();
let q: QueueCell<i32> = QueueCell::new(&ctx);
let producer = q.clone();
producer.try_push(&ctx, 42).unwrap();
assert_eq!(q.try_pop(&ctx).unwrap(), 42);
}
#[derive(Default)]
struct MinimalFifoStorage<T> {
elements: VecDeque<T>,
closed: bool,
}
impl<T> QueueStorage<T> for MinimalFifoStorage<T> {
fn try_push(&mut self, value: T) -> Result<(), QueuePushError> {
if self.closed {
return Err(QueuePushError::Closed);
}
self.elements.push_back(value);
Ok(())
}
fn try_pop(&mut self) -> Result<T, QueuePopError> {
match self.elements.pop_front() {
Some(v) => Ok(v),
None if self.closed => Err(QueuePopError::Closed),
None => Err(QueuePopError::Empty),
}
}
fn len(&self) -> usize {
self.elements.len()
}
fn is_closed(&self) -> bool {
self.closed
}
fn close(&mut self) {
self.closed = true;
}
}
#[test]
fn raw_channel_backend_conforms_to_minimal_contract() {
let ctx = Context::new();
let q: QueueCell<i32, MinimalFifoStorage<i32>> =
QueueCell::with_storage(&ctx, MinimalFifoStorage::default());
assert!(q.is_empty(&ctx));
q.try_push(&ctx, 1).unwrap();
q.try_push(&ctx, 2).unwrap();
assert_eq!(q.len(&ctx), 2);
assert!(!q.is_empty(&ctx));
assert_eq!(q.head(&ctx), None);
assert!(!q.is_full(&ctx));
assert_eq!(q.capacity(), None);
assert_eq!(q.try_pop(&ctx).unwrap(), 1);
assert_eq!(q.try_pop(&ctx).unwrap(), 2);
assert!(q.is_empty(&ctx));
q.close(&ctx);
assert!(q.is_closed(&ctx));
assert_eq!(q.try_push(&ctx, 3), Err(QueuePushError::Closed));
assert_eq!(q.try_pop(&ctx), Err(QueuePopError::Closed));
}
#[test]
fn raw_channel_backend_reader_kinds_stay_reactive() {
let ctx = Context::new();
let q: QueueCell<i32, MinimalFifoStorage<i32>> =
QueueCell::with_storage(&ctx, MinimalFifoStorage::default());
let len_reader = ctx.computed({
let q = q.clone();
move |ctx| q.len(ctx)
});
assert_eq!(ctx.get(&len_reader), 0);
q.try_push(&ctx, 10).unwrap();
assert!(!ctx.is_set(&len_reader), "push must invalidate len reader");
assert_eq!(ctx.get(&len_reader), 1);
q.try_pop(&ctx).unwrap();
assert!(!ctx.is_set(&len_reader), "pop must invalidate len reader");
assert_eq!(ctx.get(&len_reader), 0);
}
}