diff --git a/package-lock.json b/package-lock.json index 7dda6b7bf..9b5cd50a3 100644 --- a/package-lock.json +++ b/package-lock.json @@ -19386,6 +19386,11 @@ "dev": true, "license": "MIT" }, + "node_modules/queue": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/queue/-/queue-7.0.0.tgz", + "integrity": "sha512-sphwS7HdfQnvrJAXUNAUgpf9H/546IE3p/5Lf2jr71O4udEYlqAhkevykumas2FYuMkX/29JMOgrRdRoYZ/X9w==" + }, "node_modules/queue-microtask": { "version": "1.2.3", "dev": true, @@ -24350,7 +24355,8 @@ "license": "Apache-2.0", "dependencies": { "just-throttle": "^4.2.0", - "p5": "^1.11.0" + "p5": "^1.11.0", + "queue": "^7.0.0" } }, "packages/extension": { diff --git a/packages/explorable-explanations/package.json b/packages/explorable-explanations/package.json index 72566fc55..9488a6fb7 100644 --- a/packages/explorable-explanations/package.json +++ b/packages/explorable-explanations/package.json @@ -32,6 +32,7 @@ "homepage": "https://github.com/GoogleChromeLabs/ps-analysis-tool", "dependencies": { "just-throttle": "^4.2.0", - "p5": "^1.11.0" + "p5": "^1.11.0", + "queue": "^7.0.0" } } diff --git a/packages/explorable-explanations/src/protectedAudience/app.js b/packages/explorable-explanations/src/protectedAudience/app.js index afc62d402..e00050298 100644 --- a/packages/explorable-explanations/src/protectedAudience/app.js +++ b/packages/explorable-explanations/src/protectedAudience/app.js @@ -62,6 +62,7 @@ const app = { visitedIndexOrderTracker: -1, isRevisitingNodeInInteractiveMode: false, usedNextOrPrev: false, + promiseQueue: null, canvasEventListerners: { main: { mouseOver: {}, diff --git a/packages/explorable-explanations/src/protectedAudience/components/branches.js b/packages/explorable-explanations/src/protectedAudience/components/branches.js index eb7b2ca8f..7fd69fc5b 100644 --- a/packages/explorable-explanations/src/protectedAudience/components/branches.js +++ b/packages/explorable-explanations/src/protectedAudience/components/branches.js @@ -150,11 +150,6 @@ const drawAnimatedTimeline = (x, y, branches) => { return; } - if (app.cancelPromise) { - resolve(); - return; - } - if (progress >= i * spacing && !renderedBranchIds.includes(branch.id)) { // Draw vertical line once the horizontal line reaches its position p.push(); diff --git a/packages/explorable-explanations/src/protectedAudience/components/progressLine.js b/packages/explorable-explanations/src/protectedAudience/components/progressLine.js index 669553ac3..12c64a70a 100644 --- a/packages/explorable-explanations/src/protectedAudience/components/progressLine.js +++ b/packages/explorable-explanations/src/protectedAudience/components/progressLine.js @@ -102,12 +102,6 @@ const ProgressLine = ({ return new Promise((resolve) => { const animate = () => { - if (noAnimation || app.isRevisitingNodeInInteractiveMode) { - drawInstantly(); - resolve(returnCoordinates); - return; - } - if (app.cancelPromise) { resolve(); return; @@ -118,6 +112,12 @@ const ProgressLine = ({ return; } + if (noAnimation || app.isRevisitingNodeInInteractiveMode) { + drawInstantly(); + resolve(returnCoordinates); + return; + } + p.push(); p.stroke(0); p.strokeWeight(1); diff --git a/packages/explorable-explanations/src/protectedAudience/index.js b/packages/explorable-explanations/src/protectedAudience/index.js index 73ebe13a1..b185ab51d 100644 --- a/packages/explorable-explanations/src/protectedAudience/index.js +++ b/packages/explorable-explanations/src/protectedAudience/index.js @@ -18,6 +18,7 @@ */ import p5 from 'p5'; import * as d3 from 'd3'; +import Queue from 'queue'; /** * Internal dependencies. @@ -31,7 +32,6 @@ import joinInterestGroup from './modules/joinInterestGroup'; import icons from '../icons.json'; import bubbles from './modules/bubbles'; import app from './app'; -import promiseQueue from './lib/promiseQueue'; import { setupInterestGroupCanvas, setupMainCanvas, @@ -80,11 +80,17 @@ app.play = (resumed = false, doNotPlay = false) => { } app.timeline.isPaused = false; + if (!resumed) { app.setupLoop(doNotPlay); return; } - promiseQueue.resume(); + + try { + app.promiseQueue.start(); + } catch (error) { + //Fail silently since this gives an error even after stopping the queue. + } }; app.pause = () => { @@ -93,6 +99,7 @@ app.pause = () => { app.pauseButton.classList.add('hidden'); app.playButton.classList.remove('hidden'); } + app.promiseQueue.stop(); app.timeline.isPaused = true; }; @@ -145,39 +152,42 @@ app.minifiedBubbleClickListener = (event, expandOverride) => { } }; +app.addToPromiseQueue = (indexToStartFrom) => { + let currentIndex = indexToStartFrom; + while (currentIndex < config.timeline.circles.length) { + app.promiseQueue.push((cb) => { + flow.clearBelowTimelineCircles(); + utils.markVisitedValue(app.timeline.currentIndex, true); + bubbles.generateBubbles(); + bubbles.showMinifiedBubbles(); + timeline.eraseAndRedraw(); + timeline.renderUserIcon(); + cb(null, true); + }); + + app.drawFlows(currentIndex); + app.promiseQueue.push((cb) => { + app.bubbles.interestGroupCounts += + config.timeline.circles[app.timeline.currentIndex]?.igGroupsCount ?? 0; + cb(null, true); + }); + + app.promiseQueue.push((cb) => { + app.timeline.currentIndex += 1; + flow.setButtonsDisabilityState(); + cb(null, true); + }); + + currentIndex++; + } +}; + app.setupLoop = (doNotPlay) => { try { flow.setButtonsDisabilityState(); - let currentIndex = 0; - promiseQueue.nextNodeSkipIndex.push(0); - while (currentIndex < config.timeline.circles.length) { - promiseQueue.add(() => { - flow.clearBelowTimelineCircles(); - utils.markVisitedValue(app.timeline.currentIndex, true); - bubbles.generateBubbles(); - bubbles.showMinifiedBubbles(); - timeline.eraseAndRedraw(); - timeline.renderUserIcon(); - }); - - app.drawFlows(currentIndex); - promiseQueue.add(() => { - app.bubbles.interestGroupCounts += - config.timeline.circles[app.timeline.currentIndex]?.igGroupsCount ?? - 0; - }); - promiseQueue.nextNodeSkipIndex.push(promiseQueue.queue.length); - promiseQueue.add(() => { - app.timeline.currentIndex += 1; - flow.setButtonsDisabilityState(); - }); - - currentIndex++; - } + app.addToPromiseQueue(0); } catch (error) { //Silently fail. - // eslint-disable-next-line no-console - console.log(error); } timeline.eraseAndRedraw(); timeline.renderUserIcon(); @@ -186,7 +196,7 @@ app.setupLoop = (doNotPlay) => { return; } - promiseQueue.start(); + app.promiseQueue.start(); }; app.drawFlows = (index) => { @@ -200,20 +210,20 @@ app.minifiedBubbleKeyPressListener = (event) => { } }; -app.handleNonInteractivePrev = () => { +app.handleNonInteractivePrev = async () => { if (app.timeline.currentIndex <= 0) { return; } + app.promiseQueue.end(); app.cancelPromise = true; app.timeline.isPaused = true; - const nextIndexPromiseGetter = app.timeline.currentIndex - 1; app.timeline.currentIndex -= 1; - flow.setButtonsDisabilityState(); - const nextIndex = promiseQueue.nextNodeSkipIndex[nextIndexPromiseGetter]; + await utils.delay(100); - promiseQueue.skipTo(nextIndex + 1); + app.addToPromiseQueue(app.timeline.currentIndex); + flow.setButtonsDisabilityState(); utils.markVisitedValue(app.timeline.currentIndex, true); @@ -224,6 +234,7 @@ app.handleNonInteractivePrev = () => { app.bubbles.interestGroupCounts = bubbles.calculateTotalBubblesForAnimation( app.timeline.currentIndex ); + app.promiseQueue.start(); }; app.handleInteractivePrev = () => { @@ -231,7 +242,7 @@ app.handleInteractivePrev = () => { return; } - promiseQueue.clear(); + app.promiseQueue.end(); flow.setButtonsDisabilityState(); app.shouldRespondToClick = false; @@ -245,12 +256,13 @@ app.handleInteractivePrev = () => { app.drawFlows(visitedIndex); - promiseQueue.add(() => { + app.promiseQueue.push((cb) => { app.shouldRespondToClick = true; app.isRevisitingNodeInInteractiveMode = false; config.timeline.circles[visitedIndex].visited = true; bubbles.showMinifiedBubbles(); timeline.renderUserIcon(); + cb(null, true); }); if (app.visitedIndexOrderTracker >= 0) { @@ -262,8 +274,8 @@ app.handleInteractivePrev = () => { utils.wipeAndRecreateMainCanvas(); utils.wipeAndRecreateUserCanvas(); timeline.renderUserIcon(); - promiseQueue.skipTo(0); - promiseQueue.start(); + + app.promiseQueue.start(); }; app.handlePrevButton = () => { @@ -292,24 +304,21 @@ app.handleNextButton = () => { app.handleNonInteravtiveNext(); }; -app.handleNonInteravtiveNext = () => { +app.handleNonInteravtiveNext = async () => { if ( app.bubbles.isExpanded || app.timeline.currentIndex > config.timeline.circles.length - 1 ) { return; } - + app.promiseQueue.end(); app.timeline.isPaused = true; app.cancelPromise = true; app.timeline.currentIndex += 1; + await utils.delay(100); + app.addToPromiseQueue(app.timeline.currentIndex); flow.setButtonsDisabilityState(); - const nextIndexPromiseGetter = app.timeline.currentIndex; - const nextIndex = promiseQueue.nextNodeSkipIndex[nextIndexPromiseGetter]; - - promiseQueue.skipTo(nextIndex + 1); - utils.markVisitedValue(app.timeline.currentIndex, true); utils.wipeAndRecreateMainCanvas(); @@ -319,6 +328,8 @@ app.handleNonInteravtiveNext = () => { app.bubbles.interestGroupCounts = bubbles.calculateTotalBubblesForAnimation( app.timeline.currentIndex ); + + app.promiseQueue.start(); }; app.handleInteravtiveNext = () => { @@ -337,7 +348,7 @@ app.handleInteravtiveNext = () => { } } - promiseQueue.clear(); + app.promiseQueue.end(); flow.setButtonsDisabilityState(); app.shouldRespondToClick = false; @@ -351,12 +362,13 @@ app.handleInteravtiveNext = () => { app.drawFlows(visitedIndex); - promiseQueue.add(() => { + app.promiseQueue.push((cb) => { app.shouldRespondToClick = true; app.isRevisitingNodeInInteractiveMode = false; config.timeline.circles[visitedIndex].visited = true; bubbles.showMinifiedBubbles(); timeline.renderUserIcon(); + cb(null, true); }); flow.setButtonsDisabilityState(); @@ -364,8 +376,7 @@ app.handleInteravtiveNext = () => { utils.wipeAndRecreateMainCanvas(); utils.wipeAndRecreateUserCanvas(); timeline.renderUserIcon(); - promiseQueue.skipTo(0); - promiseQueue.start(); + app.promiseQueue.start(); }; app.handleControls = () => { @@ -427,8 +438,7 @@ app.handleControls = () => { }; app.toggleInteractiveMode = async () => { - promiseQueue.clear(); - promiseQueue.stop(); + app.promiseQueue.end(); app.cancelPromise = true; app.timeline.isPaused = true; @@ -449,14 +459,12 @@ app.toggleInteractiveMode = async () => { setupUserCanvas(app.up); setupMainCanvas(app.p, true); - promiseQueue.skipTo(0); - if (app.isInteractiveMode) { flow.setButtonsDisabilityState(); return; } - promiseQueue.start(); + app.promiseQueue.start(); }; // Write a callback function to get the value of the checkbox. @@ -466,6 +474,22 @@ app.toggleMultSeller = (event) => { // Define the sketch export const sketch = (p) => { + app.promiseQueue = new Queue({ + concurrency: 1, + autostart: false, + results: [], + }); + + app.promiseQueue.addEventListener('end', () => { + app.cancelPromise = true; + app.timeline.isPaused = true; + }); + + app.promiseQueue.addEventListener('start', () => { + app.cancelPromise = false; + app.timeline.isPaused = false; + }); + app.handleControls(); p.setup = () => { @@ -525,10 +549,9 @@ export const userSketch = (p) => { }; app.reset = async (callFromExtension = false) => { - promiseQueue.stop(); + app.promiseQueue.end(); app.cancelPromise = true; app.timeline.isPaused = true; - promiseQueue.clear(); app.timeline.currentIndex = 0; app.bubbles.interestGroupCounts = 0; @@ -548,7 +571,6 @@ app.reset = async (callFromExtension = false) => { app.timeline.isPaused = true; app.cancelPromise = false; - promiseQueue.skipTo(0); app.timeline.isPaused = false; app.shouldRespondToClick = true; diff --git a/packages/explorable-explanations/src/protectedAudience/lib/promiseQueue.js b/packages/explorable-explanations/src/protectedAudience/lib/promiseQueue.js deleted file mode 100644 index ee5985900..000000000 --- a/packages/explorable-explanations/src/protectedAudience/lib/promiseQueue.js +++ /dev/null @@ -1,130 +0,0 @@ -/* - * Copyright 2024 Google LLC - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -/** - * Internal dependencies - */ -import flow from '../modules/flow'; -import app from '../app'; - -class PromiseQueue { - constructor() { - this.queue = []; - this.isProcessing = false; - this.isPaused = false; - this.skipToIndex = -1; - this.nextNodeSkipIndex = []; - this.nextStepSkipIndex = []; - this.currentPromiseIndex = 0; - } - - // Add a promise-returning function to the queue - add(promiseFunction) { - this.queue.push(promiseFunction); - } - - // Start processing the queue - start(resumed = false) { - if (this.isProcessing) { - return; - } - this.isProcessing = true; - if (!resumed) { - this.processQueue(); - } - } - - // Stop processing the queue - stop() { - this.isProcessing = false; - } - - // Pause the queue - pause() { - this.isPaused = true; - } - - // Resume the queue - resume() { - if (!this.isPaused) { - return; - } - this.isPaused = false; - } - - // Process the queue sequentially - async processQueue() { - if (!this.isProcessing || this.isPaused) { - return; - } - - while (this.currentPromiseIndex < this.queue.length) { - if (this.isPaused) { - continue; - } - - if (this.skipToIndex > -1) { - this.currentPromiseIndex = this.skipToIndex; - this.skipToIndex = -1; - continue; - } - - const current = this.queue[this.currentPromiseIndex]; - - try { - // eslint-disable-next-line no-await-in-loop - await current(); - this.currentPromiseIndex++; - if (app.cancelPromise) { - flow.clearBelowTimelineCircles(); - app.cancelPromise = false; - app.timeline.isPaused = false; - if (this.queue.length === 0) { - return; - } - } - } catch (error) { - this.currentPromiseIndex++; - // eslint-disable-next-line no-console - console.error('Error in promise execution:', error); - } - } - - this.isProcessing = false; - } - - // Skip to a specific queue index - skipTo(index) { - if (index > this.queue.length) { - this.clear(); - this.currentPromiseIndex = 0; - } else { - this.skipToIndex = index; - } - } - - // Clear the queue - clear() { - this.queue = []; - this.isProcessing = false; - this.isPaused = false; - this.currentPromiseIndex = 0; - this.nextNodeSkipIndex = []; - this.nextStepSkipIndex = []; - this.skipToIndex = -1; - } -} - -export default new PromiseQueue(); diff --git a/packages/explorable-explanations/src/protectedAudience/modules/auctions.js b/packages/explorable-explanations/src/protectedAudience/modules/auctions.js index 0838476ea..48641e2a8 100644 --- a/packages/explorable-explanations/src/protectedAudience/modules/auctions.js +++ b/packages/explorable-explanations/src/protectedAudience/modules/auctions.js @@ -22,7 +22,6 @@ import config from '../config'; import * as utils from '../utils'; import { Box, ProgressLine, Branches, RippleEffect } from '../components'; import bubbles from './bubbles'; -import promiseQueue from '../lib/promiseQueue.js'; /** * @module Auction @@ -487,8 +486,7 @@ auction.draw = (index) => { } for (const step of steps) { - promiseQueue.nextStepSkipIndex.push(promiseQueue.queue.length - 1); - promiseQueue.add(async () => { + app.promiseQueue.push(async (cb) => { const { component, props, callBack } = step; const returnValue = await component(props); // eslint-disable-line no-await-in-loop @@ -532,13 +530,15 @@ auction.draw = (index) => { } await utils.delay(delay); // eslint-disable-line no-await-in-loop } + cb(null, true); }); } - promiseQueue.add(() => { + app.promiseQueue.push((cb) => { if (!app.isRevisitingNodeInInteractiveMode || !app.isInteractiveMode) { flow.clearBelowTimelineCircles(); } + cb(null, true); }); }; diff --git a/packages/explorable-explanations/src/protectedAudience/modules/joinInterestGroup.js b/packages/explorable-explanations/src/protectedAudience/modules/joinInterestGroup.js index 48aca5cc5..044533358 100644 --- a/packages/explorable-explanations/src/protectedAudience/modules/joinInterestGroup.js +++ b/packages/explorable-explanations/src/protectedAudience/modules/joinInterestGroup.js @@ -22,7 +22,6 @@ import config from '../config'; import * as utils from '../utils'; import { Box, ProgressLine } from '../components'; import bubbles from './bubbles'; -import promiseQueue from '../lib/promiseQueue'; import info from '../info.json'; /** @@ -167,9 +166,7 @@ joinInterestGroup.draw = (index) => { } for (const step of steps) { - promiseQueue.nextStepSkipIndex.push(promiseQueue.queue.length - 1); - - promiseQueue.add(async () => { + app.promiseQueue.push(async (cb) => { const { component, props, callBack, delay } = step; const returnValue = await component(props); // eslint-disable-line no-await-in-loop @@ -180,10 +177,11 @@ joinInterestGroup.draw = (index) => { if (!app.isRevisitingNodeInInteractiveMode) { await utils.delay(delay); // eslint-disable-line no-await-in-loop } + cb(null, true); }); } - promiseQueue.add(async () => { + app.promiseQueue.push(async (cb) => { if (!app.isRevisitingNodeInInteractiveMode) { await bubbles.reverseBarrageAnimation(index); } @@ -193,12 +191,14 @@ joinInterestGroup.draw = (index) => { } else { bubbles.showMinifiedBubbles(); } + cb(null, true); }); - promiseQueue.add(() => { + app.promiseQueue.push((cb) => { if (!app.isRevisitingNodeInInteractiveMode || !app.isInteractiveMode) { flow.clearBelowTimelineCircles(); } + cb(null, true); }); }; diff --git a/packages/explorable-explanations/src/protectedAudience/modules/timeline.js b/packages/explorable-explanations/src/protectedAudience/modules/timeline.js index dbe8222f6..b5e6cfdd5 100644 --- a/packages/explorable-explanations/src/protectedAudience/modules/timeline.js +++ b/packages/explorable-explanations/src/protectedAudience/modules/timeline.js @@ -20,7 +20,6 @@ import config from '../config'; import app from '../app'; import * as utils from '../utils'; import bubbles from './bubbles'; -import promiseQueue from '../lib/promiseQueue'; import flow from './flow'; /** @@ -129,18 +128,21 @@ timeline.init = () => { clickedIndex !== undefined && !app.isRevisitingNodeInInteractiveMode ) { - promiseQueue.clear(); + app.promiseQueue.end(); flow.clearBelowTimelineCircles(); + app.shouldRespondToClick = false; app.timeline.currentIndex = clickedIndex; + utils.wipeAndRecreateUserCanvas(); timeline.renderUserIcon(); bubbles.generateBubbles(); if (config.timeline.circles[clickedIndex].visited) { - promiseQueue.add(() => { + app.promiseQueue.push((cb) => { utils.wipeAndRecreateUserCanvas(); utils.wipeAndRecreateMainCanvas(); + app.p.push(); app.p.stroke(config.timeline.colors.grey); @@ -167,12 +169,13 @@ timeline.init = () => { }); app.p.pop(); + cb(null, true); }); } app.drawFlows(clickedIndex); - promiseQueue.add(() => { + app.promiseQueue.push((cb) => { if (config.timeline.circles[clickedIndex].visited) { app.visitedIndexOrder = app.visitedIndexOrder.filter((indexes) => { if (indexes === clickedIndex) { @@ -223,19 +226,20 @@ timeline.init = () => { bubbles.showMinifiedBubbles(); app.shouldRespondToClick = true; + utils.wipeAndRecreateUserCanvas(); utils.wipeAndRecreateMainCanvas(); timeline.renderUserIcon(); flow.setButtonsDisabilityState(); + cb(null, true); }); - promiseQueue.skipTo(0); - promiseQueue.start(); + app.promiseQueue.start(); } else if ( clickedIndex !== undefined && app.isRevisitingNodeInInteractiveMode ) { - promiseQueue.clear(); + app.promiseQueue.end(); flow.clearBelowTimelineCircles(); if (config.timeline.circles[clickedIndex].type === 'advertiser') { @@ -247,7 +251,7 @@ timeline.init = () => { app.shouldRespondToClick = false; app.drawFlows(clickedIndex); - promiseQueue.add(() => { + app.promiseQueue.push((cb) => { app.shouldRespondToClick = true; timeline.renderUserIcon(); app.isRevisitingNodeInInteractiveMode = false; @@ -256,10 +260,10 @@ timeline.init = () => { } else { app.auction.auctions[clickedIndex][0].props.y1 -= 20; } + cb(null, true); }); - promiseQueue.skipTo(0); - promiseQueue.start(); + app.promiseQueue.start(); utils.wipeAndRecreateUserCanvas(); utils.wipeAndRecreateMainCanvas(); diff --git a/tests/shared.jest.config.cjs b/tests/shared.jest.config.cjs index 36a015d19..93fecd1df 100644 --- a/tests/shared.jest.config.cjs +++ b/tests/shared.jest.config.cjs @@ -26,7 +26,7 @@ module.exports = { '^.+\\.[tj]sx?$': 'babel-jest', }, transformIgnorePatterns: [ - 'node_modules/(?!(p-queue|p-timeout|d3(-.*)?|internmap|delaunator|robust-predicates|pretty-print-json)/)', + 'node_modules/(?!(p-queue|p-timeout|d3(-.*)?|internmap|delaunator|robust-predicates|pretty-print-json|queue)/)', ], moduleNameMapper: { '^@google-psat\\/(.*)': '/packages/$1/src/',