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;
#[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.cell(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,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
struct TopicSubscription {
cursor: u64,
durability: TopicDurability,
connected: bool,
}
struct TopicState<T, I> {
base_offset: u64,
elements: VecDeque<T>,
subscriptions: HashMap<I, TopicSubscription>,
}
struct TopicCellInner<T, I> {
state: Rc<std::cell::RefCell<TopicState<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 {
state: Rc::new(std::cell::RefCell::new(TopicState {
base_offset: 0,
elements: VecDeque::new(),
subscriptions: HashMap::new(),
})),
readers: std::cell::RefCell::new(HashMap::new()),
}),
}
}
pub fn from_snapshot(ctx: &Context, snapshot: TopicSnapshot<T, I>) -> Self {
let end_offset = snapshot.base_offset + snapshot.elements.len() as u64;
for sub in snapshot.subscriptions.values() {
assert!(
(snapshot.base_offset..=end_offset).contains(&sub.cursor),
"TopicCell cursor must be within the retained absolute offset range"
);
assert!(
sub.durability != TopicDurability::Ephemeral || sub.connected,
"disconnected ephemeral TopicCell subscriptions must be removed"
);
}
let state = Rc::new(std::cell::RefCell::new(TopicState {
base_offset: snapshot.base_offset,
elements: snapshot.elements.into(),
subscriptions: snapshot
.subscriptions
.into_iter()
.map(|(id, sub)| {
(
id,
TopicSubscription {
cursor: sub.cursor,
durability: sub.durability,
connected: sub.connected,
},
)
})
.collect(),
}));
let topic = Self {
inner: Rc::new(TopicCellInner {
state,
readers: std::cell::RefCell::new(HashMap::new()),
}),
};
let ids: Vec<I> = topic
.inner
.state
.borrow()
.subscriptions
.keys()
.cloned()
.collect();
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 state = Rc::clone(&self.inner.state);
let reader_id = id.clone();
let handle = ctx.computed(move |_ctx| {
let state = state.borrow();
let Some(sub) = state.subscriptions.get(&reader_id) else {
return Vec::new();
};
if !sub.connected {
return Vec::new();
}
let skip = sub.cursor.saturating_sub(state.base_offset) as usize;
state.elements.iter().skip(skip).cloned().collect()
});
self.inner.readers.borrow_mut().insert(id, handle);
handle
}
pub fn subscribe(
&self,
ctx: &Context,
id: I,
durability: TopicDurability,
) -> TopicSubscribeOutcome {
let existing = {
let mut state = self.inner.state.borrow_mut();
if let Some(sub) = state.subscriptions.get_mut(&id) {
if sub.connected {
Some((TopicSubscribeOutcome::AlreadyConnected, false))
} else {
sub.connected = true;
Some((TopicSubscribeOutcome::Reconnected, true))
}
} else {
let cursor = state.base_offset + state.elements.len() as u64;
state.subscriptions.insert(
id.clone(),
TopicSubscription {
cursor,
durability,
connected: true,
},
);
None
}
};
let reader = self.ensure_reader(ctx, id);
match existing {
Some((outcome, true)) => {
ctx.clear_slots(&[reader.id]);
outcome
}
Some((outcome, false)) => outcome,
None => TopicSubscribeOutcome::Created,
}
}
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 (changed, remove_reader) = {
let mut state = self.inner.state.borrow_mut();
let Some(sub) = state.subscriptions.get_mut(id) else {
return false;
};
if !sub.connected {
return false;
}
if sub.durability == TopicDurability::Ephemeral {
state.subscriptions.remove(id);
(true, true)
} else {
sub.connected = false;
(true, 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]);
}
changed
}
pub fn publish(&self, ctx: &Context, value: T) -> u64 {
let (offset, connected): (u64, Vec<I>) = {
let mut state = self.inner.state.borrow_mut();
let offset = state.base_offset + state.elements.len() as u64;
state.elements.push_back(value);
let connected = state
.subscriptions
.iter()
.filter(|(_, sub)| sub.connected && sub.cursor <= offset)
.map(|(id, _)| id.clone())
.collect();
(offset, connected)
};
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 = {
let mut state = self.inner.state.borrow_mut();
let (cursor, connected) = state
.subscriptions
.get(id)
.map(|sub| (sub.cursor, sub.connected))?;
let end_offset = state.base_offset + state.elements.len() as u64;
if !connected || cursor >= end_offset {
return None;
}
let index = cursor.saturating_sub(state.base_offset) as usize;
let value = state.elements.get(index).cloned()?;
state
.subscriptions
.get_mut(id)
.expect("subscription exists")
.cursor += 1;
value
};
if let Some(reader) = self.inner.readers.borrow().get(id) {
ctx.clear_slots(&[reader.id]);
}
Some(value)
}
pub fn gc(&self) -> usize {
let mut state = self.inner.state.borrow_mut();
let end_offset = state.base_offset + state.elements.len() as u64;
let frontier = state
.subscriptions
.values()
.filter(|sub| sub.durability == TopicDurability::Durable)
.map(|sub| sub.cursor)
.min()
.unwrap_or(end_offset);
let remove = frontier.saturating_sub(state.base_offset) as usize;
state.elements.drain(..remove);
state.base_offset = frontier;
remove
}
pub fn base_offset(&self) -> u64 {
self.inner.state.borrow().base_offset
}
pub fn end_offset(&self) -> u64 {
let state = self.inner.state.borrow();
state.base_offset + state.elements.len() as u64
}
pub fn elements(&self) -> Vec<T> {
self.inner.state.borrow().elements.iter().cloned().collect()
}
pub fn subscription(&self, id: &I) -> Option<TopicSubscriptionSnapshot> {
self.inner
.state
.borrow()
.subscriptions
.get(id)
.map(|sub| TopicSubscriptionSnapshot {
cursor: sub.cursor,
durability: sub.durability,
connected: sub.connected,
})
}
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> {
let state = self.inner.state.borrow();
TopicSnapshot {
base_offset: state.base_offset,
elements: state.elements.iter().cloned().collect(),
subscriptions: state
.subscriptions
.iter()
.map(|(id, sub)| {
(
id.clone(),
TopicSubscriptionSnapshot {
cursor: sub.cursor,
durability: sub.durability,
connected: sub.connected,
},
)
})
.collect(),
}
}
}
#[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);
}
}