> > - I'm not sure if I want to make this OT type invertable. If we don't,
finding the data that was actually deleted will be difficult > What are the costs of making it invertable? i.e. are there particular reasons not to? I guess for proper invertability, you'd need to copy all the data being moved? > - Any concurrent edits on the data that conflicted might would be > lost, because we'd replace a move with a delete-reinsert > Could the move get transformed into a no-op (or, a move to another place)? As that would permit concurrent edits. > But we could implement this much more cleanly if move operations > specify a fallback location for conflicting data to get moved into. If > that fallback location was a list, we can guarantee we won't have any > cascaded conflicts. To resolve the conflict, the user can just move or > delete the conflicted object like normal. > Out of interest, is this something that conflicting 'set' operations would want too? e.g. (Set 'c' {v: 1}) & (Set 'c' {v: 2}) seems to have similar loss semantics, should _recovery then be [{set: 'c', data: {v: 1}}] > Some observations: > >> - This will almost never actually happen in real life. > >> - Even if we turn the operation will transform into a 'replace', we > >> could add a flag to the operation to mark that it was unintentionally > >> overwritten, and then the process which is making the corresponding > >> changes on disk could see that and do some special > >> application-specific behaviour instead of actually deleting the file. > > > > > > Indeed - your structure could instead be: > > { "name": "librope", > > "package": { > > "src": { > > "rope.c": "somelonguniqueid", > > "rope.h": "anotherfileid", > > }}, > > data: { > > "somelonguniqueid": { "text": "#include <rope.h>\n ..." }, > > "anotherfileid": { "text": "// This is a cool header" }, > > }} > > > > Then deletion is some separate offline index/cleanup, and resolving > > conflicts is simpler. > > (although 'move' is less useful here as there are no child properties). > > Yeah - thats definitely usable. It kind of moves the mess around your > plate a little. Its very similar to the idea of just not allowing > move-to-object operations at all, which would force every > dictionary-like structure to be managed using lists with manual > (custom!) dedup in the case of conflicts. Either way, application > writers would need to do some manual garbage collection. Yep, I was assuming everyone wants manual garbage collection anyway :) I realized though that moving folders messes this up a bit; either the folders are also files (possible, but then every N-deep folder traversal does N id lookups) or they are objects (e.g. "src": {"js": { "file1": id1, "file2": id2, ...}}) in which case you're back to the original problem. > Does move distinguish between PUT vs. PATCH? > > I would assume PUT (x: {a: 1}) -> z and PUT (y: {b: 2}) -> z would end > up > > in z: {b:2} > > but if the second operation received was a PATCH operation, they would be > > merged rather than replaced. > > For file system moves they'd probably be PUT, but their might be > instances > > where a PATCH is useful. > > I was just thinking of move as a PUT operation. When is PATCH useful? > In what use cases do you actually want two object structures to get > merged? > Only thing I could think of was batch file moves - i.e. moving a set of string-id'd files from one directory to another, you'd want to PATCH move {'f1.js': {...}, 'f2.js': {...}} from src -> dest but then it's a bit weird, as you'd want PATCH on the top level and PUT underneath, and it'd probably be better off as a list of PUT moves. Otherwise, I couldn't think of one, unless a client wanted to get really abstract and e.g. batch a 'local edits' object which then get 'applied' by a PATCH move onto the persisted object, but that also sounds like something that could be done better in other ways.