{"id":"72Qe5sQSGf","url":"https://pastebin.ca/72Qe5sQSGf","raw_url":"https://raw.anybin.ca/72Qe5sQSGf","visibility":"public","access":"public","created_at":1780451600900,"expires_at":null,"fetch_limit":null,"fetches_used":0,"reads_remaining":null,"size_bytes":2976,"syntax_hint":"hew","title":"Hew v0.5 — redis-style KV server (Session machine + Store actor, MULTI/EXEC)","filename":"redis_server.hew","change_note":null,"cipher":null,"cipher_meta":null,"parent_id":null,"root_id":"72Qe5sQSGf","version":1,"owner_id":"06F6D6Y710X9HQV90HF93KWQT8","recipient_id":null,"body":"// A redis-style key/value server: a Store actor owns the data, a Session\n// machine drives MULTI/EXEC transactions on the client side.\n//\n// Invariants:\n//   * All key/value state lives in the Store actor and is mutated only inside\n//     its receive fn, so every command is serialized through one mailbox.\n//   * A Session is always in exactly one state. MULTI moves Ready -> Buffering;\n//     each command then appends to the buffer; EXEC replays the buffer in order\n//     and returns to Ready; DISCARD drops the buffer and returns to Ready.\n//   * exec applies exactly one command and returns its RESP-style reply.\n\nmachine Session {\n    events {\n        Begin;\n        Enqueue { cmd: string; }\n        Commit;\n        Abort;\n    }\n\n    state Ready;\n    state Buffering { pending: Vec<string>; }\n\n    on Begin: Ready => Buffering {\n        Buffering { pending: Vec::new() }\n    }\n    on Enqueue(cmd): Buffering => Buffering reenter {\n        self.pending.push(cmd);\n        Buffering { pending: self.pending }\n    }\n    on Commit: Buffering => Ready { Ready }\n    on Abort: Buffering => Ready { Ready }\n\n    default { state }\n}\n\nactor Store {\n    let data: HashMap<string, string>;\n\n    receive fn exec(line: string) -> string {\n        let parts = line.trim().split(\" \");\n        let verb = parts.get(0);\n        match verb {\n            \"PING\" => \"+PONG\",\n            \"DBSIZE\" => f\":{data.len()}\",\n            \"SET\" => {\n                data.insert(parts.get(1), parts.get(2));\n                \"+OK\"\n            },\n            \"GET\" => match data.get(parts.get(1)) {\n                Some(value) => f\"+{value}\",\n                None => \"-NOT_FOUND\",\n            },\n            \"DEL\" => {\n                let key = parts.get(1);\n                if data.contains_key(key) {\n                    data.remove(key);\n                    \"+OK\"\n                } else {\n                    \"-NOT_FOUND\"\n                }\n            },\n            \"EXISTS\" => if data.contains_key(parts.get(1)) { \":1\" } else { \":0\" },\n            _ => \"-ERR unknown command\",\n        }\n    }\n}\n\nfn apply(store: LocalPid<Store>, line: string) {\n    let reply = match await store.exec(line.clone()) {\n        Ok(r) => r,\n        Err(_) => \"-ERR no reply\",\n    };\n    println(f\"{line}  ->  {reply}\");\n}\n\nfn main() {\n    let store = spawn Store(data: HashMap::new());\n\n    apply(store, \"PING\");\n    apply(store, \"SET greeting hello\");\n    apply(store, \"GET greeting\");\n    apply(store, \"EXISTS greeting\");\n\n    // A transaction: MULTI buffers, EXEC replays the buffer in order.\n    var session = Ready;\n    session.step(Begin);\n    session.step(Enqueue { cmd: \"SET a 1\" });\n    session.step(Enqueue { cmd: \"SET b 2\" });\n    match session {\n        Buffering { pending } => {\n            for i in 0 .. pending.len() {\n                apply(store, pending.get(i));\n            }\n        },\n        Ready => {},\n    }\n    session.step(Commit);\n\n    apply(store, \"DBSIZE\");\n    apply(store, \"GET a\");\n}\n"}