Skip to content

Commit a9383d2

Browse files
CodFrmCopilotCopilot
authored
🐛 修复云同步问题 (#1133)
* 优化同步逻辑 * 文件系统操作限制速率 * 修复同步状态同步问题 * 修复排序顺序问题 #964 * Update packages/filesystem/limiter.ts Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> * Update packages/filesystem/limiter.ts Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> * Update packages/filesystem/factory.ts Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> * 对文件系统操作应用速率限制 Apply rate limiting to filesystem operations (#1134) * Initial plan * Apply rate limiting to open, openDir and create operations Co-authored-by: CodFrm <22783163+CodFrm@users.noreply.github.com> * Merge fix/cloud-sync and apply rate limiting to open() method Co-authored-by: CodFrm <22783163+CodFrm@users.noreply.github.com> --------- Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Co-authored-by: CodFrm <22783163+CodFrm@users.noreply.github.com> --------- Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> Co-authored-by: Copilot <198982749+Copilot@users.noreply.github.com>
1 parent bea0192 commit a9383d2

3 files changed

Lines changed: 191 additions & 59 deletions

File tree

packages/filesystem/factory.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import DropboxFileSystem from "./dropbox/dropbox";
66
import WebDAVFileSystem from "./webdav/webdav";
77
import ZipFileSystem from "./zip/zip";
88
import { t } from "@App/locales/locales";
9+
import LimiterFileSystem from "./limiter";
910

1011
export type FileSystemType = "zip" | "webdav" | "baidu-netdsik" | "onedrive" | "googledrive" | "dropbox";
1112

@@ -42,7 +43,8 @@ export default class FileSystemFactory {
4243
default:
4344
throw new Error("not found filesystem");
4445
}
45-
return fs.verify().then(() => fs);
46+
const limitedFs = new LimiterFileSystem(fs);
47+
return limitedFs.verify().then(() => limitedFs);
4648
}
4749

4850
static params(): { [key: string]: FileSystemParams } {

packages/filesystem/limiter.ts

Lines changed: 102 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,102 @@
1+
import type FileSystem from "./filesystem";
2+
import type { File, FileReader, FileWriter } from "./filesystem";
3+
4+
/**
5+
* 速率限制器
6+
* 控制并发操作数量,防止过多并发请求
7+
*/
8+
export class RateLimiter {
9+
private queue: Array<() => void> = [];
10+
11+
private running = 0;
12+
13+
private maxConcurrent: number;
14+
15+
constructor(maxConcurrent = 5) {
16+
this.maxConcurrent = maxConcurrent;
17+
}
18+
19+
/**
20+
* 执行限速操作
21+
* @param fn 要执行的操作函数
22+
* @returns 操作结果
23+
*/
24+
async execute<T>(fn: () => Promise<T>): Promise<T> {
25+
// 如果当前运行的操作数已达到上限,则等待
26+
while (this.running >= this.maxConcurrent) {
27+
await new Promise<void>((resolve) => {
28+
this.queue.push(resolve);
29+
});
30+
}
31+
32+
this.running++;
33+
try {
34+
return await fn();
35+
} finally {
36+
this.running--;
37+
// 执行完成后,从队列中取出下一个等待的操作
38+
const next = this.queue.shift();
39+
if (next) {
40+
next();
41+
}
42+
}
43+
}
44+
}
45+
46+
// 文件系统限速器,防止并发请求过多达到服务器限制
47+
// 也防止上传/下载带宽占用过多超时导致失败/数据不全的问题
48+
export default class LimiterFileSystem implements FileSystem {
49+
private fs: FileSystem;
50+
51+
private limiter: RateLimiter;
52+
53+
constructor(fs: FileSystem, limiter?: RateLimiter) {
54+
this.fs = fs;
55+
this.limiter = limiter || new RateLimiter();
56+
}
57+
58+
verify(): Promise<void> {
59+
return this.limiter.execute(() => this.fs.verify());
60+
}
61+
62+
async open(file: File): Promise<FileReader> {
63+
return this.limiter.execute(async () => {
64+
const reader = await this.fs.open(file);
65+
return {
66+
read: (type) => this.limiter.execute(() => reader.read(type)),
67+
};
68+
});
69+
}
70+
71+
async openDir(path: string): Promise<FileSystem> {
72+
return this.limiter.execute(async () => {
73+
const fs = await this.fs.openDir(path);
74+
return new LimiterFileSystem(fs, this.limiter);
75+
});
76+
}
77+
78+
async create(path: string): Promise<FileWriter> {
79+
return this.limiter.execute(async () => {
80+
const writer = await this.fs.create(path);
81+
return {
82+
write: (content) => this.limiter.execute(() => writer.write(content)),
83+
};
84+
});
85+
}
86+
87+
createDir(dir: string): Promise<void> {
88+
return this.limiter.execute(() => this.fs.createDir(dir));
89+
}
90+
91+
delete(path: string): Promise<void> {
92+
return this.limiter.execute(() => this.fs.delete(path));
93+
}
94+
95+
list(): Promise<File[]> {
96+
return this.limiter.execute(() => this.fs.list());
97+
}
98+
99+
getDirUrl(): Promise<string> {
100+
return this.limiter.execute(() => this.fs.getDirUrl());
101+
}
102+
}

src/app/service/service_worker/synchronize.ts

Lines changed: 86 additions & 58 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import LoggerCore from "@App/app/logger/core";
22
import Logger from "@App/app/logger/logger";
33
import type { Resource } from "@App/app/repo/resource";
4-
import { type Script, SCRIPT_STATUS_ENABLE, type ScriptDAO } from "@App/app/repo/scripts";
4+
import { type Script, SCRIPT_STATUS_DISABLE, SCRIPT_STATUS_ENABLE, type ScriptDAO } from "@App/app/repo/scripts";
55
import BackupExport from "@App/pkg/backup/export";
66
import type { BackupData, ResourceBackup, ScriptBackupData, ScriptOptions, ValueStorage } from "@App/pkg/backup/struct";
77
import type { File } from "@Packages/filesystem/filesystem";
@@ -46,23 +46,25 @@ type ScriptcatSync = {
4646
version: string; // 脚本猫版本
4747
status: {
4848
scripts: {
49-
[key: string]: {
50-
enable: boolean;
51-
sort: number;
52-
updatetime: number; // 更新时间
53-
};
49+
[key: string]: ScriptcatSyncStatus | undefined;
5450
};
5551
};
5652
};
5753

54+
type ScriptcatSyncStatus = {
55+
enable: boolean;
56+
sort: number;
57+
updatetime: number; // 更新时间
58+
};
59+
5860
type PushScriptParam = TInstallScriptParams;
5961

6062
export class SynchronizeService {
6163
logger: Logger;
6264

6365
scriptCodeDAO = this.scriptDAO.scriptCodeDAO;
6466

65-
storage: ChromeStorage = new ChromeStorage("sync", true);
67+
storage: ChromeStorage = new ChromeStorage("sync", false);
6668

6769
constructor(
6870
private msgSender: MessageSend,
@@ -340,13 +342,6 @@ export class SynchronizeService {
340342
[key: string]: string;
341343
}) || {};
342344

343-
let scriptcatSync = {
344-
version: ExtVersion,
345-
status: {
346-
scripts: {},
347-
},
348-
} as ScriptcatSync;
349-
350345
for (const file of list) {
351346
if (file.name.endsWith(".user.js")) {
352347
const uuid = file.name.substring(0, file.name.length - 8);
@@ -374,8 +369,25 @@ export class SynchronizeService {
374369
scriptList.forEach((script) => {
375370
scriptMap.set(script.uuid, script);
376371
});
372+
373+
// 判断文件系统是否有脚本猫同步文件
374+
const file = list.find((file) => file.name === "scriptcat-sync.json");
375+
const scriptcatSync = {
376+
version: ExtVersion,
377+
status: {
378+
scripts: {},
379+
},
380+
} as ScriptcatSync;
381+
let cloudStatus: ScriptcatSync["status"]["scripts"] = {};
382+
if (file) {
383+
// 如果有,则读取文件内容
384+
const cloudScriptCatSync = JSON.parse(await fs.open(file).then((f) => f.read("string"))) as ScriptcatSync;
385+
cloudStatus = cloudScriptCatSync.status.scripts;
386+
}
387+
377388
// 对比脚本列表和文件列表,进行同步
378389
const result: Promise<void>[] = [];
390+
const updateScript: Map<string, boolean> = new Map();
379391
uuidMap.forEach((file, uuid) => {
380392
const script = scriptMap.get(uuid);
381393
if (script) {
@@ -421,14 +433,16 @@ export class SynchronizeService {
421433
result.push(this.pushScript(fs, script));
422434
} else {
423435
// 如果脚本更新时间小于文件更新时间,则更新脚本
424-
result.push(this.pullScript(fs, file as SyncFiles, script));
436+
updateScript.set(uuid, true);
437+
result.push(this.pullScript(fs, file as SyncFiles, cloudStatus[uuid], script));
425438
}
426439
scriptMap.delete(uuid);
427440
return;
428441
}
429442
// 如果脚本不存在,且文件存在,则安装脚本
430443
if (file.script) {
431-
result.push(this.pullScript(fs, file as SyncFiles));
444+
updateScript.set(uuid, true);
445+
result.push(this.pullScript(fs, file as SyncFiles, cloudStatus[uuid]));
432446
}
433447
});
434448
// 上传剩下的脚本
@@ -439,51 +453,53 @@ export class SynchronizeService {
439453
await Promise.allSettled(result);
440454
// 同步状态
441455
if (syncConfig.syncStatus) {
442-
// 判断文件系统是否有脚本猫同步文件
443-
const file = list.find((file) => file.name === "scriptcat-sync.json");
444-
if (file) {
445-
// 如果有,则读取文件内容
446-
scriptcatSync = JSON.parse(await fs.open(file).then((f) => f.read("string"))) as ScriptcatSync;
447-
}
448456
const scriptlist = await this.scriptDAO.all();
449-
const status = scriptcatSync.status.scripts;
450-
scriptlist.forEach(async (script) => {
451-
// 判断云端状态是否与本地状态一致
452-
if (!status[script.uuid]) {
453-
status[script.uuid] = {
454-
enable: script.status === SCRIPT_STATUS_ENABLE,
455-
sort: script.sort,
456-
updatetime: script.updatetime || script.createtime,
457-
};
458-
} else {
459-
// 判断时间
460-
if (script.updatetime) {
457+
await Promise.allSettled(
458+
scriptlist.map(async (script) => {
459+
// 判断云端状态是否与本地状态一致
460+
const status = cloudStatus[script.uuid];
461+
const updatetime = script.updatetime || script.createtime;
462+
if (!status) {
463+
scriptcatSync.status.scripts[script.uuid] = {
464+
enable: script.status === SCRIPT_STATUS_ENABLE,
465+
sort: script.sort,
466+
updatetime: updatetime,
467+
};
468+
} else {
469+
if (updateScript.has(script.uuid)) {
470+
// 脚本已经更新过了,跳过状态同步
471+
scriptcatSync.status.scripts[script.uuid] = status;
472+
return;
473+
}
474+
// 判断时间
461475
// 如果云端状态的更新时间小于本地状态的更新时间,则更新云端状态
462-
if (status[script.uuid].updatetime < script.updatetime) {
463-
status[script.uuid].enable = script.status === SCRIPT_STATUS_ENABLE;
464-
status[script.uuid].sort = script.sort;
465-
status[script.uuid].updatetime = script.updatetime;
476+
if (status.updatetime < updatetime) {
477+
scriptcatSync.status.scripts[script.uuid] = {
478+
enable: script.status === SCRIPT_STATUS_ENABLE,
479+
sort: script.sort,
480+
updatetime: updatetime,
481+
};
466482
return;
467483
}
484+
// 否则采用云端状态
485+
scriptcatSync.status.scripts[script.uuid] = status;
486+
// 脚本顺序
487+
if (status.sort !== script.sort) {
488+
await this.scriptDAO.update(script.uuid, {
489+
sort: status.sort,
490+
});
491+
}
492+
// 脚本状态
493+
if (status.enable !== (script.status === SCRIPT_STATUS_ENABLE)) {
494+
// 开启脚本
495+
await this.script.enableScript({
496+
uuid: script.uuid,
497+
enable: status.enable,
498+
});
499+
}
468500
}
469-
// 否则采用云端状态
470-
// 脚本顺序
471-
if (status[script.uuid].sort !== script.sort) {
472-
await this.scriptDAO.update(script.uuid, {
473-
sort: status[script.uuid].sort,
474-
updatetime: Date.now(),
475-
});
476-
}
477-
// 脚本状态
478-
if (status[script.uuid].enable !== (script.status === SCRIPT_STATUS_ENABLE)) {
479-
// 开启脚本
480-
this.script.enableScript({
481-
uuid: script.uuid,
482-
enable: status[script.uuid].enable,
483-
});
484-
}
485-
}
486-
});
501+
})
502+
);
487503
// 保存脚本猫同步状态
488504
const syncFile = await fs.create("scriptcat-sync.json");
489505
await syncFile.write(JSON.stringify(scriptcatSync, null, 2));
@@ -568,7 +584,7 @@ export class SynchronizeService {
568584
return;
569585
}
570586

571-
async pullScript(fs: FileSystem, file: SyncFiles, existingScript?: Script) {
587+
async pullScript(fs: FileSystem, file: SyncFiles, status: ScriptcatSyncStatus | undefined, existingScript?: Script) {
572588
const logger = this.logger.with({
573589
uuid: existingScript?.uuid || "",
574590
name: existingScript?.name || "",
@@ -588,6 +604,19 @@ export class SynchronizeService {
588604
existingScript?.uuid || metaObj.uuid
589605
);
590606
script.origin = script.origin || metaObj.origin;
607+
if (status) {
608+
if (existingScript) {
609+
if (!existingScript.updatetime || status.updatetime > existingScript.updatetime) {
610+
// 如果云端状态的更新时间大于本地状态的更新时间,则采用云端状态
611+
script.sort = status.sort;
612+
script.status = status.enable ? SCRIPT_STATUS_ENABLE : SCRIPT_STATUS_DISABLE;
613+
}
614+
} else {
615+
// 新安装的脚本采用云端状态
616+
script.sort = status.sort;
617+
script.status = status.enable ? SCRIPT_STATUS_ENABLE : SCRIPT_STATUS_DISABLE;
618+
}
619+
}
591620
this.script.installScript({
592621
script,
593622
code,
@@ -597,7 +626,6 @@ export class SynchronizeService {
597626
} catch (e) {
598627
logger.error("pull script error", Logger.E(e));
599628
}
600-
return;
601629
}
602630

603631
cloudSyncConfigChange(value: CloudSyncConfig) {

0 commit comments

Comments
 (0)