oxedyne/ore/oracle/src/mrep.rs
8.5 KiB, 1 run
created by r2848102244:85, which is this file's identity for as long as the history lasts, whatever it is later renamed to
download · who wrote it · its history
| 1 | //! A simulated replica of a whole repository: the frontend that turns |
| 2 | //! index-based editing intent, in a named file, into content-anchored |
| 3 | //! operations. |
| 4 | //! |
| 5 | //! The only thing that differs between the candidates here is how a gap at the |
| 6 | //! start of a file is spelled. Under a derived identity it is the gap after |
| 7 | //! the file's origin anchor, which is a content id like any other; under a |
| 8 | //! recorded identity it is `None`, and the file field carries the meaning |
| 9 | //! instead. Everything else -- what an insertion anchors to, what a move |
| 10 | //! names -- is identical, which is itself a result worth having. |
| 11 | |
| 12 | use crate::id::{ |
| 13 | after, |
| 14 | before, |
| 15 | Anchor, |
| 16 | ContentId, |
| 17 | ContentRange, |
| 18 | OpId, |
| 19 | }; |
| 20 | use crate::repo::{ |
| 21 | shared, |
| 22 | CycleRule, |
| 23 | FileId, |
| 24 | Identity, |
| 25 | MOp, |
| 26 | Repo, |
| 27 | RepoRender, |
| 28 | Shared, |
| 29 | }; |
| 30 | |
| 31 | use std::collections::BTreeSet; |
| 32 | use std::rc::Rc; |
| 33 | |
| 34 | use oxedyne_fe2o3_core::prelude::*; |
| 35 | |
| 36 | /// Coalesces a run of content ids into the fewest content ranges. |
| 37 | pub fn coalesce(ids: &[ContentId]) -> Outcome<Vec<ContentRange>> { |
| 38 | let mut out: Vec<ContentRange> = Vec::new(); |
| 39 | for cid in ids { |
| 40 | match out.last_mut() { |
| 41 | Some(r) if r.op == cid.op && r.to == cid.off => r.to += 1, |
| 42 | _ => out.push(res!( |
| 43 | ContentRange::new(cid.op, cid.off, cid.off + 1))), |
| 44 | } |
| 45 | } |
| 46 | Ok(out) |
| 47 | } |
| 48 | |
| 49 | /// One replica of the repository. |
| 50 | pub struct MRep { |
| 51 | /// Replica number, the second component of every op id it mints. |
| 52 | pub id: u32, |
| 53 | /// The operation set. |
| 54 | pub repo: Repo, |
| 55 | } |
| 56 | |
| 57 | impl MRep { |
| 58 | /// Creates a replica under the given identity rule, with the status quo |
| 59 | /// cycle rule and a metadata table of its own. |
| 60 | pub fn new(id: u32, ident: Identity) -> Self { |
| 61 | Self { id, repo: Repo::new(ident) } |
| 62 | } |
| 63 | |
| 64 | /// Creates a replica under both rules, sharing a metadata table. |
| 65 | pub fn with_rule(id: u32, ident: Identity, rule: CycleRule, meta: Shared) -> Self { |
| 66 | Self { id, repo: Repo::with_rule(ident, rule, meta) } |
| 67 | } |
| 68 | |
| 69 | /// Creates `n` replicas, numbered from one, sharing one metadata table -- |
| 70 | /// which is what a repository has, the causal graph being a global fact |
| 71 | /// rather than a per-replica opinion. |
| 72 | pub fn group(n: u32, ident: Identity, rule: CycleRule) -> (Vec<Self>, Shared) { |
| 73 | let meta = shared(); |
| 74 | let reps = (1..=n) |
| 75 | .map(|i| Self::with_rule(i, ident, rule, Rc::clone(&meta))) |
| 76 | .collect(); |
| 77 | (reps, meta) |
| 78 | } |
| 79 | |
| 80 | /// Records, against an operation this replica is about to mint, everything |
| 81 | /// its author had applied. This is the parent list of a real record, kept |
| 82 | /// where the prototype can reach it. |
| 83 | fn note(&self, id: OpId) { |
| 84 | let seen: BTreeSet<OpId> = self.repo.ops() |
| 85 | .iter() |
| 86 | .map(|o| o.id()) |
| 87 | .filter(|d| *d != id) |
| 88 | .collect(); |
| 89 | self.repo.note_seen(id, seen); |
| 90 | } |
| 91 | |
| 92 | /// Renders the replica's view of the whole repository. |
| 93 | pub fn view(&self) -> Outcome<RepoRender> { |
| 94 | self.repo.render() |
| 95 | } |
| 96 | |
| 97 | /// Mints the next operation id, with a Lamport counter. |
| 98 | pub fn next_id(&self) -> OpId { |
| 99 | OpId::new(self.repo.max_counter() + 1, self.id) |
| 100 | } |
| 101 | |
| 102 | /// Receives an operation from another replica. |
| 103 | pub fn recv(&mut self, op: MOp) { |
| 104 | self.repo.apply(op); |
| 105 | } |
| 106 | |
| 107 | /// Creates a file, returning the operation and the file's identity. |
| 108 | pub fn create(&mut self, path: &[u8]) -> Outcome<(MOp, FileId)> { |
| 109 | let id = self.next_id(); |
| 110 | let op = MOp::FileCreate { id, path: path.to_vec() }; |
| 111 | self.note(id); |
| 112 | self.repo.apply(op.clone()); |
| 113 | Ok((op, id)) |
| 114 | } |
| 115 | |
| 116 | /// Applies an operation this replica did not author through its frontend, |
| 117 | /// recording what its author had seen as though it had. |
| 118 | /// |
| 119 | /// A frontend cannot name an origin anchor and cannot name content it has |
| 120 | /// already moved away, so a case that needs either has to hand the operation |
| 121 | /// in, and it must still carry a causal past or the guards cannot judge it. |
| 122 | pub fn adopt(&mut self, op: MOp) -> Outcome<MOp> { |
| 123 | self.note(op.id()); |
| 124 | self.repo.apply(op.clone()); |
| 125 | Ok(op) |
| 126 | } |
| 127 | |
| 128 | /// Renames a file. |
| 129 | pub fn rename(&mut self, file: FileId, path: &[u8]) -> Outcome<MOp> { |
| 130 | let op = MOp::FileRename { id: self.next_id(), file, path: path.to_vec() }; |
| 131 | self.note(op.id()); |
| 132 | self.repo.apply(op.clone()); |
| 133 | Ok(op) |
| 134 | } |
| 135 | |
| 136 | /// Deletes a file. |
| 137 | pub fn remove(&mut self, file: FileId) -> Outcome<MOp> { |
| 138 | let op = MOp::FileDelete { id: self.next_id(), file }; |
| 139 | self.note(op.id()); |
| 140 | self.repo.apply(op.clone()); |
| 141 | Ok(op) |
| 142 | } |
| 143 | |
| 144 | /// How long a file is, in this replica's view. |
| 145 | pub fn file_len(&self, file: FileId) -> Outcome<usize> { |
| 146 | let v = res!(self.view()); |
| 147 | Ok(v.file(file).map(|f| f.bytes.len()).unwrap_or(0)) |
| 148 | } |
| 149 | |
| 150 | /// The provenance of a file in the replica's current view. |
| 151 | fn prov(&self, file: FileId) -> Outcome<Vec<ContentId>> { |
| 152 | let v = res!(self.view()); |
| 153 | match v.file(file) { |
| 154 | Some(f) => Ok(f.prov.clone()), |
| 155 | None => Err(err!( |
| 156 | "The replica has no file {}.", file; Invalid, Input, Missing)), |
| 157 | } |
| 158 | } |
| 159 | |
| 160 | /// The anchors bracketing a gap at an index of a file's rendered view. |
| 161 | /// |
| 162 | /// The left anchor of the gap at the start of a file is the file's origin |
| 163 | /// anchor under a derived identity, and nothing at all under a recorded one. |
| 164 | fn gap(&self, file: FileId, prov: &[ContentId], index: usize) |
| 165 | -> (Anchor, Anchor) |
| 166 | { |
| 167 | let l = if index > 0 { |
| 168 | after(prov[index - 1]) |
| 169 | } else if self.repo.ident().seeds() { |
| 170 | after(ContentId::new(file, 0)) |
| 171 | } else { |
| 172 | None |
| 173 | }; |
| 174 | let r = if index < prov.len() { before(prov[index]) } else { None }; |
| 175 | (l, r) |
| 176 | } |
| 177 | |
| 178 | /// The file field an operation carries, which is nothing under a derived |
| 179 | /// identity. |
| 180 | fn field(&self, file: FileId) -> Option<FileId> { |
| 181 | if self.repo.ident().seeds() { |
| 182 | None |
| 183 | } else { |
| 184 | Some(file) |
| 185 | } |
| 186 | } |
| 187 | |
| 188 | /// Inserts bytes at an index of a file. |
| 189 | pub fn insert(&mut self, file: FileId, index: usize, bytes: &[u8]) |
| 190 | -> Outcome<MOp> |
| 191 | { |
| 192 | let prov = res!(self.prov(file)); |
| 193 | if index > prov.len() { |
| 194 | return Err(err!("Insert index {} beyond length {}.", |
| 195 | index, prov.len(); Invalid, Input)); |
| 196 | } |
| 197 | let (left, right) = self.gap(file, &prov, index); |
| 198 | let op = MOp::Splice { |
| 199 | id: self.next_id(), |
| 200 | file: self.field(file), |
| 201 | left, |
| 202 | right, |
| 203 | remove: Vec::new(), |
| 204 | insert: bytes.to_vec(), |
| 205 | }; |
| 206 | self.note(op.id()); |
| 207 | self.repo.apply(op.clone()); |
| 208 | Ok(op) |
| 209 | } |
| 210 | |
| 211 | /// Deletes a run at an index of a file. |
| 212 | pub fn delete(&mut self, file: FileId, index: usize, len: usize) |
| 213 | -> Outcome<MOp> |
| 214 | { |
| 215 | let prov = res!(self.prov(file)); |
| 216 | if index + len > prov.len() { |
| 217 | return Err(err!("Delete {}..{} beyond length {}.", |
| 218 | index, index + len, prov.len(); Invalid, Input)); |
| 219 | } |
| 220 | let op = MOp::Splice { |
| 221 | id: self.next_id(), |
| 222 | file: self.field(file), |
| 223 | left: None, |
| 224 | right: None, |
| 225 | remove: res!(coalesce(&prov[index..index + len])), |
| 226 | insert: Vec::new(), |
| 227 | }; |
| 228 | self.note(op.id()); |
| 229 | self.repo.apply(op.clone()); |
| 230 | Ok(op) |
| 231 | } |
| 232 | |
| 233 | /// Replaces a run at an index of a file, in one splice. |
| 234 | pub fn replace(&mut self, file: FileId, index: usize, len: usize, bytes: &[u8]) |
| 235 | -> Outcome<MOp> |
| 236 | { |
| 237 | let prov = res!(self.prov(file)); |
| 238 | if index + len > prov.len() { |
| 239 | return Err(err!("Replace {}..{} beyond length {}.", |
| 240 | index, index + len, prov.len(); Invalid, Input)); |
| 241 | } |
| 242 | let (left, right) = self.gap(file, &prov, index); |
| 243 | let op = MOp::Splice { |
| 244 | id: self.next_id(), |
| 245 | file: self.field(file), |
| 246 | left, |
| 247 | right, |
| 248 | remove: res!(coalesce(&prov[index..index + len])), |
| 249 | insert: bytes.to_vec(), |
| 250 | }; |
| 251 | self.note(op.id()); |
| 252 | self.repo.apply(op.clone()); |
| 253 | Ok(op) |
| 254 | } |
| 255 | |
| 256 | /// Moves a run out of one file and into another, at an index of each. |
| 257 | /// |
| 258 | /// Nothing distinguishes this from a move within one file except which file |
| 259 | /// the destination index is read from, which is the claim of design note |
| 260 | /// section 4.8 stated as code. |
| 261 | pub fn move_across( |
| 262 | &mut self, |
| 263 | from: FileId, |
| 264 | index: usize, |
| 265 | len: usize, |
| 266 | to: FileId, |
| 267 | dest: usize, |
| 268 | ) |
| 269 | -> Outcome<MOp> |
| 270 | { |
| 271 | let src_prov = res!(self.prov(from)); |
| 272 | if index + len > src_prov.len() { |
| 273 | return Err(err!("Move source {}..{} beyond length {}.", |
| 274 | index, index + len, src_prov.len(); Invalid, Input)); |
| 275 | } |
| 276 | let dst_prov = res!(self.prov(to)); |
| 277 | if dest > dst_prov.len() { |
| 278 | return Err(err!("Move destination {} beyond length {}.", |
| 279 | dest, dst_prov.len(); Invalid, Input)); |
| 280 | } |
| 281 | let (left, right) = self.gap(to, &dst_prov, dest); |
| 282 | let op = MOp::Move { |
| 283 | id: self.next_id(), |
| 284 | file: self.field(to), |
| 285 | src: res!(coalesce(&src_prov[index..index + len])), |
| 286 | left, |
| 287 | right, |
| 288 | }; |
| 289 | // What the author aimed at, kept for the sweep to compare against what |
| 290 | // the renderer did. No rule reads it and no record carries it. |
| 291 | self.repo.note_intent(op.id(), to); |
| 292 | self.note(op.id()); |
| 293 | self.repo.apply(op.clone()); |
| 294 | Ok(op) |
| 295 | } |
| 296 | |
| 297 | /// Moves a run within one file. |
| 298 | pub fn move_within(&mut self, file: FileId, index: usize, len: usize, dest: usize) |
| 299 | -> Outcome<MOp> |
| 300 | { |
| 301 | self.move_across(file, index, len, file, dest) |
| 302 | } |
| 303 | } |