fix: a save merges this page's tracked token change into the stored list (closes #717)
A save wrote `trackedTokens` whole, so two pages on one network dropped each other's adds and brought back each other's removals. A save that keeps the network now merges this page's change into the stored list by contract address. A save that switches networks is unchanged. `mergeTrackedTokens()` does that merge, and the two other merges of tracked tokens now call it. It keeps one record per contract address, this page's for a token both lists hold, so a token held twice no longer multiplies. Model: opus-5-5
This commit was merged in pull request #726.
This commit is contained in:
+66
-22
@@ -404,6 +404,36 @@ function mergeNetworkEndpoints(base, ours, theirs) {
|
||||
return mergeMapByKey(base, ours, theirs, mergeEndpointEntry);
|
||||
}
|
||||
|
||||
// Merges one network's tracked tokens by contract address:
|
||||
//
|
||||
// - A token another page added or removed since this page's last save stays
|
||||
// added or removed.
|
||||
// - A token that this page and `theirs` both hold keeps this page's record.
|
||||
// - The result holds one record per contract address.
|
||||
//
|
||||
// mergeListByIdentity() keeps records that share an identity, on purpose, for
|
||||
// wallets. A contract address names one token, so this keeps one record for it.
|
||||
// Without that, a token that `ours` and `theirs` each hold twice comes back four
|
||||
// times, and each later merge multiplies it again.
|
||||
//
|
||||
// `theirs` takes this page's records first. mergeListByIdentity() would
|
||||
// otherwise log an error for two different records of one token.
|
||||
function mergeTrackedTokens(base, ours, theirs) {
|
||||
const contract = (token) => token.address.toLowerCase();
|
||||
const oursByContract = new Map(
|
||||
ours.map((token) => [contract(token), token]),
|
||||
);
|
||||
const merged = mergeListByIdentity(
|
||||
base,
|
||||
ours,
|
||||
theirs.map((token) => oursByContract.get(contract(token)) || token),
|
||||
contract,
|
||||
(baseToken, ourToken) => ourToken,
|
||||
);
|
||||
const byContract = new Map(merged.map((token) => [contract(token), token]));
|
||||
return [...byContract.values()];
|
||||
}
|
||||
|
||||
// Returns a copy of `stored.networkTrackedTokens` for when this page has not yet
|
||||
// taken the switch in `stored`. `page` is this page's state. A token that this
|
||||
// page tracked or removed since its last save is tracked or removed in the list
|
||||
@@ -411,12 +441,10 @@ function mergeNetworkEndpoints(base, ours, theirs) {
|
||||
function networkTrackedTokensWithChange(stored, page) {
|
||||
return {
|
||||
...stored.networkTrackedTokens,
|
||||
[page.networkId]: mergeListByIdentity(
|
||||
[page.networkId]: mergeTrackedTokens(
|
||||
baseline.trackedTokens,
|
||||
page.trackedTokens,
|
||||
stored.networkTrackedTokens[page.networkId],
|
||||
(token) => token.address.toLowerCase(),
|
||||
(base, ours) => ours,
|
||||
stored.networkTrackedTokens[page.networkId] || [],
|
||||
),
|
||||
};
|
||||
}
|
||||
@@ -461,23 +489,27 @@ function networkTrackedTokensWithChange(stored, page) {
|
||||
// comment at mergeEndpointEntry() for why the collision this closes is
|
||||
// milder than the other two.
|
||||
//
|
||||
// `trackedTokens` is merged by contract address (mergeTrackedTokens()) when this
|
||||
// page has changed it, so a token another page added or removed since this
|
||||
// page's last save stays added or removed. A token both lists hold keeps this
|
||||
// page's record. Two saves differ:
|
||||
//
|
||||
// - When the stored record is on another network than this page's, a save keeps
|
||||
// the stored `trackedTokens`. It merges this page's change into the list in
|
||||
// `networkTrackedTokens` for this page's network, by contract address.
|
||||
// - A save that switches networks writes this page's list whole. A switch in
|
||||
// Settings first merges that list by contract address
|
||||
// (takeStoredTrackedTokens()).
|
||||
//
|
||||
// Every other persisted field stays a whole-field diff:
|
||||
// `fraudContracts`/`viewStack` are arrays of scalars with no per-element
|
||||
// identity to merge by; a save diffs `trackedTokens` whole, and a switch in
|
||||
// Settings first merges that list by contract address
|
||||
// (takeStoredTrackedTokens()); `networkTrackedTokens` is diffed whole like
|
||||
// `trackedTokens` and `networkId`, the two fields a network switch moves
|
||||
// together with it; `tokenHolderCache` is a map shaped like the ones above,
|
||||
// but nothing in src/ ever writes an entry into it — it is only ever reset
|
||||
// wholesale to `{}` (applyChainSwitchFields()) — so there is no in-place
|
||||
// mutation for a whole-field diff to collide with; `viewData` is this page's
|
||||
// own UI scratch space, not data another page has any reason to share
|
||||
// membership of.
|
||||
//
|
||||
// One exception: when the stored record is on another network than this
|
||||
// page's, a save keeps the stored `trackedTokens`. It merges this page's change
|
||||
// into the list in `networkTrackedTokens` for this page's network, by contract
|
||||
// address.
|
||||
// identity to merge by; `networkTrackedTokens` is diffed whole like `networkId`,
|
||||
// which a network switch moves together with it; `tokenHolderCache` is a map
|
||||
// shaped like the ones above, but nothing in src/ ever writes an entry into it —
|
||||
// it is only ever reset wholesale to `{}` (applyChainSwitchFields()) — so there
|
||||
// is no in-place mutation for a whole-field diff to collide with; `viewData` is
|
||||
// this page's own UI scratch space, not data another page has any reason to
|
||||
// share membership of.
|
||||
//
|
||||
// This does not make two pages that both change the SAME leaf concurrently
|
||||
// safe: last write wins on that one leaf, same as before. What it removes
|
||||
@@ -564,6 +596,20 @@ async function saveStateOnce(beforeWrite) {
|
||||
fresh,
|
||||
current,
|
||||
);
|
||||
} else if (
|
||||
baseline !== null &&
|
||||
baseline.networkId === current.networkId &&
|
||||
!deepEqual(current.trackedTokens, baseline.trackedTokens)
|
||||
) {
|
||||
// This page's network is the one it last saved on, and the stored
|
||||
// record is on it too, so all three lists are of one network. This
|
||||
// page's change goes into the stored list, and another page's add or
|
||||
// removal stays (https://git.eeqj.de/sneak/AutistMask/issues/717).
|
||||
merged.trackedTokens = mergeTrackedTokens(
|
||||
baseline.trackedTokens,
|
||||
current.trackedTokens,
|
||||
fresh.trackedTokens,
|
||||
);
|
||||
}
|
||||
merged.hasWallet = Boolean(merged.wallets && merged.wallets.length > 0);
|
||||
// Stamped on every write, never merged or diffed: the record that goes to
|
||||
@@ -680,12 +726,10 @@ function takeStoredNetwork(stored) {
|
||||
// `baseline` takes the stored list, so the save stores the list in `state`
|
||||
// whenever the two differ.
|
||||
function takeStoredTrackedTokens(stored) {
|
||||
rawState.trackedTokens = mergeListByIdentity(
|
||||
rawState.trackedTokens = mergeTrackedTokens(
|
||||
baseline.trackedTokens,
|
||||
rawState.trackedTokens,
|
||||
stored.trackedTokens,
|
||||
(token) => token.address.toLowerCase(),
|
||||
(base, ours) => ours,
|
||||
);
|
||||
baseline.trackedTokens = structuredClone(stored.trackedTokens);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user