From 72868fed7f1ba36c20871eb3b2abe9f69e2dc3c5 Mon Sep 17 00:00:00 2001 From: Jeremy Sharpe Date: Sun, 23 Aug 2026 17:50:33 -0400 Subject: [PATCH] Fix verification and editor sync against current Chrome and Scratch Two regressions from the platform moving under the extension made it silently do nothing: verification never completed, and no edit ever reached the server. Verification (background.js): the getUsername onMessage listener was `async`, so it returned a Promise for every message, and Chrome now delivers the resolved value (undefined) as the response, closing the channel before slower handlers like auth.js's `verify?` could reply. Neither branch awaits before calling sendResponse, so dropping `async` is safe. Editor sync (scripts/editor.js): several Blockly/scratch-blocks APIs no longer exist, and the first failure aborted activateLivescratch() before blockListener was registered. Republish window.Blockly from the trapped ScratchBlocks instance (the page's Blockly global lost getMainWorkspace()), and guard its call sites so they can't throw before the trap fires. Resolve the workspace via getMainWorkspace() and the dragged block via the current gesture, replacing the retired WorkspaceDB_, topBlocks_, and block drag surface (also dropping the now-dead getFlyout). Skip events flagged isUiEvent, whose 'drag' payload port.postMessage() cannot serialize. Anchor the revert button on any menu-bar button now that the "See Project Page" span is gone. Verified the API and DOM changes against the live scratch.mit.edu editor. Co-Authored-By: Claude Opus 5 Co-Authored-By: Claude Fable 5 --- extension/background.js | 5 ++++- extension/scripts/editor.js | 39 +++++++++++++++++-------------------- 2 files changed, 22 insertions(+), 22 deletions(-) diff --git a/extension/background.js b/extension/background.js index 0bb60f7..501dfd8 100755 --- a/extension/background.js +++ b/extension/background.js @@ -511,7 +511,10 @@ async function backgroundScript() { }); - chrome.runtime.onMessage.addListener(async function (request, sender, sendResponse) { + // NOTE: must NOT be `async`. An async listener returns a Promise for every + // message, which Chrome resolves (to undefined here) and sends as the response, + // closing the channel before slower handlers like auth.js's `verify?` can reply. + chrome.runtime.onMessage.addListener(function (request, sender, sendResponse) { if (request.meta == 'getUsername') { sendResponse(uname); } else if (request.meta == 'getUsernamePlus') { diff --git a/extension/scripts/editor.js b/extension/scripts/editor.js index 1db7c3a..298dafc 100755 --- a/extension/scripts/editor.js +++ b/extension/scripts/editor.js @@ -425,6 +425,9 @@ async function activateLivescratch() { reactLoopInst = reactLoopInst.child; } ScratchBlocks = reactLoopInst.stateNode.ScratchBlocks; //reactInst.child.child.child.child.child.child.child.stateNode.ScratchBlocks; + // The editor no longer exposes a global Blockly with getMainWorkspace(), which + // this file calls in several places. ScratchBlocks is that same instance. + if(typeof window.Blockly?.getMainWorkspace != 'function') { window.Blockly = ScratchBlocks; } getWorkspace().removeChangeListener(blockListener); getWorkspace().addChangeListener(blockListener); }); @@ -451,29 +454,18 @@ async function activateLivescratch() { } function getWorkspace() { - let retVal = Blockly.getMainWorkspace(); - if(typeof ScratchBlocks == 'undefined') {return retVal;} - Object.entries(ScratchBlocks.Workspace.WorkspaceDB_).forEach(wkv=>{ - if(!wkv[1].isFlyout && wkv[1].deleteAreaToolbox_) {retVal = wkv[1];} - }); - return retVal; - } - function getFlyout() { - if(typeof ScratchBlocks == 'undefined') {return null;} - Object.entries(ScratchBlocks.Workspace.WorkspaceDB_).forEach(wkv=>{ - if(wkv[1].isFlyout /*&& wkv[1].deleteAreaToolbox_*/) {retVal = wkv[1];} - }); - return retVal; + // getMainWorkspace is missing until the ScratchBlocks trap republishes window.Blockly + return window.Blockly?.getMainWorkspace?.(); } function getWorkspaceId() { return getWorkspace()?.id; } function getDraggingId() { - return Blockly.getMainWorkspace().getBlockDragSurface().getCurrentBlock()?.getAttribute('data-id'); + return window.Blockly?.getMainWorkspace?.()?.currentGesture_?.targetBlock?.id; } function isDragging() { - return Blockly.getMainWorkspace()?.isDragging(); + return window.Blockly?.getMainWorkspace?.()?.isDragging(); } // STAGE IDENTIFIER. DO NOT SET SPRITE NAME TO THIS UNLESS YOU WANT TO PURPOSEFULLY BREAK LINKAGE!!!! @@ -508,7 +500,6 @@ async function activateLivescratch() { BL_UTILS = { isWorkspaceAccessable, getWorkspace, - getFlyout, getWorkspaceId, getDraggingId, isDragging, targetToName, @@ -859,7 +850,10 @@ async function activateLivescratch() { let stringRep = getStringEventRep(e); if(stringRep in livescratchEvents) {delete livescratchEvents[stringRep];} else if( - !e.isLivescratch && + !e.isLivescratch && + // ui events now carry distinct types (a 'drag' event holds payload that + // port.postMessage() cannot serialize) and are flagged isUiEvent instead + !e.isUiEvent && ['endDrag','ui','dragOutside'].indexOf(e.type) == -1 && !isBadToSend(e,vm.editingTarget) && e.element != 'stackclick' @@ -1195,7 +1189,7 @@ async function activateLivescratch() { livescratchEvents[getStringEventRep({type:'comment_create',commentId})] = true; }); // add deletes for top blocks in current workspace - getWorkspace()?.topBlocks_.forEach(block=>{ + (getWorkspace()?.getTopBlocks?.(false) ?? []).forEach(block=>{ livescratchEvents[getStringEventRep({type:'delete',blockId:block.id})] = true; }); // add creates for all blocks in new workspace @@ -2932,9 +2926,12 @@ function addButtonInjectors() { function addRevertButton() { - let seeProjectPage = Array.from(document.querySelectorAll('span[class*="community-button_community-button"]')).find(e=>e.innerText?.includes('Page')); + // the "See Project Page" control is no longer a span; fall back to any + // menu-bar button (the first one is the community button) as the anchor + let seeProjectPage = Array.from(document.querySelectorAll('span[class*="community-button_community-button"]')).find(e=>e.innerText?.includes('Page')) + ?? document.querySelector('[class*="menu-bar_menu-bar-button"]'); - if(!blId) {return;} + if(!blId || !seeProjectPage) {return;} // let container = document.createElement('revertContainer') // container.style.display = 'flex' @@ -2968,7 +2965,7 @@ function addRevertButton() { seeProjectPage.before(button); // delete tutorials text - Array.from(document.querySelectorAll('span')).find(e=>e.className.includes('menu-bar_tutorials-label')).remove(); + Array.from(document.querySelectorAll('span')).find(e=>e.className.includes('menu-bar_tutorials-label'))?.remove(); }