Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
56 commits
Select commit Hold shift + click to select a range
ba712f6
x
cyfung1031 Feb 3, 2026
29cc3c6
处理并行fetch问题
cyfung1031 Feb 3, 2026
79b68f7
修复 src/app/service/service_worker/runtime.ts
cyfung1031 Feb 3, 2026
e83aae2
本地资源代码更新逻辑修正
cyfung1031 Feb 3, 2026
96f34be
Update script.ts
cyfung1031 Feb 3, 2026
983f89e
fix
cyfung1031 Feb 3, 2026
bbe6c1d
getResourceByType -> getResourceByTypes
cyfung1031 Feb 4, 2026
49579bf
加入注意
cyfung1031 Feb 4, 2026
3e8f04e
getScriptResources -> getScriptResourceValue
cyfung1031 Feb 4, 2026
bee32f0
代码调整
cyfung1031 Feb 4, 2026
495a782
`mdValue.startsWith("file:///")` -> `resourcePath.startsWith("file://…
cyfung1031 Feb 4, 2026
1a84afd
代码调整
cyfung1031 Feb 4, 2026
658b256
代码调整
cyfung1031 Feb 4, 2026
5823855
代码调整
cyfung1031 Feb 4, 2026
b63960b
Merge branch 'release/v1.3' into pr-updateResourceByType-return
cyfung1031 Feb 4, 2026
c3d395c
`loadByUrl` -> `createResourceByUrlFetch`
cyfung1031 Feb 4, 2026
1a434b8
简化 updateResource signature
cyfung1031 Feb 4, 2026
da70c26
简化 createResourceByUrlFetch
cyfung1031 Feb 4, 2026
8b1fc24
lint
cyfung1031 Feb 4, 2026
0c991f2
`updateResourceByType` -> `updateResourceByTypes`
cyfung1031 Feb 4, 2026
d6d2a29
加注釋
cyfung1031 Feb 4, 2026
8892d60
代码优化 - 资源更新条件修改
cyfung1031 Feb 4, 2026
ab7a082
注釋
cyfung1031 Feb 4, 2026
0dcd7ef
lint
cyfung1031 Feb 4, 2026
360f6d4
调整代码 - updateResource & createResourceByUrlFetch
cyfung1031 Feb 4, 2026
89d7a0f
统一 try catch 在 updateResource 里进行
cyfung1031 Feb 4, 2026
05e54f7
修正 Semaphore 相关代码
cyfung1031 Feb 4, 2026
b0f9318
把并行控制的代码移动至 concurrency-control.ts
cyfung1031 Feb 4, 2026
12a5b67
Merge branch 'release/v1.3' into pr-updateResourceByType-return
cyfung1031 Feb 8, 2026
14a267f
Merge branch 'release/v1.3' into pr-updateResourceByType-return
cyfung1031 Feb 24, 2026
72aa9d5
Merge branch 'release/v1.4' into pr-updateResourceByType-return
cyfung1031 Mar 15, 2026
1b6a234
Merge branch 'release/v1.4' into pr-updateResourceByType-return
cyfung1031 Mar 21, 2026
8bd97d8
Merge branch 'release/v1.4' into pr-updateResourceByType-return
cyfung1031 Mar 22, 2026
12cfcf3
Merge remote-tracking branch 'origin/release/v1.4' into pr-updateReso…
CodFrm Mar 29, 2026
59dcad3
Update src/app/service/service_worker/resource.ts
CodFrm Mar 29, 2026
e49f730
Update src/app/service/service_worker/utils.ts
CodFrm Mar 29, 2026
0b1ded9
♻️ 优化资源加载代码:提取魔法数字、清理疑问注释、添加并发控制单元测试
CodFrm Mar 29, 2026
1fa3981
改回注释
cyfung1031 Mar 29, 2026
2cfd75c
Merge branch 'release/v1.4' into pr-updateResourceByType-return
cyfung1031 Apr 4, 2026
d5e7815
Merge branch 'release/v1.4' into pr/1193
cyfung1031 Apr 11, 2026
39b2e4a
按AI指示语意更新
cyfung1031 Apr 11, 2026
5b81d89
根据AI意见修正
cyfung1031 Apr 11, 2026
147e33a
fix
cyfung1031 Apr 11, 2026
6c96ef7
修正 Semaphore
cyfung1031 Apr 11, 2026
a69f1ec
fix unit test
cyfung1031 Apr 11, 2026
3509a74
Merge branch 'release/v1.4' into pr/1193
cyfung1031 Apr 23, 2026
57a30cd
unit test fix
cyfung1031 Apr 23, 2026
a6f8083
Commits 整合
cyfung1031 Apr 23, 2026
ea04cce
Merge branch 'pr-updateResourceByType-return' of https://github.com/c…
cyfung1031 Apr 23, 2026
4010471
Merge branch 'release/v1.4' into pr/1193
cyfung1031 May 1, 2026
17c87af
Merge branch 'release/v1.4' into pr/1193
cyfung1031 Jun 1, 2026
e263b4a
✅ add failing tests for resource concurrency regressions
CodFrm Jun 10, 2026
11c6538
Merge remote-tracking branch 'origin/release/v1.4' into pr-updateReso…
CodFrm Jun 10, 2026
23578db
🐛 修复资源更新缓存与并发控制
CodFrm Jun 11, 2026
ba2a084
🐛 修复资源 TTL 命中时未登记 link 导致共享资源被误删
CodFrm Jun 11, 2026
c3a0734
✅ 修复 unwrap e2e 测试与并发测试的全局状态污染
CodFrm Jun 11, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions e2e/gm-api.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -177,7 +177,7 @@ function patchTargetMatchCode(code: string, targetUrl: string): string {
const url = new URL(targetUrl);
const targetPattern = `${url.protocol}//${url.hostname}/*${url.search}`;
return code.replace(
/^\/\/\s*@match\s+.*\?(gm_api_sync|gm_api_async|inject_content|WINDOW_MESSAGE_TEST_SC|SANDBOX_TEST_SC)$/gm,
/^\/\/\s*@match\s+.*\?(gm_api_sync|gm_api_async|inject_content|WINDOW_MESSAGE_TEST_SC|SANDBOX_TEST_SC|unwrap_e2e_test)$/gm,
`// @match ${targetPattern}`
);
}
Expand Down Expand Up @@ -366,7 +366,7 @@ test.describe("GM API", () => {
context,
extensionId,
"unwrap_e2e_test.js",
TARGET_URL,
`${gmApiMockServer.cspOrigin}/?unwrap_e2e_test`,
60_000
);

Expand Down
2 changes: 1 addition & 1 deletion example/tests/unwrap_e2e_test.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
// @version 1.0.0
// @description E2E 测试 @unwrap 功能
// @author ScriptCat
// @match https://content-security-policy.com/*
// @match https://content-security-policy.com/?unwrap_e2e_test
// @grant GM_setValue
// @unwrap
// ==/UserScript==
Expand Down
2 changes: 1 addition & 1 deletion src/app/repo/resource.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ export interface Resource {
hash: ResourceHash;
type: ResourceType;
link: { [key: string]: boolean }; // 关联的脚本
contentType: string;
contentType: string; // 下载成功的话必定有 contentType. 下载失败的话则没有 (空Resource)
createtime: number;
updatetime?: number;
}
Expand Down
140 changes: 134 additions & 6 deletions src/app/service/service_worker/resource.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,10 @@ import { ResourceService } from "./resource";
import { vi, describe, it, expect, beforeEach } from "vitest";
import type { Group } from "@Packages/message/server";
import type { IMessageQueue } from "@Packages/message/message_queue";
import { parseUrlSRI } from "./utils";
import type { Script } from "@App/app/repo/scripts";
import { SCRIPT_RUN_STATUS_COMPLETE, SCRIPT_STATUS_ENABLE, SCRIPT_TYPE_NORMAL } from "@App/app/repo/scripts";
import type { Resource } from "@App/app/repo/resource";

initTestEnv();

Expand All @@ -27,7 +31,42 @@ function mockResponse(blob: Blob, status = 200, contentType?: string) {
} as unknown as Response;
}

describe("ResourceService - loadByUrl", () => {
function normalScript(uuid: string, metadata: Script["metadata"]): Script {
return {
uuid,
name: uuid,
namespace: "test",
metadata,
type: SCRIPT_TYPE_NORMAL,
status: SCRIPT_STATUS_ENABLE,
sort: 0,
runStatus: SCRIPT_RUN_STATUS_COMPLETE,
createtime: Date.now(),
checktime: 0,
} as Script;
}

function resourceModel(url: string, content: string, updatetime = Date.now()): Resource {
return {
url,
content,
contentType: "text/plain",
hash: {
md5: "mock-md5",
sha1: "",
sha256: "",
sha384: "",
sha512: "",
},
base64: btoa(content),
link: { "old-script": true },
type: "resource",
createtime: updatetime,
updatetime,
};
}

describe("ResourceService - createResourceByUrlFetch", () => {
let service: ResourceService;

beforeEach(() => {
Expand All @@ -49,20 +88,21 @@ describe("ResourceService - loadByUrl", () => {
const jsCode = "console.log('hello');";
mockFetch.mockResolvedValue(mockResponse(textBlob(jsCode), 200, "application/javascript; charset=utf-8"));

const res = await service.loadByUrl("https://example.com/lib.js", "require");
const res = await service.createResourceByUrlFetch(parseUrlSRI("https://example.com/lib.js"), "require");

expect(res.url).toBe("https://example.com/lib.js");
expect(res.content).toBeTruthy();
expect(res.contentType).toBe("application/javascript");
expect(res.base64).toBeTruthy();
expect(res.type).toBe("require");
expect(res.updatetime).toEqual(expect.any(Number));
});

it("加载文本资源(resource)时应通过 blob.text() 设置 content", async () => {
const text = "plain text content";
mockFetch.mockResolvedValue(mockResponse(textBlob(text), 200, "text/plain"));

const res = await service.loadByUrl("https://example.com/data.txt", "resource");
const res = await service.createResourceByUrlFetch(parseUrlSRI("https://example.com/data.txt"), "resource");

expect(res.content).toBe(text);
expect(res.type).toBe("resource");
Expand All @@ -73,7 +113,7 @@ describe("ResourceService - loadByUrl", () => {
const bytes = [0x89, 0x50, 0x4e, 0x47, 0x00, 0x00, 0x00, 0x00];
mockFetch.mockResolvedValue(mockResponse(binaryBlob(bytes), 200, "image/png"));

const res = await service.loadByUrl("https://example.com/img.png", "resource");
const res = await service.createResourceByUrlFetch(parseUrlSRI("https://example.com/img.png"), "resource");

expect(res.content).toBe("");
expect(res.base64).toBeTruthy();
Expand All @@ -83,16 +123,104 @@ describe("ResourceService - loadByUrl", () => {
it("响应非200时应抛出异常", async () => {
mockFetch.mockResolvedValue(mockResponse(textBlob(""), 404));

await expect(service.loadByUrl("https://example.com/404", "require")).rejects.toThrow(
await expect(service.createResourceByUrlFetch(parseUrlSRI("https://example.com/404"), "require")).rejects.toThrow(
"resource response status not 200: 404"
);
});

it("没有 content-type 时应默认为 application/octet-stream", async () => {
mockFetch.mockResolvedValue(mockResponse(textBlob("data"), 200));

const res = await service.loadByUrl("https://example.com/noct", "resource");
const res = await service.createResourceByUrlFetch(parseUrlSRI("https://example.com/noct"), "resource");

expect(res.contentType).toBe("application/octet-stream");
});

it("已下载成功的远程资源在24小时内不应重复 fetch", async () => {
const url = "https://example.com/cache-ttl.js";
const script = normalScript("resource-cache-ttl-test", { require: [url] });

mockFetch.mockResolvedValue(mockResponse(textBlob("console.log('cache');"), 200, "application/javascript"));

await service.updateResourceByTypes(script, ["require"]);
expect(mockFetch).toHaveBeenCalledTimes(1);

mockFetch.mockClear();
await service.updateResourceByTypes(script, ["require"]);

expect(mockFetch).not.toHaveBeenCalled();
});

it("多个脚本复用同一 URL 时, TTL 命中也应登记当前脚本的 link", async () => {
const url = "https://example.com/shared-lib.js";
const scriptA = normalScript("shared-script-a", { require: [url] });
const scriptB = normalScript("shared-script-b", { require: [url] });

mockFetch.mockResolvedValue(mockResponse(textBlob("console.log('shared');"), 200, "application/javascript"));

// A 安装:实际下载并登记 link
await service.updateResourceByTypes(scriptA, ["require"]);
expect(mockFetch).toHaveBeenCalledTimes(1);

// B 安装:24 小时内 TTL 命中,不应重新 fetch
mockFetch.mockClear();
await service.updateResourceByTypes(scriptB, ["require"]);
expect(mockFetch).not.toHaveBeenCalled();

// 但 B 仍应被登记到该资源的 link,否则删除 A 时会误删仍被 B 使用的资源
const stored = await service.resourceDAO.get(url);
expect(stored?.link).toMatchObject({
"shared-script-a": true,
"shared-script-b": true,
});
});

it("已过期的远程资源应重新 fetch 并更新内容", async () => {
const url = "https://example.com/expired.css";
const script = normalScript("resource-expired-test", { resource: [`expired ${url}`] });
const oldResource = resourceModel(url, "old", Date.now() - 86_400_000 - 1000);
vi.spyOn(service, "getResourceModel").mockResolvedValue(oldResource);
const updateResource = vi.spyOn(service, "updateResource").mockResolvedValue({
...oldResource,
content: "new",
updatetime: Date.now(),
});

await service.updateResourceByTypes(script, ["resource"]);

expect(updateResource).toHaveBeenCalledWith(script.uuid, expect.objectContaining({ url }), "resource", oldResource);
});

it("file 协议资源即使未过期也应尝试更新", async () => {
const url = "file:///tmp/scriptcat-resource.txt";
const script = normalScript("resource-file-test", { resource: [`localFile ${url}`] });
const oldResource = resourceModel(url, "old");
vi.spyOn(service, "getResourceModel").mockResolvedValue(oldResource);
const updateResource = vi.spyOn(service, "updateResource").mockResolvedValue({
...oldResource,
content: "new",
updatetime: Date.now(),
});

await service.updateResourceByTypes(script, ["resource"]);

expect(updateResource).toHaveBeenCalledWith(script.uuid, expect.objectContaining({ url }), "resource", oldResource);
});

it("已有旧资源时下载失败应返回旧资源", async () => {
const url = "https://example.com/fallback.css";
const oldResource = resourceModel(url, "old-content");
vi.spyOn(service, "createResourceByUrlFetch").mockRejectedValue(new Error("network failed"));
const consoleError = vi.spyOn(console, "error").mockImplementation(() => {});
const loggerError = vi.spyOn(service.logger, "error").mockImplementation(() => {});

try {
const res = await service.updateResource("resource-fallback-test", parseUrlSRI(url), "resource", oldResource);

expect(res).toBe(oldResource);
} finally {
loggerError.mockRestore();
consoleError.mockRestore();
}
});
});
Loading
Loading