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