use crate::ctx::{Ctx, decode, encode};
use crate::defs::{Make, Op};
use crate::engine::{Engine, StartOptions, outcome_of, status_error};
use crate::error::{Error, Result};
use crate::runtime::{LocalFuture, RunDetail, RunOutcome, WorkflowFn};
use fusee_core::record::{Name, Payload, Record, RecordBuf, Status, policy};
use fusee_core::wire::{Reader, put_bytes};
use rustc_hash::{FxHashMap, FxHashSet};
use serde::Serialize;
use serde::de::DeserializeOwned;
use serde_json::Value;
use std::collections::{BTreeMap, VecDeque};
use std::future::Future;
use std::pin::Pin;
use std::rc::Rc;
use std::sync::Arc;
pub mod edit;
pub use edit::EDIT_HANDLER;
pub const STATE_QUERY: &str = "fusee.state";
pub const COMPACT_OPS: u32 = 128;
pub const KEPT_CALLS: usize = 1_024;
pub const KEPT_CALL_MS: u64 = 3_600_000;
pub const MAX_WAITING: usize = 10_000;
pub const SEND_PREFIX: &str = "fusee.send/";
pub fn object_id(wtype: &str, key: &str) -> String {
format!("{wtype}/{key}")
}
pub fn key_of<'a>(id: &'a str, wtype: &str) -> Option<&'a str> {
id.strip_prefix(wtype)?.strip_prefix('/')
}
pub fn is_auto_id(id: &str) -> bool {
let id = id.strip_prefix(SEND_PREFIX).unwrap_or(id);
id.is_empty() || id.starts_with("auto-")
}
pub fn is_send(id: &str) -> bool {
id.starts_with(SEND_PREFIX)
}
#[derive(Clone, Debug, Default, PartialEq, Eq)]
pub struct ObjectDef {
pub shared: FxHashSet<String>,
}
impl ObjectDef {
pub fn new<I: IntoIterator<Item = S>, S: Into<String>>(shared: I) -> Self {
Self {
shared: shared.into_iter().map(Into::into).collect(),
}
}
pub fn is_shared(&self, handler: &str) -> bool {
self.shared.contains(handler)
}
}
pub fn check_type(name: &str) -> Result<()> {
if name.is_empty() || name.contains('/') {
return Err(Error::Rejected(format!(
"an object type's name is not empty and has no '/': '{name}'"
)));
}
Ok(())
}
pub fn encode_call(handler: &str, id: &str, arg: &[u8]) -> Vec<u8> {
let mut out = Vec::with_capacity(handler.len() + id.len() + arg.len() + 4);
put_bytes(&mut out, handler.as_bytes());
put_bytes(&mut out, id.as_bytes());
out.extend_from_slice(arg);
out
}
pub fn decode_call(p: &[u8]) -> Result<(String, String, Payload)> {
let bad = |_| Error::Codec("a malformed object call".into());
let mut r = Reader::new(p);
let handler = r.str().map_err(bad)?.to_owned();
let id = r.str().map_err(bad)?.to_owned();
Ok((handler, id, Arc::from(r.rest())))
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct Kept {
pub id: Name,
pub status: Status,
pub result: Payload,
pub at_ms: u64,
}
#[derive(Clone, Debug, Default, PartialEq, Eq)]
pub struct Snapshot {
pub state: BTreeMap<String, Payload>,
pub calls: VecDeque<Kept>,
}
fn json_str(s: &str, out: &mut Vec<u8>) {
let _ = serde_json::to_writer(&mut *out, s);
}
fn object_json<'a>(
entries: impl Iterator<Item = (&'a String, Option<&'a Payload>)>,
out: &mut Vec<u8>,
) {
out.push(b'{');
let mut first = true;
for (k, v) in entries {
if !first {
out.push(b',');
}
first = false;
json_str(k, out);
out.push(b':');
match v {
Some(v) if !v.is_empty() => out.extend_from_slice(v),
_ => out.extend_from_slice(b"null"),
}
}
out.push(b'}');
}
fn raw(v: &Value) -> Payload {
Arc::from(serde_json::to_vec(v).unwrap_or_else(|_| b"null".to_vec()))
}
fn status_name(s: Status) -> &'static str {
s.as_str()
}
fn status_of(name: &str) -> Status {
match name {
"ok" => Status::Ok,
"cancelled" => Status::Cancelled,
"timed_out" => Status::TimedOut,
"terminated" => Status::Terminated,
_ => Status::Failed,
}
}
impl Snapshot {
pub fn encode(&self) -> Vec<u8> {
let mut out = Vec::new();
out.push(b'{');
if !self.state.is_empty() {
out.extend_from_slice(b"\"state\":");
object_json(self.state.iter().map(|(k, v)| (k, Some(v))), &mut out);
}
if !self.calls.is_empty() {
if !self.state.is_empty() {
out.push(b',');
}
out.extend_from_slice(b"\"calls\":[");
for (i, c) in self.calls.iter().enumerate() {
if i > 0 {
out.push(b',');
}
out.extend_from_slice(b"{\"id\":");
json_str(&c.id, &mut out);
out.extend_from_slice(b",\"status\":");
json_str(status_name(c.status), &mut out);
let json = c.status == Status::Ok
&& serde_json::from_slice::<serde::de::IgnoredAny>(&c.result).is_ok();
if json {
out.extend_from_slice(b",\"result\":");
out.extend_from_slice(&c.result);
} else if c.status == Status::Ok {
let mut s = String::new();
crate::stream::base64_encode(&c.result, &mut s);
out.extend_from_slice(b",\"result64\":");
json_str(&s, &mut out);
} else {
out.extend_from_slice(b",\"error\":");
json_str(&String::from_utf8_lossy(&c.result), &mut out);
}
out.extend_from_slice(format!(",\"at\":{}}}", c.at_ms).as_bytes());
}
out.push(b']');
}
out.push(b'}');
out
}
pub fn decode(p: &[u8]) -> Result<Self> {
if p.is_empty() {
return Ok(Self::default());
}
let v: Value = serde_json::from_slice(p)?;
let mut snap = Self::default();
if let Some(Value::Object(m)) = v.get("state") {
for (k, v) in m {
if !v.is_null() {
snap.state.insert(k.clone(), raw(v));
}
}
}
if let Some(Value::Array(calls)) = v.get("calls") {
for c in calls {
let id = c.get("id").and_then(Value::as_str).unwrap_or_default();
let status = status_of(c.get("status").and_then(Value::as_str).unwrap_or("failed"));
let result = if let Some(r) = c.get("result") {
raw(r)
} else if let Some(r) = c.get("result64").and_then(Value::as_str) {
Arc::from(crate::stream::base64_decode(r).unwrap_or_default())
} else {
Arc::from(
c.get("error")
.and_then(Value::as_str)
.unwrap_or_default()
.as_bytes(),
)
};
snap.calls.push_back(Kept {
id: Arc::from(id),
status,
result,
at_ms: c.get("at").and_then(Value::as_u64).unwrap_or(0),
});
}
}
Ok(snap)
}
pub fn prune(&mut self, now_ms: u64) {
let cutoff = now_ms.saturating_sub(KEPT_CALL_MS);
let mut seen = FxHashSet::default();
let mut kept = VecDeque::new();
for c in self.calls.drain(..).rev() {
if c.at_ms < cutoff || kept.len() >= KEPT_CALLS || !seen.insert(c.id.clone()) {
continue;
}
kept.push_front(c);
}
self.calls = kept;
}
}
pub fn encode_delta(writes: &BTreeMap<String, Option<Payload>>) -> Vec<u8> {
let mut out = Vec::new();
object_json(writes.iter().map(|(k, v)| (k, v.as_ref())), &mut out);
out
}
pub fn decode_delta(p: &[u8]) -> Result<Vec<(String, Option<Payload>)>> {
if p.is_empty() {
return Ok(Vec::new());
}
let Value::Object(m) = serde_json::from_slice::<Value>(p)? else {
return Err(Error::Codec("a state change is a JSON object".into()));
};
Ok(m.into_iter()
.map(|(k, v)| {
let v = (!v.is_null()).then(|| raw(&v));
(k, v)
})
.collect())
}
pub fn state_json(state: &BTreeMap<String, Payload>) -> Payload {
let mut out = Vec::new();
object_json(state.iter().map(|(k, v)| (k, Some(v))), &mut out);
Arc::from(out)
}
#[derive(Clone, Debug, PartialEq, Eq, Serialize)]
pub struct PendingCall {
pub handler: String,
pub id: String,
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct ObjectDetail {
pub state: Value,
pub version: String,
pub running: Vec<PendingCall>,
pub waiting: Vec<PendingCall>,
}
pub fn view(
journal: &[RecordBuf],
open: &dyn Fn(Payload) -> Result<Payload>,
) -> Result<(BTreeMap<String, Payload>, Vec<PendingCall>)> {
let mut state = BTreeMap::new();
let mut running: Vec<(u64, PendingCall)> = Vec::new();
let mut ordinal = 0u64;
for r in journal {
match r {
Record::Start { input, update, .. } => {
state = Snapshot::decode(&open(input.clone())?)?.state;
if let Some(u) = update {
running.push((
0,
PendingCall {
handler: u.name.to_string(),
id: u.id.to_string(),
},
));
ordinal = 1;
}
}
Record::Update { name, id, .. } => {
running.push((
ordinal,
PendingCall {
handler: name.to_string(),
id: id.to_string(),
},
));
ordinal += 1;
}
Record::UpdateDone {
ordinal: o,
state: delta,
..
} => {
running.retain(|(n, _)| n != o);
if !delta.is_empty() {
for (k, v) in decode_delta(&open(delta.clone())?)? {
match v {
Some(v) => state.insert(k, v),
None => state.remove(&k),
};
}
}
}
_ => {}
}
}
Ok((state, running.into_iter().map(|(_, c)| c).collect()))
}
pub fn is_object_journal(journal: &[RecordBuf]) -> bool {
matches!(journal.first(), Some(Record::Start { policy: p, .. }) if p & policy::OBJECT != 0)
}
#[derive(Clone, Debug)]
pub(crate) struct Waiting {
pub name: Name,
pub id: Name,
pub payload: Payload,
pub shared: bool,
}
#[derive(Clone, Copy, Debug)]
pub(crate) struct CallWaiter {
pub parent: Option<fusee_core::record::ParentRef>,
pub from: Option<(u32, u64)>,
}
#[derive(Debug, Default)]
pub(crate) struct ObjectRun {
pub state: BTreeMap<String, Payload>,
pub owner: Option<u64>,
pub writes: BTreeMap<String, Option<Payload>>,
pub calls: VecDeque<Kept>,
pub waiting: VecDeque<Waiting>,
pub offered: Option<Name>,
pub compacting: bool,
}
impl ObjectRun {
pub fn reset(&mut self, snap: Snapshot) {
self.state = snap.state;
self.calls = snap.calls;
self.owner = None;
self.writes.clear();
self.offered = None;
}
pub fn kept(&self, id: &str) -> Option<&Kept> {
self.calls.iter().rev().find(|k| *k.id == *id)
}
pub fn get(&self, key: &str, exclusive: bool) -> Option<Payload> {
if exclusive
&& self.owner.is_some()
&& let Some(w) = self.writes.get(key)
{
return w.clone();
}
self.state.get(key).cloned()
}
pub fn keys(&self, exclusive: bool) -> Vec<String> {
let mut keys: Vec<String> = self.state.keys().cloned().collect();
if exclusive && self.owner.is_some() {
for (k, v) in &self.writes {
match v {
Some(_) if !self.state.contains_key(k) => keys.push(k.clone()),
None => keys.retain(|x| x != k),
_ => {}
}
}
keys.sort();
}
keys
}
}
#[derive(Debug, Default)]
pub(crate) struct Objects {
pub runs: FxHashMap<fusee_core::record::RunId, ObjectRun>,
pub compact_due: Vec<fusee_core::record::RunId>,
pub callers: FxHashMap<(fusee_core::record::RunId, Name), CallWaiter>,
}
type Handler = Arc<dyn Fn(ObjectCtx, Payload) -> LocalFuture + Send + Sync>;
pub struct ObjectType {
engine: Engine,
name: String,
version: u32,
handlers: ObjectHandlers,
}
#[derive(Default)]
pub struct ObjectHandlers {
handlers: BTreeMap<String, (bool, Handler)>,
}
impl ObjectHandlers {
pub fn new() -> Self {
Self::default()
}
pub fn exclusive<A, R, F, Fut>(mut self, name: &str, f: F) -> Self
where
A: DeserializeOwned + 'static,
R: Serialize + 'static,
F: Fn(ObjectCtx, A) -> Fut + Send + Sync + 'static,
Fut: Future<Output = Result<R>> + 'static,
{
self.handlers
.insert(name.to_owned(), (false, erase_handler(f)));
self
}
pub fn shared<A, R, F, Fut>(mut self, name: &str, f: F) -> Self
where
A: DeserializeOwned + 'static,
R: Serialize + 'static,
F: Fn(ObjectCtx, A) -> Fut + Send + Sync + 'static,
Fut: Future<Output = Result<R>> + 'static,
{
self.handlers
.insert(name.to_owned(), (true, erase_handler(f)));
self
}
pub fn into_code(self) -> (WorkflowFn, ObjectDef) {
let def = ObjectDef {
shared: self
.handlers
.iter()
.filter(|(_, (s, _))| *s)
.map(|(n, _)| n.clone())
.collect(),
};
let handlers = Arc::new(self.handlers);
let f: WorkflowFn = Arc::new(move |ctx: Ctx| {
let handlers = handlers.clone();
Box::pin(async move {
for (name, (shared, h)) in handlers.iter() {
let (c, h, exclusive) = (ctx.clone(), h.clone(), !*shared);
ctx.on_update_raw(
name,
Rc::new(|_| Ok(())),
Rc::new(move |arg: Payload| {
h(
ObjectCtx {
ctx: c.clone(),
exclusive,
},
arg,
)
}),
);
}
let c = ctx.clone();
ctx.on_update_raw(
EDIT_HANDLER,
Rc::new(|_| Ok(())),
Rc::new(move |edit: Payload| {
let r = c
.host_ref()
.object_edit(c.run_id(), &edit)
.map_err(outcome_of);
Box::pin(async move { r }) as Pin<Box<dyn Future<Output = RunOutcome>>>
}),
);
let c = ctx.clone();
ctx.on_query_raw(
STATE_QUERY,
Rc::new(move |_| {
c.host_ref()
.object_state(c.run_id())
.map_err(|e| e.to_string())
}),
);
std::future::pending::<()>().await;
Ok(Arc::from(&b"null"[..]))
}) as Pin<Box<dyn Future<Output = RunOutcome>>>
});
(f, def)
}
}
fn erase_handler<A, R, F, Fut>(f: F) -> Handler
where
A: DeserializeOwned + 'static,
R: Serialize + 'static,
F: Fn(ObjectCtx, A) -> Fut + Send + Sync + 'static,
Fut: Future<Output = Result<R>> + 'static,
{
let f = Arc::new(f);
Arc::new(move |ctx: ObjectCtx, arg: Payload| {
let f = f.clone();
Box::pin(async move {
let a: A = decode(&arg).map_err(outcome_of)?;
let r = f(ctx, a).await.map_err(|e| match e {
Error::Failed(m) => (Status::Failed, m.to_payload()),
e => outcome_of(e),
})?;
encode(&r).map_err(outcome_of)
}) as Pin<Box<dyn Future<Output = RunOutcome>>>
})
}
impl ObjectType {
pub(crate) fn new(engine: Engine, name: &str) -> Self {
Self {
engine,
name: name.to_owned(),
version: 1,
handlers: ObjectHandlers::new(),
}
}
pub fn exclusive<A, R, F, Fut>(mut self, name: &str, f: F) -> Self
where
A: DeserializeOwned + 'static,
R: Serialize + 'static,
F: Fn(ObjectCtx, A) -> Fut + Send + Sync + 'static,
Fut: Future<Output = Result<R>> + 'static,
{
self.handlers = self.handlers.exclusive(name, f);
self
}
pub fn shared<A, R, F, Fut>(mut self, name: &str, f: F) -> Self
where
A: DeserializeOwned + 'static,
R: Serialize + 'static,
F: Fn(ObjectCtx, A) -> Fut + Send + Sync + 'static,
Fut: Future<Output = Result<R>> + 'static,
{
self.handlers = self.handlers.shared(name, f);
self
}
pub fn version(mut self, version: u32) -> Self {
self.version = version;
self
}
pub fn register(self) -> Result<Object> {
check_type(&self.name)?;
let (f, def) = self.handlers.into_code();
self.engine
.register_object(&self.name, self.version, f, def);
Ok(Object {
engine: self.engine,
name: self.name,
})
}
}
#[derive(Clone)]
pub struct Object {
engine: Engine,
name: String,
}
impl Object {
pub fn name(&self) -> &str {
&self.name
}
pub fn key(&self, key: &str) -> ObjectRef {
self.engine.object(&self.name, key)
}
}
#[derive(Clone)]
pub struct ObjectCtx {
ctx: Ctx,
exclusive: bool,
}
impl std::ops::Deref for ObjectCtx {
type Target = Ctx;
fn deref(&self) -> &Ctx {
&self.ctx
}
}
impl ObjectCtx {
pub fn state(&self) -> State<'_> {
State {
ctx: &self.ctx,
exclusive: self.exclusive,
}
}
pub fn object_key(&self) -> &str {
key_of(self.ctx.workflow_id(), self.ctx.workflow_type()).unwrap_or_default()
}
pub fn is_exclusive(&self) -> bool {
self.exclusive
}
pub fn ctx(&self) -> &Ctx {
&self.ctx
}
}
pub struct State<'a> {
ctx: &'a Ctx,
exclusive: bool,
}
impl State<'_> {
pub fn get<T: DeserializeOwned>(&self, key: &str) -> Result<Option<T>> {
match self.get_raw(key)? {
Some(p) => decode(&p).map(Some),
None => Ok(None),
}
}
pub fn get_raw(&self, key: &str) -> Result<Option<Payload>> {
self.ctx
.host_ref()
.object_get(self.ctx.run_id(), key, self.exclusive)
}
pub fn set<T: Serialize>(&self, key: &str, value: &T) -> Result<()> {
let p = encode(value)?;
let v = (&*p != b"null").then_some(p);
self.write(key, v)
}
pub fn set_raw(&self, key: &str, json: Payload) -> Result<()> {
self.write(key, Some(json))
}
pub fn clear(&self, key: &str) -> Result<()> {
self.write(key, None)
}
pub fn clear_all(&self) -> Result<()> {
for k in self.keys()? {
self.write(&k, None)?;
}
Ok(())
}
pub fn keys(&self) -> Result<Vec<String>> {
self.ctx
.host_ref()
.object_keys(self.ctx.run_id(), self.exclusive)
}
fn write(&self, key: &str, value: Option<Payload>) -> Result<()> {
if !self.exclusive {
return Err(Error::Rejected(
"a shared handler cannot change state".into(),
));
}
self.ctx
.host_ref()
.object_set(self.ctx.run_id(), key, value)
}
}
#[derive(Clone)]
pub struct ObjectRef {
engine: Engine,
wtype: String,
key: String,
}
impl std::fmt::Debug for ObjectRef {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "ObjectRef({}/{})", self.wtype, self.key)
}
}
#[derive(Clone, Debug, Default, PartialEq, Eq)]
pub struct CallOptions {
pub id: String,
}
impl<M: Make<CallOptions>> Op<CallOptions, M> {
pub fn id(mut self, id: &str) -> Self {
self.options_mut().id = id.to_owned();
self
}
}
impl ObjectRef {
pub(crate) fn new(engine: Engine, wtype: &str, key: &str) -> Self {
Self {
engine,
wtype: wtype.to_owned(),
key: key.to_owned(),
}
}
pub fn id(&self) -> String {
object_id(&self.wtype, &self.key)
}
pub fn key(&self) -> &str {
&self.key
}
pub fn call<'a, A: Serialize + 'a, R: DeserializeOwned + 'a>(
&'a self,
handler: &'a str,
arg: &'a A,
) -> Op<
CallOptions,
impl Make<CallOptions, Output = Result<R>, Fut = impl Future<Output = Result<R>> + 'a> + 'a,
> {
Op::new(CallOptions::default(), move |o: CallOptions| async move {
let (status, p) = self.call_raw(handler, encode(arg)?, &o.id).await?;
match status {
Status::Ok => decode(&p),
s => Err(status_error(s, &p)),
}
})
}
pub async fn call_raw(
&self,
handler: &str,
arg: Payload,
id: &str,
) -> Result<(Status, Payload)> {
self.engine
.update_with_start_raw(
&self.wtype,
&self.id(),
Arc::from(&b"null"[..]),
StartOptions::default().use_existing(),
handler,
id,
arg,
)
.await
}
pub fn send<'a, A: Serialize + 'a>(
&'a self,
handler: &'a str,
arg: &'a A,
) -> Op<
CallOptions,
impl Make<CallOptions, Output = Result<()>, Fut = impl Future<Output = Result<()>> + 'a> + 'a,
> {
Op::new(CallOptions::default(), move |o: CallOptions| async move {
let (status, p) = self
.call_raw(handler, encode(arg)?, &format!("{SEND_PREFIX}{}", o.id))
.await?;
match status {
Status::Ok => Ok(()),
s => Err(status_error(s, &p)),
}
})
}
pub async fn edit_state(&self, edit: &Value) -> Result<Value> {
let (status, p) = self
.call_raw(EDIT_HANDLER, Arc::from(edit.to_string().into_bytes()), "")
.await?;
match status {
Status::Ok => decode(&p),
s => Err(status_error(s, &p)),
}
}
pub async fn set_state<V: Serialize>(&self, field: &str, value: &V) -> Result<Value> {
let mut set = serde_json::Map::new();
set.insert(field.to_owned(), serde_json::to_value(value)?);
self.edit_state(&serde_json::json!({ "set": set })).await
}
pub async fn patch_state(&self, ops: &Value) -> Result<Value> {
self.edit_state(&serde_json::json!({ "patch": ops })).await
}
pub async fn clear_state(&self, field: Option<&str>) -> Result<Value> {
match field {
Some(f) => self.edit_state(&serde_json::json!({ "clear": [f] })).await,
None => {
self.edit_state(&serde_json::json!({ "clearAll": true }))
.await
}
}
}
pub async fn state(&self) -> Result<Value> {
match self
.engine
.query_raw(&self.id(), STATE_QUERY, Arc::from(&b"null"[..]))
.await
{
Ok(p) => Ok(serde_json::from_slice(&p)?),
Err(Error::NotFound(_)) => Ok(Value::Object(Default::default())),
Err(e) => Err(e),
}
}
pub async fn describe(&self) -> Result<RunDetail> {
self.engine
.describe(&self.id())
.await?
.ok_or_else(|| Error::NotFound(self.id()))
}
pub async fn delete(&self) -> Result<()> {
match self
.engine
.run::<Value>(&self.id())
.terminate("object deleted")
.await
{
Err(Error::NotFound(_)) => Ok(()),
r => r,
}
}
}
pub struct ObjectCall<'a> {
pub(crate) ctx: &'a Ctx,
pub(crate) wtype: String,
pub(crate) key: String,
}
impl ObjectCall<'_> {
pub async fn call<A: Serialize, R: DeserializeOwned>(
&self,
handler: &str,
arg: &A,
) -> Result<R> {
let seq = self.ctx.host_ref().begin_call(
self.ctx.run_id(),
&self.wtype,
&self.key,
handler,
encode(arg)?,
false,
)?;
let Some(seq) = seq else {
return Err(Error::Failed("the call did not register".into()));
};
let o = self.ctx.delivery(seq).await;
match o.status {
Status::Ok => decode(&o.payload),
s => Err(status_error(s, &o.payload)),
}
}
pub fn send<A: Serialize>(&self, handler: &str, arg: &A) -> Result<()> {
self.ctx.host_ref().begin_call(
self.ctx.run_id(),
&self.wtype,
&self.key,
handler,
encode(arg)?,
true,
)?;
Ok(())
}
}
#[cfg(test)]
mod tests {
use super::*;
fn p(s: &str) -> Payload {
Arc::from(s.as_bytes())
}
#[test]
fn snapshots_round_trip() {
let mut snap = Snapshot::default();
assert_eq!(snap.encode(), b"{}");
snap.state.insert("items".into(), p("[1,2]"));
snap.state.insert("n".into(), p("3"));
snap.calls.push_back(Kept {
id: Arc::from("a"),
status: Status::Ok,
result: p("{\"x\":1}"),
at_ms: 5,
});
snap.calls.push_back(Kept {
id: Arc::from("b"),
status: Status::Failed,
result: p("out of stock"),
at_ms: 6,
});
snap.calls.push_back(Kept {
id: Arc::from("c"),
status: Status::Ok,
result: Arc::from(&[0xff, 0x00][..]),
at_ms: 7,
});
let e = snap.encode();
let back = Snapshot::decode(&e).unwrap();
assert_eq!(back, snap, "{}", String::from_utf8_lossy(&e));
}
#[test]
fn pruning_keeps_the_newest_per_id() {
let mut snap = Snapshot::default();
for (i, id) in ["a", "b", "a", "c"].iter().enumerate() {
snap.calls.push_back(Kept {
id: Arc::from(*id),
status: Status::Ok,
result: p(&i.to_string()),
at_ms: 1_000 + i as u64,
});
}
snap.prune(1_003);
let ids: Vec<(&str, &[u8])> = snap.calls.iter().map(|c| (&*c.id, &c.result[..])).collect();
assert_eq!(ids, vec![("b", &b"1"[..]), ("a", b"2"), ("c", b"3")]);
snap.prune(1_000 + KEPT_CALL_MS + 3);
assert_eq!(snap.calls.len(), 1);
}
#[test]
fn deltas_and_calls_encode() {
let mut w = BTreeMap::new();
w.insert("a".to_owned(), Some(p("{\"q\":1}")));
w.insert("b\"".to_owned(), None);
let d = encode_delta(&w);
assert_eq!(d, b"{\"a\":{\"q\":1},\"b\\\"\":null}");
let back = decode_delta(&d).unwrap();
assert_eq!(back.len(), 2);
let c = encode_call("add", "wf/1/3", b"[1]");
let (h, id, arg) = decode_call(&c).unwrap();
assert_eq!(
(h.as_str(), id.as_str(), &arg[..]),
("add", "wf/1/3", &b"[1]"[..])
);
assert_eq!(key_of("cart/u/7", "cart"), Some("u/7"));
assert!(is_auto_id("fusee.send/auto-1-2") && is_auto_id("") && !is_auto_id("req-1"));
}
}