/* This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ "use strict"; /* globals MessageLoop */ var EXPORTED_SYMBOLS = ["DelayedInit"]; /** * Use DelayedInit to schedule initializers to run some time after startup. * Initializers are added to a list of pending inits. Whenever the main thread * message loop is idle, DelayedInit will start running initializers from the * pending list. To prevent monopolizing the message loop, every idling period * has a maximum duration. When that's reached, we give up the message loop and * wait for the next idle. * * DelayedInit is compatible with lazy getters like those from XPCOMUtils. When * the lazy getter is first accessed, its corresponding initializer is run * automatically if it hasn't been run already. Each initializer also has a * maximum wait parameter that specifies a mandatory timeout; when the timeout * is reached, the initializer is forced to run. * * DelayedInit.schedule(() => Foo.init(), null, null, 5000); * * In the example above, Foo.init will run automatically when the message loop * becomes idle, or when 5000ms has elapsed, whichever comes first. * * DelayedInit.schedule(() => Foo.init(), this, "Foo", 5000); * * In the example above, Foo.init will run automatically when the message loop * becomes idle, when |this.Foo| is accessed, or when 5000ms has elapsed, * whichever comes first. * * It may be simpler to have a wrapper for DelayedInit.schedule. For example, * * function InitLater(fn, obj, name) { * return DelayedInit.schedule(fn, obj, name, 5000); // constant max wait * } * InitLater(() => Foo.init()); * InitLater(() => Bar.init(), this, "Bar"); */ var DelayedInit = { schedule(fn, object, name, maxWait) { return Impl.scheduleInit(fn, object, name, maxWait); }, scheduleList(fns, maxWait) { for (const fn of fns) { Impl.scheduleInit(fn, null, null, maxWait); } }, }; // Maximum duration for each idling period. Pending inits are run until this // duration is exceeded; then we wait for next idling period. const MAX_IDLE_RUN_MS = 50; var Impl = { pendingInits: [], onIdle() { const startTime = Cu.now(); let time = startTime; let nextDue; // Go through all the pending inits. Even if we don't run them, // we still need to find out when the next timeout should be. for (const init of this.pendingInits) { if (init.complete) { continue; } if (time - startTime < MAX_IDLE_RUN_MS) { init.maybeInit(); time = Cu.now(); } else { // We ran out of time; find when the next closest due time is. nextDue = nextDue ? Math.min(nextDue, init.due) : init.due; } } // Get rid of completed ones. this.pendingInits = this.pendingInits.filter(init => !init.complete); if (nextDue !== undefined) { // Schedule the next idle, if we still have pending inits. ChromeUtils.idleDispatch(() => this.onIdle(), { timeout: Math.max(0, nextDue - time), }); } }, addPendingInit(fn, wait) { const init = { fn, due: Cu.now() + wait, complete: false, maybeInit() { if (this.complete) { return false; } this.complete = true; this.fn.call(); this.fn = null; return true; }, }; if (!this.pendingInits.length) { // Schedule for the first idle. ChromeUtils.idleDispatch(() => this.onIdle(), { timeout: wait }); } this.pendingInits.push(init); return init; }, scheduleInit(fn, object, name, wait) { const init = this.addPendingInit(fn, wait); if (!object || !name) { // No lazy getter needed. return; } // Get any existing information about the property. let prop = Object.getOwnPropertyDescriptor(object, name) || { configurable: true, enumerable: true, writable: true, }; if (!prop.configurable) { // Object.defineProperty won't work, so just perform init here. init.maybeInit(); return; } // Define proxy getter/setter that will call first initializer first, // before delegating the get/set to the original target. Object.defineProperty(object, name, { get: function proxy_getter() { init.maybeInit(); // If the initializer actually ran, it may have replaced our proxy // property with a real one, so we need to reload he property. const newProp = Object.getOwnPropertyDescriptor(object, name); if (newProp.get !== proxy_getter) { // Set prop if newProp doesn't refer to our proxy property. prop = newProp; } else { // Otherwise, reset to the original property. Object.defineProperty(object, name, prop); } if (prop.get) { return prop.get.call(object); } return prop.value; }, set(newVal) { init.maybeInit(); // Since our initializer already ran, // we can get rid of our proxy property. if (prop.get || prop.set) { Object.defineProperty(object, name, prop); prop.set.call(object); return; } prop.value = newVal; Object.defineProperty(object, name, prop); }, configurable: true, enumerable: true, }); }, };