Inwerpsel / use-theme-editor

A React theme editor
GNU General Public License v3.0
4 stars 0 forks source link

Preserve history across page loads #58

Closed Inwerpsel closed 3 months ago

Inwerpsel commented 7 months ago

Current behavior

Currently, local storage is already used to store a part of the current state, however when reloading the page you lose all editor history (prior states and, when not at the tail of history, future states).

New behavior

It should preserve the entire history timeline in browser storage, and restore it as much as possible when you load the page. The history offset should be restored to the same position and locks should be preserved.

Technical choices

It can still save state in local storage, so that the current state can be synchronously read.

IndexedDB

Local storage is great for storing small values and values that don't frequently change, because it's synchronous. However, if you want to store a large list/object and incrementally update it, you can only really overwrite the whole value, which gets expensive and blocks the UI thread.

IndexedDB is actually not that great for UI state in general, the async interaction makes rendering components that use this data significantly more complicated, compared to local storage. For this use case, this is not a real problem, since the data only needs to be read once, when the page loads.

For writing it's even better, because it happens in a separate thread, and so doesn't block the UI thread regardless of the size of what is stored.

Challenges

Handling reducers that are not yet added at the time history is restored

Currently, reducers for state in the history timeline are only added when a component needs them.

If history is replayed without rendering every state, it won't have the reducer for some of the actions yet. This particularly affects state that uses dynamic keys, but can happen to any state.

This can be solved by creating a queue with actions for every key, so that the queue can be replayed the first time the state is accessed. While replaying, every intermediate value should be stored in the corresponding history entry.

Locks can make this challenging, as the actions in the queue have to be on top of this locked state, not the previous state. Probably only way about this is to store the locked value on those actions, as the lock value could come from a future history entry that gets removed.

Keeping storage size low

Some state, in particular the "open groups" state, gets quite big and inflates the DB.

Could also be interesting to see how exactly IndexedDB stores strings. If it's not that efficient, it might be better to store a numeric index to a table of frequent string values.

Upper limit and sunsetting older entries

Every N entries, it can create a snapshot from which history replaying can be started. N should balance total replay time versus storage size.

Entries older than N can be stored in a separate storage, perhaps filtered by only some interesting events and squashed. To keep things simple, they can just be removed for now.

Validating the replayed history

If local storage is also used to store the current point in time, it could take precedence over state that is created by replaying the timeline.

Restoring inspection state

If the page doesn't change, the position in the document can be serialized and restored for elements in the inspection history.

This can work purely with the index within the parent, starting from the document body.

It can also store element id attributes, which can be used as a recovery mechanism, where it can start looking from the deepest id if there was no structural match. Perhaps the id can even take precedence.

Inspection performance

While it's easiest to immediately restore all inspection state, it's way too slow to do an arbitrary amount of times when you start the page. While inspection performance is expected to improve a lot, it still will be necessary to delay the inspection until it has to be displayed.

Handling changes to the page structure

Only some types of changes to page structure are accounted for by using the id fallback.

Other possibilities:

Handling multiple pages

2 options:

Second is probably easier for now, and along with using the id fallback could actually be quite useful.