mirror of
https://github.com/vector-im/element-web.git
synced 2026-05-06 04:36:21 +02:00
fix(docs): send RTC delta on every keystroke, Matrix only on 500ms debounce
Previously both channels shared the same 500ms debounce, which defeated the point of the low-latency RTC data channel. New behaviour: - RTC (LiveKit): fires on every call to scheduleDeltaSend using save_after(lastRtcHeadsRef) to capture only the changes since the previous RTC publish. Updated on each send so each message is a minimal delta. - Matrix timeline: unchanged 500ms debounce using save_incremental(), which has its own independent internal cursor unaffected by the RTC path. Also advance lastRtcHeadsRef when remote deltas are applied (applyDeltaBytes) and after the initial document load drain, so the RTC cursor never echoes received or pre-existing content back to peers.
This commit is contained in:
parent
e584c7d107
commit
e39fc255a0
@ -184,6 +184,9 @@ function useDocumentSync(
|
||||
const debounceTimer = useRef<ReturnType<typeof setTimeout> | null>(null);
|
||||
const savedClearTimer = useRef<ReturnType<typeof setTimeout> | null>(null);
|
||||
const suppressMutations = useRef(false);
|
||||
// Tracks the Automerge heads at the time of the last RTC publish so that
|
||||
// save_after() gives exactly the delta since the previous keystroke send.
|
||||
const lastRtcHeadsRef = useRef<string[]>([]);
|
||||
|
||||
// Set actor ID as hex-encoded userId:deviceId for correct CRDT attribution.
|
||||
// set_actor_id() requires a hex string (decoded to raw bytes internally).
|
||||
@ -249,6 +252,9 @@ function useDocumentSync(
|
||||
// call, the first save_incremental() after load would return the entire document
|
||||
// history and hit the Matrix 65KB event size limit.
|
||||
composerModel.save_incremental();
|
||||
// Seed the RTC heads cursor so the first save_after() on a keystroke
|
||||
// captures only the changes made after this load point.
|
||||
lastRtcHeadsRef.current = composerModel.get_heads();
|
||||
|
||||
// 3. Update the editor DOM to reflect the loaded + replayed state.
|
||||
if (editorRef.current) {
|
||||
@ -281,6 +287,9 @@ function useDocumentSync(
|
||||
// Drain the incremental save cursor so that received changes are not
|
||||
// re-included in the next save_incremental() call from this client.
|
||||
model.save_incremental();
|
||||
// Advance the RTC cursor too so the next save_after() on a local
|
||||
// keystroke doesn't re-transmit the just-received remote changes.
|
||||
lastRtcHeadsRef.current = model.get_heads();
|
||||
if (editorRef.current) {
|
||||
suppressMutations.current = true;
|
||||
const caretOffset = saveCaretOffset(editorRef.current);
|
||||
@ -335,19 +344,41 @@ function useDocumentSync(
|
||||
useEffect(() => { rtcRef.current = rtc; });
|
||||
|
||||
/**
|
||||
* Called after every local edit. Immediately surfaces the "Editing"
|
||||
* indicator and schedules a 500 ms debounced send to BOTH channels:
|
||||
* Called after every local edit.
|
||||
*
|
||||
* 1. MatrixRTC / LiveKit (if connected) — real-time collaboration.
|
||||
* 2. Matrix room timeline — durable persistence.
|
||||
* Two independent channels run at different cadences:
|
||||
*
|
||||
* Sending to the timeline on every debounce (not just on unmount) ensures
|
||||
* the document is never lost even if the user exits quickly.
|
||||
* 1. MatrixRTC / LiveKit (if connected) — fires on EVERY call (every
|
||||
* keystroke). Uses save_after(lastRtcHeads) so each publish contains
|
||||
* only the changes since the previous RTC send, giving sub-50 ms
|
||||
* delivery to connected peers.
|
||||
*
|
||||
* 2. Matrix room timeline — fires after a 500 ms debounce. Uses
|
||||
* save_incremental() which has its own independent cursor, so it
|
||||
* always captures and persists the full batch of changes made during
|
||||
* the quiet period.
|
||||
*
|
||||
* The two cursors (lastRtcHeadsRef for RTC, internal automerge cursor for
|
||||
* Matrix) are entirely independent so neither path affects the other.
|
||||
*/
|
||||
const scheduleDeltaSend = useCallback(() => {
|
||||
if (!isCollaborative(composerModel)) return;
|
||||
|
||||
// Immediately surface the editing indicator.
|
||||
// ── Channel 1: RTC — immediate, every keystroke ───────────────────
|
||||
const rtc = rtcRef.current;
|
||||
if (rtc?.isConnected) {
|
||||
try {
|
||||
const rtcDelta = composerModel.save_after(lastRtcHeadsRef.current);
|
||||
if (rtcDelta.length > 0) {
|
||||
rtc.publishDelta(rtcDelta);
|
||||
lastRtcHeadsRef.current = composerModel.get_heads();
|
||||
}
|
||||
} catch (e) {
|
||||
logger.warn("[DocumentView] Failed to publish RTC delta", e);
|
||||
}
|
||||
}
|
||||
|
||||
// ── Channel 2: Matrix — debounced, persistent ─────────────────────
|
||||
setSaveStatus("editing");
|
||||
if (savedClearTimer.current !== null) {
|
||||
clearTimeout(savedClearTimer.current);
|
||||
@ -378,11 +409,6 @@ function useDocumentSync(
|
||||
|
||||
const heads = composerModel.get_heads();
|
||||
|
||||
// Real-time channel: deliver immediately to connected peers.
|
||||
if (rtcRef.current?.isConnected) {
|
||||
rtcRef.current.publishDelta(delta);
|
||||
}
|
||||
|
||||
// Persistence channel: always write to the Matrix timeline so
|
||||
// the document survives session boundaries and reconnects.
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user