starweaver_session/
publication.rs1use chrono::{DateTime, Utc};
4use serde::{Deserialize, Serialize};
5use sha2::{Digest, Sha256};
6use starweaver_core::{RunId, SessionId};
7use starweaver_stream::{AgentStreamRecord, DisplayMessage, ReplayEvent, ReplaySnapshot};
8
9#[derive(Clone, Copy, Debug, Default, Deserialize, Eq, PartialEq, Serialize)]
11pub struct StreamPublicationTargets {
12 pub archive: bool,
14 pub replay: bool,
16}
17
18impl StreamPublicationTargets {
19 #[must_use]
21 pub const fn new(archive: bool, replay: bool) -> Self {
22 Self { archive, replay }
23 }
24
25 #[must_use]
27 pub const fn is_empty(self) -> bool {
28 !self.archive && !self.replay
29 }
30}
31
32#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
34pub struct PendingStreamPublication {
35 pub publication_id: String,
37 pub session_id: SessionId,
39 pub run_id: RunId,
41 #[serde(default, skip_serializing_if = "Vec::is_empty")]
43 pub stream_records: Vec<AgentStreamRecord>,
44 #[serde(default, skip_serializing_if = "Vec::is_empty")]
46 pub display_messages: Vec<DisplayMessage>,
47 #[serde(default, skip_serializing_if = "Vec::is_empty")]
49 pub replay_events: Vec<ReplayEvent>,
50 #[serde(default, skip_serializing_if = "Option::is_none")]
52 pub display_snapshot: Option<ReplaySnapshot>,
53 pub archive_pending: bool,
55 pub replay_pending: bool,
57 pub created_at: DateTime<Utc>,
59}
60
61impl starweaver_core::VersionedRecord for PendingStreamPublication {
62 const SCHEMA: &'static str = "starweaver.session.stream_publication";
63}
64
65fn publication_id(session_id: &SessionId, run_id: &RunId) -> String {
66 let mut digest = Sha256::new();
67 for component in [session_id.as_str(), run_id.as_str()] {
68 digest.update(component.len().to_string().as_bytes());
69 digest.update(b":");
70 digest.update(component.as_bytes());
71 digest.update(b";");
72 }
73 format!("publication-sha256:{:x}", digest.finalize())
74}
75
76impl PendingStreamPublication {
77 #[must_use]
79 pub fn new(
80 session_id: SessionId,
81 run_id: RunId,
82 targets: StreamPublicationTargets,
83 created_at: DateTime<Utc>,
84 ) -> Self {
85 let publication_id = publication_id(&session_id, &run_id);
86 Self {
87 publication_id,
88 session_id,
89 run_id,
90 stream_records: Vec::new(),
91 display_messages: Vec::new(),
92 replay_events: Vec::new(),
93 display_snapshot: None,
94 archive_pending: targets.archive,
95 replay_pending: targets.replay,
96 created_at,
97 }
98 }
99
100 #[must_use]
102 pub const fn is_complete(&self) -> bool {
103 !self.archive_pending && !self.replay_pending
104 }
105}
106
107#[derive(Clone, Copy, Debug, Deserialize, Eq, PartialEq, Serialize)]
109#[serde(rename_all = "snake_case")]
110pub enum StreamPublicationTarget {
111 Archive,
113 Replay,
115}
116
117#[cfg(test)]
118mod tests {
119 use chrono::Utc;
120 use starweaver_core::{RunId, SessionId};
121
122 use super::{PendingStreamPublication, StreamPublicationTargets};
123
124 #[test]
125 fn publication_identity_is_unambiguous_for_delimiter_bearing_ids() {
126 let first = PendingStreamPublication::new(
127 SessionId::from_string("a:b"),
128 RunId::from_string("c"),
129 StreamPublicationTargets::new(true, true),
130 Utc::now(),
131 );
132 let second = PendingStreamPublication::new(
133 SessionId::from_string("a"),
134 RunId::from_string("b:c"),
135 StreamPublicationTargets::new(true, true),
136 Utc::now(),
137 );
138 assert_ne!(first.publication_id, second.publication_id);
139 }
140}