fix: stable rtc object ref + per-device Matrix event filter

This commit is contained in:
David Langley 2026-03-05 22:19:38 +00:00
parent ce1ea1c91d
commit 0fd952708b
2 changed files with 20 additions and 4 deletions

View File

@ -287,8 +287,11 @@ function useDocumentSync(
const applyDeltaEvent = (event: import("matrix-js-sdk/src/matrix").MatrixEvent): void => {
if (event.getRoomId() !== room.roomId) return;
if (event.getType() !== DOC_DELTA_EVENT_TYPE) return;
// Skip our own events — local model already has our changes.
if (event.getSender() === client.getUserId()) return;
// Skip events sent by this exact device — we already have those
// changes in our local model. We must NOT skip events from the same
// user on a different device (e.g. two tabs open).
const senderDeviceId = event.getContent<{ device_id?: string }>().device_id;
if (event.getSender() === client.getUserId() && senderDeviceId === client.getDeviceId()) return;
const data = event.getContent<{ data?: string }>().data;
if (!data) return;
logger.info(`[DocumentView] Matrix delta event from ${event.getSender()}, applying ${data.length}b (base64)`);
@ -384,6 +387,7 @@ function useDocumentSync(
await client.sendEvent(room.roomId, DOC_DELTA_EVENT_TYPE as any, {
data: base64Encode(delta),
heads,
device_id: client.getDeviceId(),
});
deltaSendCount.current++;
@ -432,6 +436,7 @@ function useDocumentSync(
client.sendEvent(room.roomId, DOC_DELTA_EVENT_TYPE as any, {
data: base64Encode(delta),
heads,
device_id: client.getDeviceId(),
}).catch((e) => logger.warn("[DocumentView] Failed to send final delta on unmount", e));
}
// Always save snapshot on close so next load starts fresh.

View File

@ -19,7 +19,7 @@ Please see LICENSE files in the repository root for full details.
* `isConnected` stays false and the caller should fall back to Matrix events.
*/
import { useCallback, useEffect, useRef, useState } from "react";
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
import { type Room as MatrixRoom, type MatrixClient } from "matrix-js-sdk/src/matrix";
import { isLivekitTransportConfig } from "matrix-js-sdk/src/matrixrtc";
import { logger } from "matrix-js-sdk/src/logger";
@ -245,5 +245,16 @@ export function useDocumentRTC(
.catch((e: unknown) => logger.warn("[DocumentRTC] Failed to publish cursor", e));
}, []);
return { publishDelta, publishCursor, onDeltaRef, onCursorRef, onPeerLeaveRef, isConnected };
// Wrap in useMemo so the returned object reference is stable across renders
// (only changes when isConnected changes). Without this, DocumentView's
// wiring useEffect ([rtc, applyDeltaBytes]) fires on every render because
// the plain object literal `{}` produces a new reference each time.
const result = useMemo(
() => ({ publishDelta, publishCursor, onDeltaRef, onCursorRef, onPeerLeaveRef, isConnected }),
// publishDelta/publishCursor are useCallback stable; refs are always stable;
// only isConnected can change.
// eslint-disable-next-line react-hooks/exhaustive-deps
[isConnected],
);
return result;
}