Oregami
Repositories/oxedyne/ore

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
12use crate::id::{
13 after,
14 before,
15 Anchor,
16 ContentId,
17 ContentRange,
18 OpId,
19};
20use crate::repo::{
21 shared,
22 CycleRule,
23 FileId,
24 Identity,
25 MOp,
26 Repo,
27 RepoRender,
28 Shared,
29};
30
31use std::collections::BTreeSet;
32use std::rc::Rc;
33
34use oxedyne_fe2o3_core::prelude::*;
35
36/// Coalesces a run of content ids into the fewest content ranges.
37pub 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.
50pub 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
57impl 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}