@@ -150,6 +150,8 @@ interface FileDocRoom {
150150 /** socketId → (clientId → its presence ownership). A socket owns one entry per collaborative provider
151151 * it mounted for this file (see {@link FileDocPresenceOwner}); an empty inner map is never kept. */
152152 owners : Map < string , Map < number , FileDocPresenceOwner > >
153+ /** socketId → the write capability last sent to it, so per-frame checks publish only a change. */
154+ publishedCanWrite : Map < string , boolean >
153155 /**
154156 * The in-flight server seed for this room, or `null`. Concurrent joins await THIS promise rather
155157 * than each starting a fetch — and, unlike a "started" boolean, awaiting it is what lets a second
@@ -1043,6 +1045,7 @@ function getOrCreateRoom(io: Server, target: FileDocTarget): FileDocRoom {
10431045 doc,
10441046 awareness,
10451047 owners : new Map ( ) ,
1048+ publishedCanWrite : new Map ( ) ,
10461049 seeding : null ,
10471050 lastEditorUserId : null ,
10481051 edited : false ,
@@ -1161,11 +1164,15 @@ function resolveFileDocPermission(
11611164 const room = fileDocRoom ( document )
11621165 const accept = ( permission : string | null ) => {
11631166 if ( socketToRoomName . get ( socket . id ) !== name || fileDocRooms . get ( name ) !== document ) return null
1164- socket . emit ( FILE_DOC_EVENTS . PERMISSION , {
1165- fileId : document . fileId ,
1166- ...fileDocOwnerWireFields ( document . owner ) ,
1167- canWrite : permission === 'write' || permission === 'admin' ,
1168- } satisfies FileDocPermission )
1167+ const canWrite = permission === 'write' || permission === 'admin'
1168+ if ( document . publishedCanWrite . get ( socket . id ) !== canWrite ) {
1169+ document . publishedCanWrite . set ( socket . id , canWrite )
1170+ socket . emit ( FILE_DOC_EVENTS . PERMISSION , {
1171+ fileId : document . fileId ,
1172+ ...fileDocOwnerWireFields ( document . owner ) ,
1173+ canWrite,
1174+ } satisfies FileDocPermission )
1175+ }
11691176 if ( permission === null )
11701177 evictSocketFromRoom ( socket , room , 'Your access to this document has been revoked' , io )
11711178 return permission
@@ -1216,7 +1223,16 @@ async function handleMessage(socket: AuthenticatedSocket, io: Server, data: unkn
12161223 }
12171224 const decoder = decoding . createDecoder ( bytes )
12181225 const messageType = decoding . readVarUint ( decoder )
1219- const permission = await resolveFileDocPermission ( socket , io , name )
1226+ let permission : string | null
1227+ try {
1228+ permission = await resolveFileDocPermission ( socket , io , name )
1229+ } catch ( error ) {
1230+ logger . warn ( 'Dropping file-doc frame while its authorization is unavailable' , {
1231+ socketId : socket . id ,
1232+ error,
1233+ } )
1234+ return
1235+ }
12201236 if ( permission === null ) return
12211237 const canWrite = permission === 'write' || permission === 'admin'
12221238 if ( messageType === FILE_DOC_MESSAGE_TYPE . SYNC ) {
@@ -1460,6 +1476,7 @@ export function cleanupFileDocForSocket(socketId: string, io: Server, endOfLife
14601476 // caret via its awareness removal.
14611477 const clientMap = room . owners . get ( socketId )
14621478 room . owners . delete ( socketId )
1479+ room . publishedCanWrite . delete ( socketId )
14631480 clearFileDocPresence ( io , socketId , name )
14641481 if ( clientMap !== undefined && clientMap . size > 0 ) {
14651482 // Fires the awareness `update` handler with a non-socket origin → the removals
@@ -1788,6 +1805,7 @@ export function setupWorkspaceFileDocHandlers(
17881805 awarenessProtocol . removeAwarenessStates ( entry . awareness , [ clientId ] , null )
17891806 if ( clientMap . size === 0 ) {
17901807 entry . owners . delete ( otherSid )
1808+ entry . publishedCanWrite . delete ( otherSid )
17911809 socketToRoomName . delete ( otherSid )
17921810 clearFileDocPresence ( io , otherSid , name )
17931811 io . in ( otherSid ) . socketsLeave ( name )
@@ -1832,6 +1850,8 @@ export function setupWorkspaceFileDocHandlers(
18321850 roomName : name ,
18331851 user : { socketId : socket . id , userId, userName, avatarUrl } ,
18341852 }
1853+ /** Co-mounted providers learn from the next frame whether this admission changed access. */
1854+ entry . publishedCanWrite . delete ( socket . id )
18351855
18361856 // Name the document this room holds, so a client that still carries a DIFFERENT one (its room
18371857 // outlived by a document rebuilt in its place) can refuse to merge instead of unioning two
0 commit comments