jkomoros / card-web

The web app behind thecompendium.cards
Apache License 2.0
46 stars 8 forks source link

Modify local cards on edit instead of waiting for server #581

Closed jkomoros closed 2 years ago

jkomoros commented 2 years ago

A lot of performance problems happen because cards are updated by the server one at time (e.g. when the timestamp finalizes, or after updateInboundLinks fires).

One thing we could do is have it so on every edit we make local edits to the cards collection in state mirroring what we expect the server to ultimately do. Then when we receive new cards in updateCards, we compare each one to the ones we have locally, and if they're equivalent (skipping any timestamp fields) then we drop that update on the floor.

One of the reasons that updateInboundLinks is done on the server is because we can't guarantee that the editor of the card has access to all of the cards pointed to, although realistically that's very very rare (the only time it would happen is if a card already had a reference to a card the editor can't see, and then it's removed it).

Related to #538 and #517, because those problems would be obviated by this

jkomoros commented 2 years ago

applyCardFirebaseUpdate should be pretty useful

jkomoros commented 2 years ago

modifyCardWithBatch is the one callsite that needs to change-- should take another object (instead of or in addition to batch) to make the updates on

One oddity is if sections/tags change, then filters might not update until later when those changes come back, leading to out of sync behavior (which is technically possible now, too). Maybe we should move to having section/tag membership be driven entirely based on what the card itself says (as of when it was seen by updateCards), and have the section/tag firestore get be used entirely for prettyString/color/permission information, not filter membership?

... Or just live with the fact that it's possible those two are slightly different, which is true today. And plus, those should come back from firestore natively very quickly (the thing that is slow is updateInboundLinks and timestamps)

jkomoros commented 2 years ago

(Just use Firestore.Timestamp.now() to generate faux firestore fields)

jkomoros commented 2 years ago

134 gets much more important now that the finicky inboundLinksUpdates is duplicated on server and client

jkomoros commented 2 years ago

Actually turning on the local inboundLinks updating can't be done yet because of a common problem.

Say you have a collection of 3 cards, and you're adding a see_also reference from each of them in a batch edit to ANOTHER_CARD.

The machinery correctly identifies that each of the three cards will update, as well as the ANOTHER_CARD, which will receive three new inbound updates. The firestore layer immediately tells updateCards that the 3 outward cards will update, and then right after that the local overlay is applied and also says, "and ANOTHER_CARD will also get three new inbound references"

... But then as each of the three outboudn cards has its updateInboundLinks run on the server, updateCards is called on ANOTHER_CARD three separate times as its inbound links incrementally extends. And the first time, the machinery sees that it's not equivalent to the one that was set via overlay, so it swaps out with the new one (which only has one of the inbound liknks set yet). And then the next two stream in, and each doesn't match.

The more common case of "edit one card to add/drop multiple other references in one edit" should work. But this multi-edit case is pretty common too, and ideally we should be able to have it not thrash, too.

This is not an easy one to fix; it's not that we can say "ignore all future updates to this card, this version we're giving you now is the canonical" (because at some point the incremental inboudn links updates will finish and at that point we should listen to them), or "ignore the next 5 updates" (because there could be an interleaved real edit from elsewhere that needs to be incorporated).

I guess there will have to be a solution of "look, I recognize these incremental updates will stream in; if you see any of these references being added, just drop that update", and as each one streams in, remove it from the "expected changes" list.

So maybe we store a diff somewhere in state and chip away at it as the updates stream in until no diff is left? So the logic is "either the existing card already fully incorporates the edit OR the expected diff has parts, matching the diff of the change, that has not been consumed."

jkomoros commented 2 years ago

A generalized system would be something like "here is the diff from previous state to expected prior state" and every incremental change that comes in would either be a no-op (discarded) or a incremental or full part of that diff that eats away at it until it's gone. (Note this wouldn't naively work right now because the initial main update for cards happens BEFORE the overlay is applied, since firestore does the local optimistic updates immediately).

But that's quite a bit of machinery, diffing, etc. Another thing is just to observe that this only ever happens for inbound references, and handle that specially. Basically it treats updates thare are only inbound links to be no-ops unless updateCards is called with a forceUpdateInboundLinks, which is only called from updateCardOverlay. There's still a small problem with this: what if another clients edits lead to inboundLinks updates that were not initiated by this client? That system would erroneously ignore them.

Anything that special-cases inbound links behaviors in the diffing machinery will need to be updated (perhaps in non-obvious ways) if updateInboundLinks ever is updated to do anything else in addition.

jkomoros commented 2 years ago

The remainder of the work on this will be obviated when we fix #585

jkomoros commented 2 years ago

Now that #585 is fixed, this issue can be fully closed