@@ -291,17 +291,18 @@ function attachYjsProvider() {
291291 if ( constants . debugging ) console . log ( 'Collab: Initial sync data already exists, applying it now.' ) ;
292292 const syncData = constants . mutableRefs . yProjectDataSync . get ( 'sync' ) ;
293293 if ( syncData && syncData . data ) {
294- let isCorrupt = false ;
294+ let corruptionDetails = null ;
295295 for ( const targetData of syncData . data ) {
296296 const blocksObject = JSON . parse ( targetData . blockData ) ;
297- if ( helper . hasCircularDependency ( blocksObject ) ) {
298- isCorrupt = true ;
297+ const cycleCheckResult = helper . findCircularDependency ( blocksObject , targetData . targetName ) ;
298+ if ( cycleCheckResult . hasCycle ) {
299+ corruptionDetails = cycleCheckResult ;
299300 break ;
300301 }
301302 }
302303
303- if ( isCorrupt ) {
304- console . error ( "Collab FATAL: Received corrupt master copy with circular dependency. Aborting project load." ) ;
304+ if ( corruptionDetails ) {
305+ console . error ( "Collab FATAL: Received corrupt master copy with circular dependency. Aborting project load." , corruptionDetails ) ;
305306 collabUI . hideSyncingPopup ( ) ; // Hide the "syncing" message
306307
307308 const popup = document . createElement ( 'div' ) ;
@@ -622,18 +623,19 @@ function attachYjsProvider() {
622623 }
623624 // --- 'savedProject' Trigger (for project save operations) ---
624625 else if ( detail . triggerId === 'savedProject' ) {
625- let isCorrupt = false ;
626+ let corruptionDetails = null ;
626627 // Loop through every target (Sprite, Stage) in the project.
627628 for ( const target of constants . mutableRefs . vm . runtime . targets ) {
628629 const blocksToValidate = target . blocks . _blocks ;
629- if ( helper . hasCircularDependency ( blocksToValidate ) ) {
630+ const cycleCheckResult = helper . findCircularDependency ( blocksToValidate , target . getName ( ) ) ;
631+ if ( cycleCheckResult . hasCycle ) {
630632 console . error ( `Collab FATAL: Circular dependency detected in target "${ target . getName ( ) } ". Aborting sync.` ) ;
631- isCorrupt = true ;
632- break ; // Stop checking as soon as one corrupt target is found.
633+ corruptionDetails = cycleCheckResult ;
634+ break ;
633635 }
634636 }
635637
636- if ( isCorrupt ) {
638+ if ( corruptionDetails ) {
637639 // Create the main popup container.
638640 const popup = document . createElement ( 'div' ) ;
639641 popup . className = 'collab-popup' ;
@@ -665,38 +667,130 @@ function attachYjsProvider() {
665667 const downloadLogButton = document . createElement ( 'button' ) ;
666668 downloadLogButton . innerText = 'Download Debug Log' ;
667669 downloadLogButton . addEventListener ( 'click' , ( ) => {
670+ let sessionInfo , yjsState , localCollabState , vmProjectJSON , yEventsData , yProjectEventsData , corruptionLog ;
671+
672+ // Helper to convert Maps to plain objects for JSON.stringify
673+ const mapToObject = ( map ) => {
674+ const obj = { } ;
675+ if ( ! map || ! ( map instanceof Map ) ) return { } ;
676+ map . forEach ( ( value , key ) => {
677+ obj [ String ( key ) ] = value ;
678+ } ) ;
679+ return obj ;
680+ } ;
681+
682+ // --- Section 0: Corruption Details ---
683+ try {
684+ if ( corruptionDetails ) {
685+ const blockTypes = { } ;
686+ const targetName = corruptionDetails . targetName ;
687+ const target = targetName === 'Stage' ?
688+ constants . mutableRefs . vm . runtime . getTargetForStage ( ) :
689+ constants . mutableRefs . vm . runtime . getSpriteTargetByName ( targetName ) ;
690+
691+ if ( target && corruptionDetails . path ) {
692+ corruptionDetails . path . forEach ( blockId => {
693+ const block = target . blocks . getBlock ( blockId ) ;
694+ blockTypes [ blockId ] = block ? block . opcode : 'Not Found' ;
695+ } ) ;
696+ }
697+ corruptionLog = JSON . stringify ( { ...corruptionDetails , blockTypes } , null , 2 ) ;
698+ } else {
699+ corruptionLog = '"No corruption details available."' ;
700+ }
701+ } catch ( e ) {
702+ corruptionLog = `"Error generating Corruption Details: ${ e . message } "` ;
703+ }
704+
705+ // --- Section 1: Session Info ---
706+ try {
707+ sessionInfo = JSON . stringify ( {
708+ timestamp : new Date ( ) . toISOString ( ) ,
709+ url : window . location . href ,
710+ userAgent : navigator . userAgent ,
711+ } , null , 2 ) ;
712+ } catch ( e ) {
713+ sessionInfo = `"Error generating Session Info: ${ e . message } "` ;
714+ }
715+
716+ // --- Section 2: Yjs & Provider State ---
717+ try {
718+ yjsState = JSON . stringify ( {
719+ localClientID : constants . mutableRefs . ydoc ?. clientID || 'N/A' ,
720+ provider : {
721+ connected : constants . mutableRefs . provider ?. wsconnected || false ,
722+ synced : constants . mutableRefs . provider ?. synced || false ,
723+ url : constants . mutableRefs . provider ?. url || 'N/A' ,
724+ } ,
725+ awareness : mapToObject ( constants . mutableRefs . yjsAwarenessInstance ?. getStates ( ) ) ,
726+ } , null , 2 ) ;
727+ } catch ( e ) {
728+ yjsState = `"Error generating Yjs & Provider State: ${ e . message } "` ;
729+ }
730+
731+ // --- Section 3: Local Collaboration State ---
732+ try {
733+ localCollabState = JSON . stringify ( {
734+ localUserInfo : constants . localUserInfo ,
735+ syncFlags : {
736+ hasProcessedInitialProjectEvents : constants . mutableRefs . hasProcessedInitialProjectEvents ,
737+ hasProcessedInitialBlockEvents : constants . mutableRefs . hasProcessedInitialBlockEvents ,
738+ alreadyRanSetup : constants . mutableRefs . alreadyRanSetup ,
739+ } ,
740+ eventTransactionBuffer : mapToObject ( constants . mutableRefs . eventTransactionBuffer ) ,
741+ } , null , 2 ) ;
742+ } catch ( e ) {
743+ localCollabState = `"Error generating Local Collaboration State: ${ e . message } "` ;
744+ }
745+
746+ // --- Section 4: Project & VM State ---
747+ try {
748+ vmProjectJSON = constants . mutableRefs . vm ? constants . mutableRefs . vm . toJSON ( ) : '{"error": "VM instance not found."}' ;
749+ } catch ( e ) {
750+ vmProjectJSON = `{"error": "Failed to serialize project from VM", "message": "${ e . message } "}` ;
751+ }
752+
753+ // --- Section 5: YEvents History ---
754+ try {
755+ yEventsData = JSON . stringify ( constants . mutableRefs . yEvents ?. toArray ( ) || [ ] , null , 2 ) ;
756+ } catch ( e ) {
757+ yEventsData = `"Error generating YEvents History: ${ e . message } "` ;
758+ }
759+
760+ // --- Section 6: YProjectEvents History ---
761+ try {
762+ yProjectEventsData = JSON . stringify ( constants . mutableRefs . yProjectEvents ?. toArray ( ) || [ ] , null , 2 ) ;
763+ } catch ( e ) {
764+ yProjectEventsData = `"Error generating YProjectEvents History: ${ e . message } "` ;
765+ }
766+
767+ // Assemble the log string
768+ const logContent = `Collaboration Addon Debug Log\n\n` +
769+ `==================== CORRUPTION DETAILS ====================\n${ corruptionLog } \n\n` +
770+ `==================== Session Info ====================\n${ sessionInfo } \n\n` +
771+ `==================== Yjs & Provider State ====================\n${ yjsState } \n\n` +
772+ `==================== Local Collaboration State ====================\n${ localCollabState } \n\n` +
773+ `==================== Full Project State (from vm.toJSON()) ====================\n${ vmProjectJSON } \n\n` +
774+ `==================== YEvents History (Block Actions) ====================\n${ yEventsData } \n\n` +
775+ `==================== YProjectEvents History (Asset & Project Actions) ====================\n${ yProjectEventsData } \n\n` +
776+ `==================== END OF LOG ====================` ;
777+
778+ // Create and trigger download
668779 try {
669- // Get the current state of the Yjs event arrays. .toArray() converts them to standard JS arrays.
670- const yEventsData = constants . mutableRefs . yEvents . toArray ( ) ;
671- const yProjectEventsData = constants . mutableRefs . yProjectEvents . toArray ( ) ;
672-
673- // Format the data into a readable string with headers.
674- const logContent = `Collaboration Addon Debug Log\n` +
675- `Timestamp: ${ new Date ( ) . toISOString ( ) } \n\n` +
676- `--- YEvents (Block Actions) ---\n\n` +
677- `${ JSON . stringify ( yEventsData , null , 2 ) } \n\n` +
678- `--- YProjectEvents (Asset & Project Actions) ---\n\n` +
679- `${ JSON . stringify ( yProjectEventsData , null , 2 ) } ` ;
680-
681- // Create a Blob from the string content.
682780 const blob = new Blob ( [ logContent ] , { type : 'text/plain;charset=utf-8' } ) ;
683-
684- // Create a temporary link element to trigger the download.
685781 const url = URL . createObjectURL ( blob ) ;
686782 const a = document . createElement ( 'a' ) ;
687783 a . style . display = 'none' ;
688784 a . href = url ;
689- a . download = 'collaboration_debug_log.txt' ;
690-
785+ const timestampForFile = new Date ( ) . toISOString ( ) . replace ( / [: . ] / g , '-' ) ;
786+ a . download = `collaboration_debug_log_ ${ timestampForFile } .txt` ;
691787 document . body . appendChild ( a ) ;
692788 a . click ( ) ;
693-
694- // Clean up by revoking the URL and removing the link.
695789 window . URL . revokeObjectURL ( url ) ;
696790 document . body . removeChild ( a ) ;
697- } catch ( e ) {
698- console . error ( "Collab: Failed to generate or download debug log." , e ) ;
699- alert ( "Sorry, the debug log could not be created ." ) ;
791+ } catch ( downloadError ) {
792+ console . error ( "Collab: Failed to trigger debug log download ." , downloadError ) ;
793+ alert ( "Sorry, the debug log could not be downloaded ." ) ;
700794 }
701795 } ) ;
702796 popupContent . appendChild ( downloadLogButton ) ;
0 commit comments