DelayedInit.sys.mjs (5480B)
1 /* This Source Code Form is subject to the terms of the Mozilla Public 2 * License, v. 2.0. If a copy of the MPL was not distributed with this 3 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ 4 5 /** 6 * Use DelayedInit to schedule initializers to run some time after startup. 7 * Initializers are added to a list of pending inits. Whenever the main thread 8 * message loop is idle, DelayedInit will start running initializers from the 9 * pending list. To prevent monopolizing the message loop, every idling period 10 * has a maximum duration. When that's reached, we give up the message loop and 11 * wait for the next idle. 12 * 13 * DelayedInit is compatible with lazy getters like those from XPCOMUtils. When 14 * the lazy getter is first accessed, its corresponding initializer is run 15 * automatically if it hasn't been run already. Each initializer also has a 16 * maximum wait parameter that specifies a mandatory timeout; when the timeout 17 * is reached, the initializer is forced to run. 18 * 19 * DelayedInit.schedule(() => Foo.init(), null, null, 5000); 20 * 21 * In the example above, Foo.init will run automatically when the message loop 22 * becomes idle, or when 5000ms has elapsed, whichever comes first. 23 * 24 * DelayedInit.schedule(() => Foo.init(), this, "Foo", 5000); 25 * 26 * In the example above, Foo.init will run automatically when the message loop 27 * becomes idle, when |this.Foo| is accessed, or when 5000ms has elapsed, 28 * whichever comes first. 29 * 30 * It may be simpler to have a wrapper for DelayedInit.schedule. For example, 31 * 32 * function InitLater(fn, obj, name) { 33 * return DelayedInit.schedule(fn, obj, name, 5000); // constant max wait 34 * } 35 * InitLater(() => Foo.init()); 36 * InitLater(() => Bar.init(), this, "Bar"); 37 */ 38 export var DelayedInit = { 39 schedule(fn, object, name, maxWait) { 40 return Impl.scheduleInit(fn, object, name, maxWait); 41 }, 42 43 scheduleList(fns, maxWait) { 44 for (const fn of fns) { 45 Impl.scheduleInit(fn, null, null, maxWait); 46 } 47 }, 48 }; 49 50 // Maximum duration for each idling period. Pending inits are run until this 51 // duration is exceeded; then we wait for next idling period. 52 const MAX_IDLE_RUN_MS = 5; 53 54 var Impl = { 55 pendingInits: [], 56 57 onIdle() { 58 const startTime = ChromeUtils.now(); 59 let time = startTime; 60 let nextDue; 61 62 // Go through all the pending inits. Even if we don't run them, 63 // we still need to find out when the next timeout should be. 64 for (const init of this.pendingInits) { 65 if (init.complete) { 66 continue; 67 } 68 69 if (time - startTime < MAX_IDLE_RUN_MS) { 70 init.maybeInit(); 71 time = ChromeUtils.now(); 72 } else { 73 // We ran out of time; find when the next closest due time is. 74 nextDue = nextDue ? Math.min(nextDue, init.due) : init.due; 75 } 76 } 77 78 // Get rid of completed ones. 79 this.pendingInits = this.pendingInits.filter(init => !init.complete); 80 81 if (nextDue !== undefined) { 82 // Schedule the next idle, if we still have pending inits. 83 ChromeUtils.idleDispatch(() => this.onIdle(), { 84 timeout: Math.max(0, nextDue - time), 85 }); 86 } 87 }, 88 89 addPendingInit(fn, wait) { 90 const init = { 91 fn, 92 due: ChromeUtils.now() + wait, 93 complete: false, 94 maybeInit() { 95 if (this.complete) { 96 return false; 97 } 98 this.complete = true; 99 this.fn.call(); 100 this.fn = null; 101 return true; 102 }, 103 }; 104 105 if (!this.pendingInits.length) { 106 // Schedule for the first idle. 107 ChromeUtils.idleDispatch(() => this.onIdle(), { timeout: wait }); 108 } 109 this.pendingInits.push(init); 110 return init; 111 }, 112 113 scheduleInit(fn, object, name, wait) { 114 const init = this.addPendingInit(fn, wait); 115 116 if (!object || !name) { 117 // No lazy getter needed. 118 return; 119 } 120 121 // Get any existing information about the property. 122 let prop = Object.getOwnPropertyDescriptor(object, name) || { 123 configurable: true, 124 enumerable: true, 125 writable: true, 126 }; 127 128 if (!prop.configurable) { 129 // Object.defineProperty won't work, so just perform init here. 130 init.maybeInit(); 131 return; 132 } 133 134 // Define proxy getter/setter that will call first initializer first, 135 // before delegating the get/set to the original target. 136 Object.defineProperty(object, name, { 137 get: function proxy_getter() { 138 init.maybeInit(); 139 140 // If the initializer actually ran, it may have replaced our proxy 141 // property with a real one, so we need to reload he property. 142 const newProp = Object.getOwnPropertyDescriptor(object, name); 143 if (newProp.get !== proxy_getter) { 144 // Set prop if newProp doesn't refer to our proxy property. 145 prop = newProp; 146 } else { 147 // Otherwise, reset to the original property. 148 Object.defineProperty(object, name, prop); 149 } 150 151 if (prop.get) { 152 return prop.get.call(object); 153 } 154 return prop.value; 155 }, 156 set(newVal) { 157 init.maybeInit(); 158 159 // Since our initializer already ran, 160 // we can get rid of our proxy property. 161 if (prop.get || prop.set) { 162 Object.defineProperty(object, name, prop); 163 prop.set.call(object); 164 return; 165 } 166 167 prop.value = newVal; 168 Object.defineProperty(object, name, prop); 169 }, 170 configurable: true, 171 enumerable: true, 172 }); 173 }, 174 };