Skip to main content

hara_native/live_session/
model.rs

1use serde_json::{json, Value as JsonValue};
2use std::fmt;
3
4pub const LIVE_SESSION_PROTOCOL: &str = "hara.live-session/0-alpha";
5pub const LIVE_SESSION_STATE_SCHEMA: &str = "hara.live-session.state/0-alpha";
6pub const LIVE_SESSION_REPLY_SCHEMA: &str = "hara.live-session.reply/0-alpha";
7pub const LIVE_SESSION_CAPABILITIES_SCHEMA: &str = "hara.live-session.capabilities/0-alpha";
8
9#[derive(Clone, Copy, Debug, PartialEq, Eq)]
10pub enum LiveBackend {
11    Interpreter,
12    Hbc,
13    WholeWasm,
14}
15
16impl LiveBackend {
17    pub const fn as_str(self) -> &'static str {
18        match self {
19            Self::Interpreter => "interpreter",
20            Self::Hbc => "hbc",
21            Self::WholeWasm => "whole-wasm",
22        }
23    }
24}
25
26#[derive(Clone, Copy, Debug, PartialEq, Eq)]
27pub enum LiveSessionStatus {
28    Ready,
29    Running,
30    Paused,
31    Suspended,
32    Returned,
33    Failed,
34    Cancelled,
35    Disposed,
36}
37
38impl LiveSessionStatus {
39    pub const fn as_str(self) -> &'static str {
40        match self {
41            Self::Ready => "ready",
42            Self::Running => "running",
43            Self::Paused => "paused",
44            Self::Suspended => "suspended",
45            Self::Returned => "returned",
46            Self::Failed => "failed",
47            Self::Cancelled => "cancelled",
48            Self::Disposed => "disposed",
49        }
50    }
51
52    pub const fn is_terminal(self) -> bool {
53        matches!(
54            self,
55            Self::Returned | Self::Failed | Self::Cancelled | Self::Disposed
56        )
57    }
58}
59
60#[derive(Clone, Copy, Debug, PartialEq, Eq)]
61pub enum LiveSessionOperation {
62    Snapshot,
63    Step,
64    Run,
65    Call,
66    Pause,
67    Resume,
68    Resolve,
69    Reject,
70    Update,
71    Reset,
72    Cancel,
73    Dispose,
74}
75
76impl LiveSessionOperation {
77    pub const fn as_str(self) -> &'static str {
78        match self {
79            Self::Snapshot => "snapshot",
80            Self::Step => "step",
81            Self::Run => "run",
82            Self::Call => "call",
83            Self::Pause => "pause",
84            Self::Resume => "resume",
85            Self::Resolve => "resolve",
86            Self::Reject => "reject",
87            Self::Update => "update",
88            Self::Reset => "reset",
89            Self::Cancel => "cancel",
90            Self::Dispose => "dispose",
91        }
92    }
93}
94
95#[derive(Clone, Copy, Debug, PartialEq, Eq)]
96pub enum LiveReplacementPolicy {
97    Restart,
98    ReplaceOnNextStart,
99    PreserveRuntime,
100}
101
102impl LiveReplacementPolicy {
103    pub const fn as_str(self) -> &'static str {
104        match self {
105            Self::Restart => "restart",
106            Self::ReplaceOnNextStart => "replace-on-next-start",
107            Self::PreserveRuntime => "preserve-runtime",
108        }
109    }
110}
111
112#[derive(Clone, Debug, PartialEq, Eq)]
113pub struct LiveSource {
114    source_id: String,
115    revision: String,
116    source: String,
117}
118
119impl LiveSource {
120    pub fn new(
121        source_id: impl Into<String>,
122        revision: impl Into<String>,
123        source: impl Into<String>,
124    ) -> Result<Self, LiveSessionError> {
125        Ok(Self {
126            source_id: required_text(source_id.into(), "source id")?,
127            revision: required_text(revision.into(), "revision")?,
128            source: required_text(source.into(), "source")?,
129        })
130    }
131
132    pub fn source_id(&self) -> &str {
133        &self.source_id
134    }
135
136    pub fn revision(&self) -> &str {
137        &self.revision
138    }
139
140    pub fn source(&self) -> &str {
141        &self.source
142    }
143}
144
145#[derive(Clone, Debug, PartialEq, Eq)]
146pub struct LiveSessionState {
147    pub session_id: String,
148    pub source_id: String,
149    pub generation: u64,
150    pub revision: String,
151    pub sequence: u64,
152    pub backend: LiveBackend,
153    pub status: LiveSessionStatus,
154}
155
156impl LiveSessionState {
157    pub fn to_json(&self) -> JsonValue {
158        json!({
159            "schema": LIVE_SESSION_STATE_SCHEMA,
160            "protocol": LIVE_SESSION_PROTOCOL,
161            "session-id": self.session_id,
162            "source-id": self.source_id,
163            "generation": self.generation,
164            "revision": self.revision,
165            "sequence": self.sequence,
166            "backend": self.backend.as_str(),
167            "status": self.status.as_str(),
168        })
169    }
170}
171
172#[derive(Clone, Debug, PartialEq, Eq)]
173pub struct LiveSessionCapabilities {
174    pub backend: LiveBackend,
175    pub operations: Vec<LiveSessionOperation>,
176    pub replacement_policies: Vec<LiveReplacementPolicy>,
177}
178
179impl LiveSessionCapabilities {
180    pub fn supports(&self, operation: LiveSessionOperation) -> bool {
181        self.operations.contains(&operation)
182    }
183
184    pub fn supports_replacement(&self, policy: LiveReplacementPolicy) -> bool {
185        self.replacement_policies.contains(&policy)
186    }
187
188    pub fn to_json(&self) -> JsonValue {
189        json!({
190            "schema": LIVE_SESSION_CAPABILITIES_SCHEMA,
191            "protocol": LIVE_SESSION_PROTOCOL,
192            "backend": self.backend.as_str(),
193            "operations": self.operations.iter().map(|operation| operation.as_str()).collect::<Vec<_>>(),
194            "replacement-policies": self.replacement_policies.iter().map(|policy| policy.as_str()).collect::<Vec<_>>(),
195        })
196    }
197}
198
199#[derive(Clone, Debug, PartialEq)]
200pub enum LiveSettlement {
201    Fulfilled(JsonValue),
202    Rejected(JsonValue),
203}
204
205#[derive(Clone, Debug, PartialEq)]
206pub enum LiveSessionCommand {
207    Snapshot,
208    Step,
209    Run {
210        boundary_limit: usize,
211    },
212    Call {
213        function: u16,
214        arguments: Vec<JsonValue>,
215    },
216    Pause,
217    Resume {
218        settlement: Option<LiveSettlement>,
219    },
220    Resolve {
221        value: JsonValue,
222    },
223    Reject {
224        error: JsonValue,
225    },
226    Update {
227        source: LiveSource,
228        policy: LiveReplacementPolicy,
229    },
230    Reset,
231    Cancel,
232    Dispose,
233}
234
235impl LiveSessionCommand {
236    pub const fn operation(&self) -> LiveSessionOperation {
237        match self {
238            Self::Snapshot => LiveSessionOperation::Snapshot,
239            Self::Step => LiveSessionOperation::Step,
240            Self::Run { .. } => LiveSessionOperation::Run,
241            Self::Call { .. } => LiveSessionOperation::Call,
242            Self::Pause => LiveSessionOperation::Pause,
243            Self::Resume { .. } => LiveSessionOperation::Resume,
244            Self::Resolve { .. } => LiveSessionOperation::Resolve,
245            Self::Reject { .. } => LiveSessionOperation::Reject,
246            Self::Update { .. } => LiveSessionOperation::Update,
247            Self::Reset => LiveSessionOperation::Reset,
248            Self::Cancel => LiveSessionOperation::Cancel,
249            Self::Dispose => LiveSessionOperation::Dispose,
250        }
251    }
252}
253
254#[derive(Clone, Debug, PartialEq)]
255pub struct LiveSessionRequest {
256    pub protocol: String,
257    pub request_id: String,
258    pub session_id: String,
259    pub generation: Option<u64>,
260    pub revision: Option<String>,
261    pub command: LiveSessionCommand,
262}
263
264impl LiveSessionRequest {
265    pub fn for_state(
266        request_id: impl Into<String>,
267        state: &LiveSessionState,
268        command: LiveSessionCommand,
269    ) -> Self {
270        Self {
271            protocol: LIVE_SESSION_PROTOCOL.into(),
272            request_id: request_id.into(),
273            session_id: state.session_id.clone(),
274            generation: Some(state.generation),
275            revision: Some(state.revision.clone()),
276            command,
277        }
278    }
279
280    fn validate(&self, state: &LiveSessionState) -> Result<(), LiveSessionError> {
281        if self.protocol != LIVE_SESSION_PROTOCOL {
282            return Err(LiveSessionError::new(
283                "live-session/protocol",
284                format!("unsupported live-session protocol: {}", self.protocol),
285            ));
286        }
287        required_text(self.request_id.clone(), "request id")?;
288        required_text(self.session_id.clone(), "session id")?;
289        if self.session_id != state.session_id {
290            return Err(LiveSessionError::new(
291                "live-session/session-mismatch",
292                format!(
293                    "request targets session {} but adapter owns {}",
294                    self.session_id, state.session_id
295                ),
296            ));
297        }
298        if let Some(generation) = self.generation {
299            if generation != state.generation {
300                return Err(LiveSessionError::new(
301                    "live-session/stale-generation",
302                    format!(
303                        "request generation {generation} does not match current generation {}",
304                        state.generation
305                    ),
306                ));
307            }
308        }
309        if let Some(revision) = self.revision.as_deref() {
310            if revision != state.revision {
311                return Err(LiveSessionError::new(
312                    "live-session/stale-revision",
313                    format!(
314                        "request revision {revision} does not match current revision {}",
315                        state.revision
316                    ),
317                ));
318            }
319        }
320        Ok(())
321    }
322}
323
324#[derive(Clone, Debug, PartialEq)]
325pub struct LiveSessionReply {
326    pub request_id: String,
327    pub state: LiveSessionState,
328    pub payload: JsonValue,
329}
330
331impl LiveSessionReply {
332    pub fn to_json(&self) -> JsonValue {
333        json!({
334            "schema": LIVE_SESSION_REPLY_SCHEMA,
335            "protocol": LIVE_SESSION_PROTOCOL,
336            "request-id": self.request_id,
337            "state": self.state.to_json(),
338            "payload": self.payload,
339        })
340    }
341}
342
343#[derive(Clone, Debug, PartialEq, Eq)]
344pub struct LiveSessionError {
345    code: String,
346    message: String,
347}
348
349impl LiveSessionError {
350    pub fn new(code: impl Into<String>, message: impl Into<String>) -> Self {
351        Self {
352            code: code.into(),
353            message: message.into(),
354        }
355    }
356
357    pub fn backend(message: impl Into<String>) -> Self {
358        Self::new("live-session/backend", message)
359    }
360
361    pub fn code(&self) -> &str {
362        &self.code
363    }
364
365    pub fn message(&self) -> &str {
366        &self.message
367    }
368}
369
370impl fmt::Display for LiveSessionError {
371    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
372        write!(formatter, "{}: {}", self.code, self.message)
373    }
374}
375
376impl std::error::Error for LiveSessionError {}
377
378pub trait LiveSession {
379    fn state(&self) -> LiveSessionState;
380
381    fn capabilities(&self) -> LiveSessionCapabilities;
382
383    fn dispatch_command(
384        &mut self,
385        command: LiveSessionCommand,
386    ) -> Result<JsonValue, LiveSessionError>;
387
388    fn dispatch(
389        &mut self,
390        request: LiveSessionRequest,
391    ) -> Result<LiveSessionReply, LiveSessionError> {
392        let before = self.state();
393        request.validate(&before)?;
394        let operation = request.command.operation();
395        match before.status {
396            LiveSessionStatus::Disposed if operation != LiveSessionOperation::Dispose => {
397                return Err(LiveSessionError::new(
398                    "live-session/disposed",
399                    "disposed live session accepts only dispose",
400                ));
401            }
402            LiveSessionStatus::Cancelled if operation != LiveSessionOperation::Dispose => {
403                return Err(LiveSessionError::new(
404                    "live-session/cancelled",
405                    "cancelled live session accepts only dispose",
406                ));
407            }
408            _ => {}
409        }
410        let capabilities = self.capabilities();
411        if !capabilities.supports(operation) {
412            return Err(LiveSessionError::new(
413                "live-session/unsupported-operation",
414                format!(
415                    "{} backend does not support {}",
416                    before.backend.as_str(),
417                    operation.as_str()
418                ),
419            ));
420        }
421        if let LiveSessionCommand::Update { policy, .. } = &request.command {
422            if !capabilities.supports_replacement(*policy) {
423                return Err(LiveSessionError::new(
424                    "live-session/unsupported-replacement",
425                    format!(
426                        "{} backend does not support {} replacement",
427                        before.backend.as_str(),
428                        policy.as_str()
429                    ),
430                ));
431            }
432        }
433        let request_id = request.request_id;
434        let payload = self.dispatch_command(request.command)?;
435        Ok(LiveSessionReply {
436            request_id,
437            state: self.state(),
438            payload,
439        })
440    }
441}
442
443pub(crate) fn required_text(value: String, label: &str) -> Result<String, LiveSessionError> {
444    if value.trim().is_empty() {
445        Err(LiveSessionError::new(
446            "live-session/invalid-identity",
447            format!("live-session {label} must not be empty"),
448        ))
449    } else {
450        Ok(value)
451    }
452}