1pub mod account_metadata;
3pub mod approval_groups;
4pub mod client;
5pub mod custodial_recovery;
6pub mod identity_management;
7pub mod invitation;
8pub mod invitation_code;
9pub mod membership_floor;
10pub mod notifications;
11pub mod passkey_label;
12pub mod people;
13pub mod read_budget;
14pub mod spool_settings;
15use crate::StreamingShape;
16use crate::heddle::api::common::{
17 AuthorizationAccess, AuthorizationExistence, AuthorizationRole, AuthorizationScopeSource,
18 CallContext, DeploymentTarget, RetryBehavior, RpcEffect, ServiceMaturity, SigningTier,
19};
20use crate::heddle::api::v1alpha2::{StreamDataKind, StreamFrame, stream_frame};
21pub use read_budget::{
22 GUARANTEED_READ_BUDGET, ReadBudgetError, negotiate_read_budget, validate_accepted_read_budget,
23};
24
25include!(concat!(env!("OUT_DIR"), "/heddle_api_v2_methods.rs"));
26
27#[derive(Clone, Copy, Debug, Eq, PartialEq)]
29pub struct AuthorizationTarget {
30 pub path: &'static str,
31 pub role: AuthorizationRole,
32}
33
34#[derive(Clone, Copy, Debug, Eq, PartialEq)]
38pub struct AuthorizationPolicy {
39 pub role: AuthorizationRole,
40 pub scope_source: AuthorizationScopeSource,
41 pub existence: AuthorizationExistence,
42 pub targets: &'static [AuthorizationTarget],
43}
44
45#[derive(Debug)]
47pub struct RoutedCall<'a> {
48 pub method: &'static MethodDescriptor,
49 pub context: &'a CallContext,
50}
51
52impl<'a> RoutedCall<'a> {
53 pub fn new(path: &str, context: &'a CallContext) -> Option<Self> {
54 method_descriptor(path).map(|method| Self { method, context })
55 }
56}
57
58impl MethodDescriptor {
59 pub const fn allows_zero_rtt(&self) -> bool {
61 matches!(self.effect, RpcEffect::ReadOnly)
62 && matches!(self.retry_behavior, RetryBehavior::Safe)
63 }
64 pub fn client_operation_id<'a>(
67 &self,
68 request: &'a [u8],
69 ) -> Result<Option<&'a str>, crate::RequestMetadataError> {
70 self.client_operation_id_field_number
71 .map(|field| crate::transport::protobuf_string_field(request, field))
72 .transpose()
73 .map(Option::flatten)
74 }
75}
76
77pub const MAX_CURSOR_BYTES: usize = 4096;
79
80#[derive(Clone, Copy, Debug, PartialEq, Eq, thiserror::Error)]
82pub enum StreamProtocolError {
83 #[error("non-contiguous stream sequence")]
84 Sequence,
85 #[error("stream query binding mismatch")]
86 Binding,
87 #[error("stream resumed from an unexpected cursor")]
88 Resume,
89 #[error("invalid stream phase")]
90 Phase,
91 #[error("payload does not match the frame kind")]
92 Payload,
93 #[error("invalid or oversized checkpoint cursor")]
94 Cursor,
95 #[error("missing or invalid accepted ReadBudget")]
96 Budget,
97}
98
99#[derive(Clone, Copy, Debug, PartialEq, Eq)]
100enum Phase {
101 Opening,
102 Snapshot,
103 Live,
104 Reset,
105 Complete,
106}
107
108#[derive(Clone, Copy, Debug, PartialEq, Eq)]
111pub enum ObservationAction {
112 BeginSnapshot,
113 Resumed,
114 Stage(StreamDataKind),
115 Commit,
116 Heartbeat,
117 Reset,
118 Complete,
119}
120
121#[derive(Debug, thiserror::Error)]
122pub enum ObservationApplyError<E> {
123 #[error("stream protocol failed: {0}")]
124 Protocol(#[from] StreamProtocolError),
125 #[error("view reducer failed: {0}")]
126 Reducer(E),
127}
128
129#[derive(Clone)]
131pub struct ObservationState {
132 binding_digest: [u8; 32],
133 cursor: Vec<u8>,
134 sequence: u64,
135 phase: Phase,
136 pending: bool,
137}
138
139impl ObservationState {
140 pub async fn apply<E, F, Fut>(
144 &mut self,
145 frame: &StreamFrame,
146 has_payload: bool,
147 reducer: F,
148 ) -> Result<ObservationAction, ObservationApplyError<E>>
149 where
150 F: FnOnce(ObservationAction, Vec<u8>) -> Fut,
151 Fut: std::future::Future<Output = Result<(), E>>,
152 {
153 let mut next = self.clone();
154 let action = next.accept(frame, has_payload)?;
155 reducer(action, next.cursor.clone())
156 .await
157 .map_err(ObservationApplyError::Reducer)?;
158 *self = next;
159 Ok(action)
160 }
161
162 pub fn new(binding_digest: [u8; 32], cursor: Vec<u8>) -> Self {
163 Self {
164 binding_digest,
165 cursor,
166 sequence: 0,
167 phase: Phase::Opening,
168 pending: false,
169 }
170 }
171
172 pub fn cursor(&self) -> &[u8] {
173 &self.cursor
174 }
175
176 pub fn is_complete(&self) -> bool {
177 self.phase == Phase::Complete
178 }
179
180 pub fn accept(
187 &mut self,
188 frame: &StreamFrame,
189 has_payload: bool,
190 ) -> Result<ObservationAction, StreamProtocolError> {
191 use stream_frame::Body;
192 if matches!(self.phase, Phase::Reset | Phase::Complete) {
193 return Err(StreamProtocolError::Phase);
194 }
195 if self.sequence.checked_add(1) != Some(frame.sequence) {
196 return Err(StreamProtocolError::Sequence);
197 }
198 let body = frame.body.as_ref().ok_or(StreamProtocolError::Phase)?;
199 if has_payload != matches!(body, Body::Data(_)) {
200 return Err(StreamProtocolError::Payload);
201 }
202 let action = match body {
203 Body::Open(open) => {
204 if self.phase != Phase::Opening {
205 return Err(StreamProtocolError::Phase);
206 }
207 if open.binding_digest.as_slice() != self.binding_digest {
208 return Err(StreamProtocolError::Binding);
209 }
210 if open.resumed_from != self.cursor {
211 return Err(StreamProtocolError::Resume);
212 }
213 if self.cursor.len() > MAX_CURSOR_BYTES {
214 return Err(StreamProtocolError::Cursor);
215 }
216 validate_accepted_read_budget(&Default::default(), open.accepted_budget.as_ref())
217 .map_err(|_| StreamProtocolError::Budget)?;
218 if self.cursor.is_empty() {
219 self.phase = Phase::Snapshot;
220 ObservationAction::BeginSnapshot
221 } else {
222 self.phase = Phase::Live;
223 ObservationAction::Resumed
224 }
225 }
226 Body::Data(data) => {
227 let kind =
228 StreamDataKind::try_from(data.kind).map_err(|_| StreamProtocolError::Phase)?;
229 let valid = match self.phase {
230 Phase::Snapshot => kind == StreamDataKind::Snapshot,
231 Phase::Live => matches!(kind, StreamDataKind::Upsert | StreamDataKind::Remove),
232 _ => false,
233 };
234 if !valid {
235 return Err(StreamProtocolError::Phase);
236 }
237 self.pending = true;
238 ObservationAction::Stage(kind)
239 }
240 Body::Checkpoint(checkpoint) => {
241 if !matches!(self.phase, Phase::Snapshot | Phase::Live)
242 || checkpoint.snapshot_complete != (self.phase == Phase::Snapshot)
243 {
244 return Err(StreamProtocolError::Phase);
245 }
246 if checkpoint.previous_cursor != self.cursor
247 || checkpoint.cursor.is_empty()
248 || checkpoint.cursor.len() > MAX_CURSOR_BYTES
249 || checkpoint.cursor == self.cursor
250 {
251 return Err(StreamProtocolError::Cursor);
252 }
253 self.cursor.clone_from(&checkpoint.cursor);
254 self.phase = Phase::Live;
255 self.pending = false;
256 ObservationAction::Commit
257 }
258 Body::Reset(_) => {
259 self.phase = Phase::Reset;
260 self.cursor.clear();
261 self.pending = false;
262 ObservationAction::Reset
263 }
264 Body::Complete(complete) => {
265 if self.phase != Phase::Live || self.pending {
266 return Err(StreamProtocolError::Phase);
267 }
268 if complete.cursor != self.cursor {
269 return Err(StreamProtocolError::Cursor);
270 }
271 self.phase = Phase::Complete;
272 ObservationAction::Complete
273 }
274 Body::Heartbeat(_) => {
275 if self.phase == Phase::Opening {
276 return Err(StreamProtocolError::Phase);
277 }
278 ObservationAction::Heartbeat
279 }
280 };
281 self.sequence = frame.sequence;
282 Ok(action)
283 }
284}