Skip to content

RTC: Remove excess autosave notices (when not useful) - #80539

Merged
alecgeatches merged 36 commits into
trunkfrom
improve/rtc-autosave-notice
Jul 31, 2026
Merged

RTC: Remove excess autosave notices (when not useful)#80539
alecgeatches merged 36 commits into
trunkfrom
improve/rtc-autosave-notice

Conversation

@alecgeatches

@alecgeatches alecgeatches commented Jul 21, 2026

Copy link
Copy Markdown
Contributor

What?

Closes #80077.

Image

Avoid showing the "There is an autosave of this post that is more recent than the version below" notice in RTC when content in the autosave is already present in the current document.

Note that the line count change is big here, but that is currently ~50% test code. Most functional changes are in use-autosave-notice.js, which refactors autosave-related functionality from the general provider into a hook, and most of the rest is plumbing for autosave markers in the CRDT.

Why?

In RTC, all user autosaves target a revision and not the actual parent post. In a traditional WordPress draft, the post author updates the actual parent post, but other users update revisions. As described in #75105 this makes post reconciliation differ per-user, so instead all users in RTC target revisions until the post is manually saved.

As a result, this makes the autosave notice much more visible in RTC. After an autosave fires, WordPress detects that the latest autosave is newer than the parent post and sets an autosave flag on load. Gutenberg reads the flag and shows the more recent autosave notice. Because we're always targeting a revision until a real save happens, the autosave warning is always triggered.

This is very common in RTC. If users A and B are working in a document and haven't saved recently, user C joining will always see a more recent autosave notice, despite the fact that the CRDT document is up-to-date and there is no missing content to "restore".

How?

The fix is to store when a user created an autosave in the CRDT doc:

  1. User A and B are editing a post in RTC.

  2. User A makes changes and autosaves.

  3. User C joins, and WordPress detects a newer autosave than the parent post from user A.

  4. User C connects to the CRDT doc and reads user A's autosave marker, whose timestamp matches the autosave that WordPress flagged as new. Both values are the server's own modified time for the same autosave revision, so no client clocks are involved. The marker was written by user A into the CRDT document after the autosave succeeded, which means after the content the autosave captured had already entered the document.

    Because Yjs delivers a client's updates in order, any copy of the document that contains User A's marker necessarily contains that content too. User C can trust that all of the prior content in user A's autosave is also present in the live document.

  5. Because the autosave was successfully recorded in the CRDT doc, user C skips showing the autosave notice.

There are a few different scenarios that this PR handles. For the videos below, AUTOSAVE_INTERVAL is set to 20 seconds to make reproduction faster.

Author autosaves while synced with peers (above)

This is the general annoying state where during a collaborative editing session, any user joining a post sees the autosave warning. Now it's hidden:

autosave-with-peers.mov

Above, users A and B join a post. The post contains "saved content", with a new paragraph of "unsaved content" persisted via autosave. User A persists the autosave and also updates the autosave timestamp in the CRDT document. When user C joins, they can match user A's autosave timestamp with the timestamp in the CRDT doc and ignore the "unsaved" content warning.

Author autosaves solo (queue paused)

When a user using HTTP polling edits a post by themselves, we pause sending CRDT updates until another collaborator has joined to avoid unnecessary chatter. In this scenario, a user's autosaved changes will not end up in the live CRDT document, and the autosave warning is desirable:

autosave-solo-2.mov

Above, a user creates a post with content, saves, and later adds unsaved content. After an autosave fires, the user leaves. When the user rejoins, there is a recent autosave but no matching CRDT entry for the autosave, so we show the warning. Note that the warning appears roughly immediately: as soon as a sync happens, we're able to verify the autosave key is missing from the document.

RTC backend is unreachable

This situation is very similar to the one above (where we pause updates to the backend), but we can demonstrate how this works with WebSockets when an sync server connection fails during editing. Below, we use the built-in npm run rtc:ws:slow setup but kill the WebSockets server. In the grace period between connecting to a post and seeing the disconnection dialog, a user still may persist an autosave that does not existing CRDT content.

A user starts a post with a successful WebSockets connection and saves content. The user then loses access to the sync backend, and during the disconnection grace period add content that produces an autosave. This content is not persisted in the CRDT due to the connection loss. When the user reloads the post, one of two things happen:

  1. If the WebSocket server is still down, the user attempts to connect to the server. After not receiving a response for 3 seconds the editor can't tell if the autosave content has been correctly persisted, and shows the notice. The idea is "fail visible". If we can't know for sure the autosave is available in the CRDT, show the autosave notice.

    ws-disconnected-autosave.mov
  2. If the WebSocket server is back, the user will reconnect to the WebSockets CRDT doc and know that the autosave is newer than the content and show the notice immediately:

    ws-reconnect-autosave.mov

    Note that this requires the transport to implement an onInitialSync callback, but if a provider doesn't have that flag the 3-second timeout still will fire either way and show the notice as above.

Autosave predates RTC

One issue discussed in fully removing the autosave notice in RTC is pre-RTC autosaves. The solution in this PR will detect that the autosave is not represented in the CRDT after RTC is enabled and still show the notice:

autosave-pre-rtc.mov

Above, we need two users as user A (who created the post) will autosave directly to the post content. With RTC disabled, user A creates a post and saves. User B joins and makes a change that results in an autosave. Later, RTC is enabled for the site. After RTC is enabled, both users (only user B shown in video) can correctly see the autosave notice for the unsaved content.

RTC is disabled

No video here, but when RTC is fully disabled we will still always show the autosave notice when reported by the editor, which is how it currently works.

Testing Instructions

Please see the scenarios above. In general, try some different situations where RTC is enabled and a user fires an autosave request in DevTools. If their changes are represented in the CRDT (available when the page loads), the autosave notice should not be triggered on post load. Otherwise, if content isn't in the CRDT due to solo mode or connection issues, the autosave notice should still be visible.

Please give it a try yourself in situations with one and multiple users.

Use of AI Tools

Claude for the whole process.

@alecgeatches alecgeatches self-assigned this Jul 21, 2026
@github-actions github-actions Bot added [Package] Core data /packages/core-data [Package] Editor /packages/editor [Package] Sync /packages/sync labels Jul 21, 2026
@github-actions

github-actions Bot commented Jul 21, 2026

Copy link
Copy Markdown

Size Change: +1.07 kB (+0.01%)

Total Size: 7.77 MB

📦 View Changed
Filename Size Change
build/scripts/core-data/index.min.js 36.5 kB +152 B (+0.42%)
build/scripts/editor/index.min.js 501 kB +759 B (+0.15%)
build/scripts/sync/index.min.js 42.2 kB +157 B (+0.37%)

compressed-size-action

@alecgeatches alecgeatches added the [Feature] Real-time Collaboration Phase 3 of the Gutenberg roadmap around real-time collaboration label Jul 21, 2026
@github-actions

github-actions Bot commented Jul 21, 2026

Copy link
Copy Markdown

Flaky tests detected in 56a6042.
Some tests passed with failed attempts. The failures may not be related to this commit but are still reported for visibility. See the documentation for more information.

🔍 Workflow run URL: https://github.com/WordPress/gutenberg/actions/runs/30563767865
📝 Reported issues:

@alecgeatches alecgeatches added No Core Sync Required Indicates that any changes do not need to be synced to WordPress Core [Type] Bug An existing feature does not function as intended [Type] Task Issues or PRs that have been broken down into an individual action to take and removed [Type] Bug An existing feature does not function as intended labels Jul 22, 2026
@alecgeatches

alecgeatches commented Jul 29, 2026

Copy link
Copy Markdown
Contributor Author

@ingeniumed @chriszarate This is ready for another round of reviews! Thank you for your thoughtful first round. I've hopefully addressed all comments above and made these architectural changes:

  1. Instead of a CRDT document marker, we now store a Y.Snapshot in autosave meta:

    A snapshot (a marked point in time in the Yjs history) is specified using both the set of (clientID, clock) pairs and the set of all deleted item IDs. The deleted set is O(n), but because deletions usually happen in runs, this data set is usually tiny in practice. (The real world editing trace from the B4 benchmark document contains 182k inserts and 77k deleted characters. The deleted set size in a snapshot is only 4.5Kb).

    A snapshot is much smaller than a full CRDT document and can be used to confirm a full set of content + deletion sets is present in a live document. On editor load we now check that a live CRDT content contains the content described by the snapshot. This gives us a more robust check than an autosave timestamp, which may be wrong within a peer-to-peer transport.

    The snapshot is passed on page load in gutenberg_add_autosave_details_to_editor_settings() along with the existing autosave flag.

  2. @ingeniumed noticed that local sessionStorage autosave notices could appear in some new circumstances in this PR. I think this was due to trunk's code always skipping a local notice when a remote autosave was present. Prior to this PR, a remote autosave was pretty much always present which is what we are fixing here, but it shows a similar problem with local session storage. Now the same new snapshot logic has been added to local session saves. This code is shared between local and remote autosave checks in a useEntityContainsSnapshot() hook.

  3. I've removed the initialSync API per @chriszarate's comments above to ensure we don't crowd the sync provider API space with a dubiously effective hasInitialSync flag that may be misleading in peer-to-peer transports. This can delay the autosave notice by 3 seconds in RTC, but only when it contains valid content not in the live document, so it will be more rarely seen.

  4. Some more tests.

I've run through the PR description scenarios again to confirm the autosave notice behavior works the same. The only difference is we now show an autosave after a 3-second delay for the Author autosaves solo and RTC backend is unreachable + WS server is present (part 2) scenarios.

@ingeniumed ingeniumed left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

I can still replicate the same bug that I noticed with the previous implementation, so I'm not recording that once more.

const snapshotStatus = useEntityContainsSnapshot( {
postType,
postId,
snapshot: localAutosave?.crdt_snapshot,

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Would store the crdt_snapshot in a utility and use that instead. Would recommend the same for the PHP usages as well. That way it'll be easy to see where it's used and if we do want to change it, do it in one place. Though I fully get if package deps make this tedious.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

It's a bit awkward to get a private key from core-data to this part of the editor, but I agree .crdt_snapshot-type accessors are fragile. Routed everything through constants and utility functions in 247cdfe.

Comment thread packages/sync/src/crdt-snapshot.ts Outdated
// contained changes nothing, so equality with the local delete set proves
// containment.
const mergedDeleteSet = Y.mergeDeleteSets( [
Y.snapshot( ydoc ).ds,

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Could you re-use the localSnapshot instead?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Fair question! This was on purpose but wasn't documented, added that in 663df41. The quick answer is that Y.mergeDeleteSets also mutates, and we didn't want to mutate localSnapshot here.

Comment thread packages/sync/src/manager.ts Outdated
// Wrap and return the public API.
return {
createPersistedCRDTDoc: debugWrap( createPersistedCRDTDoc ),
entityContainsSnapshot,

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Is there a benefit to debugWrap this?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Can't hurt, fixed in e988a58.

Comment thread packages/core-data/src/actions.js Outdated
);

if ( Number.isFinite( autosavedAt ) ) {
getSyncManager()?.markEntityAutosaved(

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

The new location of getEntitySnapshot() doesn't fully solve this as its still called before direct autosave changes are applied to the CRDT.

A caller (plugin for instance) can pass new content directly to saveEntityRecord (see the example above). That means not via the editor as it's done now. That content enters the autosave payload, but the snapshot describes the previous Yjs state. On reload, that older state satisfies the snapshot and the recovery notice can be suppressed.

The quick fix is to move the existing direct-record CRDT update before the autosave/non-autosave branch. Essentially move the call in line 840 to line 731

Note: This problem exists in trunk, but due to the excess autosave notice issue there's a workaround. One can hit recover autosave and get those untracked changes. Your PR fixes this workaround and makes the bug unrecoverable.

Hopefully that makes sense.

$snapshot = $request->get_param( 'crdt_snapshot' );

if ( ! is_string( $snapshot ) || '' === $snapshot ) {
return;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Would it be worth clearing the existing metadata, if any, when there’s no valid snapshot to replace it? The likelihood of this happening seems small because the existing Gutenberg code supplies the snapshot.

If an autosave already has _crdt_autosave_snapshot metadata and a later request doesn’t include a valid snapshot, the old snapshot will remain attached to the new content. This would probably require a plugin calling the endpoint directly, an older client, or snapshot capture failing.

I don’t think it’s blocking, but it may be worth considering as a safeguard.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Oh, good catch! I didn't realize that autosaves were reused and I think that's a reasonable safeguard to add. Addressed in 50a5cfc, and added tests in 56a6042.

@alecgeatches

alecgeatches commented Jul 30, 2026

Copy link
Copy Markdown
Contributor Author

@ingeniumed Thank you for taking another look! Quick question here:

I can still replicate the same bug that I noticed with the previous implementation, so I'm not recording that once more.

Can you let me know which bug you're able to replicate? If it's the revision bug at the top of your last review, local autosaves have been redesigned to also use the new snapshot system, and I'm not able to locally reproduce. Please let me know if you still can reproduce on a fresh build, or otherwise which bug you are referring to. Thanks!

@alecgeatches
alecgeatches requested a review from ingeniumed July 30, 2026 16:48
@ingeniumed

Copy link
Copy Markdown
Contributor

@ingeniumed Thank you for taking another look! Quick question here:

I can still replicate the same bug that I noticed with the previous implementation, so I'm not recording that once more.

Can you let me know which bug you're able to replicate? If it's the revision bug at the top of your last review, local autosaves have been redesigned to also use the new snapshot system, and I'm not able to locally reproduce. Please let me know if you still can reproduce on a fresh build, or otherwise which bug you are referring to. Thanks!

Before e988a58 I was able to consistently replicate #80539 (review).

But, after 56a6042 I'm no longer able to replicate this consistently. I can still replicate it but it requires me to go in and out 2 times or reload 2 times.

It's def better than what used to happen before so IMO this shouldn't be a blocker. This PR is going to get trunk into a much better shape when it comes to these notices.

@ingeniumed ingeniumed left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Awesome work!

@alecgeatches
alecgeatches merged commit cf6b796 into trunk Jul 31, 2026
46 of 47 checks passed
@alecgeatches
alecgeatches deleted the improve/rtc-autosave-notice branch July 31, 2026 15:44
@github-actions github-actions Bot added this to the Gutenberg 23.8 milestone Jul 31, 2026
@sirreal

sirreal commented Aug 12, 2026

Copy link
Copy Markdown
Member

Recategorizing this as type: bug.

@sirreal sirreal added [Type] Bug An existing feature does not function as intended and removed [Type] Task Issues or PRs that have been broken down into an individual action to take labels Aug 12, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

[Feature] Real-time Collaboration Phase 3 of the Gutenberg roadmap around real-time collaboration No Core Sync Required Indicates that any changes do not need to be synced to WordPress Core [Package] Core data /packages/core-data [Package] Editor /packages/editor [Package] Sync /packages/sync [Type] Bug An existing feature does not function as intended

Projects

None yet

Development

Successfully merging this pull request may close these issues.

RTC: Autosaves on a collaborative document often result in "more recent" autosave warning

4 participants

Sponsor
SponsoredKunjungi sekarang
Promo