page_localStorage.js (5019B)
1 /** 2 * Helper page used by browser_localStorage_xxx.js. 3 * 4 * We expose methods to be invoked by SpecialPowers.spawn() calls. 5 * SpecialPowers.spawn() uses the message manager and is PContent-based. When 6 * LocalStorage was PContent-managed, ordering was inherently ensured so we 7 * could assume each page had already received all relevant events. Now some 8 * explicit type of coordination is required. 9 * 10 * This gets complicated because: 11 * - LocalStorage is an ugly API that gives us almost unlimited implementation 12 * flexibility in the face of multiple processes. It's also an API that sites 13 * may misuse which may encourage us to leverage that flexibility in the 14 * future to improve performance at the expense of propagation latency, and 15 * possibly involving content-observable coalescing of events. 16 * - The Quantum DOM effort and its event labeling and separate task queues and 17 * green threading and current LocalStorage implementation mean that using 18 * other PBackground-based APIs such as BroadcastChannel may not provide 19 * reliable ordering guarantees. Specifically, it's hard to guarantee that 20 * a BroadcastChannel postMessage() issued after a series of LocalStorage 21 * writes won't be received by the target window before the writes are 22 * perceived. At least not without constraining the implementations of both 23 * APIs. 24 * - Some of our tests explicitly want to verify LocalStorage behavior without 25 * having a "storage" listener, so we can't add a storage listener if the test 26 * didn't already want one. 27 * 28 * We use 2 approaches for coordination: 29 * 1. If we're already listening for events, we listen for the sentinel value to 30 * be written. This is efficient and appropriate in this case. 31 * 2. If we're not listening for events, we use setTimeout(0) to poll the 32 * localStorage key and value until it changes to our expected value. 33 * setTimeout(0) eventually clamps to setTimeout(4), so in the event we are 34 * experiencing delays, we have reasonable, non-CPU-consuming back-off in 35 * place that leaves the CPU free to time out and fail our test if something 36 * broke. This is ugly but makes us less brittle. 37 * 38 * Both of these involve mutateStorage writing the sentinel value at the end of 39 * the batch. All of our result-returning methods accordingly filter out the 40 * sentinel key/value pair. 41 */ 42 43 var pageName = document.location.search.substring(1); 44 window.addEventListener("load", () => { 45 document.getElementById("pageNameH").textContent = pageName; 46 }); 47 48 // Key that conveys the end of a write batch. Filtered out from state and 49 // events. 50 const SENTINEL_KEY = "WRITE_BATCH_SENTINEL"; 51 52 var storageEventsPromise = null; 53 function listenForStorageEvents(sentinelValue) { 54 const recordedEvents = []; 55 storageEventsPromise = new Promise(function (resolve, reject) { 56 window.addEventListener("storage", function thisHandler(event) { 57 if (event.key === SENTINEL_KEY) { 58 // There should be no way for this to have the wrong value, but reject 59 // if it is wrong. 60 if (event.newValue === sentinelValue) { 61 window.removeEventListener("storage", thisHandler); 62 resolve(recordedEvents); 63 } else { 64 reject(event.newValue); 65 } 66 } else { 67 recordedEvents.push([event.key, event.newValue, event.oldValue]); 68 } 69 }); 70 }); 71 } 72 73 function mutateStorage({ mutations, sentinelValue }) { 74 mutations.forEach(function ([key, value]) { 75 if (key !== null) { 76 if (value === null) { 77 localStorage.removeItem(key); 78 } else { 79 localStorage.setItem(key, value); 80 } 81 } else { 82 localStorage.clear(); 83 } 84 }); 85 localStorage.setItem(SENTINEL_KEY, sentinelValue); 86 } 87 88 // Returns a promise that is resolve when the sentinel key has taken on the 89 // sentinel value. Oddly structured to make sure promises don't let us 90 // accidentally side-step the timeout clamping logic. 91 function waitForSentinelValue(sentinelValue) { 92 return new Promise(function (resolve) { 93 function checkFunc() { 94 if (localStorage.getItem(SENTINEL_KEY) === sentinelValue) { 95 resolve(); 96 } else { 97 // I believe linters will only yell at us if we use a non-zero constant. 98 // Other forms of back-off were considered, including attempting to 99 // issue a round-trip through PBackground, but that still potentially 100 // runs afoul of labeling while also making us dependent on unrelated 101 // APIs. 102 setTimeout(checkFunc, 0); 103 } 104 } 105 checkFunc(); 106 }); 107 } 108 109 async function getStorageState(maybeSentinel) { 110 if (maybeSentinel) { 111 await waitForSentinelValue(maybeSentinel); 112 } 113 114 let numKeys = localStorage.length; 115 let state = {}; 116 for (var iKey = 0; iKey < numKeys; iKey++) { 117 let key = localStorage.key(iKey); 118 if (key !== SENTINEL_KEY) { 119 state[key] = localStorage.getItem(key); 120 } 121 } 122 return state; 123 } 124 125 function returnAndClearStorageEvents() { 126 return storageEventsPromise; 127 }