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}