Skip to main content

riffdb_client_rust/
reactive.rs

1//! Application-safe durable consumer and live-query transport shapes.
2
3use std::collections::BTreeMap;
4use std::marker::PhantomData;
5
6use crate::application::{
7    lower_value, raise_application_command_result, raise_value, typed_command_result,
8};
9use crate::generated::{GeneratedCommand, GeneratedEventConsumer, GeneratedLiveQuery};
10use crate::{
11    ApplicationCardinality, ApplicationClientError, ApplicationCommand, ApplicationCommandResult,
12    ApplicationRecord, ApplicationResultField, ApplicationValue, CallMetadata,
13    EventConsumerResponseStream, GeneratedExecutionError, LiveQueryUpdateStream, NamedQueryResult,
14    QueryResponseIdentity, StableApplicationClient, TypedCommandResult, generate_request_id, v1,
15};
16
17/// Exact immutable reactive operation selected by generated code.
18#[derive(Clone, Debug, Eq, PartialEq)]
19pub struct ApplicationReactiveOperation {
20    module_hash: [u8; 32],
21    operation_name: String,
22    parameters: BTreeMap<String, ApplicationValue>,
23}
24
25impl ApplicationReactiveOperation {
26    /// Constructs one checked application-only operation selection.
27    pub fn new(
28        module_hash: [u8; 32],
29        operation_name: impl Into<String>,
30        parameters: BTreeMap<String, ApplicationValue>,
31    ) -> Result<Self, ApplicationClientError> {
32        let operation_name = operation_name.into();
33        if operation_name.is_empty()
34            || operation_name.len() > 256
35            || parameters.len() > 256
36            || parameters
37                .keys()
38                .any(|name| name.is_empty() || name.len() > 256)
39        {
40            return Err(ApplicationClientError::InvalidInput);
41        }
42        Ok(Self {
43            module_hash,
44            operation_name,
45            parameters,
46        })
47    }
48
49    fn lowered_parameters(
50        &self,
51    ) -> Result<Vec<v1::EventConsumerParameter>, ApplicationClientError> {
52        self.parameters
53            .iter()
54            .map(|(name, value)| {
55                Ok(v1::EventConsumerParameter {
56                    name: name.clone(),
57                    value: Some(lower_value(value.clone())?),
58                })
59            })
60            .collect()
61    }
62
63    fn live_parameters(&self) -> Result<Vec<v1::LiveQueryParameter>, ApplicationClientError> {
64        self.parameters
65            .iter()
66            .map(|(name, value)| {
67                Ok(v1::LiveQueryParameter {
68                    name: name.clone(),
69                    value: Some(lower_value(value.clone())?),
70                })
71            })
72            .collect()
73    }
74}
75
76/// Checked durable consumer identity including the bounded caller-owned name.
77#[derive(Clone, Debug, Eq, PartialEq)]
78pub struct ApplicationEventConsumer {
79    operation: ApplicationReactiveOperation,
80    consumer_name: String,
81}
82
83impl ApplicationEventConsumer {
84    /// Binds one generated stream operation to one durable consumer name.
85    pub fn new(
86        operation: ApplicationReactiveOperation,
87        consumer_name: impl Into<String>,
88    ) -> Result<Self, ApplicationClientError> {
89        let consumer_name = consumer_name.into();
90        let valid = consumer_name.len() <= 64
91            && consumer_name
92                .as_bytes()
93                .first()
94                .is_some_and(u8::is_ascii_alphabetic)
95            && consumer_name
96                .bytes()
97                .all(|byte| byte.is_ascii_alphanumeric() || matches!(byte, b'_' | b'-'));
98        if !valid {
99            return Err(ApplicationClientError::InvalidInput);
100        }
101        Ok(Self {
102            operation,
103            consumer_name,
104        })
105    }
106
107    fn selection(&self) -> Result<v1::EventConsumerSelection, ApplicationClientError> {
108        Ok(v1::EventConsumerSelection {
109            reactive_module_hash: self.operation.module_hash.to_vec(),
110            operation_name: self.operation.operation_name.clone(),
111            parameters: self.operation.lowered_parameters()?,
112            consumer_name: self.consumer_name.clone(),
113        })
114    }
115}
116
117/// Bounded pull/stream policy accepted by the public consumer service.
118#[derive(Clone, Copy, Debug, Eq, PartialEq)]
119pub struct EventConsumerOptions {
120    /// Maximum events in one response.
121    pub batch_limit: u32,
122    /// Maximum live leases for this consumer.
123    pub in_flight_limit: u32,
124    /// Lease lifetime in seconds.
125    pub lease_seconds: u64,
126    /// Long-poll bound in nanoseconds.
127    pub maximum_wait_nanos: u64,
128}
129
130impl Default for EventConsumerOptions {
131    fn default() -> Self {
132        Self {
133            batch_limit: 1,
134            in_flight_limit: 16,
135            lease_seconds: 60,
136            maximum_wait_nanos: 0,
137        }
138    }
139}
140
141impl EventConsumerOptions {
142    fn validate(self) -> Result<Self, ApplicationClientError> {
143        if !(1..=64).contains(&self.batch_limit)
144            || !(1..=64).contains(&self.in_flight_limit)
145            || !(5..=900).contains(&self.lease_seconds)
146            || self.maximum_wait_nanos > 30_000_000_000
147        {
148            return Err(ApplicationClientError::InvalidInput);
149        }
150        Ok(self)
151    }
152}
153
154/// Stable symbolic event identity.
155#[derive(Clone, Copy, Debug, Eq, Ord, PartialEq, PartialOrd)]
156pub struct ApplicationEventId {
157    /// Authoritative application commit sequence.
158    pub commit_sequence: u64,
159    /// Zero-based event ordinal in that commit.
160    pub event_ordinal: u32,
161}
162
163/// Opaque consistency cursor safe to persist and return on reconnect.
164#[derive(Clone, Debug, Eq, PartialEq)]
165pub struct LiveQueryCursor(Vec<u8>);
166
167impl LiveQueryCursor {
168    /// Constructs a bounded opaque cursor returned previously by RiffDB.
169    pub fn new(bytes: Vec<u8>) -> Result<Self, ApplicationClientError> {
170        if bytes.is_empty() || bytes.len() > 16_384 {
171            return Err(ApplicationClientError::InvalidInput);
172        }
173        Ok(Self(bytes))
174    }
175
176    /// Opaque cursor bytes for durable application persistence.
177    #[must_use]
178    pub fn as_bytes(&self) -> &[u8] {
179        &self.0
180    }
181
182    /// Decodes the canonical standard padded Base64 presentation used by
183    /// text-only application transports.
184    pub fn from_standard_base64(value: &str) -> Result<Self, ApplicationClientError> {
185        decode_standard_base64(value).and_then(Self::new)
186    }
187
188    /// Returns the canonical standard padded Base64 presentation.
189    #[must_use]
190    pub fn to_standard_base64(&self) -> String {
191        encode_standard_base64(&self.0)
192    }
193}
194
195const BASE64_ALPHABET: &[u8; 64] =
196    b"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
197
198fn encode_standard_base64(value: &[u8]) -> String {
199    let mut output = String::with_capacity(value.len().div_ceil(3) * 4);
200    for chunk in value.chunks(3) {
201        let first = chunk[0];
202        let second = chunk.get(1).copied().unwrap_or(0);
203        let third = chunk.get(2).copied().unwrap_or(0);
204        output.push(char::from(BASE64_ALPHABET[usize::from(first >> 2)]));
205        output.push(char::from(
206            BASE64_ALPHABET[usize::from(((first & 0x03) << 4) | (second >> 4))],
207        ));
208        if chunk.len() >= 2 {
209            output.push(char::from(
210                BASE64_ALPHABET[usize::from(((second & 0x0f) << 2) | (third >> 6))],
211            ));
212        } else {
213            output.push('=');
214        }
215        if chunk.len() == 3 {
216            output.push(char::from(BASE64_ALPHABET[usize::from(third & 0x3f)]));
217        } else {
218            output.push('=');
219        }
220    }
221    output
222}
223
224fn decode_standard_base64(value: &str) -> Result<Vec<u8>, ApplicationClientError> {
225    let bytes = value.as_bytes();
226    if bytes.is_empty() || !bytes.len().is_multiple_of(4) || bytes.len() > 21_848 {
227        return Err(ApplicationClientError::InvalidInput);
228    }
229    let mut output = Vec::with_capacity(bytes.len() / 4 * 3);
230    let chunks = bytes.chunks_exact(4);
231    let chunk_count = chunks.len();
232    for (index, chunk) in chunks.enumerate() {
233        let last = index + 1 == chunk_count;
234        let first = base64_value(chunk[0]).ok_or(ApplicationClientError::InvalidInput)?;
235        let second = base64_value(chunk[1]).ok_or(ApplicationClientError::InvalidInput)?;
236        output.push((first << 2) | (second >> 4));
237        if chunk[2] == b'=' {
238            if !last || chunk[3] != b'=' || second & 0x0f != 0 {
239                return Err(ApplicationClientError::InvalidInput);
240            }
241            continue;
242        }
243        let third = base64_value(chunk[2]).ok_or(ApplicationClientError::InvalidInput)?;
244        output.push((second << 4) | (third >> 2));
245        if chunk[3] == b'=' {
246            if !last || third & 0x03 != 0 {
247                return Err(ApplicationClientError::InvalidInput);
248            }
249            continue;
250        }
251        let fourth = base64_value(chunk[3]).ok_or(ApplicationClientError::InvalidInput)?;
252        output.push((third << 6) | fourth);
253    }
254    Ok(output)
255}
256
257const fn base64_value(value: u8) -> Option<u8> {
258    match value {
259        b'A'..=b'Z' => Some(value - b'A'),
260        b'a'..=b'z' => Some(value - b'a' + 26),
261        b'0'..=b'9' => Some(value - b'0' + 52),
262        b'+' => Some(62),
263        b'/' => Some(63),
264        _ => None,
265    }
266}
267
268/// Safe symbolic event selected by the compiled stream definition.
269#[derive(Clone, Debug, Eq, PartialEq)]
270pub struct ApplicationEvent {
271    /// Stable event identity.
272    pub id: ApplicationEventId,
273    /// Symbolic event name.
274    pub name: String,
275    /// Writer contract version.
276    pub writer_contract_version: u64,
277    /// Writer command name.
278    pub command_name: String,
279    /// Safe actor kind name.
280    pub actor_kind: String,
281    /// Canonical provenance resource locator.
282    pub provenance_uri: String,
283    /// Restore-incarnation fence.
284    pub history_incarnation: u64,
285    /// Explicitly selected payload fields only.
286    pub fields: BTreeMap<String, ApplicationValue>,
287}
288
289/// One attempt-specific leased event.
290#[derive(Clone, Debug, Eq, PartialEq)]
291pub struct ApplicationEventDelivery {
292    /// Authorized selected event.
293    pub event: ApplicationEvent,
294    /// One-based delivery attempt.
295    pub attempt: u32,
296    /// Opaque attempt token. Possession is not authority.
297    pub lease_token: Vec<u8>,
298    /// Lease expiration timestamp.
299    pub expires_at: (i64, u32),
300}
301
302/// Minimal exact evidence required to mutate one live event lease.
303#[derive(Clone, Debug, Eq, PartialEq)]
304pub struct ApplicationEventLeaseEvidence {
305    event_id: ApplicationEventId,
306    lease_token: Vec<u8>,
307    history_incarnation: u64,
308}
309
310impl ApplicationEventLeaseEvidence {
311    /// Checks the public lease identity without treating possession as authority.
312    pub fn new(
313        event_id: ApplicationEventId,
314        lease_token: Vec<u8>,
315        history_incarnation: u64,
316    ) -> Result<Self, ApplicationClientError> {
317        if event_id.commit_sequence == 0 || lease_token.len() != 32 || history_incarnation == 0 {
318            return Err(ApplicationClientError::InvalidInput);
319        }
320        Ok(Self {
321            event_id,
322            lease_token,
323            history_incarnation,
324        })
325    }
326}
327
328impl ApplicationEventDelivery {
329    /// Retains only the exact attempt identity needed by ack or nack.
330    #[must_use]
331    pub fn lease_evidence(&self) -> ApplicationEventLeaseEvidence {
332        ApplicationEventLeaseEvidence {
333            event_id: self.event.id,
334            lease_token: self.lease_token.clone(),
335            history_incarnation: self.event.history_incarnation,
336        }
337    }
338}
339
340/// Durable consumer checkpoint.
341#[derive(Clone, Copy, Debug, Eq, PartialEq)]
342pub enum ApplicationEventCheckpoint {
343    /// No event has been contiguously resolved.
344    BeforeFirst,
345    /// Every selected event through this identity is resolved.
346    After(ApplicationEventId),
347}
348
349/// Bounded durable consumer status.
350#[derive(Clone, Debug, Eq, PartialEq)]
351pub struct ApplicationEventConsumerStatus {
352    /// Compare-and-transition revision.
353    pub revision: u64,
354    /// Contiguous checkpoint.
355    pub checkpoint: ApplicationEventCheckpoint,
356    /// Restore-incarnation fence.
357    pub history_incarnation: u64,
358    /// Current live leases.
359    pub live_leases: u32,
360    /// Current retry records.
361    pub retries: u32,
362    /// Current dead letters.
363    pub dead_letters: u32,
364}
365
366/// Opaque protected-consumer continuation. No event identity or checkpoint is
367/// recoverable from these bytes.
368#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
369pub struct ApplicationEventProgressCursor([u8; 16]);
370
371impl ApplicationEventProgressCursor {
372    /// Checks one exact cursor returned by RiffDB.
373    pub fn new(bytes: Vec<u8>) -> Result<Self, ApplicationClientError> {
374        bytes
375            .try_into()
376            .map(Self)
377            .map_err(|_| ApplicationClientError::InvalidInput)
378    }
379
380    /// Borrows the opaque bytes for persistence or a subsequent pull.
381    #[must_use]
382    pub const fn as_bytes(&self) -> &[u8; 16] {
383        &self.0
384    }
385}
386
387/// Metadata-safe status for a row-policy-protected consumer.
388#[derive(Clone, Copy, Debug, Eq, PartialEq)]
389pub struct ApplicationProtectedEventConsumerStatus {
390    /// Restore-incarnation fence.
391    pub history_incarnation: u64,
392    /// Opaque authority- and progress-bound continuation.
393    pub progress_cursor: ApplicationEventProgressCursor,
394}
395
396/// Public consumer status selected by the stream's authorization model.
397#[derive(Clone, Debug, Eq, PartialEq)]
398pub enum ApplicationEventConsumerPublicStatus {
399    /// Exact queue metadata for an unprotected stream.
400    Exact(ApplicationEventConsumerStatus),
401    /// Metadata-safe continuation for a protected stream.
402    Protected(ApplicationProtectedEventConsumerStatus),
403}
404
405/// Closed completion class for one bounded pull.
406#[derive(Clone, Copy, Debug, Eq, PartialEq)]
407pub enum ApplicationEventPullDisposition {
408    /// Bounded inspection completed normally.
409    Ready,
410    /// Long polling elapsed without visible work.
411    WaitTimedOut,
412    /// Candidate work was exhausted before a visible item was found.
413    BoundedProgress,
414}
415
416/// One bounded consumer response.
417#[derive(Clone, Debug, Eq, PartialEq)]
418pub struct ApplicationEventBatch {
419    /// Leased events in service order.
420    pub events: Vec<ApplicationEventDelivery>,
421    /// Status after the transition.
422    pub status: ApplicationEventConsumerPublicStatus,
423    /// Closed completion class.
424    pub disposition: ApplicationEventPullDisposition,
425    /// Whether a nonzero long poll expired without work.
426    pub wait_timed_out: bool,
427}
428
429/// One freshly executed hydration in a contextual work item.
430#[derive(Clone, Debug, Eq, PartialEq)]
431pub struct ApplicationContextualHydration {
432    /// Subscription-local declared hydration name.
433    pub name: String,
434    /// Named-query result branch.
435    pub outcome: String,
436    /// Name-addressed typed result fields.
437    pub fields: BTreeMap<String, ApplicationResultField>,
438}
439
440/// One currently authorized declared reaction.
441#[derive(Clone, Debug, Eq, PartialEq)]
442pub struct ApplicationContextualReaction {
443    /// Subscription-local reaction name.
444    pub name: String,
445    /// Target contract command name.
446    pub command_name: String,
447    /// Stable target command identity.
448    pub command_id: u32,
449    causation_token: Vec<u8>,
450}
451
452impl ApplicationContextualReaction {
453    /// Checks one transport-raised reaction without treating its token as authority.
454    pub fn checked(
455        name: impl Into<String>,
456        command_name: impl Into<String>,
457        command_id: u32,
458        causation_token: Vec<u8>,
459    ) -> Result<Self, ApplicationClientError> {
460        let name = name.into();
461        let command_name = command_name.into();
462        if name.is_empty()
463            || name.len() > 256
464            || command_name.is_empty()
465            || command_name.len() > 256
466            || command_id == 0
467            || causation_token.len() <= 32
468            || causation_token.len() > 1_024
469        {
470            return Err(ApplicationClientError::InvalidInput);
471        }
472        Ok(Self {
473            name,
474            command_name,
475            command_id,
476            causation_token,
477        })
478    }
479
480    /// Borrows the opaque proof for transport forwarding only.
481    #[must_use]
482    pub fn causation_token(&self) -> &[u8] {
483        &self.causation_token
484    }
485}
486
487/// One leased event with fresh same-snapshot context and authorized reactions.
488#[derive(Clone, Debug, Eq, PartialEq)]
489pub struct ApplicationContextualWorkItem {
490    /// Attempt-specific event delivery.
491    pub delivery: ApplicationEventDelivery,
492    /// Authoritative application head shared by every hydration.
493    pub context_head: u64,
494    /// Fresh hydration results in declaration order.
495    pub hydrations: Vec<ApplicationContextualHydration>,
496    /// Reactions authorized when this item was produced.
497    pub available_reactions: Vec<ApplicationContextualReaction>,
498}
499
500/// One bounded contextual pull.
501#[derive(Clone, Debug, Eq, PartialEq)]
502pub struct ApplicationContextualBatch {
503    /// Zero or one leased contextual work item.
504    pub items: Vec<ApplicationContextualWorkItem>,
505    /// Durable consumer status after leasing.
506    pub status: ApplicationEventConsumerPublicStatus,
507    /// Closed completion class.
508    pub disposition: ApplicationEventPullDisposition,
509    /// Whether the bounded wait elapsed without work.
510    pub wait_timed_out: bool,
511}
512
513/// One generated event plus its contextual work and lease evidence.
514#[derive(Clone, Debug, Eq, PartialEq)]
515pub struct TypedContextualWorkItem<E> {
516    /// Generated closed event union.
517    pub event: E,
518    evidence: ApplicationContextualWorkItem,
519}
520
521impl<E> TypedContextualWorkItem<E> {
522    /// Context, reactions, and exact lease evidence used by checked operations.
523    #[must_use]
524    pub const fn evidence(&self) -> &ApplicationContextualWorkItem {
525        &self.evidence
526    }
527}
528
529/// One generated contextual pull.
530#[derive(Clone, Debug, Eq, PartialEq)]
531pub struct TypedContextualBatch<E> {
532    /// Zero or one typed contextual work item.
533    pub items: Vec<TypedContextualWorkItem<E>>,
534    /// Durable consumer status after leasing.
535    pub status: ApplicationEventConsumerPublicStatus,
536    /// Closed completion class.
537    pub disposition: ApplicationEventPullDisposition,
538    /// Whether the bounded wait elapsed without work.
539    pub wait_timed_out: bool,
540}
541
542/// One generated typed event together with its exact lease evidence.
543#[derive(Clone, Debug, Eq, PartialEq)]
544pub struct TypedEventDelivery<E> {
545    /// Generated closed event union.
546    pub event: E,
547    evidence: ApplicationEventDelivery,
548}
549
550impl<E> TypedEventDelivery<E> {
551    /// Attempt-specific evidence used by checked acknowledge operations.
552    #[must_use]
553    pub const fn evidence(&self) -> &ApplicationEventDelivery {
554        &self.evidence
555    }
556}
557
558/// One bounded generated consumer batch.
559#[derive(Clone, Debug, Eq, PartialEq)]
560pub struct TypedEventBatch<E> {
561    /// Typed leased events.
562    pub events: Vec<TypedEventDelivery<E>>,
563    /// Durable status after leasing.
564    pub status: ApplicationEventConsumerPublicStatus,
565    /// Closed completion class.
566    pub disposition: ApplicationEventPullDisposition,
567    /// Whether the bounded wait elapsed without work.
568    pub wait_timed_out: bool,
569}
570
571/// Closed result for consumer metadata mutations.
572#[derive(Clone, Copy, Debug, Eq, PartialEq)]
573pub enum ApplicationEventMutationResult {
574    /// Transition applied.
575    Applied,
576    /// Consumer changed since the supplied evidence.
577    StateChanged,
578    /// Consumer was not found.
579    NotFound,
580    /// A seek or retire was blocked by a live lease.
581    OutstandingLease,
582    /// Lease evidence is stale.
583    StaleLease,
584    /// Lease evidence is expired.
585    LeaseExpired,
586}
587
588/// Stream of checked, application-safe durable consumer batches.
589pub struct ApplicationEventResponseStream {
590    inner: EventConsumerResponseStream,
591}
592
593impl ApplicationEventResponseStream {
594    /// Receives the next authorized bounded batch.
595    pub async fn message(
596        &mut self,
597    ) -> Result<Option<ApplicationEventBatch>, ApplicationClientError> {
598        self.inner
599            .message()
600            .await?
601            .map(raise_event_batch)
602            .transpose()
603    }
604}
605
606/// Closed application live-query update.
607#[derive(Clone, Debug, Eq, PartialEq)]
608pub enum ApplicationLiveQueryUpdate {
609    /// Initial complete snapshot.
610    Snapshot {
611        /// Typed name-addressed result.
612        result: NamedQueryResult,
613        /// Opaque reconnect evidence.
614        cursor: LiveQueryCursor,
615        /// Restore-incarnation frontier.
616        history_incarnation: u64,
617        /// Application-head frontier.
618        application_head: u64,
619    },
620    /// Bounded keyed patch. Generated clients apply its closed operations.
621    Patch(LiveQueryPatch),
622    /// Complete bounded replacement after convergence reset.
623    Reset {
624        /// Stable reset reason name.
625        reason: String,
626        /// Complete replacement result.
627        result: NamedQueryResult,
628        /// Opaque reconnect evidence.
629        cursor: LiveQueryCursor,
630        /// Restore-incarnation frontier.
631        history_incarnation: u64,
632        /// Application-head frontier.
633        application_head: u64,
634    },
635    /// Frontier-only convergence marker.
636    Checkpoint(LiveQueryCheckpoint),
637    /// Terminal update; clients must clear retained protected state.
638    Terminal(LiveQueryTerminal),
639}
640
641/// One generated complete snapshot with its exact reconnect frontier.
642#[derive(Clone, Debug, Eq, PartialEq)]
643pub struct TypedLiveQuerySnapshot<T> {
644    /// Generated declared-result union.
645    pub result: T,
646    /// Frontier and opaque reconnect cursor for this exact view.
647    pub checkpoint: LiveQueryCheckpoint,
648}
649
650/// One generated complete reset with its reason and reconnect frontier.
651#[derive(Clone, Debug, Eq, PartialEq)]
652pub struct TypedLiveQueryReset<T> {
653    /// Stable closed reset reason name.
654    pub reason: String,
655    /// Generated complete replacement result.
656    pub result: T,
657    /// Frontier and opaque reconnect cursor for the replacement view.
658    pub checkpoint: LiveQueryCheckpoint,
659}
660
661/// One bounded keyed patch.
662#[derive(Clone, Debug, Eq, PartialEq)]
663pub struct LiveQueryPatch {
664    /// Top-level query result field receiving the operations.
665    pub result_field: String,
666    /// Closed ordered patch operations.
667    pub operations: Vec<LiveQueryPatchOperation>,
668    /// Convergence frontier.
669    pub checkpoint: LiveQueryCheckpoint,
670}
671
672/// Closed keyed list mutation.
673#[derive(Clone, Debug, Eq, PartialEq)]
674pub enum LiveQueryPatchOperation {
675    /// Inserts one complete authorized record.
676    Insert {
677        /// Destination index.
678        index: u32,
679        /// Complete inserted record.
680        record: ApplicationRecord,
681    },
682    /// Removes one record by complete authorized key.
683    Remove {
684        /// Existing index.
685        index: u32,
686        /// Complete authorized key.
687        key: BTreeMap<String, ApplicationValue>,
688    },
689    /// Replaces one complete authorized record.
690    Replace {
691        /// Existing index.
692        index: u32,
693        /// Complete replacement record.
694        record: ApplicationRecord,
695    },
696    /// Moves one keyed record.
697    Move {
698        /// Existing index.
699        from: u32,
700        /// Destination index.
701        to: u32,
702        /// Complete authorized key.
703        key: BTreeMap<String, ApplicationValue>,
704    },
705}
706
707/// Live-query convergence frontier and reconnect cursor.
708#[derive(Clone, Debug, Eq, PartialEq)]
709pub struct LiveQueryCheckpoint {
710    /// Restore-incarnation frontier.
711    pub history_incarnation: u64,
712    /// Application-head frontier.
713    pub application_head: u64,
714    /// Opaque reconnect cursor.
715    pub cursor: LiveQueryCursor,
716}
717
718/// Terminal live-query reason and last non-secret frontier.
719#[derive(Clone, Debug, Eq, PartialEq)]
720pub struct LiveQueryTerminal {
721    /// Stable terminal reason name.
722    pub reason: String,
723    /// Last frontier, when one was established.
724    pub last_frontier: Option<(u64, u64)>,
725}
726
727/// Stream of closed application-safe live-query updates.
728pub struct ApplicationLiveQueryStream {
729    inner: LiveQueryUpdateStream,
730    terminal: bool,
731}
732
733/// Generated closed live-query stream.
734pub struct TypedLiveQueryStream<G: GeneratedLiveQuery> {
735    inner: ApplicationLiveQueryStream,
736    marker: PhantomData<fn() -> G>,
737}
738
739impl<G: GeneratedLiveQuery> TypedLiveQueryStream<G> {
740    /// Receives and decodes the next generated update.
741    pub async fn message(&mut self) -> Result<Option<G::Update>, ApplicationClientError> {
742        self.inner
743            .message()
744            .await?
745            .map(G::decode_update)
746            .transpose()
747    }
748}
749
750impl ApplicationLiveQueryStream {
751    /// Receives the next update. A terminal update permanently closes this wrapper.
752    pub async fn message(
753        &mut self,
754    ) -> Result<Option<ApplicationLiveQueryUpdate>, ApplicationClientError> {
755        if self.terminal {
756            return Ok(None);
757        }
758        let update = self
759            .inner
760            .message()
761            .await?
762            .map(raise_live_update)
763            .transpose()?;
764        if matches!(update, Some(ApplicationLiveQueryUpdate::Terminal(_))) {
765            self.terminal = true;
766        }
767        Ok(update)
768    }
769}
770
771impl StableApplicationClient {
772    /// Pulls and decodes one generated typed event batch.
773    pub async fn consume_generated_events<G: GeneratedEventConsumer>(
774        &mut self,
775        generated: G,
776        options: EventConsumerOptions,
777        metadata: &CallMetadata,
778    ) -> Result<TypedEventBatch<G::Event>, ApplicationClientError> {
779        let consumer = generated.event_consumer()?;
780        let batch = self
781            .consume_event_stream(&consumer, options, metadata)
782            .await?;
783        let events = batch
784            .events
785            .into_iter()
786            .map(|evidence| {
787                Ok(TypedEventDelivery {
788                    event: G::decode_event(evidence.event.clone())?,
789                    evidence,
790                })
791            })
792            .collect::<Result<Vec<_>, ApplicationClientError>>()?;
793        Ok(TypedEventBatch {
794            events,
795            status: batch.status,
796            disposition: batch.disposition,
797            wait_timed_out: batch.wait_timed_out,
798        })
799    }
800
801    /// Pulls and decodes one generated contextual work item.
802    pub async fn consume_generated_contextual<G: GeneratedEventConsumer>(
803        &mut self,
804        generated: G,
805        maximum_wait_nanos: u64,
806        metadata: &CallMetadata,
807    ) -> Result<TypedContextualBatch<G::Event>, ApplicationClientError> {
808        let consumer = generated.event_consumer()?;
809        let batch = self
810            .consume_contextual_subscription(&consumer, maximum_wait_nanos, metadata)
811            .await?;
812        let items = batch
813            .items
814            .into_iter()
815            .map(|evidence| {
816                Ok(TypedContextualWorkItem {
817                    event: G::decode_event(evidence.delivery.event.clone())?,
818                    evidence,
819                })
820            })
821            .collect::<Result<Vec<_>, ApplicationClientError>>()?;
822        Ok(TypedContextualBatch {
823            items,
824            status: batch.status,
825            disposition: batch.disposition,
826            wait_timed_out: batch.wait_timed_out,
827        })
828    }
829
830    /// Executes one generated command through an available contextual reaction.
831    pub async fn execute_generated_contextual_reaction<C: GeneratedCommand>(
832        &mut self,
833        consumer: &ApplicationEventConsumer,
834        reaction: &ApplicationContextualReaction,
835        command: &C,
836        metadata: &CallMetadata,
837    ) -> Result<TypedCommandResult<C::Outcome>, GeneratedExecutionError> {
838        let request_id = generate_request_id().map_err(|error| {
839            GeneratedExecutionError::Client(crate::ClientError::IdentifierGeneration(error))
840        })?;
841        let command_request = command
842            .idempotent_command()
843            .map_err(GeneratedExecutionError::CommandShape)?;
844        if command_request.command_name() != reaction.command_name {
845            return Err(GeneratedExecutionError::CommandShape(
846                crate::generated::GeneratedCommandError::InvalidInputShape,
847            ));
848        }
849        let selection = consumer.selection().map_err(|_| {
850            GeneratedExecutionError::CommandShape(
851                crate::generated::GeneratedCommandError::InvalidInputShape,
852            )
853        })?;
854        let response = self
855            .inner
856            .execute_contextual_reaction(
857                v1::ExecuteContextualReactionRequest {
858                    request_id: request_id.as_bytes().to_vec(),
859                    selection: Some(selection),
860                    causation_token: reaction.causation_token.clone(),
861                    reaction_name: reaction.name.clone(),
862                    command: Some(command_request.request(request_id)),
863                },
864                metadata,
865            )
866            .await
867            .map_err(GeneratedExecutionError::Client)?;
868        let outcome = command
869            .decode_outcome(&response)
870            .map_err(GeneratedExecutionError::CommandShape)?;
871        typed_command_result(command, outcome, response)
872    }
873
874    /// Opens and decodes one generated exact live query.
875    pub async fn watch_generated_query<G: GeneratedLiveQuery>(
876        &mut self,
877        generated: G,
878        cursor: Option<LiveQueryCursor>,
879        metadata: &CallMetadata,
880    ) -> Result<TypedLiveQueryStream<G>, ApplicationClientError> {
881        let operation = generated.live_operation()?;
882        let inner = self.watch_named_query(&operation, cursor, metadata).await?;
883        Ok(TypedLiveQueryStream {
884            inner,
885            marker: PhantomData,
886        })
887    }
888
889    /// Pulls one bounded authorized batch from an exact generated stream.
890    pub async fn consume_event_stream(
891        &mut self,
892        consumer: &ApplicationEventConsumer,
893        options: EventConsumerOptions,
894        metadata: &CallMetadata,
895    ) -> Result<ApplicationEventBatch, ApplicationClientError> {
896        self.consume_event_stream_after(consumer, options, None, metadata)
897            .await
898    }
899
900    /// Continues one protected consumer from an opaque RiffDB cursor.
901    pub async fn consume_event_stream_after(
902        &mut self,
903        consumer: &ApplicationEventConsumer,
904        options: EventConsumerOptions,
905        progress_cursor: Option<ApplicationEventProgressCursor>,
906        metadata: &CallMetadata,
907    ) -> Result<ApplicationEventBatch, ApplicationClientError> {
908        let response = self
909            .inner
910            .consume_event_stream(
911                consumer_request(consumer, options, progress_cursor)?,
912                metadata,
913            )
914            .await?;
915        raise_event_batch(response)
916    }
917
918    /// Pulls one contextual event with fresh same-snapshot hydrations.
919    pub async fn consume_contextual_subscription(
920        &mut self,
921        consumer: &ApplicationEventConsumer,
922        maximum_wait_nanos: u64,
923        metadata: &CallMetadata,
924    ) -> Result<ApplicationContextualBatch, ApplicationClientError> {
925        self.consume_contextual_subscription_after(consumer, maximum_wait_nanos, None, metadata)
926            .await
927    }
928
929    /// Continues one protected contextual subscription from an opaque cursor.
930    pub async fn consume_contextual_subscription_after(
931        &mut self,
932        consumer: &ApplicationEventConsumer,
933        maximum_wait_nanos: u64,
934        progress_cursor: Option<ApplicationEventProgressCursor>,
935        metadata: &CallMetadata,
936    ) -> Result<ApplicationContextualBatch, ApplicationClientError> {
937        if maximum_wait_nanos > 30_000_000_000 {
938            return Err(ApplicationClientError::InvalidInput);
939        }
940        let response = self
941            .inner
942            .consume_contextual_subscription(
943                v1::ConsumeContextualSubscriptionRequest {
944                    request_id: request_id()?,
945                    selection: Some(consumer.selection()?),
946                    maximum_wait_nanos,
947                    progress_cursor: progress_cursor
948                        .map(|cursor| cursor.as_bytes().to_vec())
949                        .unwrap_or_default(),
950                },
951                metadata,
952            )
953            .await?;
954        raise_contextual_batch(response)
955    }
956
957    /// Acknowledges one exact contextual lease after fresh authorization.
958    pub async fn acknowledge_contextual_item(
959        &mut self,
960        consumer: &ApplicationEventConsumer,
961        item: &ApplicationContextualWorkItem,
962        metadata: &CallMetadata,
963    ) -> Result<ApplicationEventMutationResult, ApplicationClientError> {
964        self.acknowledge_contextual_lease(consumer, &item.delivery.lease_evidence(), metadata)
965            .await
966    }
967
968    /// Acknowledges exact contextual lease evidence after fresh authorization.
969    pub async fn acknowledge_contextual_lease(
970        &mut self,
971        consumer: &ApplicationEventConsumer,
972        evidence: &ApplicationEventLeaseEvidence,
973        metadata: &CallMetadata,
974    ) -> Result<ApplicationEventMutationResult, ApplicationClientError> {
975        let response = self
976            .inner
977            .acknowledge_contextual_subscription(
978                v1::AcknowledgeContextualSubscriptionRequest {
979                    request_id: request_id()?,
980                    selection: Some(consumer.selection()?),
981                    event_id: Some(lower_event_id(evidence.event_id)),
982                    lease_token: evidence.lease_token.clone(),
983                    history_incarnation: evidence.history_incarnation,
984                },
985                metadata,
986            )
987            .await?;
988        raise_mutation_result(response.result)
989    }
990
991    /// Negatively acknowledges one exact contextual lease with bounded delay.
992    pub async fn negative_acknowledge_contextual_item(
993        &mut self,
994        consumer: &ApplicationEventConsumer,
995        item: &ApplicationContextualWorkItem,
996        retry_delay_nanos: u64,
997        metadata: &CallMetadata,
998    ) -> Result<ApplicationEventMutationResult, ApplicationClientError> {
999        self.negative_acknowledge_contextual_lease(
1000            consumer,
1001            &item.delivery.lease_evidence(),
1002            retry_delay_nanos,
1003            metadata,
1004        )
1005        .await
1006    }
1007
1008    /// Releases exact contextual lease evidence with a bounded retry delay.
1009    pub async fn negative_acknowledge_contextual_lease(
1010        &mut self,
1011        consumer: &ApplicationEventConsumer,
1012        evidence: &ApplicationEventLeaseEvidence,
1013        retry_delay_nanos: u64,
1014        metadata: &CallMetadata,
1015    ) -> Result<ApplicationEventMutationResult, ApplicationClientError> {
1016        if retry_delay_nanos > 3_600_000_000_000 {
1017            return Err(ApplicationClientError::InvalidInput);
1018        }
1019        let response = self
1020            .inner
1021            .negative_acknowledge_contextual_subscription(
1022                v1::NegativeAcknowledgeContextualSubscriptionRequest {
1023                    request_id: request_id()?,
1024                    selection: Some(consumer.selection()?),
1025                    event_id: Some(lower_event_id(evidence.event_id)),
1026                    lease_token: evidence.lease_token.clone(),
1027                    history_incarnation: evidence.history_incarnation,
1028                    retry_delay_nanos,
1029                },
1030                metadata,
1031            )
1032            .await?;
1033        raise_mutation_result(response.result)
1034    }
1035
1036    /// Reads one contextual consumer status under contextual consume authority.
1037    pub async fn contextual_subscription_status(
1038        &mut self,
1039        consumer: &ApplicationEventConsumer,
1040        metadata: &CallMetadata,
1041    ) -> Result<Option<ApplicationEventConsumerPublicStatus>, ApplicationClientError> {
1042        let response = self
1043            .inner
1044            .get_contextual_subscription_status(
1045                v1::GetContextualSubscriptionStatusRequest {
1046                    request_id: request_id()?,
1047                    selection: Some(consumer.selection()?),
1048                },
1049                metadata,
1050            )
1051            .await?;
1052        match response.result {
1053            Some(v1::get_event_stream_consumer_status_response::Result::NotFound(_)) => Ok(None),
1054            Some(v1::get_event_stream_consumer_status_response::Result::Found(status)) => {
1055                raise_consumer_status(status)
1056                    .map(ApplicationEventConsumerPublicStatus::Exact)
1057                    .map(Some)
1058            }
1059            Some(v1::get_event_stream_consumer_status_response::Result::Protected(status)) => {
1060                raise_protected_consumer_status(status).map(Some)
1061            }
1062            None => Err(ApplicationClientError::InvalidResponse),
1063        }
1064    }
1065
1066    /// Executes one declared reaction through the ordinary typed command envelope.
1067    pub async fn execute_contextual_reaction(
1068        &mut self,
1069        consumer: &ApplicationEventConsumer,
1070        reaction: &ApplicationContextualReaction,
1071        command: ApplicationCommand,
1072        metadata: &CallMetadata,
1073    ) -> Result<ApplicationCommandResult, ApplicationClientError> {
1074        let request_id =
1075            generate_request_id().map_err(|_| ApplicationClientError::IdentifierUnavailable)?;
1076        let command = command.into_idempotent_command()?;
1077        if command.command_name() != reaction.command_name {
1078            return Err(ApplicationClientError::InvalidInput);
1079        }
1080        let response = self
1081            .inner
1082            .execute_contextual_reaction(
1083                v1::ExecuteContextualReactionRequest {
1084                    request_id: request_id.into_bytes().to_vec(),
1085                    selection: Some(consumer.selection()?),
1086                    causation_token: reaction.causation_token.clone(),
1087                    reaction_name: reaction.name.clone(),
1088                    command: Some(command.request(request_id)),
1089                },
1090                metadata,
1091            )
1092            .await?;
1093        raise_application_command_result(response)
1094    }
1095
1096    /// Opens the checked server stream for the same consumer operation.
1097    pub async fn stream_event_consumer(
1098        &mut self,
1099        consumer: &ApplicationEventConsumer,
1100        options: EventConsumerOptions,
1101        metadata: &CallMetadata,
1102    ) -> Result<ApplicationEventResponseStream, ApplicationClientError> {
1103        let inner = self
1104            .inner
1105            .stream_event_consumer(consumer_request(consumer, options, None)?, metadata)
1106            .await?;
1107        Ok(ApplicationEventResponseStream { inner })
1108    }
1109
1110    /// Acknowledges one exact live lease under the same reauthorized identity.
1111    pub async fn acknowledge_event(
1112        &mut self,
1113        consumer: &ApplicationEventConsumer,
1114        delivery: &ApplicationEventDelivery,
1115        metadata: &CallMetadata,
1116    ) -> Result<ApplicationEventMutationResult, ApplicationClientError> {
1117        self.acknowledge_event_lease(consumer, &delivery.lease_evidence(), metadata)
1118            .await
1119    }
1120
1121    /// Acknowledges one exact checked lease identity.
1122    pub async fn acknowledge_event_lease(
1123        &mut self,
1124        consumer: &ApplicationEventConsumer,
1125        evidence: &ApplicationEventLeaseEvidence,
1126        metadata: &CallMetadata,
1127    ) -> Result<ApplicationEventMutationResult, ApplicationClientError> {
1128        let response = self
1129            .inner
1130            .acknowledge_event_stream(
1131                v1::AcknowledgeEventStreamRequest {
1132                    request_id: request_id()?,
1133                    selection: Some(consumer.selection()?),
1134                    event_id: Some(lower_event_id(evidence.event_id)),
1135                    lease_token: evidence.lease_token.clone(),
1136                    history_incarnation: evidence.history_incarnation,
1137                },
1138                metadata,
1139            )
1140            .await?;
1141        raise_mutation_result(response.result)
1142    }
1143
1144    /// Negatively acknowledges one exact live lease with a bounded delay.
1145    pub async fn negative_acknowledge_event(
1146        &mut self,
1147        consumer: &ApplicationEventConsumer,
1148        delivery: &ApplicationEventDelivery,
1149        retry_delay_nanos: u64,
1150        metadata: &CallMetadata,
1151    ) -> Result<ApplicationEventMutationResult, ApplicationClientError> {
1152        self.negative_acknowledge_event_lease(
1153            consumer,
1154            &delivery.lease_evidence(),
1155            retry_delay_nanos,
1156            metadata,
1157        )
1158        .await
1159    }
1160
1161    /// Negatively acknowledges one exact checked lease identity.
1162    pub async fn negative_acknowledge_event_lease(
1163        &mut self,
1164        consumer: &ApplicationEventConsumer,
1165        evidence: &ApplicationEventLeaseEvidence,
1166        retry_delay_nanos: u64,
1167        metadata: &CallMetadata,
1168    ) -> Result<ApplicationEventMutationResult, ApplicationClientError> {
1169        if retry_delay_nanos > 3_600_000_000_000 {
1170            return Err(ApplicationClientError::InvalidInput);
1171        }
1172        let response = self
1173            .inner
1174            .negative_acknowledge_event_stream(
1175                v1::NegativeAcknowledgeEventStreamRequest {
1176                    request_id: request_id()?,
1177                    selection: Some(consumer.selection()?),
1178                    event_id: Some(lower_event_id(evidence.event_id)),
1179                    lease_token: evidence.lease_token.clone(),
1180                    history_incarnation: evidence.history_incarnation,
1181                    retry_delay_nanos,
1182                },
1183                metadata,
1184            )
1185            .await?;
1186        raise_mutation_result(response.result)
1187    }
1188
1189    /// Moves one exact consumer checkpoint after server-side membership proof.
1190    pub async fn seek_event_consumer(
1191        &mut self,
1192        consumer: &ApplicationEventConsumer,
1193        checkpoint: ApplicationEventCheckpoint,
1194        metadata: &CallMetadata,
1195    ) -> Result<ApplicationEventMutationResult, ApplicationClientError> {
1196        let position = match checkpoint {
1197            ApplicationEventCheckpoint::BeforeFirst => {
1198                v1::event_consumer_checkpoint::Position::BeforeFirst(v1::Unit {})
1199            }
1200            ApplicationEventCheckpoint::After(event_id) if event_id.commit_sequence != 0 => {
1201                v1::event_consumer_checkpoint::Position::AfterEventId(lower_event_id(event_id))
1202            }
1203            ApplicationEventCheckpoint::After(_) => {
1204                return Err(ApplicationClientError::InvalidInput);
1205            }
1206        };
1207        let response = self
1208            .inner
1209            .seek_event_stream_consumer(
1210                v1::SeekEventStreamConsumerRequest {
1211                    request_id: request_id()?,
1212                    selection: Some(consumer.selection()?),
1213                    checkpoint: Some(v1::EventConsumerCheckpoint {
1214                        position: Some(position),
1215                    }),
1216                    progress_cursor: Vec::new(),
1217                },
1218                metadata,
1219            )
1220            .await?;
1221        raise_mutation_result(response.result)
1222    }
1223
1224    /// Moves one protected consumer to an opaque position previously issued
1225    /// under the same current principal, role, capability, and stream identity.
1226    pub async fn seek_protected_event_consumer(
1227        &mut self,
1228        consumer: &ApplicationEventConsumer,
1229        cursor: ApplicationEventProgressCursor,
1230        metadata: &CallMetadata,
1231    ) -> Result<ApplicationEventMutationResult, ApplicationClientError> {
1232        let response = self
1233            .inner
1234            .seek_event_stream_consumer(
1235                v1::SeekEventStreamConsumerRequest {
1236                    request_id: request_id()?,
1237                    selection: Some(consumer.selection()?),
1238                    checkpoint: None,
1239                    progress_cursor: cursor.as_bytes().to_vec(),
1240                },
1241                metadata,
1242            )
1243            .await?;
1244        raise_mutation_result(response.result)
1245    }
1246
1247    /// Reads bounded status under consume authority.
1248    pub async fn event_consumer_status(
1249        &mut self,
1250        consumer: &ApplicationEventConsumer,
1251        metadata: &CallMetadata,
1252    ) -> Result<Option<ApplicationEventConsumerPublicStatus>, ApplicationClientError> {
1253        let response = self
1254            .inner
1255            .get_event_stream_consumer_status(
1256                v1::GetEventStreamConsumerStatusRequest {
1257                    request_id: request_id()?,
1258                    selection: Some(consumer.selection()?),
1259                },
1260                metadata,
1261            )
1262            .await?;
1263        match response.result {
1264            Some(v1::get_event_stream_consumer_status_response::Result::NotFound(_)) => Ok(None),
1265            Some(v1::get_event_stream_consumer_status_response::Result::Found(status)) => {
1266                raise_consumer_status(status)
1267                    .map(ApplicationEventConsumerPublicStatus::Exact)
1268                    .map(Some)
1269            }
1270            Some(v1::get_event_stream_consumer_status_response::Result::Protected(status)) => {
1271                raise_protected_consumer_status(status).map(Some)
1272            }
1273            None => Err(ApplicationClientError::InvalidResponse),
1274        }
1275    }
1276
1277    /// Opens one exact generated named-query watch with optional cursor evidence.
1278    pub async fn watch_named_query(
1279        &mut self,
1280        operation: &ApplicationReactiveOperation,
1281        cursor: Option<LiveQueryCursor>,
1282        metadata: &CallMetadata,
1283    ) -> Result<ApplicationLiveQueryStream, ApplicationClientError> {
1284        let inner = self
1285            .inner
1286            .watch_named_query(
1287                v1::WatchNamedQueryRequest {
1288                    request_id: request_id()?,
1289                    reactive_module_hash: operation.module_hash.to_vec(),
1290                    operation_name: operation.operation_name.clone(),
1291                    parameters: operation.live_parameters()?,
1292                    cursor: cursor.map(|value| value.0),
1293                },
1294                metadata,
1295            )
1296            .await?;
1297        Ok(ApplicationLiveQueryStream {
1298            inner,
1299            terminal: false,
1300        })
1301    }
1302}
1303
1304fn consumer_request(
1305    consumer: &ApplicationEventConsumer,
1306    options: EventConsumerOptions,
1307    progress_cursor: Option<ApplicationEventProgressCursor>,
1308) -> Result<v1::ConsumeEventStreamRequest, ApplicationClientError> {
1309    let options = options.validate()?;
1310    Ok(v1::ConsumeEventStreamRequest {
1311        request_id: request_id()?,
1312        selection: Some(consumer.selection()?),
1313        batch_limit: options.batch_limit,
1314        in_flight_limit: options.in_flight_limit,
1315        lease_seconds: options.lease_seconds,
1316        maximum_wait_nanos: options.maximum_wait_nanos,
1317        progress_cursor: progress_cursor
1318            .map(|cursor| cursor.as_bytes().to_vec())
1319            .unwrap_or_default(),
1320    })
1321}
1322
1323fn request_id() -> Result<Vec<u8>, ApplicationClientError> {
1324    generate_request_id()
1325        .map(|value| value.into_bytes().to_vec())
1326        .map_err(|_| ApplicationClientError::IdentifierUnavailable)
1327}
1328
1329const fn lower_event_id(value: ApplicationEventId) -> v1::EventId {
1330    v1::EventId {
1331        commit_sequence: value.commit_sequence,
1332        event_ordinal: value.event_ordinal,
1333    }
1334}
1335
1336fn raise_event_id(
1337    value: Option<v1::EventId>,
1338) -> Result<ApplicationEventId, ApplicationClientError> {
1339    let value = value.ok_or(ApplicationClientError::InvalidResponse)?;
1340    if value.commit_sequence == 0 {
1341        return Err(ApplicationClientError::InvalidResponse);
1342    }
1343    Ok(ApplicationEventId {
1344        commit_sequence: value.commit_sequence,
1345        event_ordinal: value.event_ordinal,
1346    })
1347}
1348
1349fn raise_event_batch(
1350    response: v1::ConsumeEventStreamResponse,
1351) -> Result<ApplicationEventBatch, ApplicationClientError> {
1352    let events = response
1353        .events
1354        .into_iter()
1355        .map(raise_event_delivery)
1356        .collect::<Result<Vec<_>, _>>()?;
1357    let status = raise_consumer_public_status(response.status, response.protected_status)?;
1358    let disposition = raise_consumer_disposition(response.disposition, response.wait_timed_out)?;
1359    if disposition != ApplicationEventPullDisposition::Ready && !events.is_empty() {
1360        return Err(ApplicationClientError::InvalidResponse);
1361    }
1362    Ok(ApplicationEventBatch {
1363        events,
1364        status,
1365        disposition,
1366        wait_timed_out: response.wait_timed_out,
1367    })
1368}
1369
1370fn raise_contextual_batch(
1371    response: v1::ConsumeContextualSubscriptionResponse,
1372) -> Result<ApplicationContextualBatch, ApplicationClientError> {
1373    let items = response
1374        .items
1375        .into_iter()
1376        .map(raise_contextual_work_item)
1377        .collect::<Result<Vec<_>, _>>()?;
1378    let status = raise_consumer_public_status(response.status, response.protected_status)?;
1379    let disposition = raise_consumer_disposition(response.disposition, response.wait_timed_out)?;
1380    if disposition != ApplicationEventPullDisposition::Ready && !items.is_empty() {
1381        return Err(ApplicationClientError::InvalidResponse);
1382    }
1383    Ok(ApplicationContextualBatch {
1384        items,
1385        status,
1386        disposition,
1387        wait_timed_out: response.wait_timed_out,
1388    })
1389}
1390
1391fn raise_contextual_work_item(
1392    item: v1::ContextualWorkItem,
1393) -> Result<ApplicationContextualWorkItem, ApplicationClientError> {
1394    let delivery = raise_event_delivery(
1395        item.delivery
1396            .ok_or(ApplicationClientError::InvalidResponse)?,
1397    )?;
1398    let hydrations = item
1399        .hydrations
1400        .into_iter()
1401        .map(raise_contextual_hydration)
1402        .collect::<Result<Vec<_>, _>>()?;
1403    let available_reactions = item
1404        .available_reactions
1405        .into_iter()
1406        .map(|reaction| {
1407            if reaction.name.is_empty()
1408                || reaction.command_name.is_empty()
1409                || reaction.command_id == 0
1410                || reaction.causation_token.len() <= 32
1411                || reaction.causation_token.len() > 1_024
1412            {
1413                return Err(ApplicationClientError::InvalidResponse);
1414            }
1415            Ok(ApplicationContextualReaction {
1416                name: reaction.name,
1417                command_name: reaction.command_name,
1418                command_id: reaction.command_id,
1419                causation_token: reaction.causation_token,
1420            })
1421        })
1422        .collect::<Result<Vec<_>, _>>()?;
1423    if item.context_head == 0 {
1424        return Err(ApplicationClientError::InvalidResponse);
1425    }
1426    Ok(ApplicationContextualWorkItem {
1427        delivery,
1428        context_head: item.context_head,
1429        hydrations,
1430        available_reactions,
1431    })
1432}
1433
1434fn raise_contextual_hydration(
1435    hydration: v1::ContextualHydration,
1436) -> Result<ApplicationContextualHydration, ApplicationClientError> {
1437    let mut fields = BTreeMap::new();
1438    for field in hydration.fields {
1439        let cardinality = match v1::ContextualQueryCardinality::try_from(field.cardinality).ok() {
1440            Some(v1::ContextualQueryCardinality::One) => ApplicationCardinality::One,
1441            Some(v1::ContextualQueryCardinality::Maybe) => ApplicationCardinality::Maybe,
1442            Some(v1::ContextualQueryCardinality::Many) => ApplicationCardinality::Many,
1443            Some(v1::ContextualQueryCardinality::Unspecified) | None => {
1444                return Err(ApplicationClientError::InvalidResponse);
1445            }
1446        };
1447        let records = field
1448            .rows
1449            .into_iter()
1450            .map(|row| {
1451                let mut values = BTreeMap::new();
1452                for value in row.fields {
1453                    if value.name.is_empty()
1454                        || values
1455                            .insert(
1456                                value.name,
1457                                raise_value(
1458                                    value.value.ok_or(ApplicationClientError::InvalidResponse)?,
1459                                )?,
1460                            )
1461                            .is_some()
1462                    {
1463                        return Err(ApplicationClientError::InvalidResponse);
1464                    }
1465                }
1466                Ok(ApplicationRecord {
1467                    entity: row.entity,
1468                    fields: values,
1469                })
1470            })
1471            .collect::<Result<Vec<_>, _>>()?;
1472        if field.name.is_empty()
1473            || fields
1474                .insert(
1475                    field.name,
1476                    ApplicationResultField {
1477                        cardinality,
1478                        records,
1479                    },
1480                )
1481                .is_some()
1482        {
1483            return Err(ApplicationClientError::InvalidResponse);
1484        }
1485    }
1486    if hydration.name.is_empty() || hydration.outcome.is_empty() {
1487        return Err(ApplicationClientError::InvalidResponse);
1488    }
1489    Ok(ApplicationContextualHydration {
1490        name: hydration.name,
1491        outcome: hydration.outcome,
1492        fields,
1493    })
1494}
1495
1496fn raise_event_delivery(
1497    delivery: v1::ConsumedEvent,
1498) -> Result<ApplicationEventDelivery, ApplicationClientError> {
1499    let event = delivery
1500        .event
1501        .ok_or(ApplicationClientError::InvalidResponse)?;
1502    let id = raise_event_id(event.event_id)?;
1503    let expires = delivery
1504        .expires_at
1505        .ok_or(ApplicationClientError::InvalidResponse)?;
1506    let mut fields = BTreeMap::new();
1507    for field in event.fields {
1508        if field.name.is_empty()
1509            || fields
1510                .insert(
1511                    field.name,
1512                    raise_value(field.value.ok_or(ApplicationClientError::InvalidResponse)?)?,
1513                )
1514                .is_some()
1515        {
1516            return Err(ApplicationClientError::InvalidResponse);
1517        }
1518    }
1519    if event.event_name.is_empty()
1520        || event.command_name.is_empty()
1521        || event.provenance_uri.is_empty()
1522        || event.history_incarnation == 0
1523        || delivery.attempt == 0
1524        || delivery.lease_token.is_empty()
1525        || expires.nanos >= 1_000_000_000
1526    {
1527        return Err(ApplicationClientError::InvalidResponse);
1528    }
1529    let actor_kind = v1::ActorKind::try_from(event.actor_kind)
1530        .ok()
1531        .filter(|kind| *kind != v1::ActorKind::Unspecified)
1532        .map(|kind| kind.as_str_name().to_owned())
1533        .ok_or(ApplicationClientError::InvalidResponse)?;
1534    Ok(ApplicationEventDelivery {
1535        event: ApplicationEvent {
1536            id,
1537            name: event.event_name,
1538            writer_contract_version: event.writer_contract_version,
1539            command_name: event.command_name,
1540            actor_kind,
1541            provenance_uri: event.provenance_uri,
1542            history_incarnation: event.history_incarnation,
1543            fields,
1544        },
1545        attempt: delivery.attempt,
1546        lease_token: delivery.lease_token,
1547        expires_at: (expires.seconds, expires.nanos),
1548    })
1549}
1550
1551fn raise_consumer_status(
1552    status: v1::EventConsumerStatus,
1553) -> Result<ApplicationEventConsumerStatus, ApplicationClientError> {
1554    let checkpoint = match status
1555        .checkpoint
1556        .and_then(|value| value.position)
1557        .ok_or(ApplicationClientError::InvalidResponse)?
1558    {
1559        v1::event_consumer_checkpoint::Position::BeforeFirst(_) => {
1560            ApplicationEventCheckpoint::BeforeFirst
1561        }
1562        v1::event_consumer_checkpoint::Position::AfterEventId(value) => {
1563            ApplicationEventCheckpoint::After(raise_event_id(Some(value))?)
1564        }
1565    };
1566    if status.revision == 0 || status.history_incarnation == 0 {
1567        return Err(ApplicationClientError::InvalidResponse);
1568    }
1569    Ok(ApplicationEventConsumerStatus {
1570        revision: status.revision,
1571        checkpoint,
1572        history_incarnation: status.history_incarnation,
1573        live_leases: status.live_leases,
1574        retries: status.retries,
1575        dead_letters: status.dead_letters,
1576    })
1577}
1578
1579fn raise_consumer_public_status(
1580    exact: Option<v1::EventConsumerStatus>,
1581    protected: Option<v1::ProtectedEventConsumerStatus>,
1582) -> Result<ApplicationEventConsumerPublicStatus, ApplicationClientError> {
1583    match (exact, protected) {
1584        (Some(status), None) => {
1585            raise_consumer_status(status).map(ApplicationEventConsumerPublicStatus::Exact)
1586        }
1587        (None, Some(status)) => raise_protected_consumer_status(status),
1588        _ => Err(ApplicationClientError::InvalidResponse),
1589    }
1590}
1591
1592fn raise_protected_consumer_status(
1593    status: v1::ProtectedEventConsumerStatus,
1594) -> Result<ApplicationEventConsumerPublicStatus, ApplicationClientError> {
1595    if status.history_incarnation == 0 {
1596        return Err(ApplicationClientError::InvalidResponse);
1597    }
1598    Ok(ApplicationEventConsumerPublicStatus::Protected(
1599        ApplicationProtectedEventConsumerStatus {
1600            history_incarnation: status.history_incarnation,
1601            progress_cursor: ApplicationEventProgressCursor::new(status.progress_cursor)
1602                .map_err(|_| ApplicationClientError::InvalidResponse)?,
1603        },
1604    ))
1605}
1606
1607fn raise_consumer_disposition(
1608    disposition: i32,
1609    wait_timed_out: bool,
1610) -> Result<ApplicationEventPullDisposition, ApplicationClientError> {
1611    let disposition = match v1::EventConsumerPullDisposition::try_from(disposition).ok() {
1612        Some(v1::EventConsumerPullDisposition::Ready) => ApplicationEventPullDisposition::Ready,
1613        Some(v1::EventConsumerPullDisposition::WaitTimedOut) => {
1614            ApplicationEventPullDisposition::WaitTimedOut
1615        }
1616        Some(v1::EventConsumerPullDisposition::BoundedProgress) => {
1617            ApplicationEventPullDisposition::BoundedProgress
1618        }
1619        _ => return Err(ApplicationClientError::InvalidResponse),
1620    };
1621    if wait_timed_out != (disposition == ApplicationEventPullDisposition::WaitTimedOut) {
1622        return Err(ApplicationClientError::InvalidResponse);
1623    }
1624    Ok(disposition)
1625}
1626
1627fn raise_mutation_result(
1628    value: i32,
1629) -> Result<ApplicationEventMutationResult, ApplicationClientError> {
1630    match v1::EventConsumerMutationResult::try_from(value).ok() {
1631        Some(v1::EventConsumerMutationResult::Applied) => {
1632            Ok(ApplicationEventMutationResult::Applied)
1633        }
1634        Some(v1::EventConsumerMutationResult::StateChanged) => {
1635            Ok(ApplicationEventMutationResult::StateChanged)
1636        }
1637        Some(v1::EventConsumerMutationResult::NotFound) => {
1638            Ok(ApplicationEventMutationResult::NotFound)
1639        }
1640        Some(v1::EventConsumerMutationResult::OutstandingLease) => {
1641            Ok(ApplicationEventMutationResult::OutstandingLease)
1642        }
1643        Some(v1::EventConsumerMutationResult::StaleLease) => {
1644            Ok(ApplicationEventMutationResult::StaleLease)
1645        }
1646        Some(v1::EventConsumerMutationResult::LeaseExpired) => {
1647            Ok(ApplicationEventMutationResult::LeaseExpired)
1648        }
1649        Some(v1::EventConsumerMutationResult::Unspecified) | None => {
1650            Err(ApplicationClientError::InvalidResponse)
1651        }
1652    }
1653}
1654
1655fn raise_live_update(
1656    update: v1::LiveQueryUpdate,
1657) -> Result<ApplicationLiveQueryUpdate, ApplicationClientError> {
1658    match update
1659        .update
1660        .ok_or(ApplicationClientError::InvalidResponse)?
1661    {
1662        v1::live_query_update::Update::Snapshot(value) => {
1663            let checkpoint = live_checkpoint(value.frontier, value.cursor)?;
1664            Ok(ApplicationLiveQueryUpdate::Snapshot {
1665                result: raise_live_result(value.result)?,
1666                cursor: checkpoint.cursor,
1667                history_incarnation: checkpoint.history_incarnation,
1668                application_head: checkpoint.application_head,
1669            })
1670        }
1671        v1::live_query_update::Update::Patch(value) => {
1672            let checkpoint = live_checkpoint(value.frontier, value.cursor)?;
1673            let operations = value
1674                .operations
1675                .into_iter()
1676                .map(raise_patch_operation)
1677                .collect::<Result<Vec<_>, _>>()?;
1678            Ok(ApplicationLiveQueryUpdate::Patch(LiveQueryPatch {
1679                result_field: value.result_field,
1680                operations,
1681                checkpoint,
1682            }))
1683        }
1684        v1::live_query_update::Update::Reset(value) => {
1685            let checkpoint = live_checkpoint(value.frontier, value.cursor)?;
1686            let reason = v1::LiveQueryResetReason::try_from(value.reason)
1687                .ok()
1688                .filter(|reason| *reason != v1::LiveQueryResetReason::Unspecified)
1689                .map(|reason| reason.as_str_name().to_owned())
1690                .ok_or(ApplicationClientError::InvalidResponse)?;
1691            Ok(ApplicationLiveQueryUpdate::Reset {
1692                reason,
1693                result: raise_live_result(value.result)?,
1694                cursor: checkpoint.cursor,
1695                history_incarnation: checkpoint.history_incarnation,
1696                application_head: checkpoint.application_head,
1697            })
1698        }
1699        v1::live_query_update::Update::Checkpoint(value) => Ok(
1700            ApplicationLiveQueryUpdate::Checkpoint(live_checkpoint(value.frontier, value.cursor)?),
1701        ),
1702        v1::live_query_update::Update::Terminal(value) => {
1703            let reason = v1::LiveQueryTerminalReason::try_from(value.reason)
1704                .ok()
1705                .filter(|reason| *reason != v1::LiveQueryTerminalReason::Unspecified)
1706                .map(|reason| reason.as_str_name().to_owned())
1707                .ok_or(ApplicationClientError::InvalidResponse)?;
1708            let last_frontier = value
1709                .last_frontier
1710                .map(|frontier| (frontier.history_incarnation, frontier.application_head));
1711            Ok(ApplicationLiveQueryUpdate::Terminal(LiveQueryTerminal {
1712                reason,
1713                last_frontier,
1714            }))
1715        }
1716    }
1717}
1718
1719fn live_checkpoint(
1720    frontier: Option<v1::LiveQueryFrontier>,
1721    cursor: Vec<u8>,
1722) -> Result<LiveQueryCheckpoint, ApplicationClientError> {
1723    let frontier = frontier.ok_or(ApplicationClientError::InvalidResponse)?;
1724    if frontier.history_incarnation == 0 {
1725        return Err(ApplicationClientError::InvalidResponse);
1726    }
1727    Ok(LiveQueryCheckpoint {
1728        history_incarnation: frontier.history_incarnation,
1729        application_head: frontier.application_head,
1730        cursor: LiveQueryCursor::new(cursor)
1731            .map_err(|_| ApplicationClientError::InvalidResponse)?,
1732    })
1733}
1734
1735fn raise_live_result(
1736    result: Option<v1::LiveQueryResult>,
1737) -> Result<NamedQueryResult, ApplicationClientError> {
1738    let result = result.ok_or(ApplicationClientError::InvalidResponse)?;
1739    let identity = result
1740        .identity
1741        .ok_or(ApplicationClientError::InvalidResponse)?;
1742    if result.outcome.is_empty() {
1743        return Err(ApplicationClientError::InvalidResponse);
1744    }
1745    let mut fields = BTreeMap::new();
1746    for field in result.fields {
1747        let cardinality = match v1::LiveQueryResultCardinality::try_from(field.cardinality).ok() {
1748            Some(v1::LiveQueryResultCardinality::One) => ApplicationCardinality::One,
1749            Some(v1::LiveQueryResultCardinality::Maybe) => ApplicationCardinality::Maybe,
1750            Some(v1::LiveQueryResultCardinality::Many) => ApplicationCardinality::Many,
1751            Some(v1::LiveQueryResultCardinality::Unspecified) | None => {
1752                return Err(ApplicationClientError::InvalidResponse);
1753            }
1754        };
1755        let records = field
1756            .records
1757            .into_iter()
1758            .map(raise_live_record)
1759            .collect::<Result<Vec<_>, _>>()?;
1760        if field.name.is_empty()
1761            || fields
1762                .insert(
1763                    field.name,
1764                    ApplicationResultField {
1765                        cardinality,
1766                        records,
1767                    },
1768                )
1769                .is_some()
1770        {
1771            return Err(ApplicationClientError::InvalidResponse);
1772        }
1773    }
1774    let contract_bundle_hash = identity
1775        .contract_bundle_hash
1776        .try_into()
1777        .map_err(|_| ApplicationClientError::InvalidResponse)?;
1778    let module_hash = identity
1779        .query_module_hash
1780        .try_into()
1781        .map_err(|_| ApplicationClientError::InvalidResponse)?;
1782    let plan_hash = identity
1783        .query_plan_hash
1784        .try_into()
1785        .map_err(|_| ApplicationClientError::InvalidResponse)?;
1786    Ok(NamedQueryResult {
1787        outcome: result.outcome,
1788        application_head: 0,
1789        fields,
1790        next_cursor: None,
1791        identity: QueryResponseIdentity {
1792            contract_lineage: identity.contract_lineage,
1793            contract_version: identity.contract_version,
1794            contract_bundle_hash,
1795            module_hash,
1796            query_name: identity.query_name,
1797            plan_hash,
1798        },
1799    })
1800}
1801
1802fn raise_live_record(
1803    record: v1::LiveQueryResultRecord,
1804) -> Result<ApplicationRecord, ApplicationClientError> {
1805    Ok(ApplicationRecord {
1806        entity: record.entity,
1807        fields: raise_value_record(record.fields)?,
1808    })
1809}
1810
1811fn raise_value_record(
1812    record: Option<v1::ValueRecord>,
1813) -> Result<BTreeMap<String, ApplicationValue>, ApplicationClientError> {
1814    let mut output = BTreeMap::new();
1815    for field in record
1816        .ok_or(ApplicationClientError::InvalidResponse)?
1817        .fields
1818    {
1819        if field.name.is_empty()
1820            || output
1821                .insert(
1822                    field.name,
1823                    raise_value(field.value.ok_or(ApplicationClientError::InvalidResponse)?)?,
1824                )
1825                .is_some()
1826        {
1827            return Err(ApplicationClientError::InvalidResponse);
1828        }
1829    }
1830    Ok(output)
1831}
1832
1833fn raise_patch_operation(
1834    operation: v1::LiveQueryPatchOperation,
1835) -> Result<LiveQueryPatchOperation, ApplicationClientError> {
1836    match operation
1837        .operation
1838        .ok_or(ApplicationClientError::InvalidResponse)?
1839    {
1840        v1::live_query_patch_operation::Operation::Insert(value) => {
1841            Ok(LiveQueryPatchOperation::Insert {
1842                index: value.index,
1843                record: raise_live_record(
1844                    value
1845                        .record
1846                        .ok_or(ApplicationClientError::InvalidResponse)?,
1847                )?,
1848            })
1849        }
1850        v1::live_query_patch_operation::Operation::Remove(value) => {
1851            Ok(LiveQueryPatchOperation::Remove {
1852                index: value.index,
1853                key: raise_value_record(value.key)?,
1854            })
1855        }
1856        v1::live_query_patch_operation::Operation::Replace(value) => {
1857            Ok(LiveQueryPatchOperation::Replace {
1858                index: value.index,
1859                record: raise_live_record(
1860                    value
1861                        .record
1862                        .ok_or(ApplicationClientError::InvalidResponse)?,
1863                )?,
1864            })
1865        }
1866        v1::live_query_patch_operation::Operation::Move(value) => {
1867            Ok(LiveQueryPatchOperation::Move {
1868                from: value.from,
1869                to: value.to,
1870                key: raise_value_record(value.key)?,
1871            })
1872        }
1873    }
1874}
1875
1876#[cfg(test)]
1877mod tests {
1878    use super::*;
1879
1880    #[test]
1881    fn consumer_names_and_limits_fail_closed() {
1882        let operation = ApplicationReactiveOperation::new([1; 32], "Events", BTreeMap::new())
1883            .expect("operation");
1884        assert!(ApplicationEventConsumer::new(operation.clone(), "worker_1").is_ok());
1885        assert!(ApplicationEventConsumer::new(operation, "not valid").is_err());
1886        assert!(
1887            EventConsumerOptions {
1888                batch_limit: 65,
1889                ..EventConsumerOptions::default()
1890            }
1891            .validate()
1892            .is_err()
1893        );
1894    }
1895
1896    #[test]
1897    fn cursor_is_opaque_and_bounded() {
1898        assert!(LiveQueryCursor::new(Vec::new()).is_err());
1899        assert!(LiveQueryCursor::new(vec![7; 16_385]).is_err());
1900        assert_eq!(
1901            LiveQueryCursor::new(vec![7; 32])
1902                .expect("cursor")
1903                .as_bytes(),
1904            &[7; 32]
1905        );
1906        let cursor = LiveQueryCursor::new(vec![1, 2, 3, 4]).expect("cursor");
1907        assert_eq!(cursor.to_standard_base64(), "AQIDBA==");
1908        assert_eq!(
1909            LiveQueryCursor::from_standard_base64("AQIDBA==")
1910                .expect("canonical Base64")
1911                .as_bytes(),
1912            &[1, 2, 3, 4]
1913        );
1914        for invalid in ["AQIDBA", "AQIDBA=", "AQIDBA__", "AZ==", "A==="] {
1915            assert!(LiveQueryCursor::from_standard_base64(invalid).is_err());
1916        }
1917    }
1918
1919    #[test]
1920    fn selected_event_fields_reject_duplicates() {
1921        let event = v1::ConsumedEvent {
1922            event: Some(v1::SymbolicEvent {
1923                event_id: Some(v1::EventId {
1924                    commit_sequence: 1,
1925                    event_ordinal: 0,
1926                }),
1927                event_name: "Changed".to_owned(),
1928                writer_contract_version: 1,
1929                command_name: "Change".to_owned(),
1930                actor_kind: v1::ActorKind::Service as i32,
1931                provenance_uri: "riffdb://provenance/x".to_owned(),
1932                history_incarnation: 1,
1933                fields: vec![
1934                    v1::SymbolicEventField {
1935                        name: "value".to_owned(),
1936                        value: Some(v1::Value {
1937                            kind: Some(v1::value::Kind::I64Value(1)),
1938                        }),
1939                    },
1940                    v1::SymbolicEventField {
1941                        name: "value".to_owned(),
1942                        value: Some(v1::Value {
1943                            kind: Some(v1::value::Kind::I64Value(2)),
1944                        }),
1945                    },
1946                ],
1947                ..v1::SymbolicEvent::default()
1948            }),
1949            attempt: 1,
1950            lease_token: vec![1; 32],
1951            expires_at: Some(v1::Timestamp {
1952                seconds: 1,
1953                nanos: 0,
1954            }),
1955        };
1956        assert!(matches!(
1957            raise_event_delivery(event),
1958            Err(ApplicationClientError::InvalidResponse)
1959        ));
1960    }
1961}