diff --git a/src/app/service/service_worker/gm_api/gm_api.ts b/src/app/service/service_worker/gm_api/gm_api.ts index 755096d21..2579d1653 100644 --- a/src/app/service/service_worker/gm_api/gm_api.ts +++ b/src/app/service/service_worker/gm_api/gm_api.ts @@ -54,6 +54,7 @@ import { headerModifierMap, headersReceivedMap } from "./gm_xhr"; import { BgGMXhr } from "@App/pkg/utils/xhr/bg_gm_xhr"; import { mightPrepareSetClipboard, setClipboard } from "../clipboard"; import { nativePageWindowOpen } from "../../offscreen/gm_api"; +import { WakeUpCommand, wakeupPingCommand } from "@App/pkg/utils/wakeup-ping"; let generatedUniqueMarkerIDs = ""; let generatedUniqueMarkerIDWhen = ""; @@ -799,11 +800,27 @@ export default class GMApi { if (!sender.isType(GetSenderType.CONNECT)) { throw new Error("GM_xmlhttpRequest ERROR: sender is not MessageConnect"); } + + // https://github.com/scriptscat/scriptcat/issues/1343 + let wakeupTrigger = true; + wakeupPingCommand(WakeUpCommand.START); + const wakeupStop = () => { + try { + if (wakeupTrigger) { + wakeupTrigger = false; + wakeupPingCommand(WakeUpCommand.STOP); + } + } catch { + // ignored + } + }; + const msgConn = sender.getConnect()!; let isConnDisconnected = false; msgConn.onDisconnect(() => { isConnDisconnected = true; + wakeupStop(); }); // 关联自己生成的请求id与chrome.webRequest的请求id @@ -854,6 +871,7 @@ export default class GMApi { useFetch = isFetch || !!redirect || anonymous || isBufferStream; } const loadendCleanUp = () => { + wakeupStop(); redirectedUrls.delete(markerID); nwErrorResults.delete(markerID); const reqId = scXhrRequests.get(markerID); @@ -905,6 +923,7 @@ export default class GMApi { msgConn.onDisconnect(offscreenCon.disconnect.bind(offscreenCon)); } } catch (e: any) { + wakeupStop(); const errorMsg = `GM_xmlhttpRequest ERROR: ${e?.message || e || "Unknown Error"}`; if (!isConnDisconnected) { msgConn.sendMessage({ diff --git a/src/offscreen.ts b/src/offscreen.ts index 9cc1cb42c..4c666c2a8 100644 --- a/src/offscreen.ts +++ b/src/offscreen.ts @@ -2,6 +2,7 @@ import LoggerCore from "./app/logger/core"; import MessageWriter from "./app/logger/message_writer"; import { OffscreenManager } from "./app/service/offscreen"; import { ServiceWorkerClientMessage } from "@Packages/message/window_message"; +import { initializeWakeupPing } from "./pkg/utils/wakeup-ping"; function main() { // 通过postMessage与SW通信,支持结构化克隆(Blob等) @@ -15,6 +16,7 @@ function main() { // 初始化管理器 const manager = new OffscreenManager(swPostMessage); manager.initManager(); + initializeWakeupPing(); } main(); diff --git a/src/pkg/utils/wakeup-ping.ts b/src/pkg/utils/wakeup-ping.ts new file mode 100644 index 000000000..ef97f9bfa --- /dev/null +++ b/src/pkg/utils/wakeup-ping.ts @@ -0,0 +1,90 @@ +const PING_INTERVAL_MS_1 = 13_225; +const PING_INTERVAL_MS_2 = 17_765; + +export const WakeUpCommand = { + START: 0x100, + STOP: 0x200, +} as const; + +export type WakeUpCommand = ValueOf; + +/** + * scheduler 用于后台排程:Chrome 94+, Firefox 142+ + * @link https://developer.mozilla.org/en-US/docs/Web/API/Scheduler/postTask + */ +const nativeScheduler = + //@ts-ignore + typeof scheduler !== "undefined" && typeof scheduler?.postTask === "function" && scheduler; + +// 高效的 BroadcastChannel 通讯:service worker 和 offscreen 共用同一通道 +const channel = new BroadcastChannel("custom-ping"); // offscreen -> sw +const channelCommand = new BroadcastChannel("custom-ping-command"); // sw -> offscreen +let startCounter = 0; + +// initializeWakeupPing only execute once in offscreen +export const initializeWakeupPing = () => { + if (typeof frameElement === "object" && typeof document === "object" && document) { + let counter = 0; + let isMutationPending = false; + + const pingNode = document.createComment("0"); + + const incrementCounter = () => { + if (startCounter >= 1 && !isMutationPending) { + isMutationPending = true; + counter = counter & 8 ? 1 : counter + 1; + pingNode.data = `${counter}`; + } + }; + + const pingTask = async () => { + channel.postMessage(true); + incrementCounter(); + }; + + const mutationObserver = new MutationObserver(() => { + if (isMutationPending) { + isMutationPending = false; + const pingIntervalMs = Math.random() * (PING_INTERVAL_MS_2 - PING_INTERVAL_MS_1) + PING_INTERVAL_MS_1; + if (nativeScheduler) { + nativeScheduler.postTask(pingTask, { priority: "background", delay: pingIntervalMs }); + } else { + setTimeout(pingTask, pingIntervalMs); + } + } + }); + mutationObserver.observe(pingNode, { characterData: true }); + // incrementCounter(); + + channelCommand.onmessage = (e) => { + if (e.data === WakeUpCommand.START) { + startCounter++; + if (startCounter === 1) { + incrementCounter(); + } + } else if (e.data === WakeUpCommand.STOP) { + if (startCounter > 0) startCounter--; + } + }; + } +}; + +// initializeWakeupPing only execute once in service worker +export const listenWakeupPing = (onWakeupPing: (...args: any) => any) => { + chrome.storage.session.onChanged.addListener((obj) => { + // 消耗 persistentWakeup + if (typeof obj.persistentWakeup !== "undefined") { + // 执行任意 callback + onWakeupPing(); + } + }); + channel.onmessage = (e) => { + // 触发 chrome storage onChanged 使 service worker 保持活跃 + chrome.storage.session.set({ persistentWakeup: `${e.timeStamp}` }); + }; +}; + +// wakeupPingCommand only execute in service worker +export const wakeupPingCommand = (command: WakeUpCommand) => { + channelCommand.postMessage(command); +}; diff --git a/src/service_worker.ts b/src/service_worker.ts index 407a27922..648372aba 100644 --- a/src/service_worker.ts +++ b/src/service_worker.ts @@ -8,6 +8,7 @@ import { MessageQueue } from "@Packages/message/message_queue"; import { ServiceWorkerMessageSend } from "@Packages/message/window_message"; import migrate, { migrateChromeStorage } from "./app/migrate"; import { cleanInvalidKeys } from "./app/repo/resource"; +import { listenWakeupPing } from "./pkg/utils/wakeup-ping"; migrate(); migrateChromeStorage(); @@ -59,6 +60,11 @@ async function setupOffscreenDocument() { } } +export const onWakeupPing = () => { + //@ts-ignore + self.lastWakeupPing = new Date().toLocaleString("zh"); // 僅在後台DevTools debug用 +}; + function main() { cleanInvalidKeys(); // 初始化管理器 @@ -77,6 +83,7 @@ function main() { manager.initManager(); // 初始化沙盒环境 setupOffscreenDocument(); + listenWakeupPing(onWakeupPing); } main();