@@ -144,7 +144,7 @@ interface FileDocPresenceOwner {
144144interface FileDocRoom {
145145 /** The `workspace_files.id` this room edits. */
146146 fileId : string
147- owner ? : FileDocOwner
147+ owner : FileDocOwner
148148 lastEditorConnectionId : string | null
149149 doc : Y . Doc
150150 awareness : awarenessProtocol . Awareness
@@ -786,12 +786,14 @@ interface MergeOrder {
786786 * reconcile treats it distinctly (retry later) rather than as "nothing to reconcile into".
787787 */
788788export function applyMarkdownToLiveFileDoc (
789- fileId : string ,
789+ ref : RoomRef ,
790790 markdown : string ,
791- order : MergeOrder = { } ,
792- owner ?: FileDocOwner
791+ order : MergeOrder = { }
793792) : Promise < 'applied' | 'no-live-room' | 'merge-unavailable' | 'stale' > {
794- const name = roomName ( fileDocRoom ( { fileId, owner } ) )
793+ const address = fileDocTargetFromRoom ( ref )
794+ if ( ! address ) throw new Error ( 'Invalid file document room' )
795+ const { fileId } = address
796+ const name = roomName ( ref )
795797 return serializeFileDocMutation ( name , ( ) => mergeMarkdownIntoRoom ( name , fileId , markdown , order ) )
796798}
797799
@@ -819,11 +821,11 @@ async function acquireFileDocMergeSlot(name: string): Promise<string | null> {
819821
820822/** Serializes and version-orders an unsupported durable replacement with live Markdown merges. */
821823export function invalidateLiveFileDocument (
822- fileId : string ,
823- version : number ,
824- owner ?: FileDocOwner
824+ ref : RoomRef ,
825+ version : number
825826) : Promise < { status : 'applied' ; docId ?: string } | { status : 'stale' } > {
826- const name = roomName ( fileDocRoom ( { fileId, owner } ) )
827+ if ( ! fileDocTargetFromRoom ( ref ) ) throw new Error ( 'Invalid file document room' )
828+ const name = roomName ( ref )
827829 return serializeFileDocMutation ( name , async ( ) => {
828830 const store = getFileDocStore ( )
829831 const token = await acquireFileDocMergeSlot ( name )
@@ -970,19 +972,13 @@ async function mergeMarkdownIntoRoom(
970972 return 'applied'
971973}
972974
973- function requireFileDocTarget ( ref : RoomRef ) : FileDocTarget {
974- const target = fileDocTargetFromRoom ( ref )
975- if ( ! target ) throw new Error ( 'Invalid file document room' )
976- return target
977- }
978-
979975/**
980976 * Get (or lazily create) the authoritative document for a room, wiring the two
981977 * relay handlers exactly once: document updates and awareness changes are
982978 * broadcast to the room.
983979 */
984- function getOrCreateRoom ( io : Server , ref : RoomRef ) : FileDocRoom {
985- const name = roomName ( ref )
980+ function getOrCreateRoom ( io : Server , target : FileDocTarget ) : FileDocRoom {
981+ const name = roomName ( fileDocRoom ( target ) )
986982 const existing = fileDocRooms . get ( name )
987983 if ( existing ) return existing
988984
@@ -994,7 +990,7 @@ function getOrCreateRoom(io: Server, ref: RoomRef): FileDocRoom {
994990 // Started BEFORE the room is registered so no join can observe a room without its hydration handle.
995991 const hydrated = getFileDocStore ( ) . attachRoom ( name , doc )
996992 const room : FileDocRoom = {
997- ...requireFileDocTarget ( ref ) ,
993+ ...target ,
998994 lastEditorConnectionId : null ,
999995 doc,
1000996 awareness,
@@ -1129,10 +1125,10 @@ function isFileDocWriteAllowed(
11291125 name : string
11301126) : boolean | Promise < boolean > {
11311127 const userId = socket . userId
1132- const fileId = fileDocRooms . get ( name ) ?. fileId
1133- if ( ! userId || ! fileId ) return false
1128+ const document = fileDocRooms . get ( name )
1129+ if ( ! userId || ! document ) return false
11341130
1135- const owner = fileDocRooms . get ( name ) ?. owner
1131+ const { fileId , owner } = document
11361132 const room = fileDocRoom ( { fileId, owner } )
11371133 if ( fileDocOwnerAdapter ( owner ) . requiresCurrentActor )
11381134 return ( async ( ) => {
@@ -1343,8 +1339,8 @@ async function handleClientUpdate(
13431339 if (
13441340 ! room ||
13451341 ( target . owner &&
1346- ( target . owner . entityType !== room . owner ? .entityType ||
1347- target . owner . entityId !== room . owner ? .entityId ) )
1342+ ( target . owner . entityType !== room . owner . entityType ||
1343+ target . owner . entityId !== room . owner . entityId ) )
13481344 ) {
13491345 reject ( 'NOT_JOINED' , true , candidate . updateId )
13501346 return
@@ -1485,7 +1481,6 @@ export function setupWorkspaceFileDocHandlers(
14851481 socket . on ( FILE_DOC_EVENTS . JOIN , async ( payload : JoinFileDocPayload ) => {
14861482 const { fileId, clientId } = payload
14871483 const target = parseFileDocTarget ( payload )
1488- const owner = target ?. owner
14891484 // Hoisted so the catch can tell whether this join was superseded (a switch to another file)
14901485 // before surfacing a retryable error for the abandoned one.
14911486 let generation : number | undefined
@@ -1547,9 +1542,9 @@ export function setupWorkspaceFileDocHandlers(
15471542 generation = joinGeneration . get ( socket . id ) ?? 0
15481543 }
15491544
1550- const room = fileDocRoom ( { fileId , owner } )
1545+ const room = fileDocRoom ( target )
15511546 const name = roomName ( room )
1552- const admissionName = fileDocAdmissionRoom ( { fileId , owner } )
1547+ const admissionName = fileDocAdmissionRoom ( target )
15531548
15541549 const authorizeJoin = ( ) =>
15551550 resolveRoomJoinAuth ( {
@@ -1569,11 +1564,21 @@ export function setupWorkspaceFileDocHandlers(
15691564 } )
15701565 const authorized = await authorizeJoin ( )
15711566 if ( ! authorized ) return
1572- if ( owner ?. entityType === 'workspace' && owner . entityId !== authorized . workspaceId ) {
1567+ if (
1568+ target . owner ?. entityType === 'workspace' &&
1569+ target . owner . entityId !== authorized . workspaceId
1570+ ) {
15731571 emitJoinError ( socket , fileId , clientId , 'File not found' , 'NOT_FOUND' , false )
15741572 return
15751573 }
15761574
1575+ const owner =
1576+ target . owner ??
1577+ ( authorized . workspaceId
1578+ ? { entityType : 'workspace' as const , entityId : authorized . workspaceId }
1579+ : null )
1580+ if ( ! owner ) throw new Error ( 'Document authorization did not resolve its owner' )
1581+
15771582 // Server-authenticated identity for the presence roster (never trusts the client-set
15781583 // awareness). Resolved here so the generation guard below also covers this await.
15791584 const avatarUrl = await resolveAvatarUrl ( socket , userId )
@@ -1606,11 +1611,7 @@ export function setupWorkspaceFileDocHandlers(
16061611 discardInvalidatedRoom ( name , io )
16071612 }
16081613
1609- const entry = getOrCreateRoom ( io , room )
1610- // The workspace the server-side persist writes back to — and what the seed is built from, so it
1611- // must be captured BEFORE the room is prepared below.
1612- if ( authorized . workspaceId )
1613- entry . owner = { entityType : 'workspace' , entityId : authorized . workspaceId }
1614+ const entry = getOrCreateRoom ( io , { fileId, owner } )
16141615
16151616 // Hold the room open across the awaits below: it has no owner until this join commits, so a
16161617 // concurrent last-leave would otherwise tear down the very document being prepared.
@@ -1795,12 +1796,12 @@ export function setupWorkspaceFileDocHandlers(
17951796 docId : docIdOf ( entry . doc ) ,
17961797 version : joinedVersion ,
17971798 schemaVersion : FILE_DOC_SCHEMA_VERSION ,
1799+ ...fileDocOwnerWireFields ( entry . owner ) ,
17981800 ...( store . enabled || fileDocOwnerAdapter ( owner ) . requiresCurrentActor
17991801 ? { acknowledgedUpdates : true as const }
18001802 : { } ) ,
18011803 ...( fileDocOwnerAdapter ( owner ) . requiresCurrentActor
18021804 ? {
1803- ...fileDocOwnerWireFields ( entry . owner ) ,
18041805 canWrite : finalPermission === 'write' || finalPermission === 'admin' ,
18051806 }
18061807 : { } ) ,
@@ -1846,7 +1847,8 @@ export function setupWorkspaceFileDocHandlers(
18461847 } catch ( error ) {
18471848 logger . error ( 'Error joining file-doc room:' , error )
18481849 try {
1849- const name = roomName ( fileDocRoom ( { fileId, owner } ) )
1850+ const name = target ? roomName ( fileDocRoom ( target ) ) : null
1851+ if ( ! name ) throw error
18501852 /**
18511853 * Roll back ownership only if this attempt committed it. A failed provisional admission must
18521854 * preserve a previous file's binding and any co-mounted provider already in the target room.
0 commit comments