summaryrefslogtreecommitdiffstats
path: root/dom/tests/browser/page_localstorage_e10s.html
blob: 8afc8f82e3fe7ebf622d324355cf27b881a777f2 (plain)
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
128
129
130
131
132
133
134
135
136
137
<!doctype html>
<html>
<head>
  <meta charset="utf-8">
<script>
/**
 * Helper page used by browser_localStorage_e10s.js.
 *
 * We expose methods to be invoked by ContentTask.spawn() calls.
 * ContentTask.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;
}
</script>
</head>
<body><h2 id="pageNameH"></h2></body>
</html>