refactor: switch to use DeferredTask in scheduling logic, p=#12480, c=live-folders

This commit is contained in:
Slowlife
2026-02-23 18:36:44 +07:00
committed by GitHub
parent bbaf779e7a
commit 529d557a38
2 changed files with 136 additions and 73 deletions

View File

@@ -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;

View File

@@ -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");
});
});