Skip to content

Commit 9fa820e

Browse files
committed
an attempt to make the debugging usable
1 parent 650b194 commit 9fa820e

File tree

2 files changed

+168
-49
lines changed

2 files changed

+168
-49
lines changed

src/addons/addons/collaboration/helpers/helper.js

Lines changed: 42 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -385,48 +385,73 @@ export function CommentMove(blocklyEvent, remoteTargetName) {
385385
}
386386
}
387387

388-
export function hasCircularDependency(blocksObject) {
388+
export function findCircularDependency(blocksObject, targetName) {
389389
const blockIds = Object.keys(blocksObject);
390+
const visitedGlobally = new Set(); // To avoid re-checking branches we know are safe.
391+
390392
for (const startId of blockIds) {
391-
const visitedInPath = new Set(); // Tracks nodes for the CURRENT traversal path
393+
if (visitedGlobally.has(startId)) continue; // Already checked this node and its descendants.
394+
395+
const visitedInPath = new Set(); // Tracks nodes for the CURRENT traversal path.
396+
const currentPath = []; // Tracks the actual block IDs in the path.
392397

393398
function traverse(blockId) {
394-
if (!blockId) return false; // End of a chain
399+
if (!blockId) return null; // End of a chain, no cycle.
400+
401+
// Cycle detected!
395402
if (visitedInPath.has(blockId)) {
396-
console.error(`Collab Validation: Circular dependency detected! Path includes block ${blockId} twice.`);
397-
return true; // Cycle detected!
403+
// Find the start of the cycle in the current path and return the cycle loop.
404+
const cycleStartIndex = currentPath.indexOf(blockId);
405+
const cyclePath = [...currentPath.slice(cycleStartIndex), blockId];
406+
console.error(`Collab Validation: Circular dependency detected in target "${targetName}"! Path: ${cyclePath.join(' -> ')}`);
407+
return {
408+
hasCycle: true,
409+
path: cyclePath,
410+
targetName: targetName
411+
};
398412
}
399413
if (!blocksObject[blockId]) {
400-
// This block is referenced but doesn't exist in the object, which is a data integrity issue but not a cycle.
401-
return false;
414+
// This block is referenced but doesn't exist. Data integrity issue, but not a cycle.
415+
return null;
402416
}
403417

404418
visitedInPath.add(blockId);
419+
currentPath.push(blockId);
405420

406421
const block = blocksObject[blockId];
407-
// Recurse through 'next' and all 'inputs'
408-
if (traverse(block.next)) return true;
422+
423+
// Recurse through 'next'
424+
let cycleResult = traverse(block.next);
425+
if (cycleResult) return cycleResult;
426+
427+
// Recurse through all 'inputs'
409428
if (block.inputs) {
410429
for (const inputName in block.inputs) {
411430
const input = block.inputs[inputName];
412431
// The input is an array, e.g., [1, 'shadow-id'], [2, 'block-id'], [3, 'block-id', 'shadow-id']
413432
// The actual connected block is the second element (index 1).
414433
if (input && Array.isArray(input) && input.length > 1 && input[1]) {
415-
if (traverse(input[1])) {
416-
return true;
434+
cycleResult = traverse(input[1]);
435+
if (cycleResult) {
436+
return cycleResult;
417437
}
418438
}
419439
}
420440
}
421441

422-
visitedInPath.delete(blockId); // Backtrack: remove from current path
423-
return false;
442+
visitedInPath.delete(blockId); // Backtrack: remove from current path.
443+
currentPath.pop();
444+
visitedGlobally.add(blockId); // Mark this node as fully explored and safe.
445+
return null; // No cycle found from this path.
424446
}
425447

426-
if (traverse(startId)) {
427-
// Found a cycle starting from this block, no need to check others.
428-
return true;
448+
const result = traverse(startId);
449+
if (result && result.hasCycle) {
450+
// Found a cycle, no need to check other blocks.
451+
return result;
429452
}
430453
}
431-
return false; // No cycles found
454+
return {
455+
hasCycle: false
456+
}; // No cycles found in any block.
432457
}

src/addons/addons/collaboration/userscript.js

Lines changed: 126 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -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

Comments
 (0)