Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
159 changes: 159 additions & 0 deletions src/pkg/config/config.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,159 @@
import { describe, expect, it, beforeEach } from "vitest";
import { SystemConfig } from "./config";
import { MessageQueue } from "@Packages/message/message_queue";

describe("SystemConfig 双 storage 与懒迁移", () => {
let mq: MessageQueue;
let config: SystemConfig;

beforeEach(() => {
// 清空 storage 数据
chrome.storage.sync.clear();
chrome.storage.local.clear();
mq = new MessageQueue();
config = new SystemConfig(mq);
});

describe("local key 读写", () => {
it("cloud_sync 应写入 local storage 而非 sync", async () => {
const cloudSync = {
enable: true,
syncDelete: false,
syncStatus: true,
filesystem: "onedrive" as const,
params: { token: "test" },
};
config.setCloudSync(cloudSync);

const result = await config.getCloudSync();
expect(result).toEqual(cloudSync);

// 验证值在 local 中
const localData = await chrome.storage.local.get("system_cloud_sync");
expect(localData["system_cloud_sync"]).toEqual(cloudSync);

// 验证值不在 sync 中
const syncData = await chrome.storage.sync.get("system_cloud_sync");
expect(syncData["system_cloud_sync"]).toBeUndefined();
});

it("language 应写入 local storage", async () => {
config.setLanguage("zh-CN");

// 通过 storage 验证写入位置
const localData = await chrome.storage.local.get("system_language");
expect(localData["system_language"]).toBe("zh-CN");

const syncData = await chrome.storage.sync.get("system_language");
expect(syncData["system_language"]).toBeUndefined();
});

it("vscode_url 应写入 local storage", async () => {
config.setVscodeUrl("ws://localhost:9999");

const localData = await chrome.storage.local.get("system_vscode_url");
expect(localData["system_vscode_url"]).toBe("ws://localhost:9999");

const syncData = await chrome.storage.sync.get("system_vscode_url");
expect(syncData["system_vscode_url"]).toBeUndefined();
});

it("enable_script 应写入 local storage", async () => {
config.setEnableScript(false);

const localData = await chrome.storage.local.get("system_enable_script");
expect(localData["system_enable_script"]).toBe(false);

const syncData = await chrome.storage.sync.get("system_enable_script");
expect(syncData["system_enable_script"]).toBeUndefined();
});
});

describe("sync key 读写", () => {
it("check_script_update_cycle 应写入 sync storage", async () => {
config.setCheckScriptUpdateCycle(3600);

const syncData = await chrome.storage.sync.get("system_check_script_update_cycle");
expect(syncData["system_check_script_update_cycle"]).toBe(3600);

const localData = await chrome.storage.local.get("system_check_script_update_cycle");
expect(localData["system_check_script_update_cycle"]).toBeUndefined();
});

it("enable_eslint 应写入 sync storage", async () => {
config.setEnableEslint(false);

const syncData = await chrome.storage.sync.get("system_enable_eslint");
expect(syncData["system_enable_eslint"]).toBe(false);

const localData = await chrome.storage.local.get("system_enable_eslint");
expect(localData["system_enable_eslint"]).toBeUndefined();
});
});

describe("懒迁移:sync → local", () => {
it("local key 的旧数据应从 sync 迁移到 local", async () => {
// 模拟旧版本数据在 sync 中
const oldCloudSync = {
enable: true,
syncDelete: false,
syncStatus: true,
filesystem: "webdav" as const,
params: { url: "https://example.com" },
};
await chrome.storage.sync.set({ system_cloud_sync: oldCloudSync });

// 读取时应自动迁移
const result = await config.getCloudSync();
expect(result).toEqual(oldCloudSync);

// 验证已迁移到 local
const localData = await chrome.storage.local.get("system_cloud_sync");
expect(localData["system_cloud_sync"]).toEqual(oldCloudSync);

// 验证已从 sync 中删除
const syncData = await chrome.storage.sync.get("system_cloud_sync");
expect(syncData["system_cloud_sync"]).toBeUndefined();
});

it("local 有值时不应回退到 sync", async () => {
const localValue = "zh-CN";
const syncValue = "en-US";
await chrome.storage.local.set({ system_language: localValue });
await chrome.storage.sync.set({ system_language: syncValue });

const result = await config.getLanguage();
expect(result).toBe(localValue);

// sync 中的值不应被删除(因为 local 有值,不触发迁移)
const syncData = await chrome.storage.sync.get("system_language");
expect(syncData["system_language"]).toBe(syncValue);
});

it("sync 和 local 都没有值时返回默认值", async () => {
const result = await config.getCloudSync();
expect(result).toEqual({
enable: false,
syncDelete: false,
syncStatus: true,
filesystem: "webdav",
params: {},
});
});

it("迁移后再次读取应走缓存", async () => {
await chrome.storage.sync.set({ system_vscode_url: "ws://old:8642" });

// 第一次读取触发迁移
const first = await config.getVscodeUrl();
expect(first).toBe("ws://old:8642");

// 修改 local storage(模拟外部写入),验证缓存生效
await chrome.storage.local.set({ system_vscode_url: "ws://new:9999" });

// 第二次读取应返回缓存值
const second = await config.getVscodeUrl();
expect(second).toBe("ws://old:8642");
});
});
});
83 changes: 59 additions & 24 deletions src/pkg/config/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import { ExtVersion } from "@App/app/const";
import defaultTypeDefinition from "@App/template/scriptcat.d.tpl";
import { toCamelCase } from "../utils/utils";
import EventEmitter from "eventemitter3";
import { STORAGE_LOCAL_KEYS } from "./consts";

export const SystemConfigChange = "systemConfigChange";

Expand Down Expand Up @@ -73,7 +74,19 @@ export type SystemConfigValueType<K extends SystemConfigKey> =
export class SystemConfig {
private readonly cache = new Map<string, any>();

private readonly storage = new ChromeStorage("system", true);
// 跨设备同步的配置项,使用 chrome.storage.sync
private readonly syncStorage = new ChromeStorage("system", true);
// 设备相关的配置项,使用 chrome.storage.local(不跨设备同步)
private readonly localStorage = new ChromeStorage("system", false);

private isLocalKey(key: string): boolean {
return STORAGE_LOCAL_KEYS.has(key);
}

// 获取 key 对应的主 storage
private getStorage(key: string): ChromeStorage {
return this.isLocalKey(key) ? this.localStorage : this.syncStorage;
}

private EE: EventEmitter<string> = new EventEmitter<string>();

Expand Down Expand Up @@ -108,21 +121,47 @@ export class SystemConfig {
return this.addListener(key, callback);
}

private resolveDefault<T>(defaultValue: WithAsyncValue<Exclude<T, undefined>>): T | Promise<T> {
//@ts-ignore
return (defaultValue?.asyncValue?.() || defaultValue) as T | Promise<T>;
}

private async transferSyncToLocal<T>(
key: SystemConfigKey,
defaultValue: WithAsyncValue<Exclude<T, undefined>>
): Promise<T> {
const syncVal = await this.syncStorage.get(key);
if (syncVal === undefined) {
this.cache.set(key, undefined);
return this.resolveDefault<T>(defaultValue);
}
// 迁移到 local storage 并从 sync 中删除
await this.syncStorage.remove(key); // 先删除
await this.localStorage.set(key, syncVal); // 删除成功后储回本地
this.cache.set(key, syncVal);
return syncVal as T;
}

private _get<T extends string | number | boolean | object>(
key: SystemConfigKey,
defaultValue: WithAsyncValue<Exclude<T, undefined>>
): Promise<T> {
if (this.cache.has(key)) {
let val = this.cache.get(key);
//@ts-ignore
val = (val === undefined ? defaultValue?.asyncValue?.() || defaultValue : val) as T | Promise<T>;
return Promise.resolve(val);
const val = this.cache.get(key);
return Promise.resolve(val === undefined ? this.resolveDefault<T>(defaultValue) : (val as T));
}
return this.storage.get(key).then((val) => {
const storage = this.getStorage(key);
return storage.get(key).then((val) => {
if (val !== undefined) {
this.cache.set(key, val);
return val as T;
}
// 对 local key,回退读取 sync storage(兼容旧版本数据迁移)
if (this.isLocalKey(key)) {
return this.transferSyncToLocal<T>(key, defaultValue);
}
this.cache.set(key, val);
//@ts-ignore
val = (val === undefined ? defaultValue?.asyncValue?.() || defaultValue : val) as T | Promise<T>;
return val;
return this.resolveDefault<T>(defaultValue);
});
}

Expand Down Expand Up @@ -150,29 +189,25 @@ export class SystemConfig {

private _set<T extends SystemConfigKey>(key: T, value: SystemConfigValueType<T> | undefined) {
const prev = this.cache.get(key);
const storage = this.getStorage(key);
let asyncOp;
if (value === undefined) {
this.cache.delete(key);
this.storage.remove(key);
asyncOp = storage.remove(key);
} else {
this.cache.set(key, value);
this.storage.set(key, value);
asyncOp = storage.set(key, value);
}
// 发送消息通知更新
this.mq.publish<TKeyValue<T>>(SystemConfigChange, {
key,
value,
prev,
asyncOp.then(() => {
// 发送消息通知更新
this.mq.publish<TKeyValue<T>>(SystemConfigChange, {
key,
value,
prev,
});
});
}

public getChangetime() {
return this._get<number>("changetime", 0);
}

public setChangetime(n: number) {
this._set("changetime", n);
}

defaultCheckScriptUpdateCycle() {
return 86400;
}
Expand Down
14 changes: 14 additions & 0 deletions src/pkg/config/consts.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
// 设备相关的配置项,存储在 chrome.storage.local 而非 sync
// 这些配置不应跨设备同步(如云同步认证、VSCode 连接、UI 布局等)
export const STORAGE_LOCAL_KEYS: Set<string> = new Set([
"cloud_sync", // 云同步配置(token 存在本地,不应跨设备同步)
"backup", // 备份配置(含设备相关 filesystem params)
"cat_file_storage", // CAT 文件存储配置
"vscode_url", // VSCode 连接地址(设备相关)
"vscode_reconnect", // VSCode 自动重连
"language", // 语言偏好(可能因设备不同)
"script_list_column_width", // UI 列宽(取决于屏幕尺寸)
"check_update", // 扩展更新通知及已读状态(各设备已读状态独立)
"enable_script", // 全局脚本开关(设备独立)
"enable_script_incognito", // 隐身模式开关(浏览器级别)
]);
Loading