cranpose_services/
incoming_share.rs1use std::{
10 collections::VecDeque,
11 sync::{
12 Arc, Mutex, OnceLock,
13 atomic::{AtomicU64, Ordering},
14 },
15};
16
17use cranpose_core::{EventStream, rememberEventStream};
18
19use crate::content::{BytesContent, ContentHandle, ContentMetadata, resolve_content};
20
21#[derive(Clone, Debug, PartialEq, Eq)]
23pub enum IncomingSource {
24 Bytes(Vec<u8>),
26 Uri(String),
29}
30
31#[derive(Clone, Debug, PartialEq, Eq)]
37pub struct IncomingContent {
38 pub name: Option<String>,
40 pub mime_type: Option<String>,
42 pub source: IncomingSource,
44}
45
46impl IncomingContent {
47 pub fn from_bytes(bytes: Vec<u8>) -> Self {
49 Self {
50 name: None,
51 mime_type: None,
52 source: IncomingSource::Bytes(bytes),
53 }
54 }
55
56 pub fn from_uri(uri: impl Into<String>) -> Self {
58 Self {
59 name: None,
60 mime_type: None,
61 source: IncomingSource::Uri(uri.into()),
62 }
63 }
64
65 pub fn with_name(mut self, name: impl Into<String>) -> Self {
67 self.name = Some(name.into());
68 self
69 }
70
71 pub fn with_mime_type(mut self, mime_type: impl Into<String>) -> Self {
73 self.mime_type = Some(mime_type.into());
74 self
75 }
76
77 pub fn display_name(&self) -> String {
79 if let Some(name) = &self.name {
80 return name.clone();
81 }
82 match &self.source {
83 IncomingSource::Bytes(_) => "shared".to_string(),
84 IncomingSource::Uri(uri) => uri
85 .rsplit(['/', ':'])
86 .find(|segment| !segment.is_empty())
87 .unwrap_or(uri)
88 .to_string(),
89 }
90 }
91
92 pub fn content(self) -> Option<ContentHandle> {
97 let name = self.display_name();
98 match self.source {
99 IncomingSource::Bytes(bytes) => {
100 let mut metadata = ContentMetadata::named(name);
101 metadata.mime_type = self.mime_type;
102 Some(BytesContent::new(metadata, bytes).handle())
103 }
104 IncomingSource::Uri(uri) => resolve_content(&uri),
105 }
106 }
107}
108
109type Observer = Arc<dyn Fn(IncomingContent) + Send + Sync>;
110
111struct Inbox {
112 observers: Vec<(u64, Observer)>,
113 backlog: VecDeque<IncomingContent>,
114}
115
116impl Inbox {
117 fn new() -> Self {
118 Self {
119 observers: Vec::new(),
120 backlog: VecDeque::new(),
121 }
122 }
123
124 fn observe(&mut self, id: u64, observer: Observer) -> Vec<IncomingContent> {
125 self.observers.push((id, observer));
126 self.backlog.drain(..).collect()
127 }
128
129 fn publish(&mut self, content: IncomingContent) -> Option<Vec<Observer>> {
130 if self.observers.is_empty() {
131 self.backlog.push_back(content);
132 return None;
133 }
134 Some(
135 self.observers
136 .iter()
137 .map(|(_, observer)| Arc::clone(observer))
138 .collect(),
139 )
140 }
141
142 fn remove_observer(&mut self, id: u64) {
143 self.observers.retain(|(existing, _)| *existing != id);
144 }
145
146 fn clear(&mut self) {
147 self.backlog.clear();
148 }
149}
150
151fn inbox() -> &'static Mutex<Inbox> {
152 static INBOX: OnceLock<Mutex<Inbox>> = OnceLock::new();
153 INBOX.get_or_init(|| Mutex::new(Inbox::new()))
154}
155
156static NEXT_ID: AtomicU64 = AtomicU64::new(1);
157
158pub struct IncomingContentObserver {
160 id: u64,
161}
162
163impl Drop for IncomingContentObserver {
164 fn drop(&mut self) {
165 if let Ok(mut inbox) = inbox().lock() {
166 inbox.remove_observer(self.id);
167 }
168 }
169}
170
171pub fn observe_incoming_content(
177 observer: impl Fn(IncomingContent) + Send + Sync + 'static,
178) -> IncomingContentObserver {
179 let id = NEXT_ID.fetch_add(1, Ordering::Relaxed);
180 let observer: Observer = Arc::new(observer);
181 let replay = {
182 let Ok(mut inbox) = inbox().lock() else {
183 return IncomingContentObserver { id };
184 };
185 inbox.observe(id, Arc::clone(&observer))
186 };
187 log::info!(
188 "incoming share: observer {id} registered, replays {} item(s)",
189 replay.len()
190 );
191 for item in replay {
192 observer(item);
193 }
194 IncomingContentObserver { id }
195}
196
197pub fn publish_incoming_content(content: IncomingContent) {
200 let Some(observers) = ({
201 let Ok(mut inbox) = inbox().lock() else {
202 return;
203 };
204 inbox.publish(content.clone())
205 }) else {
206 log::info!(
207 "incoming share: no observer yet, item {} waits in the backlog",
208 content.display_name()
209 );
210 return;
211 };
212 log::info!(
213 "incoming share: item {} goes to {} observer(s)",
214 content.display_name(),
215 observers.len()
216 );
217 for observer in observers {
218 observer(content.clone());
219 }
220}
221
222pub fn clear_incoming_content() {
224 if let Ok(mut inbox) = inbox().lock() {
225 inbox.clear();
226 }
227}
228
229#[expect(non_snake_case)]
245#[track_caller]
246pub fn rememberIncomingContent() -> EventStream<IncomingContent> {
247 rememberEventStream((), |sender| {
248 observe_incoming_content(move |content| sender.send(content))
249 })
250}
251
252#[cfg(test)]
253#[path = "tests/incoming_share_tests.rs"]
254mod tests;