From 529d557a383c2e649673416b4a9cf00ae790a49e Mon Sep 17 00:00:00 2001 From: Slowlife Date: Mon, 23 Feb 2026 18:36:44 +0700 Subject: [PATCH] refactor: switch to use DeferredTask in scheduling logic, p=#12480, c=live-folders --- src/zen/live-folders/ZenLiveFolder.sys.mjs | 101 ++++++++-------- .../tests/live-folders/browser_live_folder.js | 108 +++++++++++++----- 2 files changed, 136 insertions(+), 73 deletions(-) diff --git a/src/zen/live-folders/ZenLiveFolder.sys.mjs b/src/zen/live-folders/ZenLiveFolder.sys.mjs index 68d8f132b..489041b96 100644 --- a/src/zen/live-folders/ZenLiveFolder.sys.mjs +++ b/src/zen/live-folders/ZenLiveFolder.sys.mjs @@ -3,17 +3,13 @@ // file, You can obtain one at http://mozilla.org/MPL/2.0/. const lazy = {}; ChromeUtils.defineESModuleGetters(lazy, { - setTimeout: "resource://gre/modules/Timer.sys.mjs", - clearTimeout: "resource://gre/modules/Timer.sys.mjs", - requestIdleCallback: "resource://gre/modules/Timer.sys.mjs", - cancelIdleCallback: "resource://gre/modules/Timer.sys.mjs", NetUtil: "resource://gre/modules/NetUtil.sys.mjs", + DeferredTask: "resource://gre/modules/DeferredTask.sys.mjs", NetworkHelper: "resource://devtools/shared/network-observer/NetworkHelper.sys.mjs", }); export class nsZenLiveFolderProvider { - #timerHandle = null; - #idleCallbackHandle = null; + #task = null; constructor({ id, manager, state }) { this.id = id; @@ -30,56 +26,50 @@ export class nsZenLiveFolderProvider { } async refresh() { - this.stop(); - const result = await this.#internalFetch(); - this.start(); + this.#task.disarm(); + const result = await this.#fetchLiveFolder(); + this.#task.arm(); return result; } - start() { - const now = Date.now(); - const lastFetched = this.state.lastFetched; + start(checkDelay = true) { const interval = this.state.interval; - - const timeSinceLast = now - lastFetched; - let delay = interval - timeSinceLast; - - if (delay <= 0) { - delay = 0; + if (this.#task) { + this.#task.finalize(); } - this.#scheduleNext(delay); + if (checkDelay) { + const now = Date.now(); + const lastFetched = this.state.lastFetched; + + const timeSinceLast = now - lastFetched; + let delay = interval - timeSinceLast; + + if (delay <= 0) { + delay = 0; + } + + this.#task = new lazy.DeferredTask(async () => { + await this.#fetchLiveFolder(); + this.start(false); + }, delay); + } else { + this.#task = new lazy.DeferredTask(async () => { + await this.#fetchLiveFolder(); + this.#task.arm(); + }, interval); + } + + this.#task.arm(); } stop() { - if (this.#timerHandle) { - lazy.clearTimeout(this.#timerHandle); - this.#timerHandle = null; - } - - if (this.#idleCallbackHandle) { - lazy.cancelIdleCallback(this.#idleCallbackHandle); - this.#idleCallbackHandle = null; + if (this.#task) { + this.#task.disarm(); } } - #scheduleNext(delay) { - if (this.#timerHandle) { - lazy.clearTimeout(this.#timerHandle); - } - - this.#timerHandle = lazy.setTimeout(() => { - const fetchWhenIdle = () => { - this.#internalFetch(); - this.#idleCallbackHandle = null; - }; - - this.#idleCallbackHandle = lazy.requestIdleCallback(fetchWhenIdle); - this.#scheduleNext(this.state.interval); - }, delay); - } - - async #internalFetch() { + async #fetchLiveFolder() { try { const items = await this.fetchItems(); this.state.lastFetched = Date.now(); @@ -131,7 +121,7 @@ export class nsZenLiveFolderProvider { this.manager.saveState(); } - fetch(url, { maxContentLength = 5 * 1024 * 1024 } = {}) { + fetch(url, { maxContentLength = 5 * 1024 * 1024, method = "GET", body = null } = {}) { const uri = lazy.NetUtil.newURI(url); // TODO: Support userContextId when fetching, it should be inherited from the folder's // current space context ID. @@ -164,6 +154,27 @@ export class nsZenLiveFolderProvider { ) .QueryInterface(Ci.nsIHttpChannel); + method = method.toUpperCase(); + if (method === "POST") { + const uploadChannel = channel + .QueryInterface(Ci.nsIHttpChannel) + .QueryInterface(Ci.nsIUploadChannel2); + + if (body === null) { + body = ""; + } else if (typeof body !== "string") { + body = JSON.stringify(body); + } + + const stream = Cc["@mozilla.org/io/string-input-stream;1"].createInstance( + Ci.nsIStringInputStream + ); + + stream.setByteStringData(body); + uploadChannel.explicitSetUploadStream(stream, "application/json", -1, method, false); + } + + channel.requestMethod = method; let httpStatus = null; let contentType = ""; let headerCharset = null; diff --git a/src/zen/tests/live-folders/browser_live_folder.js b/src/zen/tests/live-folders/browser_live_folder.js index 2c66ac6c3..5aee5f92b 100644 --- a/src/zen/tests/live-folders/browser_live_folder.js +++ b/src/zen/tests/live-folders/browser_live_folder.js @@ -13,10 +13,29 @@ function sleep(ms) { } describe("Zen Live Folder Scheduling", () => { + const INTERVAL = 250; + const INTERVAL_OFFSET = 50; + let instance; let sandbox; let mockManager; + function createInstance({ id, interval, lastFetched }) { + instance = new nsZenLiveFolderProvider({ + id, + manager: mockManager, + state: { + interval, + lastFetched, + }, + }); + + const fetchStub = sandbox.stub(instance, "fetchItems").resolves(["item1"]); + sandbox.stub(instance, "getMetadata").returns({}); + + return { fetchStub }; + } + beforeEach(() => { sandbox = sinon.createSandbox(); mockManager = { @@ -33,51 +52,84 @@ describe("Zen Live Folder Scheduling", () => { }); it("should fetch correctly at an interval", async () => { - const INTERVAL = 250; - - instance = new nsZenLiveFolderProvider({ + const { fetchStub } = createInstance({ id: "test-folder", - manager: mockManager, - state: { - interval: INTERVAL, - lastFetched: Date.now(), - }, + interval: INTERVAL, + lastFetched: Date.now(), }); - const fetchStub = sandbox.stub(instance, "fetchItems").resolves(["item1"]); - sandbox.stub(instance, "getMetadata").returns({}); - instance.start(); - - sinon.assert.notCalled(fetchStub); - await sleep(INTERVAL / 2); sinon.assert.notCalled(fetchStub); - await sleep(INTERVAL * 2); - Assert.ok(fetchStub.callCount > 1, "Should have fetched more than once"); + const startSpy = sandbox.spy(instance, "start"); + + await sleep(INTERVAL + INTERVAL_OFFSET); + Assert.equal(fetchStub.callCount, 1, "Should have fetched once after the first interval"); + + await sleep(INTERVAL + INTERVAL_OFFSET); + Assert.equal(fetchStub.callCount, 2, "Should have fetched 2 times"); + Assert.deepEqual( + startSpy.firstCall.args, + [false], + "Start should have been called once with false" + ); + + await sleep(INTERVAL + INTERVAL_OFFSET); + Assert.equal(fetchStub.callCount, 3, "Should have fetched 3 times"); + Assert.equal(startSpy.callCount, 1, "Start should not been called"); sinon.assert.called(mockManager.saveState); sinon.assert.called(mockManager.onLiveFolderFetch); }); it("should fetch immediately if overdue", async () => { - const INTERVAL = 500; - - instance = new nsZenLiveFolderProvider({ + const { fetchStub } = createInstance({ id: "test-folder-overdue", - manager: mockManager, - state: { - interval: INTERVAL, - lastFetched: Date.now() - 3600000, - }, + interval: INTERVAL, + lastFetched: Date.now() - 3600000, }); - const fetchStub = sandbox.stub(instance, "fetchItems").resolves(["item1"]); - sandbox.stub(instance, "getMetadata").returns({}); - instance.start(); - await sleep(20); + await sleep(INTERVAL_OFFSET); sinon.assert.calledOnce(fetchStub); }); + + it("should fetch with correct offset", async () => { + const { fetchStub } = createInstance({ + id: "test-folder-delay", + interval: INTERVAL, + lastFetched: Date.now() - INTERVAL / 2, + }); + + instance.start(); + await sleep(INTERVAL / 2 + INTERVAL_OFFSET); + Assert.equal(fetchStub.callCount, 1, "Should have fetched once"); + + await sleep(INTERVAL + INTERVAL_OFFSET); + Assert.equal(fetchStub.callCount, 2, "Should have fetched once with normal interval"); + }); + + it("should re-start the timer if interval was changed", async () => { + const { fetchStub } = createInstance({ + id: "test-folder-interval-change", + interval: INTERVAL, + lastFetched: Date.now(), + }); + + instance.start(); + + sinon.assert.notCalled(fetchStub); + await sleep(INTERVAL + INTERVAL_OFFSET); + Assert.equal(fetchStub.callCount, 1, "Should have fetched once after the first interval"); + + const NEW_INTERVAL = 500; + instance.state.interval = NEW_INTERVAL; + + instance.stop(); + instance.start(); + + await sleep(NEW_INTERVAL + INTERVAL_OFFSET); + Assert.equal(fetchStub.callCount, 2, "Should have once after the new interval"); + }); });