Hand the photographer's place between devices
A place recorded on the tablet should be where the desktop opens. Exchanged through `.darkroom-derived/place.json`, beside the thumbnail shards and the catalog snapshot. Newest timestamp wins outright: unlike the catalog this is replaced rather than merged, because two devices cannot both be where the photographer is and so there is nothing of theirs inside ours to preserve. It still refuses to upload over a copy it could not read, for a smaller version of the reason `sync_catalog` does: a record we have not compared against may be the newer one, and overwriting it would move the other device's photographer without ever having seen where they were. Last in the pass, and its failures are logged rather than reported. Everything else in that folder is *derived* -- a faster way to learn what the device could work out for itself -- so losing it costs time. A place is a fact only the other device knew, and losing it costs a scroll. A sync that ran out of connectivity should spend what it had on the shards. The full pass runs after a thumbnail sweep or when Sync is pressed, neither of which happens on an ordinary launch -- so a handover would arrive one launch late, which is one too many for a feature whose whole claim is picking up where you stopped. `spawn_place_fetch` is the small half: one GET of a few hundred bytes, started beside the scan. And it can still be refused. A handover is welcome on the way in and unwelcome once the photographer has started: a grid that jumped elsewhere mid-scroll because a round trip finally landed would have lost their place to the feature meant to keep it. Any scroll, scrub, scope change, filter or opened photograph closes the latch, and a record arriving after that is written to disk and takes effect next launch. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -138,6 +138,18 @@ impl PlaceStore {
|
||||
}
|
||||
}
|
||||
|
||||
/// Take a record only if it is newer than what is here.
|
||||
///
|
||||
/// The one operation the sync needs on the way in, and it is written here
|
||||
/// rather than at the call site so that "newer wins" has exactly one
|
||||
/// spelling. Returns whether the stored record changed.
|
||||
pub fn adopt(&self, incoming: &Place) -> bool {
|
||||
if !incoming.supersedes(self.load().as_ref()) {
|
||||
return false;
|
||||
}
|
||||
self.save(incoming);
|
||||
true
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
@@ -249,6 +261,36 @@ mod tests {
|
||||
assert!(!place.supersedes(Some(&place.clone())));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn adopt_takes_only_what_is_newer() {
|
||||
let store = PlaceStore::open_at(dir("adopt").join("place.json"));
|
||||
store.save(&a_place(200));
|
||||
|
||||
let mut stale = a_place(100);
|
||||
stale.path = "somewhere/else.NEF".into();
|
||||
assert!(!store.adopt(&stale));
|
||||
assert_eq!(store.load().unwrap().at, 200, "ours is untouched");
|
||||
|
||||
let mut fresh = a_place(300);
|
||||
fresh.path = "somewhere/else.NEF".into();
|
||||
assert!(store.adopt(&fresh));
|
||||
assert_eq!(store.load().unwrap().path, "somewhere/else.NEF");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_record_from_another_device_is_taken_on_its_timestamp_alone() {
|
||||
// `device` is for reading the file, not for resolving a conflict: a
|
||||
// handover from the tablet is exactly the case this feature exists for,
|
||||
// and preferring our own record would defeat it.
|
||||
let store = PlaceStore::open_at(dir("device").join("place.json"));
|
||||
store.save(&a_place(100));
|
||||
|
||||
let mut theirs = a_place(101);
|
||||
theirs.device = "tablet".into();
|
||||
assert!(store.adopt(&theirs));
|
||||
assert_eq!(store.load().unwrap().device, "tablet");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn the_filter_survives_the_projection() {
|
||||
// The record and the query must narrow the grid by the same thing, or a
|
||||
|
||||
Reference in New Issue
Block a user