Skip to content

Commit 2dc7ac3

Browse files
chriszaratechriszaratepkevanmaxschmelingellatrix
authored
RTC: Compact on request with encodeStateAsUpdate (#75682)
* Use compaction request as signal for full state update * Restore deprecated code path * Clear update queue * Add to backport changelog Co-authored-by: chriszarate <czarate@git.wordpress.org> Co-authored-by: pkevan <paulkevan@git.wordpress.org> Co-authored-by: maxschmeling <maxschmeling@git.wordpress.org> Co-authored-by: ellatrix <ellatrix@git.wordpress.org>
1 parent 175075f commit 2dc7ac3

5 files changed

Lines changed: 39 additions & 25 deletions

File tree

backport-changelog/7.0/10894.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
11
https://github.com/WordPress/wordpress-develop/pull/10894
22

33
* https://github.com/WordPress/gutenberg/pull/75366
4+
* https://github.com/WordPress/gutenberg/pull/75681
5+
* https://github.com/WordPress/gutenberg/pull/75682

lib/compat/wordpress-7.0/class-wp-http-polling-sync-server.php

Lines changed: 6 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -443,17 +443,14 @@ private function get_updates( string $room, int $client_id, int $cursor, bool $i
443443
}
444444

445445
// Determine if this client should perform compaction.
446-
$compaction_request = null;
447-
if ( $is_compactor && $total_updates > self::COMPACTION_THRESHOLD ) {
448-
$compaction_request = $updates_after_cursor;
449-
}
446+
$should_compact = $is_compactor && $total_updates > self::COMPACTION_THRESHOLD;
450447

451448
return array(
452-
'compaction_request' => $compaction_request,
453-
'end_cursor' => $this->storage->get_cursor( $room ),
454-
'room' => $room,
455-
'total_updates' => $total_updates,
456-
'updates' => $typed_updates,
449+
'end_cursor' => $this->storage->get_cursor( $room ),
450+
'room' => $room,
451+
'should_compact' => $should_compact,
452+
'total_updates' => $total_updates,
453+
'updates' => $typed_updates,
457454
);
458455
}
459456
}

packages/sync/src/providers/http-polling/README.md

Lines changed: 6 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -55,7 +55,7 @@ Updates are tagged with a type to enable different server-side handling:
5555
| `sync_step1` | State vector announcement | Stored, delivered to other clients |
5656
| `sync_step2` | Missing updates response | Stored, delivered to other clients |
5757
| `update` | Regular document change | Stored until compacted |
58-
| `compaction` | Merged updates via Y.mergeUpdates | Clears older updates, then stored |
58+
| `compaction` | Full document state via Y.encodeStateAsUpdate | Clears older updates, then stored |
5959

6060
## Data Flow
6161

@@ -109,13 +109,9 @@ To prevent unbounded message growth, the server coordinates compaction:
109109

110110
1. **Threshold reached**: Server detects >50 stored updates for a room
111111
2. **Client selection**: Server nominates the lowest active client ID
112-
3. **Compaction request**: Server sends all updates to the nominated client via `compaction_request`
113-
4. **Client merges**: Uses `Y.mergeUpdates()` to combine all updates, preserving operation metadata
114-
5. **Client sends compaction**: The merged update replaces older updates on the server
115-
116-
**Why Y.mergeUpdates instead of Y.encodeStateAsUpdate?**
117-
118-
`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.
112+
3. **Compaction request**: Server sends `should_compact: true` to the nominated client
113+
4. **Client encodes**: Uses `Y.encodeStateAsUpdate()` to capture the full document state
114+
5. **Client sends compaction**: The encoded state replaces older updates on the server
119115

120116
### 5. Awareness
121117

@@ -164,7 +160,7 @@ Single endpoint for bidirectional sync including awareness. Clients send their u
164160
"updates": [
165161
{ "type": "update", "data": "base64-encoded-yjs-update" }
166162
],
167-
"compaction_request": null
163+
"should_compact": false
168164
}
169165
]
170166
}
@@ -177,7 +173,7 @@ Single endpoint for bidirectional sync including awareness. Clients send their u
177173
- `after`: Cursor timestamp; only receive updates newer than this
178174
- `awareness`: Client's awareness state (or null to disconnect)
179175
- `end_cursor`: New cursor to use in next request
180-
- `compaction_request`: Array of all updates if this client should compact (null otherwise)
176+
- `should_compact`: Boolean indicating whether this client should compact
181177
- `updates`: Array of typed updates with base64-encoded Yjs data
182178

183179
## Permissions

packages/sync/src/providers/http-polling/polling-manager.ts

Lines changed: 23 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,7 @@ interface RegisterRoomOptions {
5050

5151
interface RoomState {
5252
clientId: number;
53+
createCompactionUpdate: () => SyncUpdate;
5354
endCursor: number;
5455
localAwarenessState: LocalAwarenessState;
5556
log: LogFunction;
@@ -67,9 +68,11 @@ const roomStates: Map< string, RoomState > = new Map();
6768
* the original operation metadata (client IDs, logical clocks) so that
6869
* Yjs deduplication works correctly when the compaction is applied.
6970
*
71+
* Deprecated: The server is moving towards full state updates for compaction.
72+
*
7073
* @param updates The updates to merge
7174
*/
72-
function createCompactionUpdate( updates: SyncUpdate[] ): SyncUpdate {
75+
function createDeprecatedCompactionUpdate( updates: SyncUpdate[] ): SyncUpdate {
7376
// Extract only compaction and update types for merging (skip sync-step updates).
7477
// Decode base64 updates to Uint8Array for merging.
7578
const mergeable = updates
@@ -306,11 +309,21 @@ function poll(): void {
306309
roomState.updateQueue.addBulk( responseUpdates );
307310

308311
// Respond to compaction requests from server. The server asks only one
309-
// client at a time to compact (lowest active client ID). We merge the
310-
// received updates (the server has given us everything it has).
311-
if ( room.compaction_request ) {
312+
// client at a time to compact (lowest active client ID). We encode our
313+
// full document state to replace all prior updates on the server.
314+
if ( room.should_compact ) {
315+
roomState.log( 'Server requested compaction update' );
316+
roomState.updateQueue.clear();
317+
roomState.updateQueue.add(
318+
roomState.createCompactionUpdate()
319+
);
320+
} else if ( room.compaction_request ) {
321+
// Deprecated
322+
roomState.log( 'Server requested (old) compaction update' );
312323
roomState.updateQueue.add(
313-
createCompactionUpdate( room.compaction_request )
324+
createDeprecatedCompactionUpdate(
325+
room.compaction_request
326+
)
314327
);
315328
}
316329
} );
@@ -387,6 +400,11 @@ function registerRoom( {
387400

388401
const roomState: RoomState = {
389402
clientId: doc.clientID,
403+
createCompactionUpdate: () =>
404+
createSyncUpdate(
405+
Y.encodeStateAsUpdate( doc ),
406+
SyncUpdateType.COMPACTION
407+
),
390408
endCursor: 0,
391409
localAwarenessState: awareness.getLocalState() ?? {},
392410
log,

packages/sync/src/providers/http-polling/types.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,8 +31,9 @@ interface SyncEnvelopeFromClient {
3131

3232
interface SyncEnvelopeFromServer {
3333
awareness: AwarenessState;
34-
compaction_request?: SyncUpdate[];
34+
compaction_request?: SyncUpdate[]; // deprecated
3535
end_cursor: number; // use as `after` in next request
36+
should_compact?: boolean;
3637
room: string;
3738
updates: SyncUpdate[];
3839
}

0 commit comments

Comments
 (0)