Skip to content

Comments

fix: Improve autosave and unsaved changes detection#104

Merged
dcalhoun merged 3 commits intotrunkfrom
fix/improve-autosave-and-unsaved-changes-detection
Feb 26, 2025
Merged

fix: Improve autosave and unsaved changes detection#104
dcalhoun merged 3 commits intotrunkfrom
fix/improve-autosave-and-unsaved-changes-detection

Conversation

@dcalhoun
Copy link
Member

@dcalhoun dcalhoun commented Feb 20, 2025

Related:

What?

Improve autosave and change detection logic, aligning more closely with the
approach of Gutenberg Mobile.

Why?

Previously, host apps determined whether content changes were present by
comparing the content received from GutenbergKit to the latest server-provided
post content. This is problematic as the server makes slight modifications to
content markup, creating perpetually changed content.

How?

To address this, we need GutenbergKit to communicate changed content. We
accomplish by sending a new changed parameter alongside the title and content.
Rather than updating the post and content refs on every content change, we now
do so at the time at which the host app requests the latest content. This
ensures the host and GutenbergKit synchronize their latest perception of
changes.

We also now rely upon AutosaveMonitor to throttle how often we request the
host app persist the latest changes, rather than a generic store subscription.

Testing Instructions

See sibling PRs for platform-specific testing instructions:

Accessibility Testing Instructions

N/A, no user-facing changes.

Screenshots or screencast

N/A, no user-facing changes.

@dcalhoun dcalhoun added the [Type] Bug An existing feature does not function as intended label Feb 20, 2025
Previously, host apps determined whether content changes were present by
comparing the content received from GutenbergKit to the latest
server-provided post content. This is problematic as the server makes
slight modifications to content markup, creating perpetually changed
content.

To address this, we need GutenbergKit to communicate changed content.
We accomplish by sending a new `changed` parameter alongside the title
and content. Rather than updating the post and content refs on every
content change, we now do so at the time at which the host app requests
the latest content. This ensures the host and GutenbergKit synchronize
their latest perception of changes.

We also now rely upon `AutosaveMonitor` to throttle how often we request
the host app persist the latest changes, rather than a generic store
subscription.
We must register blocks prior to attempts to serialize the raw post
content within the editor.

This change was not tested. It should be tested once we re-engage work
on the remote editor.
receiveEntityRecords( 'postType', post.type, post );

setupEditor( post, {} );
registerCoreBlocks();
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This was relocated to the entry file as the blocks must be registered before we parse the raw post content provided by the host app.

postContentRef.current = serialize( parse( post.content.raw || '' ) );

Comment on lines +27 to +29
if ( postContentRef.current === null ) {
postContentRef.current = serialize( parse( post.content.raw || '' ) );
}
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Explicitly selectively instantiated to avoid unnecessary performance costs.

Comment on lines +59 to +62
if ( changed ) {
postTitleRef.current = title;
postContentRef.current = content;
}
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Selectively updating these refs allows us to accurately communicate a "content has changed" status to the host app, which is important for WordPress-iOS' implementation.

Comment on lines -94 to -106
useEffect( () => {
return subscribe( () => {
const { title, content } = window.editor.getTitleAndContent();
if (
title !== postTitleRef.current ||
content !== postContentRef.current
) {
onEditorContentChanged();
postTitleRef.current = title;
postContentRef.current = content;
}
} );
}, [] );
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This generic subscription is replaced by the AutosaveMonitor, which provides a more robust implementation, including intervals and dirty checks.

@dcalhoun dcalhoun marked this pull request as ready for review February 20, 2025 20:48
@dcalhoun dcalhoun requested a review from kean February 20, 2025 20:48
Copy link
Contributor

@kean kean left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Approved as it's an improvement. I would, however, considering implementing it in a slightly different way:

  • The app initialize the editor with the initial title and content
  • The editor keeps track of whether the post has changes (title/content) and communicates it via a new property EditorState/hasChanges.
  • If the app receives an update with hasChanges set to true, it only then triggers the autosave logic to put data on disk. Autosave happens infrequently and only as a crash-protection measure. This is the only time the app needs to get the latest content from the editor.
  • The app never changes the title or content on its side. If it needs to, it uses one of the editor bridge APIs to do so (e.g. when restoring from a previous revision)

This setup will minimize the need to pass serialized content via the bridge and establish the source of truth for the content – the editor. I might be missing something, but it seems sound.

@dcalhoun
Copy link
Member Author

I would, however, considering implementing it in a slightly different way:

  • The app initialize the editor with the initial title and content
    [...]
  • The app never changes the title or content on its side. If it needs to, it uses one of the editor bridge APIs to do so (e.g. when restoring from a previous revision)

My perception is that our approach accomplishes these two aspect currently. Please correct me if I'm wrong.

  • The editor keeps track of whether the post has changes (title/content) and communicates it via a new property EditorState/hasChanges.
  • If the app receives an update with hasChanges set to true, it only then triggers the autosave logic to put data on disk. Autosave happens infrequently and only as a crash-protection measure. This is the only time the app needs to get the latest content from the editor.

Given the AutosaveMonitor already limits its calls of onEditorContentChange to when changes exist, in a way, we already achieve this; we just do not rely upon a dedicated state variable in the host app. I.e., onEditorContentChange → host autosave is only ever triggered when content was changed, which then leads to the host requesting serialized content from the library.

As onEditorContentChange and getTitleAndContent are two separate loops running at different times, the new changed parameter ensures library communicates the latest state at the time the host is seeking to retrieve/persist the latest content.

This setup will minimize the need to pass serialized content via the bridge and establish the source of truth for the content – the editor. I might be missing something, but it seems sound.

I agree avoiding unnecessary bridge communication is a worthwhile target. However, it's unclear to me whether the proposed changes would reduce communication. That said, I am very open to refactors to reduce complexity. I'd just like to understand the proposal better—if you are willing to open pull requests expressing the idea, even better.

@kean
Copy link
Contributor

kean commented Feb 25, 2025

I agree avoiding unnecessary bridge communication is a worthwhile target. However, it's unclear to me whether the proposed changes would reduce communication.

One of the reasons would be to ensure that the "Publish/Save" button updates (nearly) instantly after you make a change, otherwise it feels a bit slow. It's, of course, not the end of the world.

In terms of the performance, my assumption is that serializing and sending the entire post content on every keystone is not feasible, especially with larger posts. Perhaps, there is an acceptable setting that balances performance and responsiveness. Unfortunately, I'm not sure how to measure it, but I'm suggesting a potential design where no serialization is needed*, so it never becomes a bottleneck. The logic that responds to changes and determines if the content is different from the initial content will live entirely in the JS land.

*it will only be needed once every ~10 seconds for autosave

@dcalhoun
Copy link
Member Author

One of the reasons would be to ensure that the "Publish/Save" button updates (nearly) instantly after you make a change, otherwise it feels a bit slow.

I agree, this would be nice. I'll note that Gutenberg Mobile (GBM) does not perform this way at the moment.

[...] I'm suggesting a potential design where no serialization is needed*, so it never becomes a bottleneck.

I understood the intent; I fail to understand how your proposal's execution differs from what I shared, specifically regarding performance.

My perception now is that the critical piece you suggest is immediately updating UI based on the onEditorContentChange event and only applying an interval to the persistence act. Currently, both UI updates (Update/Publish button state) and persistence (autosave) are performed on an interval managed in the host app—current is seven seconds, the proposal is one second (mirroring GBM).

I believe accomplishing immediate updates for the UI involves a larger refactor of code untouched in the proposed changes. Namely, the host manages the enablement of the Publish/Update button via changes to the post changes attribute. Updating the post changes attribute requires the latest content from the editor. We'd need to replace the UI's reliance on the post object altogether.

@kean
Copy link
Contributor

kean commented Feb 25, 2025

We'd need to replace the UI's reliance on the post object altogether.

Yeah, the way it uses changes is neat. The "Publish" button does need to track not only the content but also the post settings changes.

the proposal is one second (mirroring GBM).

Yeah, maybe it's OK. In terms of UX, I'd say it feels fine. Performance – not sure how to measure. I think you convinced me that your version is ultimately better.

@dcalhoun dcalhoun merged commit 947c531 into trunk Feb 26, 2025
8 of 9 checks passed
@dcalhoun dcalhoun deleted the fix/improve-autosave-and-unsaved-changes-detection branch February 26, 2025 15:04
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

[Type] Bug An existing feature does not function as intended

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants