appcore_ui/protocol.rs
1// =============================================================================
2// #######
3// ### ### F: protocol.rs
4// ## ## ## ## P: AppCore-Runtime
5// ## ##
6// C: 2026/10/13 00:00:00 by dnettoRaw
7// ## ## ## ## U: 2026/10/13 00:00:00 by dnettoRaw
8// ########### S: 0.1.0-beta.1
9// =============================================================================
10
11//! UI wire V1 composes existing command/query DTOs without changing their contracts.
12use crate::{CommandRequest, QueryRequest};
13use serde::{Deserialize, Serialize};
14use std::fmt;
15/// Explicit version of this UI wire family, independent of Peer RPC.
16pub const UI_PROTOCOL_V1: u16 = 1;
17/// Transport-link state, distinct from application or peer health.
18#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
19#[serde(rename_all = "snake_case")]
20pub enum UiConnectionState {
21 /// The bridge can serve authorized operations.
22 Connected,
23 /// The selected transport is not reachable.
24 Offline,
25 /// The bridge is reachable but its owning infrastructure is degraded.
26 Degraded,
27 /// The surface has been closed and must be recreated explicitly.
28 Closed,
29}
30/// One versioned frame; host identity and credentials are never client fields.
31#[derive(Clone, Serialize, Deserialize)]
32#[serde(deny_unknown_fields)]
33pub struct UiRequestV1 {
34 /// Explicit UI wire version.
35 pub version: u16,
36 /// Surface identity bound to the native/transport caller by the adapter.
37 pub surface_id: String,
38 /// Unique in-flight correlation identity.
39 pub request_id: String,
40 /// Monotonic execution budget starting at host admission.
41 pub timeout_ms: u32,
42 /// Requested operation with application-owned opaque content.
43 pub action: UiActionV1,
44}
45/// Versioned actions; the page cannot publish events or grant itself capabilities.
46#[derive(Clone, Serialize, Deserialize)]
47#[serde(tag = "kind", rename_all = "snake_case", deny_unknown_fields)]
48pub enum UiActionV1 {
49 /// Dispatch the existing V1 side-effect-free query contract.
50 Query {
51 /// Existing Runtime query.
52 query: QueryRequest,
53 },
54 /// Dispatch the existing V1 command contract through its owning handler.
55 Command {
56 /// Existing Runtime command.
57 command: CommandRequest,
58 },
59 /// Subscribe to a declared capability's permitted event topic.
60 Subscribe {
61 /// Capability used to authorize the subscription.
62 capability: String,
63 /// Bounded event topic.
64 topic: String,
65 },
66 /// Pull a bounded event page, acknowledging removal from this UI queue.
67 Poll {
68 /// Subscription identity within this surface generation.
69 subscription_id: String,
70 },
71 /// Release one subscription and all its retained notifications.
72 Unsubscribe {
73 /// Subscription identity.
74 subscription_id: String,
75 },
76 /// Cooperatively cancel one operation; completed effects are not rolled back.
77 Cancel {
78 /// Original in-flight request identity.
79 target_request_id: String,
80 },
81 /// Close this surface, cancelling work and releasing subscription queues.
82 Close,
83}
84impl fmt::Debug for UiRequestV1 {
85 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
86 f.debug_struct("UiRequestV1")
87 .field("version", &self.version)
88 .field("surface_id", &self.surface_id)
89 .field("request_id", &self.request_id)
90 .field("timeout_ms", &self.timeout_ms)
91 .field("action", &self.action)
92 .finish()
93 }
94}
95impl fmt::Debug for UiActionV1 {
96 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
97 match self {
98 Self::Query { query } => f
99 .debug_struct("Query")
100 .field("name", &query.query_name)
101 .field("payload", &"REDACTED")
102 .finish(),
103 Self::Command { command } => f
104 .debug_struct("Command")
105 .field("name", &command.command_name)
106 .field("payload", &"REDACTED")
107 .field("idempotency_key", &"REDACTED")
108 .finish(),
109 Self::Subscribe { capability, topic } => f
110 .debug_struct("Subscribe")
111 .field("capability", capability)
112 .field("topic", topic)
113 .finish(),
114 Self::Poll { subscription_id } => f.debug_tuple("Poll").field(subscription_id).finish(),
115 Self::Unsubscribe { subscription_id } => {
116 f.debug_tuple("Unsubscribe").field(subscription_id).finish()
117 }
118 Self::Cancel { target_request_id } => {
119 f.debug_tuple("Cancel").field(target_request_id).finish()
120 }
121 Self::Close => f.write_str("Close"),
122 }
123 }
124}