Compressed CRDT operation batch. Wire format: ```text [4 bytes] op count (LE u32) [2 bytes] actor dictionary size (LE u16) [actor_count × 8 bytes] actor IDs (LE u64) [N bytes] Delta-encoded Lamport timestamps (nodedb-codec delta format) [4 bytes] actor_index block size (LE u32) [M bytes] actor indices (u8 if ≤256 actors, u16 otherwise) [4 bytes] content block size (LE u32) [K bytes] FSST-compress
(ops: &[CrdtOp])
| 38 | /// [K bytes] FSST-compressed content (newline-delimited) |
| 39 | /// ``` |
| 40 | pub fn encode(ops: &[CrdtOp]) -> Result<Vec<u8>, CodecError> { |
| 41 | if ops.is_empty() { |
| 42 | return Ok(0u32.to_le_bytes().to_vec()); |
| 43 | } |
| 44 | |
| 45 | let count = ops.len() as u32; |
| 46 | |
| 47 | // Build actor dictionary. |
| 48 | let mut actor_dict: Vec<u64> = Vec::new(); |
| 49 | let mut actor_map = std::collections::HashMap::new(); |
| 50 | for op in ops { |
| 51 | actor_map.entry(op.actor_id).or_insert_with(|| { |
| 52 | let idx = actor_dict.len() as u16; |
| 53 | actor_dict.push(op.actor_id); |
| 54 | idx |
| 55 | }); |
| 56 | } |
| 57 | |
| 58 | // Delta-encode Lamport timestamps. |
| 59 | let lamports: Vec<i64> = ops.iter().map(|op| op.lamport as i64).collect(); |
| 60 | let lamport_block = crate::delta::encode(&lamports); |
| 61 | |
| 62 | // Actor indices. |
| 63 | let use_u8 = actor_dict.len() <= 256; |
| 64 | let actor_indices: Vec<u8> = if use_u8 { |
| 65 | ops.iter().map(|op| actor_map[&op.actor_id] as u8).collect() |
| 66 | } else { |
| 67 | ops.iter() |
| 68 | .flat_map(|op| actor_map[&op.actor_id].to_le_bytes()) |
| 69 | .collect() |
| 70 | }; |
| 71 | |
| 72 | // FSST-compress content (treat each op's content as a separate string). |
| 73 | let content_refs: Vec<&[u8]> = ops.iter().map(|op| op.content.as_slice()).collect(); |
| 74 | let content_block = crate::fsst::encode(&content_refs); |
| 75 | |
| 76 | // Build output. |
| 77 | let mut out = Vec::new(); |
| 78 | out.extend_from_slice(&count.to_le_bytes()); |
| 79 | out.extend_from_slice(&(actor_dict.len() as u16).to_le_bytes()); |
| 80 | for &actor in &actor_dict { |
| 81 | out.extend_from_slice(&actor.to_le_bytes()); |
| 82 | } |
| 83 | out.extend_from_slice(&(lamport_block.len() as u32).to_le_bytes()); |
| 84 | out.extend_from_slice(&lamport_block); |
| 85 | out.push(if use_u8 { 1 } else { 2 }); // index width marker |
| 86 | out.extend_from_slice(&(actor_indices.len() as u32).to_le_bytes()); |
| 87 | out.extend_from_slice(&actor_indices); |
| 88 | out.extend_from_slice(&(content_block.len() as u32).to_le_bytes()); |
| 89 | out.extend_from_slice(&content_block); |
| 90 | |
| 91 | Ok(out) |
| 92 | } |
| 93 | |
| 94 | /// Decode compressed CRDT operations. |
| 95 | pub fn decode(data: &[u8]) -> Result<Vec<CrdtOp>, CodecError> { |