From c474107617ca9b7e75b58402cd33be5c974cd6e2 Mon Sep 17 00:00:00 2001 From: cyfung1031 <44498510+cyfung1031@users.noreply.github.com> Date: Sat, 18 Apr 2026 04:40:40 +0900 Subject: [PATCH 1/6] =?UTF-8?q?=E8=A7=A3=E5=86=B3=E5=B9=B6=E5=8F=91=20xhr?= =?UTF-8?q?=20=E7=9A=84=20session=20rule=20=E6=95=B0=E9=87=8F=E9=97=AE?= =?UTF-8?q?=E9=A2=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../declarativ_net_request.ts | 32 ++- .../gm_api/dnr_id_controller.test.ts | 196 ++++++++++++++++++ .../gm_api/dnr_id_controller.ts | 56 +++++ .../service/service_worker/gm_api/gm_api.ts | 5 +- tests/vitest.setup.ts | 2 + 5 files changed, 288 insertions(+), 3 deletions(-) create mode 100644 src/app/service/service_worker/gm_api/dnr_id_controller.test.ts create mode 100644 src/app/service/service_worker/gm_api/dnr_id_controller.ts diff --git a/packages/chrome-extension-mock/declarativ_net_request.ts b/packages/chrome-extension-mock/declarativ_net_request.ts index abbf15c3e..c2a9be819 100644 --- a/packages/chrome-extension-mock/declarativ_net_request.ts +++ b/packages/chrome-extension-mock/declarativ_net_request.ts @@ -1,4 +1,8 @@ export default class DeclarativeNetRequest { + MAX_NUMBER_OF_SESSION_RULES = 5000; + + private _sessionRules: chrome.declarativeNetRequest.Rule[] = []; + HeaderOperation = { APPEND: "append", SET: "set", @@ -30,9 +34,35 @@ export default class DeclarativeNetRequest { OTHER: "other", }; - updateSessionRules() { + updateSessionRules( + options: { + addRules?: chrome.declarativeNetRequest.Rule[]; + removeRuleIds?: number[]; + } = {} + ): Promise { return new Promise((resolve) => { + const { addRules = [], removeRuleIds = [] } = options; + + // Remove rules by ID + if (removeRuleIds.length > 0) { + this._sessionRules = this._sessionRules.filter((rule) => !removeRuleIds.includes(rule.id)); + } + + // Add or update rules (upsert by ID) + for (const newRule of addRules) { + const existingIndex = this._sessionRules.findIndex((rule) => rule.id === newRule.id); + if (existingIndex !== -1) { + this._sessionRules[existingIndex] = newRule; // update + } else { + this._sessionRules.push(newRule); // add + } + } + resolve(); }); } + + getSessionRules(): Promise { + return Promise.resolve([...this._sessionRules]); + } } diff --git a/src/app/service/service_worker/gm_api/dnr_id_controller.test.ts b/src/app/service/service_worker/gm_api/dnr_id_controller.test.ts new file mode 100644 index 000000000..81b7e0e68 --- /dev/null +++ b/src/app/service/service_worker/gm_api/dnr_id_controller.test.ts @@ -0,0 +1,196 @@ +import { sleep } from "@App/pkg/utils/utils"; +import { describe, expect, it, vi } from "vitest"; +import { + getSessionRuleIds, + LIMIT_SESSION_RULES, + nextSessionRuleId, + removeSessionRuleIdEntry, +} from "./dnr_id_controller"; + +describe("getSessionRuleIds", () => { + it("initializes from existing chrome session rules", async () => { + //@ts-ignore + vi.mocked(chrome.declarativeNetRequest.getSessionRules).mockResolvedValueOnce([ + { id: 10901, priority: 1, action: { type: "block" }, condition: {} }, + { id: 10902, priority: 1, action: { type: "block" }, condition: {} }, + ]); + + const ids = await getSessionRuleIds(); + expect(ids).toContain(10901); + expect(ids).toContain(10902); + }); +}); + +describe("nextSessionRuleId", () => { + it("returns unique incrementing IDs on each call", async () => { + const id1 = await nextSessionRuleId(); + const id2 = await nextSessionRuleId(); + const id3 = await nextSessionRuleId(); + + expect(id2).toBeGreaterThan(id1); + expect(id3).toBeGreaterThan(id2); + }); + + it("skips IDs that already exist in session rules", async () => { + const ids = await getSessionRuleIds(); + const next = await nextSessionRuleId(); + + ids.add(next + 1); + const skipped = await nextSessionRuleId(); + + expect(skipped).toBeGreaterThan(next + 1); + }); +}); + +describe("removeSessionRuleIdEntry", () => { + it("removes the given ID from the tracked set", async () => { + const ids = await getSessionRuleIds(); + const id = await nextSessionRuleId(); + + ids.add(id); + removeSessionRuleIdEntry(id); + + expect(ids.has(id)).toBe(false); + }); + + it("is a no-op when called before sessionRuleIds is initialized", () => { + expect(() => removeSessionRuleIdEntry(10404)).not.toThrow(); + }); + + it("throws when ruleId is <= 10000", () => { + expect(() => removeSessionRuleIdEntry(10000)).toThrow(); + expect(() => removeSessionRuleIdEntry(1)).toThrow(); + }); + + it("rewinds SESSION_RULE_ID_BEGIN so the removed id gets reused next", async () => { + const ids = await getSessionRuleIds(); + + const id1 = await nextSessionRuleId(); + const id2 = await nextSessionRuleId(); + const id3 = await nextSessionRuleId(); + + ids.add(id1); + ids.add(id2); + ids.add(id3); + + // Remove the latest — counter should rewind and reuse it + removeSessionRuleIdEntry(id3); + const reused = await nextSessionRuleId(); + expect(reused).toBe(id3); + }); + + it("rewinds SESSION_RULE_ID_BEGIN when removed id is behind the counter", async () => { + const ids = await getSessionRuleIds(); + + const id1 = await nextSessionRuleId(); + const id2 = await nextSessionRuleId(); + const id3 = await nextSessionRuleId(); + + ids.add(id1); + ids.add(id2); + ids.add(id3); + + // Remove an older id — counter rewinds to id1 - 1, so id1 gets reused + removeSessionRuleIdEntry(id1); + const reused = await nextSessionRuleId(); + expect(reused).toBe(id1); + }); + + it("does not rewind SESSION_RULE_ID_BEGIN when removed id was pre-existing (ahead of counter)", async () => { + //@ts-ignore + vi.mocked(chrome.declarativeNetRequest.getSessionRules).mockResolvedValueOnce([ + { id: 11122, priority: 1, action: { type: "block" }, condition: {} }, + ]); + + const ids = await getSessionRuleIds(); + const nextBefore = await nextSessionRuleId(); // e.g. 10001 + + ids.add(11122); // ensure it's tracked + removeSessionRuleIdEntry(11122); // 11122 > SESSION_RULE_ID_BEGIN + 1, no rewind + + const nextAfter = await nextSessionRuleId(); + expect(nextAfter).toBe(nextBefore + 1); // counter unchanged, just increments normally + }); +}); + +describe("nextSessionRuleId limit control", () => { + it("locks when session rules reach the limit and unlocks when an entry is removed", async () => { + const ids = await getSessionRuleIds(); + expect(ids.size).toBeLessThan(100); + + const added = []; + for (let w = ids.size; w < LIMIT_SESSION_RULES; w++) { + const j = await nextSessionRuleId(); + added.push(j); + ids.add(j); + } + expect(ids.size).toBeGreaterThan(1000); + + const lockedPromise = nextSessionRuleId(); + + const raceResult1 = await Promise.race([lockedPromise.then(() => "resolved"), sleep(5).then(() => "pending")]); + expect(raceResult1).toBe("pending"); + + const m1 = Math.floor(Math.random() * (added.length - 9)); + const p1 = added[m1]; + const p2 = added[m1 + 6]; + removeSessionRuleIdEntry(p1); + removeSessionRuleIdEntry(p2); + + const raceResult2 = await Promise.race([lockedPromise.then(() => "resolved"), sleep(5).then(() => "pending")]); + expect(raceResult2).toBe("resolved"); + + const id1 = await lockedPromise; + const id2 = await nextSessionRuleId(); + expect(id1).toBe(p1); + expect(id2).toBe(p2); + + for (const k of added) { + removeSessionRuleIdEntry(k); + } + + const res = await getSessionRuleIds(); + expect(res).toBe(ids); + expect(res.size).toBeLessThan(100); + }); + + it("only one lock is created even with concurrent nextSessionRuleId calls", async () => { + const ids = await getSessionRuleIds(); + expect(ids.size).toBeLessThan(100); + + const added = []; + for (let w = ids.size; w < LIMIT_SESSION_RULES; w++) { + const j = await nextSessionRuleId(); + added.push(j); + ids.add(j); + } + expect(ids.size).toBeGreaterThan(1000); + + // Fire multiple concurrent calls while locked + const p1 = nextSessionRuleId(); + const p2 = nextSessionRuleId(); + const p3 = nextSessionRuleId(); + + const raceResult = await Promise.race([ + Promise.all([p1, p2, p3]).then(() => "resolved"), + sleep(5).then(() => "pending"), + ]); + expect(raceResult).toBe("pending"); + + // Single removal should unlock all waiters sequentially + const toRemove = [...ids].find((id) => id > 10000)!; + removeSessionRuleIdEntry(toRemove); + + // All three should eventually resolve + const results = await Promise.race([Promise.all([p1, p2, p3]).then((ids) => ids), sleep(50).then(() => null)]); + expect(results).not.toBeNull(); + + for (const k of added) { + removeSessionRuleIdEntry(k); + } + + const res = await getSessionRuleIds(); + expect(res).toBe(ids); + expect(res.size).toBeLessThan(100); + }); +}); diff --git a/src/app/service/service_worker/gm_api/dnr_id_controller.ts b/src/app/service/service_worker/gm_api/dnr_id_controller.ts new file mode 100644 index 000000000..275a4f691 --- /dev/null +++ b/src/app/service/service_worker/gm_api/dnr_id_controller.ts @@ -0,0 +1,56 @@ +import type { Deferred } from "@App/pkg/utils/utils"; +import { deferred } from "@App/pkg/utils/utils"; + +let sessionRuleIdsPromise: Promise> | null = null; +let sessionRuleIds: Set | null = null; + +let SESSION_RULE_ID_BEGIN = 10000; +export const LIMIT_SESSION_RULES = + process.env.VI_TESTING === "true" ? 1234 : chrome.declarativeNetRequest.MAX_NUMBER_OF_SESSION_RULES - 300; +let lockSessionRuleCreation: Deferred | null = null; + +export const getSessionRuleIds = async (): Promise> => { + if (!sessionRuleIdsPromise) { + sessionRuleIdsPromise = chrome.declarativeNetRequest + .getSessionRules() + .then((rules) => { + sessionRuleIds = new Set(rules.map((rule) => rule.id).filter(Boolean)); + return sessionRuleIds; + }) + .catch((e) => { + console.warn(e); + sessionRuleIds = new Set([]); + return sessionRuleIds; + }); + } + const ruleIds = sessionRuleIds || (await sessionRuleIdsPromise); + return ruleIds; +}; + +export const removeSessionRuleIdEntry = (ruleId: number) => { + if (ruleId <= 10000) { + throw new Error("removeSessionRuleIdEntry cannot remove ids not recreated by nextSessionRuleId"); + } + if (sessionRuleIds) { + if (sessionRuleIds.delete(ruleId) === true) { + if (ruleId <= SESSION_RULE_ID_BEGIN + 1) { + SESSION_RULE_ID_BEGIN = ruleId - 1; + } + if (sessionRuleIds.size < LIMIT_SESSION_RULES) { + lockSessionRuleCreation?.resolve(); + lockSessionRuleCreation = null; + } + } + } +}; + +export const nextSessionRuleId = async () => { + const ruleIds = await getSessionRuleIds(); + if (!lockSessionRuleCreation && ruleIds.size + 1 > LIMIT_SESSION_RULES) lockSessionRuleCreation = deferred(); + if (lockSessionRuleCreation) await lockSessionRuleCreation.promise; + let id; + do { + id = ++SESSION_RULE_ID_BEGIN; + } while (ruleIds.has(id)); + return id; +}; 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 1ecc56f94..d087a58cb 100644 --- a/src/app/service/service_worker/gm_api/gm_api.ts +++ b/src/app/service/service_worker/gm_api/gm_api.ts @@ -53,6 +53,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 { nextSessionRuleId, removeSessionRuleIdEntry } from "./dnr_id_controller"; let generatedUniqueMarkerIDs = ""; let generatedUniqueMarkerIDWhen = ""; @@ -685,8 +686,7 @@ export default class GMApi { } const redirectNotManual = params.redirect !== "manual"; - // 使用 cacheInstance 避免SW重启造成重复 DNR Rule ID - const ruleId = 10000 + (await cacheInstance.incr("gmXhrRequestId", 1)); + const ruleId = await nextSessionRuleId(); const rule = { id: ruleId, action: { @@ -1609,6 +1609,7 @@ export default class GMApi { if (lastError) { console.error("chrome.declarativeNetRequest.updateSessionRules:", lastError); } + removeSessionRuleIdEntry(rule.id); } ); } diff --git a/tests/vitest.setup.ts b/tests/vitest.setup.ts index 8dcc95fc4..1d349d9a6 100644 --- a/tests/vitest.setup.ts +++ b/tests/vitest.setup.ts @@ -9,6 +9,8 @@ import { mockFetch } from "./mocks/fetch"; vi.stubGlobal("chrome", chromeMock); chromeMock.init(); +vi.spyOn(chromeMock.declarativeNetRequest, "getSessionRules"); +vi.spyOn(chromeMock.declarativeNetRequest, "updateSessionRules"); initTestEnv(); chromeMock.runtime.getURL = vi.fn().mockImplementation((path: string) => { From 0a2a7dc02be8125047025a1931b3895426bf68ed Mon Sep 17 00:00:00 2001 From: cyfung1031 <44498510+cyfung1031@users.noreply.github.com> Date: Sat, 18 Apr 2026 04:44:58 +0900 Subject: [PATCH 2/6] typo --- src/app/service/service_worker/gm_api/dnr_id_controller.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/app/service/service_worker/gm_api/dnr_id_controller.ts b/src/app/service/service_worker/gm_api/dnr_id_controller.ts index 275a4f691..6b618bc52 100644 --- a/src/app/service/service_worker/gm_api/dnr_id_controller.ts +++ b/src/app/service/service_worker/gm_api/dnr_id_controller.ts @@ -29,7 +29,7 @@ export const getSessionRuleIds = async (): Promise> => { export const removeSessionRuleIdEntry = (ruleId: number) => { if (ruleId <= 10000) { - throw new Error("removeSessionRuleIdEntry cannot remove ids not recreated by nextSessionRuleId"); + throw new Error("removeSessionRuleIdEntry cannot remove ids not created by nextSessionRuleId"); } if (sessionRuleIds) { if (sessionRuleIds.delete(ruleId) === true) { From 7522c7644ce54f8f5a227a2306d7793ab1f57806 Mon Sep 17 00:00:00 2001 From: cyfung1031 <44498510+cyfung1031@users.noreply.github.com> Date: Sat, 18 Apr 2026 04:57:02 +0900 Subject: [PATCH 3/6] code fix --- .../gm_api/dnr_id_controller.test.ts | 42 ++++++++++--------- .../gm_api/dnr_id_controller.ts | 1 + tests/vitest.setup.ts | 4 +- 3 files changed, 26 insertions(+), 21 deletions(-) diff --git a/src/app/service/service_worker/gm_api/dnr_id_controller.test.ts b/src/app/service/service_worker/gm_api/dnr_id_controller.test.ts index 81b7e0e68..e2d3ddfbd 100644 --- a/src/app/service/service_worker/gm_api/dnr_id_controller.test.ts +++ b/src/app/service/service_worker/gm_api/dnr_id_controller.test.ts @@ -1,5 +1,5 @@ import { sleep } from "@App/pkg/utils/utils"; -import { describe, expect, it, vi } from "vitest"; +import { describe, expect, it } from "vitest"; import { getSessionRuleIds, LIMIT_SESSION_RULES, @@ -9,15 +9,12 @@ import { describe("getSessionRuleIds", () => { it("initializes from existing chrome session rules", async () => { - //@ts-ignore - vi.mocked(chrome.declarativeNetRequest.getSessionRules).mockResolvedValueOnce([ - { id: 10901, priority: 1, action: { type: "block" }, condition: {} }, - { id: 10902, priority: 1, action: { type: "block" }, condition: {} }, - ]); - const ids = await getSessionRuleIds(); - expect(ids).toContain(10901); - expect(ids).toContain(10902); + expect(ids.size).lessThan(100); + await nextSessionRuleId(); + expect(ids.size).greaterThanOrEqual(1); + await nextSessionRuleId(); + expect(ids.size).greaterThanOrEqual(2); }); }); @@ -97,19 +94,26 @@ describe("removeSessionRuleIdEntry", () => { }); it("does not rewind SESSION_RULE_ID_BEGIN when removed id was pre-existing (ahead of counter)", async () => { - //@ts-ignore - vi.mocked(chrome.declarativeNetRequest.getSessionRules).mockResolvedValueOnce([ - { id: 11122, priority: 1, action: { type: "block" }, condition: {} }, - ]); + const id1 = await nextSessionRuleId(); + const id2 = await nextSessionRuleId(); + const id3 = await nextSessionRuleId(); + const id4 = await nextSessionRuleId(); + const id5 = await nextSessionRuleId(); + const id6 = await nextSessionRuleId(); - const ids = await getSessionRuleIds(); + removeSessionRuleIdEntry(id1); + removeSessionRuleIdEntry(id5); + removeSessionRuleIdEntry(id3); + removeSessionRuleIdEntry(id4); + removeSessionRuleIdEntry(id2); const nextBefore = await nextSessionRuleId(); // e.g. 10001 + removeSessionRuleIdEntry(id6); - ids.add(11122); // ensure it's tracked - removeSessionRuleIdEntry(11122); // 11122 > SESSION_RULE_ID_BEGIN + 1, no rewind - - const nextAfter = await nextSessionRuleId(); - expect(nextAfter).toBe(nextBefore + 1); // counter unchanged, just increments normally + expect(await nextSessionRuleId()).toBe(nextBefore + 1); // counter unchanged, just increments normally + expect(await nextSessionRuleId()).toBe(nextBefore + 2); // counter unchanged, just increments normally + expect(await nextSessionRuleId()).toBe(nextBefore + 3); // counter unchanged, just increments normally + expect(await nextSessionRuleId()).toBe(nextBefore + 4); // counter unchanged, just increments normally + expect(await nextSessionRuleId()).toBe(nextBefore + 5); // counter unchanged, just increments normally }); }); diff --git a/src/app/service/service_worker/gm_api/dnr_id_controller.ts b/src/app/service/service_worker/gm_api/dnr_id_controller.ts index 6b618bc52..83db1ce6c 100644 --- a/src/app/service/service_worker/gm_api/dnr_id_controller.ts +++ b/src/app/service/service_worker/gm_api/dnr_id_controller.ts @@ -52,5 +52,6 @@ export const nextSessionRuleId = async () => { do { id = ++SESSION_RULE_ID_BEGIN; } while (ruleIds.has(id)); + ruleIds.add(id); return id; }; diff --git a/tests/vitest.setup.ts b/tests/vitest.setup.ts index 1d349d9a6..178d5a5e7 100644 --- a/tests/vitest.setup.ts +++ b/tests/vitest.setup.ts @@ -9,8 +9,8 @@ import { mockFetch } from "./mocks/fetch"; vi.stubGlobal("chrome", chromeMock); chromeMock.init(); -vi.spyOn(chromeMock.declarativeNetRequest, "getSessionRules"); -vi.spyOn(chromeMock.declarativeNetRequest, "updateSessionRules"); +// vi.spyOn(chromeMock.declarativeNetRequest, "getSessionRules"); +// vi.spyOn(chromeMock.declarativeNetRequest, "updateSessionRules"); initTestEnv(); chromeMock.runtime.getURL = vi.fn().mockImplementation((path: string) => { From 3f3e32a3d090b538534bbf0123c859b923c68bab Mon Sep 17 00:00:00 2001 From: cyfung1031 <44498510+cyfung1031@users.noreply.github.com> Date: Sat, 18 Apr 2026 05:10:00 +0900 Subject: [PATCH 4/6] fix mock --- .../declarativ_net_request.ts | 17 +++++++++++++---- 1 file changed, 13 insertions(+), 4 deletions(-) diff --git a/packages/chrome-extension-mock/declarativ_net_request.ts b/packages/chrome-extension-mock/declarativ_net_request.ts index c2a9be819..af50dae54 100644 --- a/packages/chrome-extension-mock/declarativ_net_request.ts +++ b/packages/chrome-extension-mock/declarativ_net_request.ts @@ -34,12 +34,20 @@ export default class DeclarativeNetRequest { OTHER: "other", }; - updateSessionRules( - options: { + updateSessionRules(arg1: any, arg2: any): Promise { + let options: { addRules?: chrome.declarativeNetRequest.Rule[]; removeRuleIds?: number[]; - } = {} - ): Promise { + } = {}; + let callback: undefined | ((...args: any) => any) = undefined; + + if (typeof arg1 === "function") { + callback = arg1; + } else if (typeof arg2 === "function") { + callback = arg2; + } + if (typeof arg1 === "object" && arg1) options = arg1; + return new Promise((resolve) => { const { addRules = [], removeRuleIds = [] } = options; @@ -59,6 +67,7 @@ export default class DeclarativeNetRequest { } resolve(); + callback?.(); }); } From da33bd2db201e984e7cb7f142421ab1ba41cac49 Mon Sep 17 00:00:00 2001 From: cyfung1031 <44498510+cyfung1031@users.noreply.github.com> Date: Sat, 18 Apr 2026 09:23:48 +0900 Subject: [PATCH 5/6] update unit test --- .../service/service_worker/gm_api/dnr_id_controller.test.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/app/service/service_worker/gm_api/dnr_id_controller.test.ts b/src/app/service/service_worker/gm_api/dnr_id_controller.test.ts index e2d3ddfbd..1b5f218ca 100644 --- a/src/app/service/service_worker/gm_api/dnr_id_controller.test.ts +++ b/src/app/service/service_worker/gm_api/dnr_id_controller.test.ts @@ -97,21 +97,21 @@ describe("removeSessionRuleIdEntry", () => { const id1 = await nextSessionRuleId(); const id2 = await nextSessionRuleId(); const id3 = await nextSessionRuleId(); - const id4 = await nextSessionRuleId(); + const _id4 = await nextSessionRuleId(); const id5 = await nextSessionRuleId(); const id6 = await nextSessionRuleId(); removeSessionRuleIdEntry(id1); removeSessionRuleIdEntry(id5); removeSessionRuleIdEntry(id3); - removeSessionRuleIdEntry(id4); + // removeSessionRuleIdEntry(id4); removeSessionRuleIdEntry(id2); const nextBefore = await nextSessionRuleId(); // e.g. 10001 removeSessionRuleIdEntry(id6); expect(await nextSessionRuleId()).toBe(nextBefore + 1); // counter unchanged, just increments normally expect(await nextSessionRuleId()).toBe(nextBefore + 2); // counter unchanged, just increments normally - expect(await nextSessionRuleId()).toBe(nextBefore + 3); // counter unchanged, just increments normally + // expect(await nextSessionRuleId()).toBe(nextBefore + 3); // counter unchanged, just increments normally expect(await nextSessionRuleId()).toBe(nextBefore + 4); // counter unchanged, just increments normally expect(await nextSessionRuleId()).toBe(nextBefore + 5); // counter unchanged, just increments normally }); From d80bc08da06aca1e2164df0d33045b5693e2db52 Mon Sep 17 00:00:00 2001 From: cyfung1031 <44498510+cyfung1031@users.noreply.github.com> Date: Mon, 20 Apr 2026 10:34:05 +0900 Subject: [PATCH 6/6] =?UTF-8?q?=E6=94=B9=E5=96=84=20session=20rule=20?= =?UTF-8?q?=E7=A7=BB=E9=99=A4=E5=A4=84=E7=90=86=20=E5=8F=8A=20scXhrRequest?= =?UTF-8?q?s=20=E5=A4=84=E7=90=86?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../service/service_worker/gm_api/gm_api.ts | 109 ++++++++++++++---- 1 file changed, 86 insertions(+), 23 deletions(-) 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 d087a58cb..4ed8529db 100644 --- a/src/app/service/service_worker/gm_api/gm_api.ts +++ b/src/app/service/service_worker/gm_api/gm_api.ts @@ -75,7 +75,42 @@ const generateUniqueMarkerID = () => { }; type OnBeforeSendHeadersOptions = `${chrome.webRequest.OnBeforeSendHeadersOptions}`; -type OnHeadersReceivedOptions = `${chrome.webRequest.OnHeadersReceivedOptions}`; +type ReceiveHeaderOptions = `${chrome.webRequest.OnHeadersReceivedOptions}` & + `${chrome.webRequest.OnResponseStartedOptions}`; + +// 删除关联与DNR: 不再处理 headerModifer 时清空 Map 关联 及 浏览器 Session Rule +const headersSettled = (markerID: string) => { + const dnrRule = headerModifierMap.get(markerID); + const ruleID = dnrRule?.rule.id; + if (markerID) { + headerModifierMap.delete(markerID); + } + if (ruleID) { + chrome.declarativeNetRequest.updateSessionRules( + { + removeRuleIds: [ruleID], + }, + () => { + const lastError = chrome.runtime.lastError; + if (lastError) { + console.error("chrome.declarativeNetRequest.updateSessionRules:", lastError); + } + removeSessionRuleIdEntry(ruleID); + } + ); + } +}; + +const cleanupOnAPIError = (requestId: string) => { + const markerID = scXhrRequests.get(requestId) as string | undefined; + scXhrRequests.delete(requestId); + if (!markerID) return; + redirectedUrls.delete(markerID); + nwErrorResults.delete(markerID); + scXhrRequests.delete(markerID); + headersReceivedMap.delete(markerID); + headersSettled(markerID); // 处理完毕 +}; // GMExternalDependencies接口定义 // 为了支持外部依赖注入,方便测试和扩展 @@ -851,7 +886,7 @@ export default class GMApi { if (reqId) scXhrRequests.delete(reqId); scXhrRequests.delete(markerID); headersReceivedMap.delete(markerID); - headerModifierMap.delete(markerID); + headersSettled(markerID); // 处理完毕 }; let strategy: GMXhrStrategy | undefined = undefined; if (useFetch) { @@ -1410,6 +1445,7 @@ export default class GMApi { if (lastError) { console.error("chrome.runtime.lastError in chrome.webRequest.onBeforeRedirect:", lastError); // webRequest API 出错不进行后续处理 + cleanupOnAPIError(details?.requestId); return undefined; } if (details.tabId === -1) { @@ -1426,7 +1462,7 @@ export default class GMApi { } ); const reqOpt: OnBeforeSendHeadersOptions[] = ["requestHeaders"]; - const respOpt: OnHeadersReceivedOptions[] = ["responseHeaders"]; + const respOpt: ReceiveHeaderOptions[] = ["responseHeaders"]; if (!isFirefox()) { reqOpt.push("extraHeaders"); respOpt.push("extraHeaders"); @@ -1438,14 +1474,16 @@ export default class GMApi { if (lastError) { console.error("chrome.runtime.lastError in chrome.webRequest.onErrorOccurred:", lastError); // webRequest API 出错不进行后续处理 + cleanupOnAPIError(details?.requestId); return undefined; } if (details.tabId === -1) { const markerID = scXhrRequests.get(details.requestId); - if (markerID) { - nwErrorResults.set(markerID, details.error); - nwErrorResultPromises.get(markerID)?.(); - } + if (!markerID) return; + nwErrorResults.set(markerID, details.error); + nwErrorResultPromises.get(markerID)?.(); + + headersSettled(markerID); // 错误发生,不处理 header modifiers } }, { @@ -1502,6 +1540,7 @@ export default class GMApi { if (lastError) { console.error("chrome.runtime.lastError in chrome.webRequest.onBeforeSendHeaders:", lastError); // webRequest API 出错不进行后续处理 + cleanupOnAPIError(details?.requestId); return undefined; } if (details.tabId === -1) { @@ -1510,7 +1549,11 @@ export default class GMApi { if (requestHeaders) { // 如 onBeforeSendHeaders 是在 modifyHeaders 前执行,可以更新一下 reqId 和 markerID 的关联 const markerID = requestHeaders?.find((h) => h.name.toLowerCase() === "x-sc-request-marker")?.value; - if (markerID) scXhrRequests.set(reqId, markerID); + if (markerID) { + // 双向关联 + scXhrRequests.set(reqId, markerID); + scXhrRequests.set(markerID, reqId); // 用于清理 + } } const markerID = scXhrRequests.get(reqId); if (!markerID) return undefined; @@ -1525,12 +1568,14 @@ export default class GMApi { }, reqOpt ); + chrome.webRequest.onHeadersReceived.addListener( (details) => { const lastError = chrome.runtime.lastError; if (lastError) { console.error("chrome.runtime.lastError in chrome.webRequest.onBeforeSendHeaders:", lastError); // webRequest API 出错不进行后续处理 + cleanupOnAPIError(details?.requestId); return undefined; } if (details.tabId === -1) { @@ -1597,21 +1642,7 @@ export default class GMApi { } } ); - } else { - // 删除关联与DNR - headerModifierMap.delete(markerID); - chrome.declarativeNetRequest.updateSessionRules( - { - removeRuleIds: [rule.id], - }, - () => { - const lastError = chrome.runtime.lastError; - if (lastError) { - console.error("chrome.declarativeNetRequest.updateSessionRules:", lastError); - } - removeSessionRuleIdEntry(rule.id); - } - ); + return; } } } @@ -1624,6 +1655,38 @@ export default class GMApi { respOpt ); + chrome.webRequest.onResponseStarted.addListener( + (details) => { + // onResponseStarted 触发后,headers 不会再有改动 + const lastError = chrome.runtime.lastError; + if (lastError) { + console.error("chrome.runtime.lastError in chrome.webRequest.onResponseStarted:", lastError); + // webRequest API 出错不进行后续处理 + cleanupOnAPIError(details?.requestId); + return undefined; + } + if (details.tabId === -1) { + const reqId = details.requestId; + + const markerID = scXhrRequests.get(reqId); + if (!markerID) return; + if (details.responseHeaders && details.statusCode) { + headersReceivedMap.set(markerID, { + responseHeaders: details.responseHeaders, + statusCode: details.statusCode, + }); + } + + headersSettled(markerID); // onResponseStarted 已触发,headers 已固定 + } + }, + { + urls: [""], + types: ["xmlhttprequest"], + }, + respOpt + ); + const ruleId = 999; const rule = { id: ruleId,