// ==UserScript== // @name Watches Favorites Viewer // @namespace Violentmonkey Scripts // @match *://*.furaffinity.net/* // @require https://cdnjs.cloudflare.com/ajax/libs/lz-string/1.4.4/lz-string.min.js // @require https://greasyfork.org/scripts/475041-furaffinity-custom-settings/code/Furaffinity-Custom-Settings.js // @grant none // @version 2.1.2 // @author Midori Dragon // @description Scans the Favorites of your Watches for new Favorites and shows a Button to view these (if any where found). (Works like Submission Page) // @icon https://www.furaffinity.net/themes/beta/img/banners/fa_logo.png?v2 // @homepageURL https://greasyfork.org/de/scripts/463464-watches-favorites-viewer // @supportURL https://greasyfork.org/de/scripts/463464-watches-favorites-viewer/feedback // @license MIT // @downloadURL none // ==/UserScript== // jshint esversion: 8 CustomSettings.name = "Extension Settings"; CustomSettings.provider = "Midori's Script Settings"; CustomSettings.headerName = `${GM_info.script.name} Settings`; const showLoadLastXFavsButtonSetting = CustomSettings.newSetting("Last X Favs", "Sets wether the Load last x Favs buttons appears after a new Fav scan found no new Favs.", SettingTypes.Boolean, "Show Last X Favs Button", false); const maxFavsLengthSetting = CustomSettings.newSetting("Max Favs Loaded", "Sets the maximum number of Favs loaded.", SettingTypes.Number, "", 100); const maxAmountRequestsSetting = CustomSettings.newSetting("Max amount of simultaneous requests", "Sets the maximum number of simultaneous requests. Higher value means faster scans but a too high value will overload Furaffinity and cause problems.", SettingTypes.Number, "", 2); const doImmediateScanSetting = CustomSettings.newSetting("Immediately Scan", "Sets wether a scan is started immediately uppon loading a Page.", SettingTypes.Boolean, "Immediately start a Scan", false); const showDublicateFavsSetting = CustomSettings.newSetting("Show dublicate Favs", "Sets wether to show dublicate Submissions. (when multiple people Faved the same Submission)", SettingTypes.Boolean, "Show dublicate Favs", false); const showExcludedUsersInLogSetting = CustomSettings.newSetting("Show excluded Users in Log", "Sets wether to show excluded Users inside the console log while scanning.", SettingTypes.Boolean, "Show excluded Users", false); const resetSynchronizationErrorSetting = CustomSettings.newSetting("Reset Synchronisation", "Resets the synchronisation variable to fix an error that no scan will start.", SettingTypes.Action, "Reset Loadingstate", null, (target) => { localStorage.removeItem("wfloadingstate"); const wfloadingstatetemp = localStorage.getItem("wfloadingstate"); if (wfloadingstatetemp == null || wfloadingstatetemp == undefined) { target.textContent = "<---- Success ---->"; setTimeout(() => { target.textContent = "Reset Loadingstate"; }, 3000); } else { target.textContent = "<---- Failed ---->"; setTimeout(() => { target.textContent = "Reset Loadingstate"; }, 3000); } }); const resetSavingVariableSetting = CustomSettings.newSetting("Reset Last seen Favs", "Resets the last seen favs variable to reinitialize the Fav-Scanner.", SettingTypes.Action, "Reset Last seen Favs", null, (target) => { localStorage.removeItem("lastFavs"); const lastfavxtemp = localStorage.getItem("lastFavs"); if (lastfavxtemp == null || lastfavxtemp == undefined) { target.textContent = "<---- Success ---->"; setTimeout(() => { target.textContent = "Reset Last seen Favs"; }, 3000); } else { target.textContent = "<---- Failed ---->"; setTimeout(() => { target.textContent = "Reset Last seen Favs"; }, 3000); } }); CustomSettings.loadSettings(); if (window.parent !== window) return; let lastFavs = {}; let _running = false; let exButtonsShown = false; let firstStart = false; let wfButton; let settingsCount = 0; let progress; let currentLength = 0; let totalLength = 0; let percent = 0; let color = "color: blue"; if (window.matchMedia && window.matchMedia("(prefers-color-scheme: dark)").matches) color = "color: aqua"; if (window.location.toString().includes("?extension")) { console.info(`%cSettings: ${GM_info.script.name} v${GM_info.script.version}`, color); return; } else console.info(`%cRunning: ${GM_info.script.name} v${GM_info.script.version} ${CustomSettings.toString()}`, color); let excludedUsers; excludedUsers = JSON.parse(localStorage.getItem("wfexcludedusers")); if (!excludedUsers) { excludedUsers = JSON.parse(localStorage.getItem("excludedUsers")); if (excludedUsers) localStorage.setItem("wfexcludedusers", JSON.stringify(excludedUsers)); else excludedUsers = []; } Object.defineProperty(window, "running", { get() { return _running; }, set(value) { _running = value; wfButton.setAttribute("loading", value); if (running) { localStorage.setItem("wfloadingstate", "running"); } else { localStorage.setItem("wfloadingstate", "finished"); localStorage.removeItem("wfloadingusers"); localStorage.removeItem("wfloadingpercent"); } }, }); // Set state to interrupted if tab is closed while running window.addEventListener("beforeunload", () => { if (running) localStorage.setItem("wfloadingstate", "interrupted"); }); if (window.location.toString().includes("buddylist")) { const controlPanel = document.getElementById("controlpanelnav"); controlPanel.innerHTML += "

"; const showExButton = document.createElement("button"); showExButton.type = "button"; showExButton.className = "button standard mobile-fix"; showExButton.textContent = exButtonsShown ? "Hide WF Buttons" : "Show WF Buttons"; showExButton.onclick = function () { exButtonsShown = !exButtonsShown; showExButton.textContent = exButtonsShown ? "Hide WF Buttons" : "Show WF Buttons"; exButtonsShown ? addExcludeButtons() : removeExcludeButtons(); }; controlPanel.appendChild(showExButton); } if (!JSON.parse(localStorage.getItem("wflastfavs"))) firstStart = true; createWFNavButton(); if (window.location.toString().includes("submissions?mode=watchesfavoriteviewer")) createWFDocument(); else createWFButton(); // Add exclude buttons async function addExcludeButtons() { const watchers = document.querySelectorAll("div.flex-item-watchlist.aligncenter"); for (const watcher of watchers) { let user = watcher.querySelector("a[href]").href; if (user.endsWith("/")) user = user.slice(0, -1); user = user.substring(user.lastIndexOf("/") + 1, user.length); const excludeButton = document.createElement("button"); excludeButton.id = "excludeButton_" + user; excludeButton.type = "button"; excludeButton.className = "button standard mobile-fix"; if (excludedUsers.includes(user)) excludeButton.textContent = "^ WF Include ^"; else excludeButton.textContent = "^ WF Exclude ^"; excludeButton.addEventListener("click", () => toggleExcludeUser(user, excludeButton)); watcher.style.paddingBottom = "18px"; watcher.appendChild(excludeButton); } } // Remove exclude buttons async function removeExcludeButtons() { let buttons = document.querySelectorAll("button[id^=excludeButton]"); for (const button of buttons) { button.parentNode.style.paddingBottom = ""; button.parentNode.removeChild(button); } } // Toggle exclude user async function toggleExcludeUser(user, button) { if (excludedUsers.includes(user)) { // Remove user from excludedUsers excludedUsers = excludedUsers.filter((name) => name !== user); if (button) button.textContent = "^ WF Exclude ^"; console.log('Including: "' + user + '"'); } else { // Add user to excludedUsers excludedUsers.push(user); if (button) button.textContent = "^ WF Include ^"; console.log('Excluding: "' + user + '"'); } localStorage.setItem("wfexcludedusers", JSON.stringify(excludedUsers)); } // Creating the WFButton and loading the favs async function createWFButton(again) { if (window.location.toString().includes("buddylist")) return; // Create WFButton wfButton = document.createElement("a"); wfButton.id = "wfButton"; wfButton.className = "notification-container inline"; wfButton.title = "Start a WF scan"; wfButton.textContent = "WF Scan"; wfButton.style.cursor = "pointer"; const messageBar = document.getElementsByClassName("message-bar-desktop")[0]; messageBar.appendChild(wfButton); lastFavs = JSON.parse(localStorage.getItem("wflastfavs")); // Check loadingstate and wait for other instance to finish let finished = false; let intSavedUsers; const state = localStorage.getItem("wfloadingstate"); if (state && state !== "finished") { console.log("Other WF instance found copying..."); running = true; let status = await waitForOtherInstance(); finished = status.successfull; if (finished) console.log("Copying instance finished successfull"); else console.log("Copying instance not finished"); intSavedUsers = status.intSavedUsers || []; } if (again) { wfButton.onclick = () => startWFScan(finished, intSavedUsers); running = false; return; } if (doImmediateScanSetting.value || finished) startWFScan(finished, intSavedUsers); else { wfButton.onclick = () => startWFScan(finished, intSavedUsers); running = false; } } async function startWFScan(finished, intSavedUsers) { let newFavs; running = true; console.log("WF-Scan started"); wfButton.title = "Watches Favorites Notifications"; wfButton.textContent = "WF: 0%"; if (finished) { // Load finished favs newFavs = JSON.parse(await decompressString(localStorage.getItem("wfloading"))); } else { // Load new favs newFavs = await loadUnreadFavsAll(maxFavsLengthSetting.value, intSavedUsers); newFavs = Array.from(newFavs); // Remove dublicate Figures if (!showDublicateFavsSetting.value) { console.log("Checking for dublicate Favs..."); let dublicatePresent = false; let uniqueFigures = {}; let uniqueNewFavs = []; for (let figure of newFavs) { if (!uniqueFigures[figure.id]) { uniqueFigures[figure.id] = true; uniqueNewFavs.push(figure); } else { dublicatePresent = true; console.log(`Fav "${figure.id}" is a dublicate`); } } newFavs = uniqueNewFavs; if (dublicatePresent) console.log("Removed all dublicate Favs"); else console.log("No dublicates found"); } newFavs = newFavs.map((newFav) => newFav.outerHTML); } // Update WFButton const totalLength = newFavs.length; if (totalLength !== 0) { wfButton.onclick = loadWFDocument; wfButton.textContent = `${totalLength}WF`; } else if (firstStart) { // Replace WFButton with Ready Text wfButton.textContent = "WF Ready"; wfButtonClone = wfButton.cloneNode(true); wfButtonClone.setAttribute("loading", false); wfButtonClone.onclick = () => location.reload(); wfButton.parentNode.replaceChild(wfButtonClone, wfButton); } else { wfButton.parentNode.removeChild(wfButton); } // Compress and save new favs const favsComp = await compressString(JSON.stringify(newFavs)); localStorage.setItem("wfcurrentfavs", favsComp); console.log("Finished scanning"); console.log(`There are "${totalLength}" unseen Favs`); running = false; // Show last XFavs button if there are no new favs if (totalLength === 0 && !firstStart) { if (showLoadLastXFavsButtonSetting.value) createLastXFavsButton(); else { currentLength = 0; percent = 0; await createWFButton(true); wfButton.textContent = "WF Scan again"; } } firstStart = false; } // Waiting for other WF instance async function waitForOtherInstance() { return new Promise((resolve, reject) => { // Get current loadingstate let state = localStorage.getItem("wfloadingstate"); if (state === null) { resolve({ successfull: false }); return; } let lpercent = 0; let intSavedUsers = []; // Check loadingstate const intervalId = setInterval(() => { state = localStorage.getItem("wfloadingstate"); if (state === "finished") { clearInterval(intervalId); resolve({ successfull: true }); } else if (state === "interrupted") { clearInterval(intervalId); intSavedUsers = JSON.parse(localStorage.getItem("wfloadingusers")) || []; resolve({ successfull: false, intSavedUsers: intSavedUsers }); } else { percent = localStorage.getItem("wfloadingpercent"); if (percent !== lpercent) { lpercent = percent; console.log(`Copying: ${percent}%`); wfButton.textContent = `WF: ${percent}%`; } } }, 100); }); } // Loads the WFDocument async function loadWFDocument() { localStorage.setItem("wflastfavs", JSON.stringify(lastFavs)); window.location.href = "https://www.furaffinity.net/msg/submissions?mode=watchesfavoriteviewer"; } // Creating the WFDocument to view the favs async function createWFDocument() { const standardPage = document.getElementById("standardpage"); const messageCenter = document.getElementById("messagecenter-submissions"); const emptyElem = messageCenter.querySelector('div[class="no-messages"]'); if (emptyElem) emptyElem.remove(); const header = standardPage.querySelector('div[class="section-header"] h2'); header.textContent = "Watches Favorites"; const oldNewButtonsButtonsTop = standardPage.querySelector('div[class="aligncenter"][style]'); oldNewButtonsButtonsTop.remove(); const selectionButtons = standardPage.querySelector('button[class="standard check-uncheck"]').parentNode.parentNode.parentNode; selectionButtons.remove(); const oldNewButtonsBottom = messageCenter.parentNode.querySelector('div[class="aligncenter"]'); oldNewButtonsBottom.remove(); const galleries = document.querySelectorAll('div[class="notifications-by-date"]'); galleries.forEach((gallery) => gallery.remove()); let gallery = document.getElementById("gallery-0"); if (!gallery) { gallery = document.createElement("section"); gallery.id = "gallery-0"; gallery.className = "gallery messagecenter with-checkboxes s-250"; messageCenter.appendChild(gallery); } gallery.innerHTML = ""; const favsDecomp = await decompressString(localStorage.getItem("wfcurrentfavs")); const figures = JSON.parse(favsDecomp); const parser = new DOMParser(); const figureElements = figures.map((figure) => parser.parseFromString(figure, "text/html").body.firstChild); console.log(`Loading "${figureElements.length}" figures`); figureElements.forEach((figure) => gallery.appendChild(figure)); const subContainer = document.getElementById("messagecenter-submissions"); const centerContainer = document.createElement("div"); centerContainer.className = "aligncenter"; subContainer.appendChild(centerContainer); const disableTitlesButton = document.createElement("button"); disableTitlesButton.id = "wfdisabletitlesbutton"; disableTitlesButton.type = "button"; disableTitlesButton.className = "button standard hideonmobile"; disableTitlesButton.style.marginRight = "4px"; disableTitlesButton.textContent = "Disable Titles"; disableTitlesButton.onclick = () => { if (gallery.classList.contains("nodesc")) { gallery.classList.remove("nodesc"); disableTitlesButton.textContent = "Disable Titles"; } else { gallery.classList.add("nodesc"); disableTitlesButton.textContent = "Enable Titles"; } }; centerContainer.appendChild(disableTitlesButton); const returnButton = document.createElement("a"); returnButton.id = "wfreturnbutton"; returnButton.className = "button standard hideonmobile"; returnButton.textContent = "Return to Submissions"; returnButton.href = "/msg/submissions/"; centerContainer.appendChild(returnButton); } // Loading all unseen favs async function loadUnreadFavsAll(maxFavsLength, intSavedUsers = []) { // Getting watchers console.log("Getting watched people..."); const watchers = await getWatchers(); totalLength = watchers.length; console.log(`You are watching "${totalLength}" people. "${excludedUsers.length}" are excluded.`); totalLength -= excludedUsers.length; console.log(`Scanning "${totalLength}" people for unseen Favs...`); // Getting lastFavs progress = { newFavs: [], percent: 0, intSavedUsers: intSavedUsers, currScanFavs: [] }; let newFavsAll = []; let promises = []; let semaphore = new Semaphore(maxAmountRequestsSetting.value); for (const watcher of watchers) { promises.push( semaphore.acquire().then(async () => { try { const watcherLink = watcher.querySelector("a").href; if (!intSavedUsers.includes(watcherLink)) { // Getting newFavs from watcher progress = await getUnreadFavsWatcher(watcherLink, maxFavsLength); if (progress.newFavs) { newFavsAll = newFavsAll.concat(progress.newFavs); } // Updating WF Button prefix if (firstStart) { wfButton.textContent = `WF Initializing: ${Math.round(percent)}%`; } else { wfButton.textContent = `WF: ${Math.round(percent)}%`; } } } catch (error) { console.error(error); } finally { semaphore.release(); } }) ); } await Promise.all(promises); // Updating firstStart if (firstStart) { localStorage.setItem("wflastfavs", JSON.stringify(lastFavs)); newFavsAll = []; } totalLength = 0; return newFavsAll; } async function getWatchers() { let watchers = []; let prevWatchers; for (let i = 1; true; i++) { // Getting watchers html from page i const watchersDoc = await getHTML(`https://www.furaffinity.net/controls/buddylist/${i}/`); const nextWatchers = Array.from(watchersDoc.querySelectorAll('div[class="flex-item-watchlist aligncenter"]')); if (prevWatchers && prevWatchers[prevWatchers.length - 1].outerHTML == nextWatchers[nextWatchers.length - 1].outerHTML) break; prevWatchers = nextWatchers; watchers.push(...nextWatchers); } return watchers; } // Getting newFavs from a specific watcher async function getUnreadFavsWatcher(watcher, maxFavsLength, ignoreLastSeen = false) { // Getting username from watcher let user = watcher.substring(0, watcher.length - 1); user = user.substring(user.lastIndexOf("/"), user.length); user = user.substring(1, user.length); // Checking if user is excluded if (excludedUsers.includes(user)) { if (showExcludedUsersInLogSetting.value) console.log(`${percent.toFixed(2)}% | ${user} is excluded`); return { intSavedUsers: progress.intSavedUsers, currScanFavs: progress.currScanFavs }; } else { // Calculating current percent percent = (currentLength / totalLength) * 100; currentLength++; console.log(`${percent.toFixed(2)}% | ${user}`); } // Getting fav figures from user const figuresAll = await getUserFavFigures(user, maxFavsLength, ignoreLastSeen); if (figuresAll && figuresAll.length !== 0) console.log(`${user}: found ${figuresAll.length} fav(s)`); // Exclude user if no images found if (figuresAll && figuresAll === "no-images") { console.log(user + " gets excluded"); let excludeButton = document.getElementById("excludeButton_" + user); // toggleExcludeUser(`/user/${user}/`, button); return { intSavedUsers: progress.intSavedUsers, currScanFavs: progress.currScanFavs }; } // Changing Caption to include user let newFavs = []; for (const figure of figuresAll) { const figcaption = figure.querySelector("figcaption"); const byElem = figcaption.childNodes[1].cloneNode(true); const linkElem = byElem.querySelector("a[href]"); const iElem = byElem.querySelector("i"); const aElem = byElem.querySelector("a"); linkElem.style.fontWeight = "400"; iElem.textContent = "from"; aElem.title = user; aElem.textContent = user; aElem.href = `https://www.furaffinity.net/favorites/${user}`; figcaption.appendChild(byElem); newFavs.push(figure); } // Removing lastFavs from figures let newCurrScanFavs = newFavs.map((figure) => figure.outerHTML); progress.currScanFavs = progress.currScanFavs.concat(newCurrScanFavs); // Saving progress to localStorage progress.intSavedUsers.push(watcher); localStorage.setItem("wfloadingusers", JSON.stringify(progress.intSavedUsers)); localStorage.setItem("wfloadingpercent", percent.toFixed(2)); setCompLocalStorageArrayItemAsync("wfloading", progress.currScanFavs); return { newFavs: newFavs, intSavedUsers: progress.intSavedUsers, currScanFavs: progress.currScanFavs }; } async function createLastXFavsButton() { let lastXFavsButton = document.createElement("a"); lastXFavsButton.id = "lastXFavsButton"; lastXFavsButton.className = "notification-container inline"; lastXFavsButton.textContent = "Load last x Favs"; lastXFavsButton.title = "Show last X Favorites"; lastXFavsButton.style.cursor = "pointer"; lastXFavsButton.onclick = () => { currentLength = 0; let amount = prompt("Enter the amount of Favs you want to load: "); while (amount && isNaN(parseInt(amount))) amount = prompt("Input was not a Number. Please enter the amount of Favs you want to load: "); if (amount && amount > 0) loadLastXFavsAll(lastXFavsButton, amount); }; document.getElementsByClassName("message-bar-desktop")[0].appendChild(lastXFavsButton); } async function loadLastXFavsAll(lastXFavsButton, amount) { // Getting watchers const watchers = await getWatchers(); totalLength = watchers.length; console.log(`You are watching "${totalLength}" people`); console.log(`Searching for last "${amount}" Favs...`); // Getting lastFavs progress = { newFavs: [], percent: 0, intSavedUsers: [], currScanFavs: [] }; let newFavsAll = []; let promises = []; let semaphore = new Semaphore(2); for (const watcher of watchers) { promises.push( semaphore.acquire().then(async () => { try { const watcherLink = watcher.querySelector("a").href; // Getting last favs from watcher progress = await getUnreadFavsWatcher(watcherLink, amount, true); if (progress.newFavs) { newFavsAll = newFavsAll.concat(progress.newFavs); } // Updating LastXButton prefix lastXFavsButton.textContent = `WF Last ${amount}: ${Math.round(percent)}%`; } catch (error) { console.error(error); } finally { semaphore.release(); } }) ); } await Promise.all(promises); // Loading last x favs const figureCount = newFavsAll.length; if (figureCount !== 0) { lastXFavsButton.setAttribute("loading", false); lastXFavsButton.textContent = figureCount + "WF"; totalLength = 0; newFavsAll = Array.from(newFavsAll); newFavsAll = newFavsAll.map((newFav) => newFav.outerHTML); var favsComp = await compressString(JSON.stringify(newFavsAll)); localStorage.setItem("wfcurrentfavs", favsComp); window.location.href = "https://www.furaffinity.net/msg/submissions?mode=watchesfavoriteviewer"; } else lastXFavsButton.parentNode.removeChild(lastXFavsButton); totalLength = 0; return newFavsAll; } // Getting fav figures from a specific user async function getUserFavFigures(user, maxFavsLength, ignoreLastSeen = false) { // Checking last seen fav const lastFavsTemp = JSON.parse(localStorage.getItem("wflastfavs")) || {}; const userInLastFavs = user in lastFavsTemp; let figuresAll = []; let lastFigureIndex = -1; for (let i = 1; lastFigureIndex == -1 && (i == 0 || figuresAll.length < maxFavsLength); i++) { // Getting figures html from page i const favLink = `https://www.furaffinity.net/favorites/${user}/${i}`; const favs = await getHTML(favLink); if (!favs || !favs.body) break; if (favs.getElementById("no-images")) { return "no-images"; } const figures = Array.from(favs.body.getElementsByTagName("figure")); if (!figures || figures.length == 0) break; // Check last seen fav if (!ignoreLastSeen && userInLastFavs) { lastFigureIndex = figuresAll.findIndex((figure) => figure.id == lastFavsTemp[user]); } figuresAll = figuresAll.concat(figures); if (!userInLastFavs) break; } if (figuresAll.length > maxFavsLength) { figuresAll = figuresAll.slice(0, maxFavsLength); } if (!ignoreLastSeen && lastFigureIndex !== -1) { figuresAll = figuresAll.slice(0, lastFigureIndex); if (figuresAll.length !== 0) { lastFavs[user] = figuresAll[0].id; } } else if (firstStart) { if (figuresAll && figuresAll.length !== 0) { if (!lastFavs) lastFavs = {}; lastFavs[user] = figuresAll[0].id; } } else { lastFavs[user] = figuresAll[0].id; } return figuresAll; } async function createWFNavButton() { const settingsNav = document.querySelector('ul[class="navhideonmobile"]').querySelector('a[href="/controls/settings/"]').parentNode; const h3Elements = settingsNav.querySelectorAll('h3'); let foundMMCElem = false; for (const h3Element of h3Elements) { if (h3Element.textContent === "Manage My Content") { foundMMCElem = true; continue; } if (foundMMCElem) { const wfNav = document.createElement('a'); wfNav.href = "https://www.furaffinity.net/msg/submissions?mode=watchesfavoriteviewer"; wfNav.textContent = "Last Watches Favorites"; h3Element.parentNode.insertBefore(wfNav, h3Element); } } } async function getHTML(url) { try { const response = await fetch(url); const html = await response.text(); const parser = new DOMParser(); const doc = parser.parseFromString(html, "text/html"); return doc; } catch (error) { console.error(error); } } async function setCompLocalStorageArrayItemAsync(itemname, item) { let itemcomp = await compressString(JSON.stringify(item)); localStorage.setItem(itemname, itemcomp); } async function compressString(str) { return LZString.compress(str); } async function decompressString(compStr) { return LZString.decompress(compStr); } class Semaphore { constructor(maxConcurrency) { this.maxConcurrency = maxConcurrency; this.currentConcurrency = 0; this.waitingQueue = []; } acquire() { return new Promise((resolve, reject) => { if (this.currentConcurrency < this.maxConcurrency) { this.currentConcurrency++; resolve(); } else { this.waitingQueue.push(resolve); } }); } release() { if (this.waitingQueue.length > 0) { let nextResolve = this.waitingQueue.shift(); nextResolve(); } else { this.currentConcurrency--; } } }