Skip to content

feat: server-side client state persistence #8314

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 11 commits into
base: main
Choose a base branch
from

Conversation

psychedelicious
Copy link
Collaborator

@psychedelicious psychedelicious commented Jul 21, 2025

Summary

Move client state persistence from browser to server.

  • Add new client state persistence service to handle reading and writing client state to db & associated router. The API mirrors that of LocalStorage/IndexedDB where the set/get methods both operate on keys. For example, when we persist the canvas state, we send only the new canvas state to the backend - not the whole app state.
  • The data is very flexibly-typed as a pydantic JsonValue. The client is expected to handle all data parsing/validation (it must do this anyways, and does this today).
  • Change persistence from debounced to throttled at 2 seconds. Maybe less is OK? Trying to not hammer the server.
  • Add new persistence storage driver in client and use it in redux-remember. It does its best to avoid extraneous persist requests, caching the last data it persisted and noop-ing if there are no changes.
  • Storage driver tracks pending persist actions using ref counts (bc each slice is persisted independently). If there user navigates away from the page during a persist request, it will give them the "you may lose something if you navigate away" alert.
    • This "lose something" alert message is not customizable (browser security reasons).
    • The alert is triggered only when the user closes the tape while a persist network request is mid-flight. It's possible that the user makes a change and closes the page before we start persisting. In this case, they will lose the last 2 seconds of data.
    • I tried making triggering the alert when a persist was waiting to start, and it felt off.
    • Maybe the alert isn't even necessary. Again you'd lose 2s of data at most, probably a non issue. IMO after trying it, a subtle indicator somewhere on the page is probably less confusing/intrusive.
  • Fix an issue where the redux-remember enhancer was added last in the enhancer chain, which prevented us detecting when a persist has succeeded. This required a small change to the unserialze utility (used during rehydration) to ensure slices enhanced with redux-undo are set up correctly as they are rehydrated.
  • Restructure the redux store code to avoid circular dependencies. I couldn't figure out how to do this without just smooshing it all into the main store.ts file. Oh well.

Implications:

  • Because client state is now on the server, different browsers will have the same studio state. For example, if I start working on something in Firefox, if I switch to Chrome, I have the same client state.
  • Incognito windows won't do anything bc client state is server-side.
  • It takes a bit longer for persistence to happen thanks to the debounce, but there's now an indicator that tells you your stuff isn't saved yet.
  • Resetting the browser won't fix an issue with your studio state. You must use Reset Web UI to fix it (or otherwise hit the appropriate endpoint). It may be possible to end up in a Catch-22 where you can't click the button and get stuck w/ a borked studio - I think to think through this a bit more, might not be an issue.
  • It probably takes a bit longer to start up, since we need to retrieve client state over network instead of directly with browser APIs.

Other notes:

  • We could explore adding an "incognito" mode, enabled via invokeai.yaml setting or maybe in the UI. This would temporarily disable persistence. Actually, I don't think this really makes sense, bc all the images would be saved to disk.
  • The studio state is stored in a single row in the DB. Currently, a static row ID is used to force the studio state to be a singleton. It is possible to support multiple saved states. Might be a solve for app workspaces.

Related Issues / Discussions

n/a

QA Instructions

Try it out. It's pretty straightforward. Error states are the main things to test - for example, network blips. The new server-side persistence driver is the only real functional change - everything else is just kinda shuffling things around to support it.

Merge Plan

n/a

Checklist

  • The PR has a short but descriptive title, suitable for a changelog
  • Tests added / updated (if applicable)
  • Documentation added / updated (if applicable)
  • Updated What's New copy (if doing a release after this PR)

@github-actions github-actions bot added api python PRs that change python files services PRs that change app services frontend PRs that change frontend files labels Jul 21, 2025
psychedelicious added a commit that referenced this pull request Jul 21, 2025
We intermittently get an error like this:
```
TypeError: Cannot read properties of undefined (reading 'length')
```

This error is caused by a `redux-undo`-enhanced slice being rehydrated
without the extra stuff it adds to the slice to make it undoable (e.g.
an array of `past` states, the `present` state, array of `future`
states, and some other metadata).

`redux-undo` may need to check the length of the past/future arrays as
part of its internal functionality. These keys don't exist so we get the
error. I'm not sure _why_ they don't exist - my understanding of
`redux-undo` is that it should be checking and wrapping the state w/ the
history stuff automatically. Seems to be related to `redux-remember` -
may be a race condition.

The solution is to ensure we wrap rehydrated state for undoable slices
as we rehydrate them. I discovered the solution while troubleshooting
#8314 when the changes therein somehow triggered the issue to start
occuring every time instead of rarely.
psychedelicious added a commit that referenced this pull request Jul 21, 2025
We intermittently get an error like this:
```
TypeError: Cannot read properties of undefined (reading 'length')
```

This error is caused by a `redux-undo`-enhanced slice being rehydrated
without the extra stuff it adds to the slice to make it undoable (e.g.
an array of `past` states, the `present` state, array of `future`
states, and some other metadata).

`redux-undo` may need to check the length of the past/future arrays as
part of its internal functionality. These keys don't exist so we get the
error. I'm not sure _why_ they don't exist - my understanding of
`redux-undo` is that it should be checking and wrapping the state w/ the
history stuff automatically. Seems to be related to `redux-remember` -
may be a race condition.

The solution is to ensure we wrap rehydrated state for undoable slices
as we rehydrate them. I discovered the solution while troubleshooting
#8314 when the changes therein somehow triggered the issue to start
occuring every time instead of rarely.
@psychedelicious psychedelicious force-pushed the psyche/feat/app/client-state-persistence branch from 63cab3f to d0d4783 Compare July 22, 2025 05:45
@github-actions github-actions bot added the frontend-deps PRs that change frontend dependencies label Jul 22, 2025
@psychedelicious psychedelicious marked this pull request as ready for review July 22, 2025 10:05
@github-actions github-actions bot added the python-tests PRs that change python tests label Jul 22, 2025
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
api frontend PRs that change frontend files frontend-deps PRs that change frontend dependencies python PRs that change python files python-tests PRs that change python tests services PRs that change app services
Projects
None yet
Development

Successfully merging this pull request may close these issues.

1 participant