diff --git a/backport-changelog/7.0/10894.md b/backport-changelog/7.0/10894.md index f55e198f0d95f3..6694148cf3f57f 100644 --- a/backport-changelog/7.0/10894.md +++ b/backport-changelog/7.0/10894.md @@ -1,3 +1,5 @@ https://github.com/WordPress/wordpress-develop/pull/10894 * https://github.com/WordPress/gutenberg/pull/75366 +* https://github.com/WordPress/gutenberg/pull/75681 +* https://github.com/WordPress/gutenberg/pull/75682 diff --git a/lib/compat/wordpress-7.0/class-wp-http-polling-sync-server.php b/lib/compat/wordpress-7.0/class-wp-http-polling-sync-server.php index 83dc8d9e619f50..680857b1749b2c 100644 --- a/lib/compat/wordpress-7.0/class-wp-http-polling-sync-server.php +++ b/lib/compat/wordpress-7.0/class-wp-http-polling-sync-server.php @@ -434,17 +434,14 @@ private function get_updates( string $room, int $client_id, int $cursor, bool $i } // Determine if this client should perform compaction. - $compaction_request = null; - if ( $is_compactor && $total_updates > self::COMPACTION_THRESHOLD ) { - $compaction_request = $updates_after_cursor; - } + $should_compact = $is_compactor && $total_updates > self::COMPACTION_THRESHOLD; return array( - 'compaction_request' => $compaction_request, - 'end_cursor' => $this->storage->get_cursor( $room ), - 'room' => $room, - 'total_updates' => $total_updates, - 'updates' => $typed_updates, + 'end_cursor' => $this->storage->get_cursor( $room ), + 'room' => $room, + 'should_compact' => $should_compact, + 'total_updates' => $total_updates, + 'updates' => $typed_updates, ); } } diff --git a/packages/sync/src/providers/http-polling/README.md b/packages/sync/src/providers/http-polling/README.md index 42d729d4758e4a..d48d0cfc251d9e 100644 --- a/packages/sync/src/providers/http-polling/README.md +++ b/packages/sync/src/providers/http-polling/README.md @@ -55,7 +55,7 @@ Updates are tagged with a type to enable different server-side handling: | `sync_step1` | State vector announcement | Stored, delivered to other clients | | `sync_step2` | Missing updates response | Stored, delivered to other clients | | `update` | Regular document change | Stored until compacted | -| `compaction` | Merged updates via Y.mergeUpdates | Clears older updates, then stored | +| `compaction` | Full document state via Y.encodeStateAsUpdate | Clears older updates, then stored | ## Data Flow @@ -109,13 +109,9 @@ To prevent unbounded message growth, the server coordinates compaction: 1. **Threshold reached**: Server detects >50 stored updates for a room 2. **Client selection**: Server nominates the lowest active client ID -3. **Compaction request**: Server sends all updates to the nominated client via `compaction_request` -4. **Client merges**: Uses `Y.mergeUpdates()` to combine all updates, preserving operation metadata -5. **Client sends compaction**: The merged update replaces older updates on the server - -**Why Y.mergeUpdates instead of Y.encodeStateAsUpdate?** - -`Y.mergeUpdates()` preserves the original operation metadata (client IDs, logical clocks). This allows Yjs to correctly deduplicate when a compaction is applied to a document that already contains some of those operations. Using `Y.encodeStateAsUpdate()` would create fresh metadata, causing content duplication on clients that already have overlapping state. +3. **Compaction request**: Server sends `should_compact: true` to the nominated client +4. **Client encodes**: Uses `Y.encodeStateAsUpdate()` to capture the full document state +5. **Client sends compaction**: The encoded state replaces older updates on the server ### 5. Awareness @@ -164,7 +160,7 @@ Single endpoint for bidirectional sync including awareness. Clients send their u "updates": [ { "type": "update", "data": "base64-encoded-yjs-update" } ], - "compaction_request": null + "should_compact": false } ] } @@ -177,7 +173,7 @@ Single endpoint for bidirectional sync including awareness. Clients send their u - `after`: Cursor timestamp; only receive updates newer than this - `awareness`: Client's awareness state (or null to disconnect) - `end_cursor`: New cursor to use in next request -- `compaction_request`: Array of all updates if this client should compact (null otherwise) +- `should_compact`: Boolean indicating whether this client should compact - `updates`: Array of typed updates with base64-encoded Yjs data ## Permissions diff --git a/packages/sync/src/providers/http-polling/polling-manager.ts b/packages/sync/src/providers/http-polling/polling-manager.ts index 2ee3df452307fa..6422fbe9f3d99a 100644 --- a/packages/sync/src/providers/http-polling/polling-manager.ts +++ b/packages/sync/src/providers/http-polling/polling-manager.ts @@ -50,6 +50,7 @@ interface RegisterRoomOptions { interface RoomState { clientId: number; + createCompactionUpdate: () => SyncUpdate; endCursor: number; localAwarenessState: LocalAwarenessState; log: LogFunction; @@ -67,9 +68,11 @@ const roomStates: Map< string, RoomState > = new Map(); * the original operation metadata (client IDs, logical clocks) so that * Yjs deduplication works correctly when the compaction is applied. * + * Deprecated: The server is moving towards full state updates for compaction. + * * @param updates The updates to merge */ -function createCompactionUpdate( updates: SyncUpdate[] ): SyncUpdate { +function createDeprecatedCompactionUpdate( updates: SyncUpdate[] ): SyncUpdate { // Extract only compaction and update types for merging (skip sync-step updates). // Decode base64 updates to Uint8Array for merging. const mergeable = updates @@ -306,11 +309,21 @@ function poll(): void { roomState.updateQueue.addBulk( responseUpdates ); // Respond to compaction requests from server. The server asks only one - // client at a time to compact (lowest active client ID). We merge the - // received updates (the server has given us everything it has). - if ( room.compaction_request ) { + // client at a time to compact (lowest active client ID). We encode our + // full document state to replace all prior updates on the server. + if ( room.should_compact ) { + roomState.log( 'Server requested compaction update' ); + roomState.updateQueue.clear(); + roomState.updateQueue.add( + roomState.createCompactionUpdate() + ); + } else if ( room.compaction_request ) { + // Deprecated + roomState.log( 'Server requested (old) compaction update' ); roomState.updateQueue.add( - createCompactionUpdate( room.compaction_request ) + createDeprecatedCompactionUpdate( + room.compaction_request + ) ); } } ); @@ -387,6 +400,11 @@ function registerRoom( { const roomState: RoomState = { clientId: doc.clientID, + createCompactionUpdate: () => + createSyncUpdate( + Y.encodeStateAsUpdate( doc ), + SyncUpdateType.COMPACTION + ), endCursor: 0, localAwarenessState: awareness.getLocalState() ?? {}, log, diff --git a/packages/sync/src/providers/http-polling/types.ts b/packages/sync/src/providers/http-polling/types.ts index 08d143c0e16694..a045f66dcf489e 100644 --- a/packages/sync/src/providers/http-polling/types.ts +++ b/packages/sync/src/providers/http-polling/types.ts @@ -31,8 +31,9 @@ interface SyncEnvelopeFromClient { interface SyncEnvelopeFromServer { awareness: AwarenessState; - compaction_request?: SyncUpdate[]; + compaction_request?: SyncUpdate[]; // deprecated end_cursor: number; // use as `after` in next request + should_compact?: boolean; room: string; updates: SyncUpdate[]; }