Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions backport-changelog/7.0/10894.md
Original file line number Diff line number Diff line change
@@ -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
15 changes: 6 additions & 9 deletions lib/compat/wordpress-7.0/class-wp-http-polling-sync-server.php
Original file line number Diff line number Diff line change
Expand Up @@ -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,
);
}
}
Expand Down
16 changes: 6 additions & 10 deletions packages/sync/src/providers/http-polling/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down Expand Up @@ -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

Expand Down Expand Up @@ -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
}
]
}
Expand All @@ -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
Expand Down
28 changes: 23 additions & 5 deletions packages/sync/src/providers/http-polling/polling-manager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,7 @@ interface RegisterRoomOptions {

interface RoomState {
clientId: number;
createCompactionUpdate: () => SyncUpdate;
endCursor: number;
localAwarenessState: LocalAwarenessState;
log: LogFunction;
Expand All @@ -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
Expand Down Expand Up @@ -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
)
);
}
} );
Expand Down Expand Up @@ -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,
Expand Down
3 changes: 2 additions & 1 deletion packages/sync/src/providers/http-polling/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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[];
}
Expand Down
Loading