From 052da3818acbfda39bd0fdd5f3a47a80a21839b1 Mon Sep 17 00:00:00 2001 From: cyfung1031 <44498510+cyfung1031@users.noreply.github.com> Date: Wed, 6 May 2026 14:11:11 +0900 Subject: [PATCH 1/3] =?UTF-8?q?perf:=20=E4=BC=98=E5=8C=96=20pushValue=20?= =?UTF-8?q?=E5=A4=84=E7=90=86?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- example/tests/gm_value_test.js | 1149 ++++++++++++++++++ src/app/service/content/script_executor.ts | 1 + src/app/service/content/scripting.ts | 48 +- src/app/service/sandbox/runtime.ts | 1 + src/app/service/service_worker/runtime.ts | 83 +- src/app/service/service_worker/value.test.ts | 50 +- src/app/service/service_worker/value.ts | 34 +- 7 files changed, 1268 insertions(+), 98 deletions(-) create mode 100644 example/tests/gm_value_test.js diff --git a/example/tests/gm_value_test.js b/example/tests/gm_value_test.js new file mode 100644 index 000000000..d0fee5b33 --- /dev/null +++ b/example/tests/gm_value_test.js @@ -0,0 +1,1149 @@ +// ==UserScript== +// @name GM_addValueChangeListener Test +// @namespace http://tampermonkey.net/ +// @version 0.1.0 +// @description Test GM_addValueChangeListener with real iframes — dashboard in main, panels in iframes +// @match https://example.com/* +// @grant GM_getValue +// @grant GM_setValue +// @grant GM_deleteValue +// @grant GM_addValueChangeListener +// @grant GM_removeValueChangeListener +// @run-at document-idle +// ==/UserScript== + +(function () { + 'use strict'; + + if (!location.search.includes('testGMAddValueChangeListener')) return; + + document.documentElement.appendChild(document.createElement("style")).textContent=`@import url('https://fonts.googleapis.com/css2?family=JetBrains+Mono:wght@400;700,800&display=swap');`; + + /* ══════════════════════════════════════════════════════════ + SHARED CONSTANTS + ══════════════════════════════════════════════════════════ */ + const FRAME_IDS = ['main', 'iframe1', 'iframe2', 'iframe3']; + + const WRITE_KEY = { + main: 'key_from_main', + iframe1: 'key_from_iframe1', + iframe2: 'key_from_iframe2', + iframe3: 'key_from_iframe3', + }; + const ALL_KEYS = Object.values(WRITE_KEY); + + const ACCENT = { + main: '#0369a1', + iframe1: '#b91c1c', + iframe2: '#15803d', + iframe3: '#a16207', + }; + + const LABEL = { + main: '🖥 Main Frame', + iframe1: '📦 iFrame #1', + iframe2: '📦 iFrame #2', + iframe3: '📦 iFrame #3', + }; + + /* ══════════════════════════════════════════════════════════ + CONTEXT DETECTION + ══════════════════════════════════════════════════════════ */ + const isMain = window.self === window.top; + const frameId = new URLSearchParams(location.search).get('frameId') + || (isMain ? 'main' : 'unknown'); + + /* ══════════════════════════════════════════════════════════ + HELPERS + ══════════════════════════════════════════════════════════ */ + function escHtml(s) { + return String(s).replace(/[&<>"']/g, ch => ({ + '&': '&', + '<': '<', + '>': '>', + '"': '"', + "'": ''', + }[ch])); + } + + function fmtVal(v) { + return v === undefined + ? 'not set' + : escHtml(JSON.stringify(v)); + } + + function nowTime() { + return new Date().toLocaleTimeString('en-GB', { hour12: false }); + } + + /* ══════════════════════════════════════════════════════════ + MSG BUS + ══════════════════════════════════════════════════════════ */ + const MSG_NS = 'GMTEST_'; + const TARGET_ORIGIN = location.origin; + + function reportLog(entry) { + top.postMessage({ t: MSG_NS + 'LOG', frameId, entry }, TARGET_ORIGIN); + } + + function reportKV(kvMap) { + top.postMessage({ t: MSG_NS + 'KV', frameId, kvMap }, TARGET_ORIGIN); + } + + function reportReady() { + top.postMessage({ t: MSG_NS + 'READY', frameId }, TARGET_ORIGIN); + } + + function reportListeners(ids) { + top.postMessage({ t: MSG_NS + 'LISTENERS', frameId, ids }, TARGET_ORIGIN); + } + + function sendCmd(win, cmd, data = {}) { + win.postMessage({ t: MSG_NS + 'CMD', cmd, ...data }, TARGET_ORIGIN); + } + + /* ══════════════════════════════════════════════════════════ + IFRAME FRAME LOGIC + ══════════════════════════════════════════════════════════ */ + if (!isMain) { + const myKey = WRITE_KEY[frameId]; + if (!myKey) return; + + let iframeShadow = null; + + buildIframeBody(); + + const listenerIds = {}; + registerAllListeners(); + + pushKV(); + reportReady(); + + window.addEventListener('message', async (e) => { + if (e.origin !== location.origin) return; + if (!e.data || !e.data.t) return; + if (e.data.t !== MSG_NS + 'CMD') return; + + const { cmd, value } = e.data; + + if (cmd === 'SET_STRING') await doSet(`hello_${Date.now()}`); + if (cmd === 'SET_NUMBER') await doSet(Math.floor(Math.random() * 99999)); + if (cmd === 'SET_OBJECT') await doSet({ ts: Date.now(), from: frameId }); + if (cmd === 'SET_NULL') await doSet(null); + if (cmd === 'SET_CUSTOM') await doSet(value); + if (cmd === 'DELETE') await doDel(); + + if (cmd === 'REMOVE_LISTENERS') { + removeAllListeners(); + } + + if (cmd === 'REREGISTER_LISTENERS') { + removeAllListeners(); + registerAllListeners(); + } + }); + + async function doSet(v) { + await GM_setValue(myKey, v); + iLog(`✏️ Set ${escHtml(myKey)} = ${escHtml(JSON.stringify(v))}`, 'info'); + await pushKV(); + } + + async function doDel() { + await GM_deleteValue(myKey); + iLog(`🗑 Deleted ${escHtml(myKey)}`, 'warn'); + await pushKV(); + } + + function registerAllListeners() { + for (const key of ALL_KEYS) { + if (listenerIds[key] != null) continue; + + const id = GM_addValueChangeListener(key, async (name, oldVal, newVal, remote) => { + const tag = remote ? '🌐 remote' : '📍 local'; + + iLog( + `${tag} ${escHtml(name)}: ${escHtml(JSON.stringify(oldVal))} → ${escHtml(JSON.stringify(newVal))}`, + remote ? 'good' : 'warn' + ); + + await pushKV(); + }); + + listenerIds[key] = id; + iLog(`👂 Listener on ${escHtml(key)} (id=${escHtml(id)})`, 'info'); + } + + reportListeners(Object.entries(listenerIds).map(([k, id]) => ({ key: k, id }))); + } + + function removeAllListeners() { + for (const [key, id] of Object.entries(listenerIds)) { + try { + GM_removeValueChangeListener(id); + } catch (_) { } + delete listenerIds[key]; + } + + iLog('🔇 All listeners removed', 'warn'); + reportListeners([]); + } + + async function pushKV() { + const kvMap = {}; + for (const k of ALL_KEYS) { + kvMap[k] = await GM_getValue(k, undefined); // iframe + } + reportKV(kvMap); + } + + function iLog(msg, type = '') { + reportLog({ msg, type, t: nowTime() }); + + if (!iframeShadow) return; + + const logBox = iframeShadow.getElementById('iframe-log'); + if (!logBox) return; + + const line = document.createElement('div'); + line.className = 'log-line'; + line.innerHTML = ` + ${escHtml(nowTime())} + ${msg} + `; + + logBox.appendChild(line); + logBox.scrollTop = logBox.scrollHeight; + } + + function buildIframeBody() { + const accent = ACCENT[frameId] || '#334155'; + + document.documentElement.style.cssText = ` + margin:0; + padding:0; + width:100%; + height:100%; + background:#f8fafc; + `; + + document.body.style.cssText = ` + margin:0; + padding:0; + width:100%; + height:100%; + background:#f8fafc; + overflow:hidden; + `; + + document.body.textContent = ''; + + const host = document.createElement('div'); + host.style.cssText = ` + all:initial; + display:block; + width:100%; + height:100%; + `; + document.body.appendChild(host); + + iframeShadow = host.attachShadow({ mode: 'open' }); + + const sty = new CSSStyleSheet(); + sty.replaceSync(` + + *, *::before, *::after { + all:unset; + box-sizing:border-box; + } + + #iframe-shell { + display:flex; + flex-direction:column; + gap:6px; + width:100%; + height:100%; + padding:8px; + overflow:hidden; + background:#f8fafc; + color:#0f172a; + font-family:'JetBrains Mono','Courier New',monospace; + font-size:12px; + } + + .iframe-title { + display:block; + color:${accent}; + font-family:'JetBrains Mono',monospace; + font-size:13px; + font-weight:800; + letter-spacing:.05em; + flex-shrink:0; + } + + .iframe-subtitle { + display:block; + color:#475569; + font-size:10px; + letter-spacing:.08em; + flex-shrink:0; + } + + .log-box { + display:block; + flex:1; + min-height:0; + overflow-y:auto; + background:#ffffff; + border:1px solid #cbd5e1; + border-radius:7px; + padding:6px 8px; + font-size:11px; + line-height:1.65; + scrollbar-width:thin; + scrollbar-color:#94a3b8 transparent; + box-shadow:0 1px 2px rgba(15,23,42,.08); + } + + .log-box::-webkit-scrollbar { + width:5px; + } + + .log-box::-webkit-scrollbar-thumb { + background:#94a3b8; + border-radius:999px; + } + + .log-line { + display:flex; + gap:6px; + align-items:baseline; + } + + .log-time { + color:#64748b; + flex-shrink:0; + } + + .log-msg { + color:#334155; + } + + .log-msg.good { + color:#15803d; + font-weight:700; + } + + .log-msg.warn { + color:#a16207; + font-weight:700; + } + + .log-msg.info { + color:#0369a1; + font-weight:700; + } + + b { + font-weight:700; + } + + i { + font-style:italic; + } + + small { + font-size:.85em; + opacity:.75; + } + `); + + // スタイルシートを適用 + iframeShadow.adoptedStyleSheets = [sty]; + + + const shell = document.createElement('div'); + shell.id = 'iframe-shell'; + shell.innerHTML = ` +
${escHtml(LABEL[frameId])}
+
Controlled by main frame dashboard ↑
+
+ `; + + iframeShadow.appendChild(shell); + } + + return; + } + + /* ══════════════════════════════════════════════════════════ + MAIN FRAME LOGIC + ══════════════════════════════════════════════════════════ */ + const state = { + kv: {}, + logs: {}, + listenerSummary: {}, + }; + + FRAME_IDS.forEach(id => { + state.kv[id] = {}; + state.logs[id] = []; + state.listenerSummary[id] = []; + }); + + const iframeWindows = {}; + + /* ── Shadow DOM setup ─────────────────────────────────── */ + const host = document.createElement('div'); + host.style.cssText = [ + 'all:initial', + 'position:fixed', + 'inset:0', + 'width:100vw', + 'height:100vh', + 'z-index:2147483647', + 'pointer-events:none', + ].join(';'); + + document.body.appendChild(host); + + const shadow = host.attachShadow({ mode: 'open' }); + + /* ── Styles ─────────────────────────────────────────────── */ + const sty = new CSSStyleSheet(); + sty.replaceSync(` + + *, *::before, *::after { + all:unset; + box-sizing:border-box; + } + + #shell { + display:grid; + position:fixed; + inset:0; + width:100vw; + height:100vh; + grid-template-columns:1fr 340px; + grid-template-rows:100vh; + overflow:hidden; + pointer-events:all; + background:#f1f5f9; + color:#0f172a; + font-family:'JetBrains Mono','Courier New',monospace; + font-size:12px; + } + + #dashboard { + display:flex; + flex-direction:column; + overflow-y:auto; + overflow-x:hidden; + padding:16px; + gap:12px; + background:#f8fafc; + border-right:1px solid #cbd5e1; + scrollbar-width:thin; + scrollbar-color:#94a3b8 transparent; + } + + #dashboard::-webkit-scrollbar { + width:6px; + } + + #dashboard::-webkit-scrollbar-thumb { + background:#94a3b8; + border-radius:999px; + } + + #iframe-strip { + display:flex; + flex-direction:column; + overflow:hidden; + background:#e2e8f0; + } + + .iframe-wrap { + display:flex; + flex:1; + flex-direction:column; + border-bottom:1px solid #cbd5e1; + overflow:hidden; + background:#f8fafc; + } + + .iframe-wrap:last-child { + border-bottom:none; + } + + .iframe-wrap iframe { + display:block; + flex:1; + width:100%; + height:100%; + border:none; + background:#f8fafc; + } + + #topbar { + display:flex; + align-items:center; + gap:10px; + flex-shrink:0; + background:#ffffff; + border:1px solid #cbd5e1; + border-radius:10px; + padding:10px 12px; + box-shadow:0 1px 3px rgba(15,23,42,.08); + } + + #topbar-title { + font-family:'JetBrains Mono',monospace; + font-size:15px; + font-weight:800; + color:#0f172a; + letter-spacing:.08em; + flex:1; + } + + button { + display:inline-block; + font-family:'JetBrains Mono','Courier New',monospace; + font-size:11px; + font-weight:700; + padding:5px 10px; + border-radius:6px; + border:1px solid; + cursor:pointer; + letter-spacing:.03em; + transition:background .12s, opacity .12s, transform .08s; + white-space:nowrap; + pointer-events:all; + background:#ffffff; + } + + button:hover { + opacity:.85; + transform:translateY(-1px); + } + + button:active { + opacity:.7; + transform:translateY(0); + } + + button.danger { + color:#991b1b !important; + border-color:#fecaca !important; + background:#fff1f2 !important; + } + + .p-card { + display:block; + border:1px solid #cbd5e1; + border-radius:12px; + padding:12px; + background:#ffffff; + flex-shrink:0; + box-shadow:0 1px 3px rgba(15,23,42,.08); + } + + .p-title { + display:block; + font-family:'JetBrains Mono',monospace; + font-size:14px; + font-weight:800; + letter-spacing:.05em; + margin-bottom:4px; + } + + .p-subtitle { + display:block; + font-size:10px; + color:#475569; + letter-spacing:.08em; + margin-bottom:10px; + } + + .sec { + display:block; + margin-top:9px; + } + + .sec-label { + display:block; + font-size:10px; + font-weight:700; + letter-spacing:.14em; + text-transform:uppercase; + color:#475569; + margin-bottom:5px; + } + + .hr { + display:block; + height:1px; + background:#e2e8f0; + margin:10px 0; + } + + .btn-row { + display:flex; + flex-wrap:wrap; + gap:5px; + } + + .kv-table { + display:grid; + grid-template-columns:1fr 1fr; + gap:5px; + } + + .kv-card { + display:block; + background:#f8fafc; + border:1px solid #cbd5e1; + border-radius:7px; + padding:6px 8px; + } + + .kv-key { + display:block; + font-size:10px; + font-weight:700; + color:#475569; + margin-bottom:2px; + } + + .kv-val { + display:block; + font-size:11px; + color:#0f172a; + word-break:break-all; + line-height:1.4; + } + + .log-box { + display:block; + height:118px; + overflow-y:auto; + background:#ffffff; + border:1px solid #cbd5e1; + border-radius:7px; + padding:6px 8px; + font-size:11px; + line-height:1.65; + scrollbar-width:thin; + scrollbar-color:#94a3b8 transparent; + } + + .log-box::-webkit-scrollbar { + width:5px; + } + + .log-box::-webkit-scrollbar-thumb { + background:#94a3b8; + border-radius:999px; + } + + .log-line { + display:flex; + gap:6px; + align-items:baseline; + } + + .log-time { + color:#64748b; + flex-shrink:0; + } + + .log-msg { + color:#334155; + } + + .log-msg.good { + color:#15803d; + font-weight:700; + } + + .log-msg.warn { + color:#a16207; + font-weight:700; + } + + .log-msg.info { + color:#0369a1; + font-weight:700; + } + + .dot { + display:inline-block; + width:7px; + height:7px; + border-radius:50%; + background:#cbd5e1; + vertical-align:middle; + border:1px solid #94a3b8; + } + + .dot.on { + background:#16a34a; + border-color:#15803d; + box-shadow:0 0 0 3px rgba(22,163,74,.16); + } + + b { + font-weight:700; + } + + i { + font-style:italic; + } + + small { + font-size:.85em; + opacity:.75; + } + `); + // スタイルシートを適用 + shadow.adoptedStyleSheets = [sty]; + + /* ── Shell ─────────────────────────────────────────────── */ + const shell = document.createElement('div'); + shell.id = 'shell'; + shadow.appendChild(shell); + + /* ── Left dashboard ─────────────────────────────────────── */ + const dashboard = document.createElement('div'); + dashboard.id = 'dashboard'; + shell.appendChild(dashboard); + + const topbar = document.createElement('div'); + topbar.id = 'topbar'; + topbar.innerHTML = `⚙ GM_addValueChangeListener Test`; + + const closeBtn = document.createElement('button'); + closeBtn.className = 'danger'; + closeBtn.textContent = '✕ close'; + closeBtn.onclick = () => host.remove(); + topbar.appendChild(closeBtn); + dashboard.appendChild(topbar); + + /* ── Panel cards ───────────────────────────────────────── */ + const panelRefs = {}; + + FRAME_IDS.forEach(id => { + const accent = ACCENT[id]; + + const card = document.createElement('div'); + card.className = 'p-card'; + card.style.borderColor = accent + '55'; + + const title = document.createElement('div'); + title.className = 'p-title'; + title.style.color = accent; + title.textContent = LABEL[id]; + card.appendChild(title); + + const subtitle = document.createElement('div'); + subtitle.className = 'p-subtitle'; + subtitle.textContent = id === 'main' + ? 'runs in this window' + : 'runs in real iframe on the right →'; + card.appendChild(subtitle); + + const wLabel = document.createElement('div'); + wLabel.className = 'sec-label'; + wLabel.textContent = 'Write value'; + card.appendChild(wLabel); + + const writeRow = document.createElement('div'); + writeRow.className = 'btn-row'; + + function makeBtn(text, danger) { + const b = document.createElement('button'); + b.textContent = text; + + if (danger) { + b.className = 'danger'; + } else { + b.style.color = accent; + b.style.borderColor = accent + '66'; + b.style.background = '#ffffff'; + } + + return b; + } + + const cmdMap = [ + ['string', 'SET_STRING'], + ['number', 'SET_NUMBER'], + ['object', 'SET_OBJECT'], + ['null', 'SET_NULL'], + ]; + + cmdMap.forEach(([label2, cmd]) => { + const b = makeBtn(label2); + b.onclick = () => dispatchCmd(id, cmd); + writeRow.appendChild(b); + }); + + const delB = makeBtn('delete', true); + delB.onclick = () => dispatchCmd(id, 'DELETE'); + writeRow.appendChild(delB); + card.appendChild(writeRow); + + const hr1 = document.createElement('div'); + hr1.className = 'hr'; + card.appendChild(hr1); + + const kvLabel = document.createElement('div'); + kvLabel.className = 'sec-label'; + kvLabel.textContent = 'GM Values'; + card.appendChild(kvLabel); + + const kvTable = document.createElement('div'); + kvTable.className = 'kv-table'; + card.appendChild(kvTable); + + const hr2 = document.createElement('div'); + hr2.className = 'hr'; + card.appendChild(hr2); + + const lcLabel = document.createElement('div'); + lcLabel.className = 'sec-label'; + lcLabel.textContent = 'Listener control'; + card.appendChild(lcLabel); + + const lcRow = document.createElement('div'); + lcRow.className = 'btn-row'; + + const rmB = makeBtn('🔇 remove all', true); + rmB.onclick = () => dispatchCmd(id, 'REMOVE_LISTENERS'); + + const reB = makeBtn('🔊 re-register'); + reB.onclick = () => dispatchCmd(id, 'REREGISTER_LISTENERS'); + + lcRow.appendChild(rmB); + lcRow.appendChild(reB); + + const dotWrap = document.createElement('div'); + dotWrap.style.cssText = 'display:flex;gap:6px;align-items:center;margin-top:7px;flex-wrap:wrap;'; + + const dotMap = {}; + + ALL_KEYS.forEach(k => { + const dot = document.createElement('span'); + dot.className = 'dot'; + dot.title = k; + + const lbl2 = document.createElement('span'); + lbl2.style.cssText = 'font-size:10px;color:#475569;'; + lbl2.textContent = k.replace('key_from_', ''); + + dotWrap.appendChild(dot); + dotWrap.appendChild(lbl2); + + dotMap[k] = dot; + }); + + card.appendChild(lcRow); + card.appendChild(dotWrap); + + const hr3 = document.createElement('div'); + hr3.className = 'hr'; + card.appendChild(hr3); + + const logLabel = document.createElement('div'); + logLabel.className = 'sec-label'; + logLabel.textContent = 'Event log'; + card.appendChild(logLabel); + + const logBox = document.createElement('div'); + logBox.className = 'log-box'; + card.appendChild(logBox); + + const clrB = makeBtn('✕ clear log', true); + clrB.style.marginTop = '5px'; + clrB.onclick = () => { + logBox.innerHTML = ''; + state.logs[id] = []; + }; + card.appendChild(clrB); + + dashboard.appendChild(card); + + panelRefs[id] = { + kvTable, + logBox, + dotMap, + accent, + myKey: WRITE_KEY[id], + }; + }); + + /* ── Right iframe strip ─────────────────────────────────── */ + const strip = document.createElement('div'); + strip.id = 'iframe-strip'; + shell.appendChild(strip); + + const BASE_URL = location.href.split('?')[0]; + + ['iframe1', 'iframe2', 'iframe3'].forEach(fid => { + const wrap = document.createElement('div'); + wrap.className = 'iframe-wrap'; + + const iframe = document.createElement('iframe'); + iframe.src = `${BASE_URL}?testGMAddValueChangeListener&frameId=${encodeURIComponent(fid)}`; + iframe.title = fid; + + iframe.onload = () => { + iframeWindows[fid] = iframe.contentWindow; + }; + + wrap.appendChild(iframe); + strip.appendChild(wrap); + + iframeWindows[fid] = iframe.contentWindow; + }); + + /* ── Main frame GM logic ───────────────────────────────── */ + (() => { + const myKey = WRITE_KEY.main; + const refs = () => panelRefs.main; + + const listenerIds = {}; + + function mLog(msg, type = '') { + const { logBox } = refs(); + + const line = document.createElement('div'); + line.className = 'log-line'; + line.innerHTML = ` + ${nowTime()} + ${msg} + `; + + logBox.appendChild(line); + logBox.scrollTop = logBox.scrollHeight; + } + + async function mRefreshKV() { + const { kvTable, myKey: mk, accent } = refs(); + + kvTable.innerHTML = ''; + + for (const k of ALL_KEYS) { + const v = await GM_getValue(k, undefined); // main frame + const own = k === mk; + + const card = document.createElement('div'); + card.className = 'kv-card'; + + if (own) card.style.borderColor = accent + '88'; + + card.innerHTML = ` +
${escHtml(k)}${own ? ' (mine)' : ''}
+
+ ${fmtVal(v)} +
+ `; + + kvTable.appendChild(card); + } + } + + function registerMainListeners(isReregister = false) { + for (const key of ALL_KEYS) { + if (listenerIds[key] != null) continue; + + const id = GM_addValueChangeListener(key, async (name, oldVal, newVal, remote) => { + const tag = remote ? '🌐 remote' : '📍 local'; + + mLog( + `${tag} ${escHtml(name)}: ${escHtml(JSON.stringify(oldVal))} → ${escHtml(JSON.stringify(newVal))}`, + remote ? 'good' : 'warn' + ); + + await mRefreshKV(); + updateDots('main', Object.entries(listenerIds).map(([k, i]) => ({ key: k, id: i }))); + }); + + listenerIds[key] = id; + + mLog( + `${isReregister ? '👂 Re-registered' : '👂 Listener on'} ${escHtml(key)} (id=${escHtml(id)})`, + 'info' + ); + } + + updateDots('main', Object.entries(listenerIds).map(([k, i]) => ({ key: k, id: i }))); + } + + function removeMainListeners() { + for (const [k, i] of Object.entries(listenerIds)) { + try { + GM_removeValueChangeListener(i); + } catch (_) { } + delete listenerIds[k]; + } + + mLog('🔇 All listeners removed', 'warn'); + updateDots('main', []); + } + + registerMainListeners(false); + mRefreshKV(); + + window._gmtest_mainDispatch = async (cmd) => { + if (cmd === 'SET_STRING') { + const v = `hello_${Date.now()}`; + await GM_setValue(myKey, v); + mLog(`✏️ Set ${escHtml(myKey)} = ${escHtml(JSON.stringify(v))}`, 'info'); + } + + if (cmd === 'SET_NUMBER') { + const v = Math.floor(Math.random() * 99999); + await GM_setValue(myKey, v); + mLog(`✏️ Set ${escHtml(myKey)} = ${escHtml(JSON.stringify(v))}`, 'info'); + } + + if (cmd === 'SET_OBJECT') { + const v = { ts: Date.now(), from: 'main' }; + await GM_setValue(myKey, v); + mLog(`✏️ Set ${escHtml(myKey)} = ${escHtml(JSON.stringify(v))}`, 'info'); + } + + if (cmd === 'SET_NULL') { + await GM_setValue(myKey, null); + mLog(`✏️ Set ${escHtml(myKey)} = null`, 'info'); + } + + if (cmd === 'DELETE') { + await GM_deleteValue(myKey); + mLog(`🗑 Deleted ${escHtml(myKey)}`, 'warn'); + } + + if (cmd === 'REMOVE_LISTENERS') { + removeMainListeners(); + } + + if (cmd === 'REREGISTER_LISTENERS') { + removeMainListeners(); + registerMainListeners(true); + } + + await mRefreshKV(); + }; + })(); + + /* ── postMessage handler ───────────────────────────────── */ + window.addEventListener('message', (e) => { + if (e.origin !== location.origin) return; + if (!e.data || !e.data.t) return; + + const { t, frameId: fid, entry, kvMap, ids } = e.data; + + if (!FRAME_IDS.includes(fid)) return; + + if (fid !== 'main' && iframeWindows[fid] && e.source !== iframeWindows[fid]) return; + + if (t === MSG_NS + 'LOG') { + appendLog(fid, entry); + } + + if (t === MSG_NS + 'KV') { + renderKV(fid, kvMap); + } + + if (t === MSG_NS + 'READY') { + appendLog(fid, { + t: nowTime(), + msg: '🚀 iframe ready', + type: 'info', + }); + } + + if (t === MSG_NS + 'LISTENERS') { + updateDots(fid, ids); + } + }); + + /* ── dispatchCmd ───────────────────────────────────────── */ + function dispatchCmd(targetId, cmd) { + if (targetId === 'main') { + window._gmtest_mainDispatch(cmd); + return; + } + + const win = iframeWindows[targetId]; + + if (win) { + sendCmd(win, cmd, {}); + } else { + appendLog(targetId, { + t: nowTime(), + msg: '⚠️ iframe not ready yet — try again', + type: 'warn', + }); + } + } + + /* ── UI helpers ─────────────────────────────────────────── */ + function appendLog(fid, entry) { + const refs = panelRefs[fid]; + if (!refs || !entry) return; + + const { logBox } = refs; + + const line = document.createElement('div'); + line.className = 'log-line'; + line.innerHTML = ` + ${escHtml(entry.t || nowTime())} + ${entry.msg || ''} + `; + + logBox.appendChild(line); + logBox.scrollTop = logBox.scrollHeight; + } + + function renderKV(fid, kvMap) { + const refs = panelRefs[fid]; + if (!refs || !kvMap) return; + + const { kvTable, myKey, accent } = refs; + + kvTable.innerHTML = ''; + + for (const k of ALL_KEYS) { + const v = kvMap[k]; + const own = k === myKey; + + const card = document.createElement('div'); + card.className = 'kv-card'; + + if (own) card.style.borderColor = accent + '88'; + + card.innerHTML = ` +
${escHtml(k)}${own ? ' (mine)' : ''}
+
+ ${fmtVal(v)} +
+ `; + + kvTable.appendChild(card); + } + } + + function updateDots(fid, ids) { + const refs = panelRefs[fid]; + if (!refs) return; + + const { dotMap } = refs; + const activeKeys = new Set((ids || []).map(x => x.key)); + + for (const [k, dot] of Object.entries(dotMap)) { + dot.className = 'dot' + (activeKeys.has(k) ? ' on' : ''); + } + } +})(); diff --git a/src/app/service/content/script_executor.ts b/src/app/service/content/script_executor.ts index 1d4715b6d..6e9f2441a 100644 --- a/src/app/service/content/script_executor.ts +++ b/src/app/service/content/script_executor.ts @@ -46,6 +46,7 @@ export class ScriptExecutor { } valueUpdate(data: ValueUpdateDataEncoded) { + // runtime/valueUpdate const { uuid, storageName } = data; for (const val of this.execScriptMap.values()) { if (val.scriptRes.uuid === uuid || getStorageName(val.scriptRes) === storageName) { diff --git a/src/app/service/content/scripting.ts b/src/app/service/content/scripting.ts index fa802ce52..65686a18c 100644 --- a/src/app/service/content/scripting.ts +++ b/src/app/service/content/scripting.ts @@ -3,13 +3,25 @@ import { type CustomEventMessage } from "@Packages/message/custom_event_message" import { forwardMessage, type Server } from "@Packages/message/server"; import type { MessageSend } from "@Packages/message/types"; import { RuntimeClient } from "../service_worker/client"; -import { makeBlobURL } from "@App/pkg/utils/utils"; +import { getStorageName, isFirefox, makeBlobURL } from "@App/pkg/utils/utils"; import type { Logger } from "@App/app/repo/logger"; import LoggerCore from "@App/app/logger/core"; import type { ValueUpdateDataEncoded } from "./types"; +const PageOrContent = { + PAGE: 1, + CONTENT: 2, +}; + +type PageOrContent = ValueOf; + +// For Firefox, StorageArea.setAccessLevel is not implemented. +// See https://bugzilla.mozilla.org/show_bug.cgi?id=1724754 +const deliveryStorage = isFirefox() ? chrome.storage.local : chrome.storage.session; + // scripting页的处理 export default class ScriptingRuntime { + private activeStorageNames = new Map(); constructor( // 监听来自service_worker的消息 private readonly extServer: Server, @@ -24,10 +36,14 @@ export default class ScriptingRuntime { ) {} // 广播消息给 content 和 inject - broadcastToPage(action: string, data?: any): Promise { + broadcastToPage( + action: string, + data?: any, + activeOn: PageOrContent = PageOrContent.PAGE | PageOrContent.CONTENT + ): Promise { return Promise.all([ - sendMessage(this.senderToContent, "content/" + action, data), - sendMessage(this.senderToInject, "inject/" + action, data), + activeOn & PageOrContent.CONTENT && sendMessage(this.senderToContent, "content/" + action, data), + activeOn & PageOrContent.PAGE && sendMessage(this.senderToInject, "inject/" + action, data), ]).then(() => undefined); } @@ -52,13 +68,15 @@ export default class ScriptingRuntime { // 类似 UDP 原理,service_worker 不会有任何「等待处理」 // 由于 changes 会包括新旧值 (Chrome: JSON serialization, Firefox: Structured Clone) // 因此需要注意资讯量不要过大导致 onChanged 的触发过慢 - chrome.storage.local.onChanged.addListener((changes) => { - if (changes["valueUpdateDelivery"]?.newValue) { - // 转发给 content 和 inject - this.broadcastToPage( - "runtime/valueUpdate", - changes["valueUpdateDelivery"]?.newValue.sendData as ValueUpdateDataEncoded - ); + deliveryStorage.onChanged.addListener((changes) => { + const record = changes["valueUpdateDelivery"]; + if (record?.newValue) { + const sendData = record.newValue.sendData as ValueUpdateDataEncoded; + const activeOn = this.activeStorageNames.get(sendData.storageName); + if (activeOn) { + // 转发给 content 和 inject + this.broadcastToPage("runtime/valueUpdate", sendData, activeOn); + } } }); @@ -121,6 +139,14 @@ export default class ScriptingRuntime { client.pageLoad().then((o) => { if (!o.ok) return; const { injectScriptList, contentScriptList, envInfo } = o; + const pairs = {} as Record; + for (const script of injectScriptList) { + pairs[getStorageName(script)] |= PageOrContent.PAGE; + } + for (const script of contentScriptList) { + pairs[getStorageName(script)] |= PageOrContent.CONTENT; + } + this.activeStorageNames = new Map(Object.entries(pairs)); // 向页面 发送脚本列表及环境信息 if (contentScriptList.length) { diff --git a/src/app/service/sandbox/runtime.ts b/src/app/service/sandbox/runtime.ts index a96f2951d..5e9207d63 100644 --- a/src/app/service/sandbox/runtime.ts +++ b/src/app/service/sandbox/runtime.ts @@ -321,6 +321,7 @@ export class Runtime { } valueUpdate(data: ValueUpdateDataEncoded) { + // runtime/valueUpdate const dataEntries = data.entries; // 转发给脚本 this.execScriptMap.forEach((val) => { diff --git a/src/app/service/service_worker/runtime.ts b/src/app/service/service_worker/runtime.ts index 8ea36659e..2e66b79d9 100644 --- a/src/app/service/service_worker/runtime.ts +++ b/src/app/service/service_worker/runtime.ts @@ -7,7 +7,7 @@ import type { Script, ScriptDAO, ScriptRunResource, ScriptSite, TScriptInfo } fr import { SCRIPT_STATUS_DISABLE, SCRIPT_STATUS_ENABLE, SCRIPT_TYPE_NORMAL } from "@App/app/repo/scripts"; import { type ValueService } from "./value"; import GMApi, { GMExternalDependencies } from "./gm_api/gm_api"; -import type { TDeleteScript, TEnableScript, TInstallScript, TScriptValueUpdate, TSortedScript } from "../queue"; +import type { TDeleteScript, TEnableScript, TInstallScript, TSortedScript } from "../queue"; import { type ScriptService } from "./script"; import { runScript, stopScript } from "../offscreen/client"; import { @@ -19,7 +19,9 @@ import { import { checkUserScriptsAvailable, getMetadataStr, + getStorageName, getUserConfigStr, + isFirefox, obtainBlackList, sourceMapTo, } from "@App/pkg/utils/utils"; @@ -41,7 +43,7 @@ import { type SystemConfig } from "@App/pkg/config/config"; import { type ResourceService } from "./resource"; import { type LocalStorageDAO } from "@App/app/repo/localStorage"; import Logger from "@App/app/logger/logger"; -import type { GMInfoEnv } from "../content/types"; +import type { GMInfoEnv, ValueUpdateDataEncoded } from "../content/types"; import { initLocalesPromise, localePath } from "@App/locales/locales"; import { DocumentationSite } from "@App/app/const"; import { extractUrlPatterns, RuleType, type URLRuleEntry } from "@App/pkg/utils/url_matcher"; @@ -82,6 +84,12 @@ export type TScriptsForTab = { scriptmenus: ScriptMenu[]; } | null; +const bgScriptStorageNames = new Set(); + +// For Firefox, StorageArea.setAccessLevel is not implemented. +// See https://bugzilla.mozilla.org/show_bug.cgi?id=1724754 +const deliveryStorage = isFirefox() ? chrome.storage.local : chrome.storage.session; + export class RuntimeService { scriptMatchEnable: UrlMatch = new UrlMatch(); scriptMatchDisable: UrlMatch = new UrlMatch(); @@ -348,7 +356,48 @@ export class RuntimeService { await this.loadPageScript(script, apiScript!); } + public async pushValueUpdate(script: Script, sendData: ValueUpdateDataEncoded) { + try { + // 前台腳本 (推送值到tab) + await deliveryStorage!.set({ + valueUpdateDelivery: { + rId: `${Date.now()}.${Math.random()}`, // 用于区分不同的更新,确保 deliveryStorage.onChanged 必能触发 + sendData, + }, + }); + + // 後台腳本 + if (bgScriptStorageNames.has(sendData.storageName)) { + // 推送到offscreen中 + await sendMessage(this.msgSender, "offscreen/runtime/valueUpdate", sendData); + } + + // valueUpdate 消息用于 early script 的处理 + if (sendData.valueUpdated) { + if (script.status === SCRIPT_STATUS_ENABLE && isEarlyStartScript(script.metadata)) { + // 如果是预加载脚本,需要更新脚本代码重新注册 + // scriptMatchInfo 里的 value 改变 => compileInjectionCode -> injectionCode 改变 + await this.updateResourceOnScriptChange(script); + } + } + } catch (e) { + console.error("pushValueUpdate error", e); + } + } + + async setSessionAccessLevel() { + try { + // 让 scripting 存取 chrome.storage.session + await chrome.storage.session.setAccessLevel({ accessLevel: "TRUSTED_AND_UNTRUSTED_CONTEXTS" }); + } catch (e) { + console.error("unable to call chrome.storage.session.setAccessLevel", e); + } + } + init() { + if (deliveryStorage === chrome.storage.session) { + this.setSessionAccessLevel(); + } // 启动gm api const permission = new PermissionVerify(this.group.group("permission"), this.mq); const gmApi = new GMApi( @@ -418,6 +467,8 @@ export class RuntimeService { // 还是要建立 CompiledResoure, 否则 Popup 看不到 Script await this.buildAndSaveCompiledResourceFromScript(script, false); } + } else { + bgScriptStorageNames.add(getStorageName(script)); } }); @@ -449,6 +500,7 @@ export class RuntimeService { if (script.type === SCRIPT_TYPE_NORMAL) { continue; } + bgScriptStorageNames.add(getStorageName(script)); res.push({ uuid: script.uuid, enable: script.status === SCRIPT_STATUS_ENABLE, @@ -466,17 +518,6 @@ export class RuntimeService { }); }); - // 监听脚本值变更 - this.mq.subscribe("valueUpdate", async ({ script, valueUpdated }: TScriptValueUpdate) => { - if (valueUpdated) { - if (script.status === SCRIPT_STATUS_ENABLE && isEarlyStartScript(script.metadata)) { - // 如果是预加载脚本,需要更新脚本代码重新注册 - // scriptMatchInfo 里的 value 改变 => compileInjectionCode -> injectionCode 改变 - await this.updateResourceOnScriptChange(script); - } - } - }); - if (chrome.extension.inIncognitoContext) { this.systemConfig.addListener("enable_script_incognito", async (enable) => { // 隐身窗口不对注册了的脚本进行实际操作 @@ -973,22 +1014,6 @@ export class RuntimeService { runtimeGlobal.registerState = failed ? RuntimeRegisterCode.UNSET : RuntimeRegisterCode.REGISTER_DONE; } - // 给指定tab发送消息 - sendMessageToTab(to: ExtMessageSender, action: string, data: any) { - if (to.tabId === -1) { - // 如果是-1, 代表给offscreen发送消息 - return sendMessage(this.msgSender, "offscreen/runtime/" + action, data); - } - return sendMessage( - new ExtensionContentMessageSend(to.tabId, { - documentId: to.documentId, - frameId: to.frameId, - }), - "scripting/runtime/" + action, - data - ); - } - // 给指定脚本触发事件 emitEventToTab(to: ExtMessageSender, req: EmitEventRequest) { if (to.tabId === -1) { diff --git a/src/app/service/service_worker/value.test.ts b/src/app/service/service_worker/value.test.ts index bd28eaaec..d6daae278 100644 --- a/src/app/service/service_worker/value.test.ts +++ b/src/app/service/service_worker/value.test.ts @@ -82,8 +82,8 @@ describe("ValueService - setValue 方法测试", () => { } as any; valueService.valueDAO = mockValueDAO; - // Mock pushValueToTab 方法 - valueService.pushValueToTab = vi.fn(); + // Mock pushValueUpdate 方法 + valueService.pushValueUpdate = vi.fn(); // Mock mq.emit 方法 mockMessageQueue.emit = vi.fn(); @@ -118,9 +118,10 @@ describe("ValueService - setValue 方法测试", () => { expect(mockScriptDAO.get).toHaveBeenCalledWith(mockScript.uuid); expect(mockValueDAO.get).toHaveBeenCalled(); expect(mockValueDAO.save).toHaveBeenCalled(); - expect(valueService.pushValueToTab).toHaveBeenCalledTimes(1); - expect(valueService.pushValueToTab).toHaveBeenNthCalledWith( + expect(valueService.pushValueUpdate).toHaveBeenCalledTimes(1); + expect(valueService.pushValueUpdate).toHaveBeenNthCalledWith( 1, + mockScript, expect.objectContaining({ entries: expect.any(Object), id: "testId-4021", @@ -133,8 +134,6 @@ describe("ValueService - setValue 方法测试", () => { valueUpdated: true, }) ); - expect(mockMessageQueue.emit).toHaveBeenCalledTimes(1); - expect(mockMessageQueue.emit).toHaveBeenCalledWith("valueUpdate", { script: mockScript, valueUpdated: true }); // 验证保存的数据结构 const saveCall = vi.mocked(mockValueDAO.save).mock.calls[0]; @@ -171,9 +170,10 @@ describe("ValueService - setValue 方法测试", () => { expect(mockScriptDAO.get).toHaveBeenCalledWith(mockScript.uuid); expect(mockValueDAO.get).toHaveBeenCalled(); expect(mockValueDAO.save).toHaveBeenCalled(); - expect(valueService.pushValueToTab).toHaveBeenCalledTimes(1); - expect(valueService.pushValueToTab).toHaveBeenNthCalledWith( + expect(valueService.pushValueUpdate).toHaveBeenCalledTimes(1); + expect(valueService.pushValueUpdate).toHaveBeenNthCalledWith( 1, + mockScript, expect.objectContaining({ entries: expect.any(Object), id: "testId-4022", @@ -186,8 +186,6 @@ describe("ValueService - setValue 方法测试", () => { valueUpdated: true, }) ); - expect(mockMessageQueue.emit).toHaveBeenCalledTimes(1); - expect(mockMessageQueue.emit).toHaveBeenCalledWith("valueUpdate", { script: mockScript, valueUpdated: true }); // 验证保存的数据结构 const saveCall = vi.mocked(mockValueDAO.save).mock.calls[0]; @@ -233,9 +231,10 @@ describe("ValueService - setValue 方法测试", () => { expect(mockScriptDAO.get).toHaveBeenCalledWith(mockScript.uuid); expect(mockValueDAO.get).toHaveBeenCalled(); expect(mockValueDAO.save).toHaveBeenCalled(); - expect(valueService.pushValueToTab).toHaveBeenCalledTimes(1); - expect(valueService.pushValueToTab).toHaveBeenNthCalledWith( + expect(valueService.pushValueUpdate).toHaveBeenCalledTimes(1); + expect(valueService.pushValueUpdate).toHaveBeenNthCalledWith( 1, + mockScript, expect.objectContaining({ entries: expect.any(Object), id: "testId-4023", @@ -248,11 +247,6 @@ describe("ValueService - setValue 方法测试", () => { valueUpdated: true, }) ); - expect(mockMessageQueue.emit).toHaveBeenCalledTimes(1); - expect(mockMessageQueue.emit).toHaveBeenCalledWith("valueUpdate", { - script: mockScript, - valueUpdated: true, - }); // 验证保存的数据被正确更新 const saveCall = vi.mocked(mockValueDAO.save).mock.calls[0]; @@ -294,9 +288,10 @@ describe("ValueService - setValue 方法测试", () => { expect(mockScriptDAO.get).toHaveBeenCalledWith(mockScript.uuid); expect(mockValueDAO.get).toHaveBeenCalled(); expect(mockValueDAO.save).not.toHaveBeenCalled(); // 值未改变,不应该保存 - expect(valueService.pushValueToTab).toHaveBeenCalledTimes(1); - expect(valueService.pushValueToTab).toHaveBeenNthCalledWith( + expect(valueService.pushValueUpdate).toHaveBeenCalledTimes(1); + expect(valueService.pushValueUpdate).toHaveBeenNthCalledWith( 1, + mockScript, expect.objectContaining({ entries: expect.any(Object), id: "testId-4024", @@ -309,8 +304,6 @@ describe("ValueService - setValue 方法测试", () => { valueUpdated: false, }) ); // 值未改变 - expect(mockMessageQueue.emit).toHaveBeenCalledTimes(1); - expect(mockMessageQueue.emit).toHaveBeenCalledWith("valueUpdate", { script: mockScript, valueUpdated: false }); // 值未改变 }); it("当设置值为undefined时应该删除该键", async () => { @@ -375,7 +368,7 @@ describe("ValueService - setValue 方法测试", () => { // 验证不会执行后续操作 expect(mockValueDAO.get).not.toHaveBeenCalled(); expect(mockValueDAO.save).not.toHaveBeenCalled(); - expect(valueService.pushValueToTab).not.toHaveBeenCalled(); + expect(valueService.pushValueUpdate).not.toHaveBeenCalled(); expect(mockMessageQueue.emit).toHaveBeenCalledTimes(0); }); @@ -394,7 +387,7 @@ describe("ValueService - setValue 方法测试", () => { vi.mocked(mockValueDAO.save).mockResolvedValue({} as any); expect(mockScriptDAO.get).toHaveBeenCalledTimes(0); expect(mockValueDAO.save).toHaveBeenCalledTimes(0); - expect(valueService.pushValueToTab).toHaveBeenCalledTimes(0); + expect(valueService.pushValueUpdate).toHaveBeenCalledTimes(0); // 并发执行两个setValue操作 const keyValuePairs1 = [[key1, encodeRValue(value1)]] satisfies TKeyValuePair[]; @@ -419,9 +412,10 @@ describe("ValueService - setValue 方法测试", () => { // 验证两个操作都被调用 expect(mockScriptDAO.get).toHaveBeenCalledTimes(2); expect(mockValueDAO.save).toHaveBeenCalledTimes(2); - expect(valueService.pushValueToTab).toHaveBeenCalledTimes(2); - expect(valueService.pushValueToTab).toHaveBeenNthCalledWith( + expect(valueService.pushValueUpdate).toHaveBeenCalledTimes(2); + expect(valueService.pushValueUpdate).toHaveBeenNthCalledWith( 1, + mockScript, expect.objectContaining({ entries: expect.any(Object), id: "testId-4041", @@ -434,8 +428,9 @@ describe("ValueService - setValue 方法测试", () => { valueUpdated: true, }) ); - expect(valueService.pushValueToTab).toHaveBeenNthCalledWith( + expect(valueService.pushValueUpdate).toHaveBeenNthCalledWith( 2, + mockScript, expect.objectContaining({ entries: expect.any(Object), id: "testId-4042", @@ -448,8 +443,5 @@ describe("ValueService - setValue 方法测试", () => { valueUpdated: true, }) ); - expect(mockMessageQueue.emit).toHaveBeenCalledTimes(2); - expect(mockMessageQueue.emit).toHaveBeenNthCalledWith(1, "valueUpdate", { script: mockScript, valueUpdated: true }); - expect(mockMessageQueue.emit).toHaveBeenNthCalledWith(2, "valueUpdate", { script: mockScript, valueUpdated: true }); }); }); diff --git a/src/app/service/service_worker/value.ts b/src/app/service/service_worker/value.ts index 0ecb34943..39dec9282 100644 --- a/src/app/service/service_worker/value.ts +++ b/src/app/service/service_worker/value.ts @@ -7,7 +7,6 @@ import { type RuntimeService } from "./runtime"; import { type PopupService } from "./popup"; import { getStorageName } from "@App/pkg/utils/utils"; import type { ValueUpdateDataEncoded, ValueUpdateDataREntry, ValueUpdateSender } from "../content/types"; -import type { TScriptValueUpdate } from "../queue"; import { type TDeleteScript } from "../queue"; import { type IMessageQueue } from "@Packages/message/message_queue"; import { CACHE_KEY_SET_VALUE } from "@App/app/cache_key"; @@ -75,30 +74,8 @@ export class ValueService { return this.getScriptValueDetails(script).then((res) => res[0]); } - // 推送值到tab - async pushValueToTab(sendData: T) { - chrome.storage.local.set( - { - valueUpdateDelivery: { - rId: `${Date.now()}.${Math.random()}`, // 用于区分不同的更新,确保 chrome.storage.local.onChanged 必能触发 - sendData, - }, - }, - () => { - const lastError = chrome.runtime.lastError; - if (lastError) { - console.error("chrome.runtime.lastError in chrome.storage.local.set", lastError); - } - } - ); - // 推送到offscreen中 - this.runtime!.sendMessageToTab( - { - tabId: -1, - }, - "valueUpdate", - sendData - ); + async pushValueUpdate(script: Script, sendData: T) { + return this.runtime!.pushValueUpdate(script, sendData); } // 批量设置 @@ -180,16 +157,15 @@ export class ValueService { }); // 推送到所有加载了本脚本的tab中 const valueUpdated = entries.length > 0; - this.pushValueToTab({ + const sendData = { id, entries: entries, uuid, storageName, sender: valueSender, valueUpdated, - } as ValueUpdateDataEncoded); - // valueUpdate 消息用于 early script 的处理 - this.mq.emit("valueUpdate", { script, valueUpdated }); + } as ValueUpdateDataEncoded; + this.pushValueUpdate(script, sendData); } setScriptValues(params: Pick, _sender: IGetSender) { From 34800fc19a236d61ce3b417761c31df03b1fff68 Mon Sep 17 00:00:00 2001 From: cyfung1031 <44498510+cyfung1031@users.noreply.github.com> Date: Wed, 6 May 2026 14:21:22 +0900 Subject: [PATCH 2/3] =?UTF-8?q?=E4=BF=AE=E6=94=B9=E5=9B=9E=20local?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/app/service/content/scripting.ts | 5 +++-- src/app/service/service_worker/runtime.ts | 4 ++-- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/src/app/service/content/scripting.ts b/src/app/service/content/scripting.ts index 65686a18c..7784279d4 100644 --- a/src/app/service/content/scripting.ts +++ b/src/app/service/content/scripting.ts @@ -3,7 +3,7 @@ import { type CustomEventMessage } from "@Packages/message/custom_event_message" import { forwardMessage, type Server } from "@Packages/message/server"; import type { MessageSend } from "@Packages/message/types"; import { RuntimeClient } from "../service_worker/client"; -import { getStorageName, isFirefox, makeBlobURL } from "@App/pkg/utils/utils"; +import { getStorageName, makeBlobURL } from "@App/pkg/utils/utils"; import type { Logger } from "@App/app/repo/logger"; import LoggerCore from "@App/app/logger/core"; import type { ValueUpdateDataEncoded } from "./types"; @@ -17,7 +17,8 @@ type PageOrContent = ValueOf; // For Firefox, StorageArea.setAccessLevel is not implemented. // See https://bugzilla.mozilla.org/show_bug.cgi?id=1724754 -const deliveryStorage = isFirefox() ? chrome.storage.local : chrome.storage.session; +// const deliveryStorage = isFirefox() ? chrome.storage.local : chrome.storage.session; +const deliveryStorage = chrome.storage.local; // 日后再处理 // scripting页的处理 export default class ScriptingRuntime { diff --git a/src/app/service/service_worker/runtime.ts b/src/app/service/service_worker/runtime.ts index 2e66b79d9..83fc7e4d1 100644 --- a/src/app/service/service_worker/runtime.ts +++ b/src/app/service/service_worker/runtime.ts @@ -21,7 +21,6 @@ import { getMetadataStr, getStorageName, getUserConfigStr, - isFirefox, obtainBlackList, sourceMapTo, } from "@App/pkg/utils/utils"; @@ -88,7 +87,8 @@ const bgScriptStorageNames = new Set(); // For Firefox, StorageArea.setAccessLevel is not implemented. // See https://bugzilla.mozilla.org/show_bug.cgi?id=1724754 -const deliveryStorage = isFirefox() ? chrome.storage.local : chrome.storage.session; +// const deliveryStorage = isFirefox() ? chrome.storage.local : chrome.storage.session; +const deliveryStorage = chrome.storage.local; // 日后再处理 export class RuntimeService { scriptMatchEnable: UrlMatch = new UrlMatch(); From 2fc9a989ad4b31771665e262cd557d5bb6cdf700 Mon Sep 17 00:00:00 2001 From: cyfung1031 <44498510+cyfung1031@users.noreply.github.com> Date: Wed, 6 May 2026 23:41:01 +0900 Subject: [PATCH 3/3] =?UTF-8?q?=E4=BB=A3=E7=A0=81=E4=BF=AE=E6=AD=A3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/app/service/content/scripting.ts | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/src/app/service/content/scripting.ts b/src/app/service/content/scripting.ts index 7784279d4..0502790d4 100644 --- a/src/app/service/content/scripting.ts +++ b/src/app/service/content/scripting.ts @@ -11,7 +11,8 @@ import type { ValueUpdateDataEncoded } from "./types"; const PageOrContent = { PAGE: 1, CONTENT: 2, -}; + PAGE_AND_CONTENT: 3, +} as const; type PageOrContent = ValueOf; @@ -22,7 +23,7 @@ const deliveryStorage = chrome.storage.local; // 日后再处理 // scripting页的处理 export default class ScriptingRuntime { - private activeStorageNames = new Map(); + private activeStorageNames: Map | null = null; constructor( // 监听来自service_worker的消息 private readonly extServer: Server, @@ -40,7 +41,7 @@ export default class ScriptingRuntime { broadcastToPage( action: string, data?: any, - activeOn: PageOrContent = PageOrContent.PAGE | PageOrContent.CONTENT + activeOn: PageOrContent = (PageOrContent.PAGE | PageOrContent.CONTENT) as PageOrContent ): Promise { return Promise.all([ activeOn & PageOrContent.CONTENT && sendMessage(this.senderToContent, "content/" + action, data), @@ -73,7 +74,10 @@ export default class ScriptingRuntime { const record = changes["valueUpdateDelivery"]; if (record?.newValue) { const sendData = record.newValue.sendData as ValueUpdateDataEncoded; - const activeOn = this.activeStorageNames.get(sendData.storageName); + const activeOn = + this.activeStorageNames === null + ? PageOrContent.PAGE_AND_CONTENT + : this.activeStorageNames.get(sendData.storageName); if (activeOn) { // 转发给 content 和 inject this.broadcastToPage("runtime/valueUpdate", sendData, activeOn);