From f65832411a1eaa0f6dacacbeb44818e999f6777b Mon Sep 17 00:00:00 2001 From: cyfung1031 <44498510+cyfung1031@users.noreply.github.com> Date: Sat, 15 Nov 2025 18:39:56 +0900 Subject: [PATCH] =?UTF-8?q?=E4=BF=AE=E8=AE=A2=20chrome.alarms=20=E7=9B=B8?= =?UTF-8?q?=E5=85=B3=E4=BB=A3=E7=A0=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/app/service/service_worker/alarm.test.ts | 376 ++++++++++++++++++ src/app/service/service_worker/alarm.ts | 153 +++++++ src/app/service/service_worker/index.ts | 81 ++-- src/app/service/service_worker/script.ts | 26 +- src/app/service/service_worker/subscribe.ts | 22 +- src/app/service/service_worker/synchronize.ts | 25 +- src/service_worker.ts | 2 + 7 files changed, 579 insertions(+), 106 deletions(-) create mode 100644 src/app/service/service_worker/alarm.test.ts create mode 100644 src/app/service/service_worker/alarm.ts diff --git a/src/app/service/service_worker/alarm.test.ts b/src/app/service/service_worker/alarm.test.ts new file mode 100644 index 000000000..1bb677be7 --- /dev/null +++ b/src/app/service/service_worker/alarm.test.ts @@ -0,0 +1,376 @@ +/* eslint-disable chrome-error/require-last-error-check */ +/* eslint-disable @typescript-eslint/no-unsafe-function-type */ +/** + * + * 测试方法说明: + * - 我们模拟 Chrome 扩展的 alarms/storage/runtime 接口,控制其行为并观察调用; + * - 使用 vi.useFakeTimers() + vi.setSystemTime() 锁定时间,便于验证“延迟触发/补偿执行(isFlushed)”; + * - 每个用例都以 buildChromeMock() 创建隔离的 mock,避免跨用例状态污染; + * - freshImport() 每次重新导入模块,确保模块级别的状态(例如回调登记表)在每个测试中都是“干净”的; + * - 通过 chrome.alarms.onAlarm.__trigger(...) 主动触发 onAlarm 事件,模拟浏览器实际调度; + * - 通过 storage.local.get/set/remove 记录/清理“待处理(pending)”信息,以模拟掉电/重启后的补偿执行逻辑。 + */ + +import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; + +const importTarget = "./alarm" as const; + +/** + * 重新导入模块,使模块内的单例或闭包状态被重置。 + * 为什么需要? + * - alarm.ts 很可能在模块级保存“回调登记表/监控标记”等状态; + * - 测试之间必须相互独立,否则前一个用例的注册会影响后续用例,导致误判。 + */ +async function freshImport() { + vi.resetModules(); + // eslint-disable-next-line @typescript-eslint/consistent-type-imports + return (await import(importTarget)) as typeof import("./alarm"); +} + +/** + * 构造最小可用的 Chrome API Mock:alarms / storage / runtime。 + * 关键点: + * - 允许我们注入 get/create 的返回值与 side-effect; + * - onAlarm.addListener 保存回调函数,并提供 __trigger() 手动触发; + * - storage.local 使用 Promise 风格便于 await; + * - runtime.lastError 用于模拟扩展 API 的错误通道(API 调用后读取)。 + */ +type OnAlarmListener = (alarm: any) => void; +function buildChromeMock() { + let onAlarmListener: OnAlarmListener | null = null; + + const chromeMock: any = { + alarms: { + /** + * chrome.alarms.get(name, cb) + * - 我们会在用例里 mockImplementation 注入具体返回: + * - cb(undefined) 表示不存在; + * - cb({ name, periodInMinutes }) 表示已存在; + */ + get: vi.fn(), + /** + * chrome.alarms.create(name, info, cb?) + * - 我们会在用例里检查是否被调用,以及调用参数是否正确; + * - 也可以设置 runtime.lastError 来模拟创建时的“配额错误”等情况。 + */ + create: vi.fn(), + onAlarm: { + /** + * 注册 onAlarm 监听器。我们把监听器保存到闭包变量 onAlarmListener 中, + * 稍后通过 __trigger() 主动触发它,模拟浏览器调度。 + */ + addListener: vi.fn((listener: OnAlarmListener) => { + onAlarmListener = listener; + }), + /** + * 手动触发 alarm 事件: + * - 传入形如 { name, periodInMinutes, scheduledTime } 的对象; + * - scheduledTime 用于判断是否“补偿执行”(isFlushed)。 + */ + __trigger(alarm: any) { + onAlarmListener?.(alarm); + }, + }, + }, + storage: { + local: { + // 读取/写入/删除“待处理(pending)”记录,用于 SW 重启补偿等场景 + get: vi.fn().mockResolvedValue({}), + set: vi.fn().mockResolvedValue(undefined), + remove: vi.fn().mockResolvedValue(undefined), + }, + }, + runtime: { lastError: null }, + }; + + (globalThis as any).chrome = chromeMock; + return chromeMock; +} + +let savedChrome: any; + +beforeEach(() => { + savedChrome = (global as any).chrome; + // 伪造时间:保持所有用例处在固定“当前时间”,便于判断延迟/补偿 + vi.useFakeTimers(); + vi.setSystemTime(new Date("2025-01-01T00:00:00.000Z")); +}); + +afterEach(() => { + (global as any).chrome = savedChrome; + savedChrome = undefined; +}); + +// ====================== mightCreatePeriodicAlarm ====================== +/** + * 目标: + * - 当 alarm 不存在时应创建; + * - 当存在且周期一致时不重新创建(复用); + * - 当存在但周期不同应重建; + * - get() 产生 lastError 也不影响后续创建; + * - create() 产生 lastError 仅记录,不抛错(行为仍然 resolve)。 + * + * 方法: + * - 通过 mock 的 get/create 行为与 runtime.lastError 状态,观察 mightCreatePeriodicAlarm 的返回值和副作用。 + */ + +describe("mightCreatePeriodicAlarm", () => { + it("当不存在同名 alarm 时:应创建", async () => { + const chrome = buildChromeMock(); + // 模拟 get 返回 undefined 表示“没有现存的 alarm” + chrome.alarms.get.mockImplementation((_n: string, cb: Function) => cb(undefined)); + // 模拟 create 正常调用(可选回调被安全调用) + chrome.alarms.create.mockImplementation((_n: string, _i: any, cb?: Function) => cb?.()); + + const { mightCreatePeriodicAlarm } = await freshImport(); + const result = await mightCreatePeriodicAlarm("DataSync", { periodInMinutes: 10 }); + + expect(result).toEqual({ justCreated: true }); + expect(chrome.alarms.create).toHaveBeenCalledWith("DataSync", { periodInMinutes: 10 }, expect.any(Function)); + }); + + it("当已存在且周期一致:应复用(不创建)", async () => { + const chrome = buildChromeMock(); + chrome.alarms.get.mockImplementation((_n: string, cb: Function) => cb({ name: "DataSync", periodInMinutes: 10 })); + + const { mightCreatePeriodicAlarm } = await freshImport(); + const result = await mightCreatePeriodicAlarm("DataSync", { periodInMinutes: 10 }); + + expect(result).toEqual({ justCreated: false }); + expect(chrome.alarms.create).not.toHaveBeenCalled(); + }); + + it("当已存在但周期不同:应重建", async () => { + const chrome = buildChromeMock(); + chrome.alarms.get.mockImplementation((_n: string, cb: Function) => cb({ name: "DataSync", periodInMinutes: 5 })); + chrome.alarms.create.mockImplementation((_n: string, _i: any, cb?: Function) => cb?.()); + + const { mightCreatePeriodicAlarm } = await freshImport(); + const result = await mightCreatePeriodicAlarm("DataSync", { periodInMinutes: 10 }); + + expect(result).toEqual({ justCreated: true }); + expect(chrome.alarms.create).toHaveBeenCalled(); + }); + + it("忽略 get() 的 runtime.lastError,仍应继续创建", async () => { + const chrome = buildChromeMock(); + // get() 产生 lastError,但我们仍按“未找到”处理 + chrome.alarms.get.mockImplementation((_n: string, cb: Function) => { + chrome.runtime.lastError = new Error("Some get error"); + cb(undefined); + }); + // create() 正常 + chrome.alarms.create.mockImplementation((_n: string, _i: any, cb?: Function) => { + chrome.runtime.lastError = null; + cb?.(); + }); + + const { mightCreatePeriodicAlarm } = await freshImport(); + const result = await mightCreatePeriodicAlarm("ErrAlarm", { periodInMinutes: 1 }); + + expect(result).toEqual({ justCreated: true }); + expect(chrome.alarms.create).toHaveBeenCalled(); + }); + + it("记录 create() 的 runtime.lastError,但仍 resolve", async () => { + const chrome = buildChromeMock(); + chrome.alarms.get.mockImplementation((_n: string, cb: Function) => cb(undefined)); + // create() 设置 lastError,代表“配额不足”等,但行为不抛错 + chrome.alarms.create.mockImplementation((_n: string, _i: any, cb?: Function) => { + chrome.runtime.lastError = new Error("quota exceeded"); + cb?.(); + }); + + const { mightCreatePeriodicAlarm } = await freshImport(); + const result = await mightCreatePeriodicAlarm("Quota", { periodInMinutes: 2 }); + + expect(result).toEqual({ justCreated: true }); + }); +}); + +// =========== setPeriodicAlarmCallback + monitorPeriodicAlarm =========== +/** + * 目标: + * - 注册回调后,onAlarm 触发应调用对应回调; + * - 根据 scheduledTime 与当前时间判断是否补偿执行(isFlushed); + * - 回调失败也要清理 pending; + * - monitorPeriodicAlarm 只能启动一次; + * - SW 重启后若发现“未变化的 pending”则执行补偿;若有变化则不补偿; + * - onAlarm 期间若出现 runtime.lastError,应中止处理。 + * + * 方法: + * - 通过 setPeriodicAlarmCallback(name, cb) 注册; + * - 调用 monitorPeriodicAlarm() 启动监听(内部可能有 100ms 延迟与 3s 补偿轮询); + * - __trigger(...) 触发 onAlarm; + * - 使用 fake timers 推进时间,等待内部 setTimeout; + * - 通过 storage.local 的调用轨迹确认“写入 pending / 清理 pending”。 + */ + +describe("setPeriodicAlarmCallback + monitorPeriodicAlarm", () => { + it("准时触发:应调用回调且 isFlushed=false,并完成 pending 记录/清理", async () => { + const chrome = buildChromeMock(); + const now = Date.now(); + const { setPeriodicAlarmCallback, monitorPeriodicAlarm } = await freshImport(); + + const cb = vi.fn().mockResolvedValue(undefined); + setPeriodicAlarmCallback("A1", cb); + chrome.storage.local.get.mockResolvedValue({}); // 初始无 pending + + const monitorPromise = monitorPeriodicAlarm(); // 启动监听 + + // 触发“准时”的 alarm:scheduledTime == now + chrome.alarms.onAlarm.__trigger({ name: "A1", periodInMinutes: 1, scheduledTime: now }); + + // monitor 内部会延迟 ~100ms 再执行回调;推进时间触发执行 + await vi.advanceTimersByTimeAsync(120); + await monitorPromise; + + expect(cb).toHaveBeenCalledTimes(1); + const arg = cb.mock.calls[0][0]; + expect(arg.alarm.name).toBe("A1"); + expect(arg.isFlushed).toBe(false); + expect(typeof arg.triggeredAt).toBe("number"); + + // 验证 pending 生命周期:回调前 set,完成后 remove + expect(chrome.storage.local.set).toHaveBeenCalledWith({ + "AlarmPending:A1": expect.objectContaining({ alarm: expect.any(Object) }), + }); + expect(chrome.storage.local.remove).toHaveBeenCalledWith("AlarmPending:A1"); + }); + + it("延迟≥65s:应判定为补偿执行 isFlushed=true", async () => { + const chrome = buildChromeMock(); + const base = Date.now(); + const { setPeriodicAlarmCallback, monitorPeriodicAlarm } = await freshImport(); + + const cb = vi.fn().mockResolvedValue(undefined); + setPeriodicAlarmCallback("Late", cb); + chrome.storage.local.get.mockResolvedValue({}); + + const monitorPromise = monitorPeriodicAlarm(); + + // 模拟“延迟 70s 后才触发”的 alarm:scheduledTime 比现在早 70s + chrome.alarms.onAlarm.__trigger({ name: "Late", periodInMinutes: 2, scheduledTime: base - 70_000 }); + + await vi.advanceTimersByTimeAsync(120); + await monitorPromise; + + const arg = cb.mock.calls[0][0]; + expect(arg.isFlushed).toBe(true); + }); + + it("回调抛错也必须清理 pending(保证下次不误判)", async () => { + const chrome = buildChromeMock(); + const { setPeriodicAlarmCallback, monitorPeriodicAlarm } = await freshImport(); + + const cb = vi.fn().mockRejectedValue(new Error("Callback failed")); + setPeriodicAlarmCallback("Err", cb); + chrome.storage.local.get.mockResolvedValue({}); + + const monitorPromise = monitorPeriodicAlarm(); + + chrome.alarms.onAlarm.__trigger({ name: "Err", periodInMinutes: 1, scheduledTime: Date.now() }); + + await vi.advanceTimersByTimeAsync(120); + await monitorPromise; + + expect(cb).toHaveBeenCalledTimes(1); + expect(chrome.storage.local.remove).toHaveBeenCalledWith("AlarmPending:Err"); + }); + + it("同一进程内 monitor 只能启动一次(第二次应抛错)", async () => { + buildChromeMock(); + const { monitorPeriodicAlarm } = await freshImport(); + + await monitorPeriodicAlarm(); + await expect(monitorPeriodicAlarm()).rejects.toThrow(/cannot be called twice/i); + }); + + it("SW 重启后:发现未变化的 pending -> 进行补偿执行", async () => { + const chrome = buildChromeMock(); + const { setPeriodicAlarmCallback, monitorPeriodicAlarm } = await freshImport(); + + const cb = vi.fn().mockResolvedValue(undefined); + setPeriodicAlarmCallback("Comp", cb); + + const now = Date.now(); + // 第一次扫描发现一个旧的 pending;3 秒后再次扫描,内容未变化 -> 说明回调上次未完成,应执行补偿 + const pending = { + ["AlarmPending:Comp"]: { + alarm: { name: "Comp", periodInMinutes: 1, scheduledTime: now - 90_000 }, + isFlushed: true, + triggeredAt: now - 10_000, + }, + }; + + chrome.storage.local.get.mockResolvedValueOnce(pending).mockResolvedValueOnce({ ...pending }); + + const monitorPromise = monitorPeriodicAlarm(); + + // monitor 内部约 3s 后再检查一次,推进时间触发补偿逻辑 + await vi.advanceTimersByTimeAsync(3050); + + expect(cb).toHaveBeenCalledTimes(1); + const arg = cb.mock.calls[0][0]; + // 补偿应更新触发时间为“现在”,避免重复补偿 + expect(arg.triggeredAt).toBeGreaterThanOrEqual(now); + + // 仍然遵循 pending 生命周期:set -> remove + expect(chrome.storage.local.set).toHaveBeenCalledWith({ + "AlarmPending:Comp": expect.objectContaining({ alarm: expect.any(Object) }), + }); + expect(chrome.storage.local.remove).toHaveBeenCalledWith("AlarmPending:Comp"); + + await monitorPromise; + }); + + it("SW 重启后:若 pending 在两次扫描间发生变化 -> 认为已处理/处理中,不做补偿", async () => { + const chrome = buildChromeMock(); + const { setPeriodicAlarmCallback, monitorPeriodicAlarm } = await freshImport(); + + const cb = vi.fn().mockResolvedValue(undefined); + setPeriodicAlarmCallback("NoComp", cb); + + const first = { + ["AlarmPending:NoComp"]: { + alarm: { name: "NoComp", periodInMinutes: 1, scheduledTime: Date.now() - 70_000 }, + isFlushed: true, + triggeredAt: 1000, + }, + }; + const second = { + ["AlarmPending:NoComp"]: { ...first["AlarmPending:NoComp"], triggeredAt: 2000 }, // 触发时间发生变化 + }; + + chrome.storage.local.get.mockResolvedValueOnce(first).mockResolvedValueOnce(second); + + const monitorPromise = monitorPeriodicAlarm(); + await vi.advanceTimersByTimeAsync(3020); + + expect(cb).not.toHaveBeenCalled(); + await monitorPromise; + }); + + it("onAlarm 期间若出现 runtime.lastError:本次事件应被忽略(不写入、不清理、不调用回调)", async () => { + const chrome = buildChromeMock(); + const { setPeriodicAlarmCallback, monitorPeriodicAlarm } = await freshImport(); + + const cb = vi.fn().mockResolvedValue(undefined); + setPeriodicAlarmCallback("E", cb); + chrome.storage.local.get.mockResolvedValue({}); + + const monitorPromise = monitorPeriodicAlarm(); + + // 模拟 onAlarm 回调执行前 runtime.lastError 非空,代表框架层错误 -> 应该直接返回 + chrome.runtime.lastError = new Error("onAlarm error"); + chrome.alarms.onAlarm.__trigger({ name: "E", periodInMinutes: 1, scheduledTime: Date.now() }); + chrome.runtime.lastError = null; // 复位 + + await vi.advanceTimersByTimeAsync(200); + await monitorPromise; + + expect(cb).not.toHaveBeenCalled(); + expect(chrome.storage.local.set).not.toHaveBeenCalled(); + expect(chrome.storage.local.remove).not.toHaveBeenCalled(); + }); +}); diff --git a/src/app/service/service_worker/alarm.ts b/src/app/service/service_worker/alarm.ts new file mode 100644 index 000000000..939f8807f --- /dev/null +++ b/src/app/service/service_worker/alarm.ts @@ -0,0 +1,153 @@ +import { sleep } from "@App/pkg/utils/utils"; + +/** + * @description Alarm 回调执行时的参数对象。 + * @property {chrome.alarms.Alarm} alarm 触发的 alarm 对象。 + * @property {boolean} isFlushed 是否为补偿执行(例如设备休眠或 SW 重启后延迟触发)。 + * @property {number} triggeredAt 实际触发的时间戳(毫秒)。 + */ +export type AlarmExecuteArgs = { + alarm: chrome.alarms.Alarm; + isFlushed: boolean; + triggeredAt: number; +}; + +const alarmCallbackStore = {} as Record any>; +let started = false; + +/** + * @function mightCreatePeriodicAlarm + * @description 创建(或复用)一个周期性执行的 Chrome Alarm。 + * 如果同名 alarm 不存在或周期不同,则会重新创建。 + * + * @param {string} alarmName Alarm 名称。 + * @param {chrome.alarms.AlarmCreateInfo} alarmInfo Alarm 创建配置。 + * @returns {Promise<{ justCreated: boolean }>} 返回对象标识该 Alarm 是否是新创建的。 + * + * @example + * await mightCreatePeriodicAlarm("DataSync", { periodInMinutes: 10 }); + */ +export const mightCreatePeriodicAlarm = ( + alarmName: string, + alarmInfo: chrome.alarms.AlarmCreateInfo +): Promise<{ justCreated: boolean }> => { + if (!alarmName || !alarmInfo?.periodInMinutes) throw new Error("Invalid Arguments for mightCreatePeriodicAlarm"); + // 用于创建周期性执行的 Alarm + return new Promise((resolve) => { + chrome.alarms.get(alarmName, (alarm) => { + const lastError = chrome.runtime.lastError; + if (lastError) { + console.error("chrome.runtime.lastError in chrome.alarms.get:", lastError); + // 忽略错误 + alarm = undefined; + } + + // 如果 alarm 不存在或周期不同,则创建/更新 alarm + if (!alarm || alarm.periodInMinutes !== alarmInfo.periodInMinutes) { + chrome.alarms.create(alarmName, alarmInfo, () => { + const lastError = chrome.runtime.lastError; + if (lastError) { + console.error("chrome.runtime.lastError in chrome.alarms.create:", lastError); + console.error("Chrome alarm 无法创建,请检查是否达到数量上限。"); + } + resolve({ justCreated: true }); // 新创建的 Alarm 将根据 delayInMinutes 或 when 进行首次触发 + }); + return; + } + resolve({ justCreated: false }); // Alarm 未被重置,继续沿用现有调度(沿用其最近一次触发时间为基准的计划) + }); + }); +}; + +/** + * @function monitorPeriodicAlarm + * @description 监听所有已注册的 Chrome Alarm,并在触发时执行回调。 + * 同时负责在 Service Worker 重启后检查并补偿未执行的回调任务。 + * + * @returns {Promise} + * + * @example + * await monitorPeriodicAlarm(); + */ +export const monitorPeriodicAlarm = async () => { + if (started) throw new Error("monitorPeriodicAlarm cannot be called twice."); + started = true; + const execute = (arg: AlarmExecuteArgs) => { + const { alarm } = arg; + const alarmCallback = alarmCallbackStore[alarm.name]; + if (alarmCallback && alarm.periodInMinutes) { + // 将当前执行参数存入 storage.local: + // 1) 防止回调等待期间浏览器关闭导致未执行; + // 2) 支持 SW 重启或浏览器重新打开后补偿执行。 + const setPromise = chrome.storage.local.set({ [`AlarmPending:${alarm.name}`]: arg }); + setPromise + .then(() => alarmCallback(arg)) + .catch(console.warn) // 避免回调出错中断执行链 + .then(() => { + // 回调执行完毕,移除对应的存储记录 + chrome.storage.local.remove(`AlarmPending:${alarm.name}`); + }); + } + }; + + // 在 SW 启动时注册,避免漏跑 Alarm + const delayPromise = sleep(100); // 避免 SW 重启时回调尚未完成注册;延迟 100ms 在宏任务阶段触发 + chrome.alarms.onAlarm.addListener((alarm: chrome.alarms.Alarm) => { + const lastError = chrome.runtime.lastError; + if (lastError) { + console.error("chrome.runtime.lastError in chrome.alarms.onAlarm:", lastError); + // 非预期的 API 异常,停止处理 + return; + } + alarm = { ...alarm }; // 拷贝为普通对象,避免引用导致潜在内存泄漏 & storage 序列化問題 + + // Chrome alarm 的触发精度约为 30 秒(新版)/ 1 分钟(旧版) + // 若触发与计划时间相差 ≥ 65 秒,视为休眠/唤醒或 SW 重启等情况 + const triggeredAt = Date.now(); + const isFlushed = triggeredAt - alarm.scheduledTime >= 65_000; + + // 使用 delayPromise 确保 SW 重启时回调已准备好 + delayPromise.then(() => { + execute({ alarm, isFlushed, triggeredAt }); + }); + }); + + // SW 重启时检查是否有未执行的回调 + try { + const store = await chrome.storage.local.get(); + const keys = Object.keys(store).filter((key) => key.startsWith("AlarmPending:")); + if (keys.length > 0) { + await sleep(3000); // 等待 onAlarm 监听器稳定(3 秒) + const storeNew = await chrome.storage.local.get(); + const triggeredAt = Date.now(); + for (const key of keys) { + // 检查上次 SW 启动时 alarmCallback 是否未成功执行 + if (storeNew[key] && store[key] && storeNew[key].triggeredAt === store[key].triggeredAt) { + // 未成功执行则手动补偿执行 + const arg = storeNew[key] as AlarmExecuteArgs; + arg.triggeredAt = triggeredAt; + execute(arg); + } + } + } + } catch (e) { + console.error(e); + } +}; + +/** + * @function setPeriodicAlarmCallback + * @description 为指定 Alarm 注册回调函数,当 Alarm 触发时自动执行对应回调。 + * + * @param {string} alarmName Alarm 名称。 + * @param {(arg: AlarmExecuteArgs) => any} callback Alarm 触发时执行的回调函数。 + * + * @example + * setPeriodicAlarmCallback("DataSync", ({ alarm, isFlushed }) => { + * console.log("Alarm 触发:", alarm.name, "是否补偿执行:", isFlushed); + * }); + */ +export const setPeriodicAlarmCallback = (alarmName: string, callback: (arg: AlarmExecuteArgs) => any) => { + // 请在SW启用后100ms内设置 + alarmCallbackStore[alarmName] = callback; +}; diff --git a/src/app/service/service_worker/index.ts b/src/app/service/service_worker/index.ts index 7a5ec150c..bf791b101 100644 --- a/src/app/service/service_worker/index.ts +++ b/src/app/service/service_worker/index.ts @@ -15,9 +15,10 @@ import { ScriptDAO } from "@App/app/repo/scripts"; import { SystemService } from "./system"; import { type Logger, LoggerDAO } from "@App/app/repo/logger"; import { localePath, t } from "@App/locales/locales"; -import { getCurrentTab, InfoNotification } from "@App/pkg/utils/utils"; +import { getCurrentTab, InfoNotification, sleep } from "@App/pkg/utils/utils"; import { onTabRemoved, onUrlNavigated, setOnUserActionDomainChanged } from "./url_monitor"; import { LocalStorageDAO } from "@App/app/repo/localStorage"; +import { mightCreatePeriodicAlarm, setPeriodicAlarmCallback } from "./alarm"; // service worker的管理器 export default class ServiceWorkerManager { @@ -100,57 +101,37 @@ export default class ServiceWorkerManager { }); // 定时器处理 - chrome.alarms.onAlarm.addListener((alarm) => { - const lastError = chrome.runtime.lastError; - if (lastError) { - console.error("chrome.runtime.lastError in chrome.alarms.onAlarm:", lastError); - // 非预期的异常API错误,停止处理 - } - switch (alarm.name) { - case "checkScriptUpdate": - regularScriptUpdateCheck(); - break; - case "cloudSync": - // 进行一次云同步 - systemConfig.getCloudSync().then((config) => { - synchronize.buildFileSystem(config).then((fs) => { - synchronize.syncOnce(config, fs); - }); - }); - break; - case "checkSubscribeUpdate": - subscribe.checkSubscribeUpdate(); - break; - case "checkUpdate": - // 检查扩展更新 - this.checkUpdate(); - break; - } + setPeriodicAlarmCallback("checkScriptUpdate", async ({ isFlushed }) => { + if (isFlushed) await sleep(9200); // 避免SW启动时所有指令一次过执行 + regularScriptUpdateCheck(); + }); + + setPeriodicAlarmCallback("cloudSync", async ({ isFlushed }) => { + if (isFlushed) await sleep(14600); // 避免SW启动时所有指令一次过执行 + // 进行一次云同步 + systemConfig.getCloudSync().then((config) => { + synchronize.buildFileSystem(config).then((fs) => { + synchronize.syncOnce(config, fs); + }); + }); + }); + + setPeriodicAlarmCallback("checkSubscribeUpdate", async ({ isFlushed }) => { + if (isFlushed) await sleep(11600); // 避免SW启动时所有指令一次过执行 + subscribe.checkSubscribeUpdate(); + }); + + setPeriodicAlarmCallback("checkUpdate", async ({ isFlushed }) => { + if (isFlushed) await sleep(8400); // 避免SW启动时所有指令一次过执行 + // 检查扩展更新 + this.checkUpdate(); }); + // 12小时检查一次扩展更新 - chrome.alarms.get("checkUpdate", (alarm) => { - const lastError = chrome.runtime.lastError; - if (lastError) { - console.error("chrome.runtime.lastError in chrome.alarms.get:", lastError); - // 非预期的异常API错误,停止处理 - } - if (!alarm) { - chrome.alarms.create( - "checkUpdate", - { - delayInMinutes: 0, - periodInMinutes: 12 * 60, - }, - () => { - const lastError = chrome.runtime.lastError; - if (lastError) { - console.error("chrome.runtime.lastError in chrome.alarms.create:", lastError); - // Starting in Chrome 117, the number of active alarms is limited to 500. Once this limit is reached, chrome.alarms.create() will fail. - console.error("Chrome alarm is unable to create. Please check whether limit is reached."); - } - } - ); - } + // 首先执行建立Alarm时会执行一次更新。2分钟延迟避免启用时马上执行 + mightCreatePeriodicAlarm("checkUpdate", { + delayInMinutes: 2, + periodInMinutes: 12 * 60, }); // 监听配置变化 diff --git a/src/app/service/service_worker/script.ts b/src/app/service/service_worker/script.ts index fd669b14e..04cba3d28 100644 --- a/src/app/service/service_worker/script.ts +++ b/src/app/service/service_worker/script.ts @@ -51,6 +51,7 @@ import { import { getSimilarityScore, ScriptUpdateCheck } from "./script_update_check"; import { LocalStorageDAO } from "@App/app/repo/localStorage"; import { CompiledResourceDAO } from "@App/app/repo/resource"; +import { mightCreatePeriodicAlarm } from "./alarm"; // import { gzip as pakoGzip } from "pako"; const cIdKey = `(cid_${Math.random()})`; @@ -265,7 +266,7 @@ export class ScriptService { return this.mq.publish("installScript", { script, ...options }); } - // 安装脚本 / 更新腳本 + // 安装脚本 / 更新脚本 async installScript(param: { script: Script; code: string; upsertBy: InstallSource }) { param.upsertBy = param.upsertBy || "user"; const { script, upsertBy } = param; @@ -304,7 +305,7 @@ export class ScriptService { ]); // 广播一下 - // Runtime 會負責更新 CompiledResource + // Runtime 会负责更新 CompiledResource this.publishInstallScript(script, { update, upsertBy }); return { update }; @@ -1306,21 +1307,10 @@ export class ScriptService { this.group.on("openBatchUpdatePage", this.openBatchUpdatePage.bind(this)); this.group.on("checkScriptUpdate", this.checkScriptUpdate.bind(this)); - // 定时检查更新, 首次执行为5分钟后,然后每30分钟检查一次 - chrome.alarms.create( - "checkScriptUpdate", - { - delayInMinutes: 5, - periodInMinutes: 30, - }, - () => { - const lastError = chrome.runtime.lastError; - if (lastError) { - console.error("chrome.runtime.lastError in chrome.alarms.create:", lastError); - // Starting in Chrome 117, the number of active alarms is limited to 500. Once this limit is reached, chrome.alarms.create() will fail. - console.error("Chrome alarm is unable to create. Please check whether limit is reached."); - } - } - ); + // 定时检查更新, 首次执行为4分钟后,然后每32分钟检查一次 + mightCreatePeriodicAlarm("checkScriptUpdate", { + delayInMinutes: 4, + periodInMinutes: 32, // 时间相隔和 checkSubscribeUpdate 错开 + }); } } diff --git a/src/app/service/service_worker/subscribe.ts b/src/app/service/service_worker/subscribe.ts index 4eb397b02..af9f47547 100644 --- a/src/app/service/service_worker/subscribe.ts +++ b/src/app/service/service_worker/subscribe.ts @@ -16,6 +16,7 @@ import { cacheInstance } from "@App/app/cache"; import { v4 as uuidv4 } from "uuid"; import { CACHE_KEY_SCRIPT_INFO } from "@App/app/cache_key"; import i18n, { i18nName } from "@App/locales/locales"; +import { mightCreatePeriodicAlarm } from "./alarm"; export class SubscribeService { logger: Logger; @@ -316,21 +317,10 @@ export class SubscribeService { this.upsertScript(message.subscribe.url); }); - // 定时检查更新, 首次執行為5分钟後,然後每30分钟检查一次 - chrome.alarms.create( - "checkSubscribeUpdate", - { - delayInMinutes: 5, - periodInMinutes: 30, - }, - () => { - const lastError = chrome.runtime.lastError; - if (lastError) { - console.error("chrome.runtime.lastError in chrome.alarms.create:", lastError); - // Starting in Chrome 117, the number of active alarms is limited to 500. Once this limit is reached, chrome.alarms.create() will fail. - console.error("Chrome alarm is unable to create. Please check whether limit is reached."); - } - } - ); + // 定时检查更新, 首次执行为5分钟后,然后每42分钟检查一次 + mightCreatePeriodicAlarm("checkSubscribeUpdate", { + delayInMinutes: 5, + periodInMinutes: 42, // 时间相隔和 checkScriptUpdate 错开 + }); } } diff --git a/src/app/service/service_worker/synchronize.ts b/src/app/service/service_worker/synchronize.ts index da3a295a8..eebdc53e6 100644 --- a/src/app/service/service_worker/synchronize.ts +++ b/src/app/service/service_worker/synchronize.ts @@ -26,6 +26,7 @@ import { prepareScriptByCode } from "@App/pkg/utils/script"; import { ExtVersion } from "@App/app/const"; import { dayFormat } from "@App/pkg/utils/day_format"; import i18n, { i18nName } from "@App/locales/locales"; +import { mightCreatePeriodicAlarm } from "./alarm"; // type SynchronizeTarget = "local"; @@ -606,28 +607,8 @@ export class SynchronizeService { this.buildFileSystem(value).then(async (fs) => { await this.syncOnce(value, fs); // 开启定时器, 一小时一次 - chrome.alarms.get("cloudSync", (alarm) => { - const lastError = chrome.runtime.lastError; - if (lastError) { - console.error("chrome.runtime.lastError in chrome.alarms.get:", lastError); - // 非预期的异常API错误,停止处理 - } - if (!alarm) { - chrome.alarms.create( - "cloudSync", - { - periodInMinutes: 60, - }, - () => { - const lastError = chrome.runtime.lastError; - if (lastError) { - console.error("chrome.runtime.lastError in chrome.alarms.create:", lastError); - // Starting in Chrome 117, the number of active alarms is limited to 500. Once this limit is reached, chrome.alarms.create() will fail. - console.error("Chrome alarm is unable to create. Please check whether limit is reached."); - } - } - ); - } + mightCreatePeriodicAlarm("cloudSync", { + periodInMinutes: 60, }); }); } else { diff --git a/src/service_worker.ts b/src/service_worker.ts index 853d503a9..a5e1e5be7 100644 --- a/src/service_worker.ts +++ b/src/service_worker.ts @@ -11,9 +11,11 @@ import { fetchIconByDomain } from "./app/service/service_worker/fetch"; import { msgResponse } from "./app/service/service_worker/utils"; import type { RuntimeMessageSender } from "@Packages/message/types"; import { cleanInvalidKeys } from "./app/repo/resource"; +import { monitorPeriodicAlarm } from "./app/service/service_worker/alarm"; migrate(); migrateChromeStorage(); +monitorPeriodicAlarm(); const OFFSCREEN_DOCUMENT_PATH = "src/offscreen.html";