fix: an imported backup no longer rolls progress back
The progress store merged like the mark stores do, keeping whichever copy had the earlier timestamp. That is right for a mark — `at` is when the reader made it, membership is the whole fact, and the oldest stamp is the truest one — and wrong here, because this record *is* the data. A status, an effort, a note and whether an event repeats all live in it, and `at` says when one of those last changed. So the earlier copy winning discarded every edit made after it, in both of the directions an import actually happens in: restoring a backup taken before an evening's work undid the evening, and importing an old file onto a device with newer progress rolled the device back. Neither is recoverable — there is no account and no server holding a second copy. The later record wins now. Nothing is removed in either direction, and taking the maximum of two timestamps is as order-independent and idempotent as the rule it replaces. `mergeProgress` came out of the hook to be tested, which is also how the ignored store's opposite rule is now written down rather than assumed. Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
This commit is contained in:
co-authored by
Claude Opus 5
parent
7bb7dc843c
commit
f9ef21a366
@@ -62,6 +62,50 @@ function isEmpty(p: Progress): boolean {
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Union merge on import, keeping whichever copy was touched last. Never removes.
|
||||
*
|
||||
* `useMarkSet` keeps the **earlier** of two marks and is right to. There `at` is
|
||||
* when the reader made the mark, membership in the set is the whole fact, and
|
||||
* the oldest timestamp is the truest answer to "when did they say this?" —
|
||||
* nothing is lost by preferring it.
|
||||
*
|
||||
* This store is the opposite shape, and it was merging the same way. Here the
|
||||
* record *is* the data — a status, an effort, a note, whether it repeats — and
|
||||
* `at` is when they last changed one of those. Keeping the earlier copy
|
||||
* therefore discards every edit made after it, in both of the directions an
|
||||
* import actually happens in: restoring a backup taken before an evening's work
|
||||
* rolls that evening back, and importing an old file into a device with newer
|
||||
* progress rolls the device back. Neither is recoverable, because nothing else
|
||||
* holds a copy.
|
||||
*
|
||||
* So the later record wins. Nothing is removed either way — an id present on
|
||||
* only one side always survives — which is the guarantee docs/DATA-MODEL.md
|
||||
* § Import makes, and taking the maximum of two timestamps keeps the merge
|
||||
* order-independent and idempotent exactly as the old rule was.
|
||||
*
|
||||
* A record whose `at` is not a string is an import that has been edited or
|
||||
* truncated. It can still land under an id nothing holds yet, but it never wins
|
||||
* a comparison against a record that does carry one.
|
||||
*/
|
||||
export function mergeProgress(
|
||||
current: ProgressMap,
|
||||
incoming: ProgressMap,
|
||||
): ProgressMap {
|
||||
const touchedAt = (p: Progress): string =>
|
||||
typeof p.at === "string" ? p.at : "";
|
||||
|
||||
const next = { ...current };
|
||||
for (const [id, value] of Object.entries(incoming)) {
|
||||
const existing = next[id];
|
||||
next[id] =
|
||||
existing === undefined || touchedAt(value) > touchedAt(existing)
|
||||
? value
|
||||
: existing;
|
||||
}
|
||||
return next;
|
||||
}
|
||||
|
||||
export function useProgress() {
|
||||
const [progress, setProgress] = useState<ProgressMap>(load);
|
||||
|
||||
@@ -112,17 +156,8 @@ export function useProgress() {
|
||||
[patch],
|
||||
);
|
||||
|
||||
/** Union merge on import, keeping the earlier entry. Never removes. */
|
||||
const merge = useCallback((incoming: ProgressMap) => {
|
||||
setProgress((prev) => {
|
||||
const next = { ...prev };
|
||||
for (const [id, value] of Object.entries(incoming)) {
|
||||
const existing = next[id];
|
||||
next[id] =
|
||||
existing === undefined || value.at < existing.at ? value : existing;
|
||||
}
|
||||
return next;
|
||||
});
|
||||
setProgress((prev) => mergeProgress(prev, incoming));
|
||||
}, []);
|
||||
|
||||
return {
|
||||
|
||||
Reference in New Issue
Block a user