Skip to content

Commit 4b2fb59

Browse files
committed
should properly fix a->b->a bug (thanks @EthanC112)
1 parent 65a0325 commit 4b2fb59

File tree

1 file changed

+97
-90
lines changed

1 file changed

+97
-90
lines changed

src/addons/addons/collaboration/helpers/yEvents.js

Lines changed: 97 additions & 90 deletions
Original file line numberDiff line numberDiff line change
@@ -530,8 +530,7 @@ export function processSpecificEvent(item) {
530530

531531
// --- Handle Standard Blockly Events ---
532532
// This section processes regular Blockly events (e.g., block creation, moves, changes, deletions)
533-
// that were broadcast by other clients. This also covers the 'echo' of locally fired events
534-
// by `vm.shareBlocksToTarget` (which are then re-broadcast by the local client for others).
533+
// that were broadcast by other clients.
535534
if (item.event && item.event.type) {
536535
const remoteTargetName = item.targetName; // The name of the target (sprite/stage) where the event occurred remotely.
537536
const eventJson = item.event; // The raw JSON representation of the Blockly event.
@@ -540,54 +539,8 @@ export function processSpecificEvent(item) {
540539
console.warn(`Collab RX: Workspace not found, cannot apply standard remote event type "${eventJson.type}". Skipping.`);
541540
return;
542541
}
543-
544-
let blocklyEvent;
545-
try {
546-
// Special handling for `BLOCK_MOVE` events that represent a block being "unplugged"
547-
// (moved to become a top-level block). Blockly's `fromJson` might not correctly capture
548-
// `oldParentId` in this scenario, so it's manually constructed to preserve the "unplug" action.
549-
if (eventJson.type === constants.mutableRefs.BlocklyInstance.Events.BLOCK_MOVE &&
550-
eventJson.newCoordinate && // Indicates a move to a specific coordinate
551-
(typeof eventJson.newParentId === 'undefined' || eventJson.newParentId === null) && // Indicates it's becoming a top-level block
552-
eventJson.blockId) {
553-
const blockToMove = workspace.getBlockById(eventJson.blockId);
554-
if (blockToMove) {
555-
// Create a new `BlockMove` event from the existing block to capture its current parentage as 'old' state.
556-
blocklyEvent = new constants.mutableRefs.BlocklyInstance.Events.BlockMove(blockToMove);
557-
// Explicitly set `newParentId` and `newInputName` to undefined/null to signify becoming a top block.
558-
blocklyEvent.newParentId = undefined;
559-
blocklyEvent.newInputName = undefined;
560-
// Parse and set the new coordinates.
561-
if (eventJson.newCoordinate) {
562-
const coords = eventJson.newCoordinate.split(',');
563-
blocklyEvent.newCoordinate = new constants.mutableRefs.BlocklyInstance.utils.Coordinate(parseFloat(coords[0]), parseFloat(coords[1]));
564-
}
565-
// Copy group ID if present.
566-
if (eventJson.group) {
567-
blocklyEvent.group = eventJson.group;
568-
}
569-
// Apply any other properties from the JSON that were not covered by the constructor.
570-
blocklyEvent.fromJson(eventJson);
571-
} else {
572-
// Fallback to standard `fromJson` if the block is not found (should be rare).
573-
console.warn(`Collab RX: Block ${eventJson.blockId} for programmatic detach not found. Falling back to direct fromJson.`);
574-
blocklyEvent = constants.mutableRefs.BlocklyInstance.Events.fromJson(eventJson, workspace);
575-
}
576-
} else {
577-
// For all other event types, or `BLOCK_MOVE`s that are not "unplugs", use standard `fromJson`.
578-
blocklyEvent = constants.mutableRefs.BlocklyInstance.Events.fromJson(eventJson, workspace);
579-
}
580-
581-
if (!blocklyEvent) {
582-
throw new Error('Blockly.Events.fromJson returned undefined or null');
583-
}
584-
} catch (e) {
585-
console.error(`Collab RX: Error deserializing standard event JSON type "${eventJson.type}":`, e, eventJson);
586-
return; // Skip this event if deserialization fails.
587-
}
588-
542+
589543
// --- Determine the correct Scratch VM Target for the event ---
590-
// The `remoteTargetName` specifies which sprite or the Stage this event applies to.
591544
let target = null;
592545
const stage = constants.mutableRefs.vm.runtime.getTargetForStage();
593546
if (remoteTargetName && stage?.getName() === remoteTargetName) {
@@ -596,54 +549,121 @@ export function processSpecificEvent(item) {
596549
target = constants.mutableRefs.vm.runtime.getSpriteTargetByName(remoteTargetName);
597550
}
598551

599-
// Fallback or error handling if the target cannot be resolved.
600552
if (!target) {
601-
if (remoteTargetName) {
602-
// If a specific target name was provided but not found locally, it's a potential issue.
603-
console.warn(`Collab RX: Target "${remoteTargetName}" for standard event type "${blocklyEvent.type}" (Block ID: ${blocklyEvent.blockId || 'N/A'}) not found. VM update may be skipped or fail.`);
604-
} else {
605-
// If no remote target name was specified, it might be a truly global event.
606-
// In such cases, attempt to use the currently editing target as a fallback.
607-
target = constants.mutableRefs.vm.runtime.getEditingTarget();
608-
if (constants.debugging && target) console.log(`Collab RX: Standard event type "${blocklyEvent.type}" had no remoteTargetName. Using current editing target "${target.getName()}" for VM update attempt.`);
553+
console.error(`Collab RX: Skipping event type "${eventJson.type}" due to inability to resolve target (Remote: "${remoteTargetName}").`);
554+
return;
555+
}
556+
557+
const currentEditingTargetName = helper.getCurrentEditingTargetName();
558+
const isViewingCorrectTarget = remoteTargetName && currentEditingTargetName === remoteTargetName;
559+
560+
// --- START: CUSTOM BLOCK_MOVE HANDLER (FIX for circular dependency) ---
561+
// This custom handler addresses a race condition where rapidly replaying `move` events
562+
// via `event.run()` can lead to an inconsistent state and create loops.
563+
// We handle it by manually performing the unplug and connect/move operations in a guaranteed order.
564+
if (eventType === constants.mutableRefs.BlocklyInstance.Events.BLOCK_MOVE) {
565+
const blockToMove = workspace.getBlockById(eventData.blockId);
566+
567+
if (!blockToMove) {
568+
console.warn(`Collab RX [move]: Block ${eventData.blockId} not found. Skipping move.`);
569+
return;
609570
}
571+
if(constants.debugging) console.log(`Collab RX [move]: Applying custom move for block ${blockToMove.id}.`);
610572

611-
if (!target) {
612-
console.error(`Collab RX: Skipping standard event type "${blocklyEvent.type}" (Block ID: ${blocklyEvent.blockId || 'N/A'}) due to inability to determine/resolve target (Remote: "${remoteTargetName}").`);
613-
return; // Critical failure to resolve target, skip event.
573+
// 1. Unplug the block first to ensure the old state is cleanly severed. This is the critical step.
574+
if (blockToMove.getParent()) {
575+
if(constants.debugging) console.log(`Collab RX [move]: Unplugging block ${blockToMove.id} from parent.`);
576+
blockToMove.unplug(false);
614577
}
615-
}
616578

617-
const currentEditingTargetName = helper.getCurrentEditingTargetName(); // Get the name of the target currently being edited locally.
579+
// 2. Perform the new move or connection.
580+
// Case A: Connecting to a new parent.
581+
if (eventData.newParentId) {
582+
const newParentBlock = workspace.getBlockById(eventData.newParentId);
583+
if (!newParentBlock) {
584+
console.warn(`Collab RX [move]: New parent block ${eventData.newParentId} not found. Skipping connect.`);
585+
return;
586+
}
587+
const childConnection = blockToMove.outputConnection || blockToMove.previousConnection;
588+
let parentConnection;
589+
if (eventData.newInputName) {
590+
const input = newParentBlock.getInput(eventData.newInputName);
591+
parentConnection = input ? input.connection : null;
592+
} else {
593+
parentConnection = newParentBlock.nextConnection;
594+
}
595+
596+
if (parentConnection && childConnection && parentConnection.checkType_(childConnection)) {
597+
if(constants.debugging) console.log(`Collab RX [move]: Connecting block ${blockToMove.id} to parent ${newParentBlock.id}.`);
598+
parentConnection.connect(childConnection);
599+
} else {
600+
console.warn(`Collab RX [move]: Could not find a valid connection to attach block ${blockToMove.id}.`);
601+
}
602+
}
603+
// Case B: Moving to a top-level coordinate.
604+
else if (eventData.newCoordinate) {
605+
const coords = eventData.newCoordinate.split(',');
606+
let newX = parseFloat(coords[0]);
607+
const newY = parseFloat(coords[1]);
608+
609+
if(isViewingCorrectTarget) {
610+
if (workspace.RTL) {
611+
newX = workspace.getWidth() - newX;
612+
}
613+
const oldXY = blockToMove.getRelativeToSurfaceXY();
614+
if(constants.debugging) console.log(`Collab RX [move]: Moving block ${blockToMove.id} to coordinates.`);
615+
blockToMove.moveBy(newX - oldXY.x, newY - oldXY.y);
616+
}
617+
}
618+
619+
// 3. Manually update the VM's block model. `blocklyListen` is the safest way.
620+
const blocklyEvent = constants.mutableRefs.BlocklyInstance.Events.fromJson(eventJson, workspace);
621+
if (target.blocks && typeof target.blocks.blocklyListen === 'function') {
622+
if(constants.debugging) console.log(`Collab RX [move]: Updating VM state for Target="${target.getName()}" via blocklyListen.`);
623+
target.blocks.blocklyListen(blocklyEvent);
624+
}
625+
626+
return; // Exit here to prevent the default handler from running.
627+
}
628+
// --- END: CUSTOM BLOCK_MOVE HANDLER ---
629+
630+
let blocklyEvent = constants.mutableRefs.BlocklyInstance.Events.fromJson(eventJson, workspace);
618631

619632
// --- Handle Comment Events ---
620-
// Comment events are handled separately via helper functions because they might need to update
621-
// a workspace that is not currently visible (i.e., not the currently editing target's workspace).
622633
if (blocklyEvent.type === constants.mutableRefs.BlocklyInstance.Events.COMMENT_CREATE &&
623-
remoteTargetName && currentEditingTargetName !== remoteTargetName) {
634+
remoteTargetName && !isViewingCorrectTarget) {
624635
helper.CommentCreate(blocklyEvent, remoteTargetName);
625-
return; // Stop further processing for this event.
636+
return;
626637
}
627638

628639
if (blocklyEvent.type === constants.mutableRefs.BlocklyInstance.Events.COMMENT_DELETE &&
629-
remoteTargetName && currentEditingTargetName !== remoteTargetName) {
640+
remoteTargetName && !isViewingCorrectTarget) {
630641
helper.CommentDelete(blocklyEvent, remoteTargetName);
631-
return; // Stop further processing for this event.
642+
return;
632643
}
633644

634645
if (blocklyEvent.type === constants.mutableRefs.BlocklyInstance.Events.COMMENT_CHANGE &&
635-
remoteTargetName && currentEditingTargetName !== remoteTargetName) {
646+
remoteTargetName && !isViewingCorrectTarget) {
636647
helper.CommentChange(blocklyEvent, remoteTargetName);
637-
return; // Stop further processing for this event.
648+
return;
638649
}
639650

640651
if (blocklyEvent.type === constants.mutableRefs.BlocklyInstance.Events.COMMENT_MOVE &&
641-
remoteTargetName && currentEditingTargetName !== remoteTargetName) {
652+
remoteTargetName && !isViewingCorrectTarget) {
642653
helper.CommentMove(blocklyEvent, remoteTargetName);
643-
return; // Stop further processing for this event.
654+
return;
644655
}
645656

646-
// --- Apply Standard Event to Blockly Workspace and VM ---
657+
// --- Apply Standard Event to Blockly Workspace and VM (Default Handler) ---
658+
try {
659+
if (!blocklyEvent) {
660+
throw new Error('Blockly.Events.fromJson returned undefined or null');
661+
}
662+
} catch (e) {
663+
console.error(`Collab RX: Error deserializing standard event JSON type "${eventJson.type}":`, e, eventJson);
664+
return;
665+
}
666+
647667
try {
648668
if (constants.debugging) {
649669
console.log(`Collab RX: Processing standard event=${blocklyEvent.type} (Block ID: ${blocklyEvent.blockId || 'N/A'})`);
@@ -652,22 +672,14 @@ export function processSpecificEvent(item) {
652672
}
653673

654674
// 1. Apply the event VISUALLY to the Blockly workspace.
655-
// This is only done if the local client is currently viewing the target (sprite or Stage)
656-
// where the event originated, or if it's a global event.
657-
const isViewingCorrectTarget = remoteTargetName && currentEditingTargetName === remoteTargetName;
658-
const isPotentiallyGlobalEvent = !remoteTargetName; // Covers events not specific to a sprite.
675+
const isPotentiallyGlobalEvent = !remoteTargetName;
659676

660-
// Only run visual updates if the local workspace is relevant.
661-
// `VAR_CREATE` events are always applied visually as they affect the toolbox regardless of current target view.
662677
if (isViewingCorrectTarget || isPotentiallyGlobalEvent || constants.mutableRefs.BlocklyInstance.Events.VAR_CREATE === blocklyEvent.type) {
663678
let canRunVisually = true;
664-
// Optimization: For BLOCK_CREATE events, if the block already exists visually, skip visual application.
665-
// This can happen if `shareBlocksToTarget` already created the blocks locally, and this is the echo.
666679
if (blocklyEvent.type === constants.mutableRefs.BlocklyInstance.Events.BLOCK_CREATE && blocklyEvent.blockId && currentWorkspace.getBlockById(blocklyEvent.blockId)) {
667680
if (constants.debugging) console.log(`Collab RX: Skipping visual .run() for standard CREATE on block ${blocklyEvent.blockId} - block already exists in current workspace view.`);
668681
canRunVisually = false;
669682
}
670-
// For other events (MOVE, CHANGE, DELETE), ensure the block exists before trying to apply changes visually.
671683
else if (blocklyEvent.blockId && (blocklyEvent.type === constants.mutableRefs.BlocklyInstance.Events.MOVE || blocklyEvent.type === constants.mutableRefs.BlocklyInstance.Events.CHANGE || blocklyEvent.type === constants.mutableRefs.BlocklyInstance.Events.DELETE)) {
672684
if (!currentWorkspace.getBlockById(blocklyEvent.blockId)) {
673685
if (constants.debugging) console.log(`Collab RX: Skipping visual .run() for ${blocklyEvent.type} on block ${blocklyEvent.blockId} - block not found in current workspace view.`);
@@ -677,9 +689,7 @@ export function processSpecificEvent(item) {
677689

678690
if (canRunVisually) {
679691
if (constants.debugging) console.log(`Collab RX: Applying standard event ${blocklyEvent.type} VISUALLY.`, item);
680-
blocklyEvent.run(true); // Apply the event forward to the Blockly workspace.
681-
// Refresh the toolbox if a procedure definition was created/modified or a block was deleted,
682-
// as these can affect which blocks are available.
692+
blocklyEvent.run(true);
683693
if (item?.event?.xml?.includes("procedures_definition") || item?.event?.newValue?.includes("argumentids") || blocklyEvent.type === "delete") {
684694
console.log(`Collab RX: Refreshing toolbox`);
685695
constants.mutableRefs.BlocklyInstance.getMainWorkspace().refreshToolboxSelection_();
@@ -688,19 +698,16 @@ export function processSpecificEvent(item) {
688698
} else if (constants.debugging) console.log(`Collab RX: Skipping visual application of standard event ${blocklyEvent.type} because remote target "${remoteTargetName}" is not the current view "${currentEditingTargetName}".`);
689699

690700
// 2. Apply the event to the Scratch VM state.
691-
// This ensures the underlying data model is consistent, regardless of what's visually shown.
692701
if (target.blocks && typeof target.blocks.blocklyListen === 'function') {
693702
let canRunVmUpdate = true;
694-
// Optimization: For BLOCK_CREATE events, if the block already exists in the VM target's blocks, skip VM update.
695-
// This handles cases where `shareBlocksToTarget` already populated the VM.
696703
if (blocklyEvent.type === constants.mutableRefs.BlocklyInstance.Events.BLOCK_CREATE && blocklyEvent.blockId && target.blocks.getBlock(blocklyEvent.blockId)) {
697704
if (constants.debugging) console.log(`Collab RX: Skipping VM .blocklyListen() for standard CREATE on block ${blocklyEvent.blockId} - block already exists in VM target "${target.getName()}".`);
698705
canRunVmUpdate = false;
699706
}
700707

701708
if (canRunVmUpdate) {
702709
if (constants.debugging) console.log(`Collab RX: Updating VM state for Target="${target.getName()}" via blocklyListen for event type ${blocklyEvent.type}.`);
703-
target.blocks.blocklyListen(blocklyEvent); // Apply the event to the VM's block manager.
710+
target.blocks.blocklyListen(blocklyEvent);
704711
}
705712
} else {
706713
console.error(`Collab RX: Failed to update VM state for standard event type ${blocklyEvent?.type}. Target "${target?.getName()}" or its 'blocks.blocklyListen' method not found.`);

0 commit comments

Comments
 (0)